Merge branch 'ui' into 'main'

Ui

See merge request opensource/answer!14
This commit is contained in:
Ren Yubin 2022-09-29 03:43:01 +00:00
commit 5f47064b3c
125 changed files with 1299 additions and 1210 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
.DS_Store
._*
/.idea
/.fleet
/.vscode/*.log
/cmd/answer/*.sh
/cmd/logs

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"eslint.workingDirectories": [
"ui"
]
}

View File

View File

@ -1,9 +0,0 @@
# The MIT License
Copyright 2022 robin, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -49,9 +49,9 @@ when cloning repo, and run `pnpm install` to init dependencies. you can use proj
## 🖥 Environment Support
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge / IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Edge, IE11 | last 2 versions | last 2 versions | last 2 versions |
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Build with

View File

@ -15,7 +15,7 @@ module.exports = {
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
'@answer/utils': path.resolve(__dirname, 'src/utils'),
'@answer/common': path.resolve(__dirname, 'src/common'),
'@answer/services': path.resolve(__dirname, 'src/services'),
'@answer/api': path.resolve(__dirname, 'src/services/api'),
};
return config;
@ -26,8 +26,7 @@ module.exports = {
const config = configFunction(proxy, allowedHost);
config.proxy = {
'/answer': {
target: 'http://10.0.20.84:8080',
// target: 'http://10.0.10.98:2060',
target: 'http://10.0.10.98:2060',
changeOrigin: true,
secure: false,
},

View File

@ -19,7 +19,7 @@
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"path": "ui/node_modules/cz-conventional-changelog"
}
},
"dependencies": {
@ -102,9 +102,10 @@
"typescript": "*",
"web-vitals": "^2.1.4"
},
"license": "MIT",
"packageManager": "pnpm@7.9.5",
"engines": {
"node": ">=16.17",
"pnpm": ">=7"
}
}
},
"license": "MIT"
}

View File

@ -34,3 +34,25 @@ export const ADMIN_LIST_STATUS = {
name: 'deleted',
},
};
export const ADMIN_NAV_MENUS = [
{
name: 'dashboard',
children: [],
},
{
name: 'contents',
child: [{ name: 'questions' }, { name: 'answers' }],
},
{
name: 'users',
},
{
name: 'flags',
// badgeContent: 5,
},
{
name: 'settings',
child: [{ name: 'general' }, { name: 'interface' }],
},
];

View File

@ -7,3 +7,302 @@ export interface FormValue<T = any> {
export interface FormDataType {
[prop: string]: FormValue;
}
export interface Paging {
page: number;
page_size?: number;
}
export type ReportType = 'question' | 'answer' | 'comment' | 'user';
export type ReportAction = 'close' | 'flag' | 'review';
export interface ReportParams {
type: ReportType;
action: ReportAction;
}
export interface TagBase {
display_name: string;
slug_name: string;
}
export interface Tag extends TagBase {
main_tag_slug_name?: string;
original_text?: string;
parsed_text?: string;
}
export interface SynonymsTag extends Tag {
tag_id: string;
tag?: string;
}
export interface TagInfo extends TagBase {
tag_id: string;
original_text: string;
parsed_text: string;
follow_count: number;
question_count: number;
is_follower: boolean;
member_actions;
created_at?;
updated_at?;
main_tag_slug_name?: string;
excerpt?;
}
export interface QuestionParams {
title: string;
content: string;
html: string;
tags: Tag[];
}
export interface ListResult<T = any> {
count: number;
list: T[];
}
export interface AnswerParams {
content: string;
html: string;
question_id: string;
id: string;
}
export interface LoginReqParams {
e_mail: string;
/** password */
pass: string;
captcha_id?: string;
captcha_code?: string;
}
export interface RegisterReqParams extends LoginReqParams {
name: string;
}
export interface ModifyPassReq {
old_pass: string;
pass: string;
}
/** User */
export interface ModifyUserReq {
display_name: string;
username?: string;
avatar: string;
bio: string;
bio_html?: string;
location: string;
website: string;
}
export interface UserInfoBase {
avatar: string;
username: string;
display_name: string;
rank: number;
website: string;
location: string;
ip_info?: string;
/** 'forbidden' | 'normal' | 'delete'
*/
status?: string;
/** roles */
is_admin?: true;
}
export interface UserInfoRes extends UserInfoBase {
bio: string;
bio_html: string;
create_time?: string;
/** value = 1 active; value = 2 inactivated
*/
mail_status: number;
e_mail?: string;
[prop: string]: any;
}
export interface AvatarUploadReq {
file: FormData;
}
export interface ImgCodeReq {
captcha_id?: string;
captcha_code?: string;
}
export interface ImgCodeRes {
captcha_id: string;
captcha_img: string;
verify: boolean;
}
export interface PssRetReq extends ImgCodeReq {
e_mail: string;
}
export interface CheckImgReq {
action: 'login' | 'e_mail' | 'find_pass';
}
export interface NoticeSetReq {
notice_switch: boolean;
}
export interface QuDetailRes {
id: string;
title: string;
content: string;
html: string;
tags: any[];
view_count: number;
unique_view_count?: number;
answer_count: number;
favorites_count: number;
follow_counts: 0;
accepted_answer_id: string;
last_answer_id: string;
create_time: string;
update_time: string;
user_info: UserInfoBase;
answered: boolean;
collected: boolean;
[prop: string]: any;
}
export interface AnswersReq extends Paging {
order?: 'default' | 'updated';
question_id: string;
}
export interface AnswerItem {
id: string;
question_id: string;
content: string;
html: string;
create_time: string;
update_time: string;
user_info: UserInfoBase;
[prop: string]: any;
}
export interface PostAnswerReq {
content: string;
html: string;
question_id: string;
}
export interface PageUser {
id;
displayName;
userName?;
avatar_url?;
}
export interface LangsType {
label: string;
value: string;
}
/**
* @description interface for Question
*/
export type QuestionOrderBy =
| 'newest'
| 'active'
| 'frequent'
| 'score'
| 'unanswered';
export interface QueryQuestionsReq extends Paging {
order: QuestionOrderBy;
tags?: string[];
}
export type AdminQuestionStatus = 'available' | 'closed' | 'deleted';
export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
export interface AdminContentsReq extends Paging {
status: AdminContentsFilterBy;
}
/**
* @description interface for Answer
*/
export type AdminAnswerStatus = 'available' | 'deleted';
/**
* @description interface for Users
*/
export type UserFilterBy = 'all' | 'inactive' | 'suspended' | 'deleted';
/**
* @description interface for Flags
*/
export type FlagStatus = 'pending' | 'completed';
export type FlagType = 'all' | 'question' | 'answer' | 'comment';
export interface AdminFlagsReq extends Paging {
status: FlagStatus;
object_type: FlagType;
}
/**
* @description interface for Admin Settings
*/
export interface AdminSettingsGeneral {
name: string;
short_description: string;
description: string;
}
export interface AdminSettingsInterface {
logo: string;
language: string;
theme: string;
}
export interface SiteSettings {
general: AdminSettingsGeneral;
interface: AdminSettingsInterface;
}
/**
* @description interface for Activity
*/
export interface FollowParams {
is_cancel: boolean;
object_id: string;
}
/**
* @description search request params
*/
export interface SearchParams {
q: string;
order: string;
page: number;
size?: number;
}
/**
* @description search response data
*/
export interface SearchResItem {
object_type: string;
object: {
id: string;
title: string;
excerpt: string;
created_at: number;
user_info: UserInfoBase;
vote_count: number;
answer_count: number;
accepted: boolean;
tags: TagBase[];
};
}
export interface SearchRes extends ListResult<SearchResItem> {
extra: any;
}

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { Icon } from '@answer/components';
import { bookmark, postVote } from '@answer/services/api';
import { bookmark, postVote } from '@answer/api';
import { isLogin } from '@answer/utils';
import { userInfoStore } from '@answer/stores';
import { useToast } from '@answer/hooks';

View File

@ -24,7 +24,7 @@ const ActionBar = ({
<div className="d-flex justify-content-between fs-14">
<div className="d-flex align-items-center">
<Link to={`/users/${username}`}>{nickName}</Link>
<span className="mx-1">·</span>
<span className="mx-1 text-secondary"></span>
<FormatTime time={createdAt} className="text-secondary me-3" />
<Button
variant="link"

View File

@ -7,16 +7,16 @@ import classNames from 'classnames';
import { unionBy } from 'lodash';
import { marked } from 'marked';
import * as Types from '@answer/common/interface';
import {
useQueryComments,
addComment,
deleteComment,
updateComment,
postVote,
} from '@answer/services/api';
} from '@answer/api';
import { Modal } from '@answer/components';
import { usePageUsers, useReportModal } from '@answer/hooks';
import * as Types from '@answer/services/types';
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
import { Form, ActionBar, Reply } from './components';

View File

@ -90,7 +90,7 @@ const Editor = ({
return;
}
if (editor.getValue() !== value) {
// editor.setValue(value);
editor.setValue(value);
}
}, [editor, value]);

View File

@ -3,7 +3,7 @@ import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Modal as AnswerModal } from '@answer/components';
import { uploadImage } from '@answer/services/api';
import { uploadImage } from '@answer/api';
import ToolItem from '../toolItem';
import { IEditorContext } from '../types';

View File

@ -14,7 +14,7 @@ const Link: FC<IEditorContext> = ({ editor }) => {
};
const [visible, setVisible] = useState(false);
const [link, setLink] = useState({
value: 'http://',
value: 'https://',
isInvalid: false,
errorMsg: '',
});

View File

@ -5,8 +5,7 @@ import { NavLink } from 'react-router-dom';
import { TagSelector, Tag } from '@answer/components';
import { isLogin } from '@answer/utils';
import { useFollowingTags, followTags } from '@/services/tag.api';
import { useFollowingTags, followTags } from '@answer/api';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });

View File

@ -10,6 +10,10 @@
font-weight: bold;
color: #fff;
}
&.icon-link {
width: 46px;
height: 38px;
}
}
.placeholder-search::placeholder {
color: rgba(255, 255, 255, 0.75);

View File

@ -14,8 +14,7 @@ import { useSearchParams, NavLink, Link, useNavigate } from 'react-router-dom';
import { Avatar, Icon } from '@answer/components';
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
import { logout } from '@answer/services/api';
import { useQueryNotificationRedDot } from '@answer/services/notification.api';
import { logout, useQueryNotificationRedDot } from '@answer/api';
import Storage from '@answer/utils/storage';
import './index.scss';
@ -108,8 +107,8 @@ const Header: FC = () => {
<Nav.Link
as={NavLink}
to="/users/notifications/inbox"
className="me-2 position-relative">
<div className="px-2 text-white text-opacity-75">
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
<div className="text-white text-opacity-75">
<Icon name="bell-fill" className="fs-5" />
</div>
{(redDot?.inbox || 0) > 0 && (
@ -120,8 +119,8 @@ const Header: FC = () => {
<Nav.Link
as={Link}
to="/users/notifications/achievement"
className="me-2 position-relative">
<div className="px-2 text-white text-opacity-75">
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
<div className="text-white text-opacity-75">
<Icon name="trophy-fill" className="fs-5" />
</div>
{(redDot?.achievement || 0) > 0 && (
@ -135,7 +134,7 @@ const Header: FC = () => {
id="dropdown-basic"
as="a"
className="no-toggle pointer">
<Avatar size="38px" avatar={user?.avatar} />
<Avatar size="36px" avatar={user?.avatar} />
</Dropdown.Toggle>
<Dropdown.Menu>

View File

@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useHotQuestions } from '@/services/question.api';
import { Icon } from '@/components';
import { useHotQuestions } from '@answer/api';
import { Icon } from '@answer/components';
const HotQuestions: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, FC } from 'react';
import { Dropdown } from 'react-bootstrap';
import * as Types from '@answer/services/types';
import * as Types from '@answer/common/interface';
interface IProps {
children: React.ReactNode;

View File

@ -3,8 +3,11 @@ import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Icon } from '@answer/components';
import type { FormValue, FormDataType } from '@answer/common/interface';
import type { ImgCodeRes } from '@answer/services/types';
import type {
FormValue,
FormDataType,
ImgCodeRes,
} from '@answer/common/interface';
interface IProps {
/** control visible */

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Modal } from '@answer/components';
import { useReportModal, useToast } from '@answer/hooks';
import { questionDelete, answerDelete } from '@answer/services/api';
import { questionDelete, answerDelete } from '@answer/api';
import { isLogin } from '@answer/utils';
import Share from '../Share';

View File

@ -1,12 +1,13 @@
import { FC, memo } from 'react';
import { Pagination } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
interface Props {
currentPage: number;
pageSize: number;
totalSize: number;
pathname?: string;
}
interface PageItemProps {
@ -58,9 +59,13 @@ const Index: FC<Props> = ({
currentPage = 1,
pageSize = 15,
totalSize = 0,
pathname = '',
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'pagination' });
const location = useLocation();
if (!pathname) {
pathname = location.pathname;
}
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const totalPage = Math.ceil(totalSize / pageSize);
@ -73,10 +78,9 @@ const Index: FC<Props> = ({
}
const handleParams = (pageNum): string => {
const basePath = window.location.pathname;
searchParams.set('page', String(pageNum));
const searchStr = searchParams.toString();
return `${basePath}?${searchStr}`;
return `${pathname}?${searchStr}`;
};
return (
<Pagination size="sm" className="d-inline-flex mb-0">

View File

@ -3,8 +3,8 @@ import { Row, Col, ButtonGroup, Button, ListGroup } from 'react-bootstrap';
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQuestionList } from '@answer/services/question.api';
import type * as Type from '@answer/services/types';
import { useQuestionList } from '@answer/api';
import type * as Type from '@answer/common/interface';
import { Icon, Tag, Pagination, FormatTime, Empty } from '@answer/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
@ -187,6 +187,7 @@ const QuestionList: FC<Props> = ({ source }) => {
currentPage={curPage}
totalSize={count}
pageSize={pageSize}
pathname="/questions"
/>
</div>
</div>

View File

@ -5,10 +5,9 @@ import { useTranslation } from 'react-i18next';
import { marked } from 'marked';
import classNames from 'classnames';
import { Icon } from '@answer/components';
import { useTagModal } from '@answer/hooks';
import { queryTags } from '@answer/services/api';
import type * as Type from '@answer/services/types';
import { queryTags } from '@answer/api';
import type * as Type from '@answer/common/interface';
import './index.scss';
@ -39,6 +38,8 @@ const TagSelector: FC<IProps> = ({
const [tag, setTag] = useState<string>('');
const [tags, setTags] = useState<Type.Tag[] | null>(null);
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
const [visibleMenu, setVisibleMenu] = useState(false);
const tagModal = useTagModal({
onConfirm: (data) => {
if (!(onChange instanceof Function)) {
@ -151,40 +152,76 @@ const TagSelector: FC<IProps> = ({
const handleSelect = (eventKey) => {
setCurrentIndex(eventKey);
};
const handleKeyDown = (e) => {
e.stopPropagation();
if (!tags) {
return;
}
const { keyCode } = e;
if (keyCode === 38 && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
if (keyCode === 40 && currentIndex < tags.length - 1) {
setCurrentIndex(currentIndex + 1);
}
if (
keyCode === 13 &&
currentIndex > -1 &&
currentIndex <= tags.length - 1
) {
e.preventDefault();
handleClick(tags[currentIndex]);
}
};
return (
<div className="tag-selector-wrap" onFocus={onFocus} onBlur={onBlur}>
<div
className="tag-selector-wrap"
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={handleKeyDown}>
<div className="d-flex flex-wrap">
{initialValue?.map((item, index) => {
return (
<Button
key={item.slug_name}
className={classNames(
'me-2 mb-2 text-nowrap',
'me-2 mb-2 text-nowrap d-flex align-items-center',
index === repeatIndex && 'warning',
)}
variant="outline-secondary"
size="sm">
{item.slug_name}
<Icon name="x" onClick={() => handleRemove(item)} />
<span className="ms-1" onMouseUp={() => handleRemove(item)}>
x
</span>
</Button>
);
})}
{initialValue?.length < 5 || alwaysShowAddBtn ? (
<Dropdown onSelect={handleSelect}>
<Dropdown onSelect={handleSelect} onToggle={setVisibleMenu}>
<Dropdown.Toggle variant="outline-secondary" size="sm">
<Icon name="plus" />
<span className="me-1">+</span>
{t('add_btn')}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Header>
<FormControl
placeholder="Search tag"
autoFocus
value={tag}
onChange={handleSearch}
/>
</Dropdown.Header>
{visibleMenu && (
<Dropdown.Header>
<Form
onSubmit={(e) => {
e.preventDefault();
}}>
<FormControl
placeholder={t('search_tag')}
autoFocus
value={tag}
onChange={handleSearch}
/>
</Form>
</Dropdown.Header>
)}
{tags?.map((item, index) => {
return (
<Dropdown.Item

View File

@ -2,10 +2,13 @@ import React, { useState, useEffect } from 'react';
import { Button, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { emailReSend, checkImgCode } from '@answer/services/api';
import { emailReSend, checkImgCode } from '@answer/api';
import { PicAuthCodeModal } from '@answer/components/Modal';
import type { ImgCodeRes, ImgCodeReq } from '@answer/services/types';
import type { FormDataType } from '@answer/common/interface';
import type {
ImgCodeRes,
ImgCodeReq,
FormDataType,
} from '@answer/common/interface';
import { userInfoStore } from '@answer/stores';
interface IProps {

View File

@ -35,7 +35,7 @@ const Index: React.FC<IProps> = ({ type, upload }) => {
};
return (
<label className="mb-2 btn btn-outline-secondary btn-sm uploadBtn fs-14">
<label className="mb-2 btn btn-outline-secondary uploadBtn">
{status ? t('upload_img.loading') : t('upload_img.name')}
<input
type="file"

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { changeUserStatus } from '@answer/services/question-admin.api';
import { changeUserStatus } from '@answer/api';
import { Modal as AnswerModal } from '@answer/components';
const div = document.createElement('div');

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { uniqBy } from 'lodash';
import * as Types from '@answer/services/types';
import * as Types from '@answer/common/interface';
let globalUsers: Types.PageUser[] = [];
const usePageUsers = () => {

View File

@ -4,10 +4,9 @@ import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { reportList, postReport, closeQuestion } from '@answer/services/api';
import { putReport } from '@answer/services/flag-admin.api';
import { reportList, postReport, closeQuestion, putReport } from '@answer/api';
import { useToast } from '@answer/hooks';
import type * as Type from '@answer/services/types';
import type * as Type from '@answer/common/interface';
interface Params {
isBackend?: boolean;

View File

@ -28,6 +28,11 @@ const useToast = () => {
});
const onClose = () => {
const parent = document.querySelector('.page-wrap');
if (parent?.contains(div)) {
parent.removeChild(div);
}
setShow(false);
};
@ -66,6 +71,7 @@ const useToast = () => {
</div>,
);
}, [show, data]);
return {
onShow,
};

View File

@ -20,7 +20,7 @@ i18next
translation: zh,
},
},
debug: process.env.NODE_ENV === 'development',
// debug: process.env.NODE_ENV === 'development',
fallbackLng: process.env.REACT_APP_LANG || 'en_US',
interpolation: {
escapeValue: false,

View File

@ -10,7 +10,7 @@
"tags": "Tags",
"tag_wiki": "tag wiki",
"edit_tag": "Edit Tag",
"ask_a_question": "Ask a Question",
"ask_a_question": "Add Question",
"edit_question": "Edit Question",
"edit_answer": "Edit Answer",
"search": "Search",
@ -34,8 +34,8 @@
},
"suspended": {
"title": "Your Account has been Suspended",
"until_time": "Your account was suspended until {{ time }}",
"forever": "This user was suspended forever. ",
"until_time": "Your account was suspended until {{ time }}.",
"forever": "This user was suspended forever.",
"end": "You dont meet a community guideline."
},
"editor": {
@ -43,7 +43,7 @@
"text": "Blockquote"
},
"bold": {
"text": "Bold"
"text": "Strong"
},
"chart": {
"text": "Chart",
@ -57,18 +57,18 @@
"pie_chart": "Pie chart"
},
"code": {
"text": "Code",
"add_code": "Add code",
"text": "Code Sample",
"add_code": "Add code sample",
"form": {
"fields": {
"code": {
"label": "Code text",
"label": "Code",
"msg": {
"empty": "Code text cannot be empty"
"empty": "Code cannot be empty."
}
},
"language": {
"label": "Code language",
"label": "Language",
"placeholder": "Automatic detection"
}
}
@ -95,7 +95,7 @@
"text": "Help"
},
"hr": {
"text": "Line"
"text": "Horizontal Rule"
},
"image": {
"text": "Image",
@ -107,13 +107,13 @@
"label": "Image file",
"btn": "Select image",
"msg": {
"empty": "Image file cannot be empty",
"only_image": "Only image files are allowed",
"max_size": "Image file size cannot exceed 4MB"
"empty": "File cannot be empty.",
"only_image": "Only image files are allowed.",
"max_size": "File size cannot exceed 4MB."
}
},
"description": {
"label": "Image description(Optional)"
"label": "Description (optional)"
}
}
},
@ -121,19 +121,19 @@
"form2": {
"fields": {
"url": {
"label": "Image url",
"label": "Image URL",
"msg": {
"empty": "Image url cannot be empty"
"empty": "Image URL cannot be empty."
}
},
"name": {
"label": "Url name(Optional)"
"label": "Description (optional)"
}
}
},
"btn_cancel": "Cancel",
"btn_confirm": "Confirm",
"uploading": "uploading"
"uploading": "Uploading"
},
"indent": {
"text": "Indent"
@ -142,21 +142,21 @@
"text": "Outdent"
},
"italic": {
"text": "Italic"
"text": "Emphasis"
},
"link": {
"text": "Link",
"add_link": "Add link",
"text": "Hyperlink",
"add_link": "Add hyperlink",
"form": {
"fields": {
"url": {
"label": "Link url",
"label": "URL",
"msg": {
"empty": "Link url cannot be empty"
"empty": "URL cannot be empty."
}
},
"name": {
"label": "Link name(Optional)"
"label": "Description (optional)"
}
}
},
@ -164,10 +164,10 @@
"btn_confirm": "Confirm"
},
"ordered_list": {
"text": "Ordered list"
"text": "Numbered List"
},
"unordered_list": {
"text": "Unordered list"
"text": "Bulleted List"
},
"table": {
"text": "Table",
@ -180,10 +180,10 @@
"btn_cancel": "Cancel",
"btn_submit": "Submit",
"remark": {
"empty": "cannot be empty"
"empty": "Cannot be empty."
},
"msg": {
"empty": "Please select a reason"
"empty": "Please select a reason."
}
},
"report_modal": {
@ -195,10 +195,10 @@
"btn_cancel": "Cancel",
"btn_submit": "Submit",
"remark": {
"empty": "cannot be empty"
"empty": "Cannot be empty."
},
"msg": {
"empty": "Please select a reason"
"empty": "Please select a reason."
}
},
"tag_modal": {
@ -206,13 +206,13 @@
"form": {
"fields": {
"display_name": {
"label": "Display Name",
"label": "Display name",
"msg": {
"empty": "Display name cannot be empty"
"empty": "Display name cannot be empty."
}
},
"slug_name": {
"label": "Url slug",
"label": "URL slug",
"description": "Spaces are not allowed, please use '-' instead.",
"msg": {
"empty": "Please enter a name for the tag."
@ -257,8 +257,8 @@
"label": "Display name"
},
"slug_name": {
"label": "Url slug",
"info": "Spaces are not allowed, please use \"-\" instead."
"label": "URL slug",
"info": "Spaces are not allowed, please use '-' instead."
},
"description": {
"label": "Description"
@ -293,7 +293,7 @@
"btn_delete": "Delete",
"btn_flag": "Flag",
"btn_save_edits": "Save edits",
"btn_cancel": "cancel",
"btn_cancel": "Cancel",
"show_more": "Show more comment",
"tip_question": "Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.",
"tip_answer": "Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting."
@ -348,7 +348,7 @@
"more": "More"
},
"ask": {
"title": "Ask a question",
"title": "Add question",
"edit_title": "Edit Question",
"default_reason": "Edit question",
"similar_questions": "Similar questions",
@ -361,25 +361,25 @@
"label": "Title",
"placeholder": "Be specific and imagine youre asking a question to another person",
"msg": {
"empty": "Title cannot be empty"
"empty": "Title cannot be empty."
}
},
"body": {
"label": "Body",
"msg": {
"empty": "Body cannot be empty"
"empty": "Body cannot be empty."
}
},
"tags": {
"label": "Tags",
"msg": {
"empty": "Tags cannot be empty"
"empty": "Tags cannot be empty."
}
},
"answer": {
"label": "Answer",
"msg": {
"empty": "Answer cannot be empty"
"empty": "Answer cannot be empty."
}
}
}
@ -408,6 +408,7 @@
"tag_selector": {
"add_btn": "Add tag",
"create_btn": "Create new tag",
"search_tag": "Search tag",
"hint": "Describe what your question is about, at least one tag is required.",
"no_result": "No tags matched"
},
@ -433,7 +434,7 @@
"title": "Captcha",
"placeholder": "Type the text above",
"msg": {
"empty": "cannot be empty"
"empty": "Cannot be empty."
}
},
"inactive": {
@ -442,7 +443,7 @@
"another": "We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.",
"btn_name": "Resend activation email",
"msg": {
"empty": "cannot be empty"
"empty": "Cannot be empty."
}
},
"login": {
@ -453,19 +454,19 @@
"name": {
"label": "Name",
"msg": {
"empty": "Name cannot be empty"
"empty": "Name cannot be empty."
}
},
"email": {
"label": "Email",
"msg": {
"empty": "Email cannot be empty"
"empty": "Email cannot be empty."
}
},
"password": {
"label": "Password",
"msg": {
"empty": "Password cannot be empty",
"empty": "Password cannot be empty.",
"different": "The passwords entered on both sides are inconsistent"
}
}
@ -477,7 +478,7 @@
"email": {
"label": "Email",
"msg": {
"empty": "Email cannot be empty"
"empty": "Email cannot be empty."
}
}
},
@ -490,7 +491,7 @@
"password": {
"label": "Password",
"msg": {
"empty": "Password cannot be empty",
"empty": "Password cannot be empty.",
"length": "The length needs to be between 8 and 32",
"different": "The passwords entered on both sides are inconsistent"
}
@ -511,7 +512,7 @@
"btn_name": "Update profile",
"display_name": {
"label": "Display name",
"msg": "name cannot be empty"
"msg": "Display name cannot be empty."
},
"avatar": {
"label": "Profile image",
@ -523,7 +524,7 @@
"website": {
"label": "Website (optional)",
"placeholder": "https://example.com",
"msg": "website incorrect format"
"msg": "Website incorrect format"
},
"location": {
"label": "Location (optional)",
@ -542,15 +543,15 @@
"change_email_info": "We've sent an email to that address. Please follow the confirmation instructions.",
"email": {
"label": "Email",
"msg": "email cannot be empty"
"msg": "Email cannot be empty."
},
"password_title": "Password",
"current_pass": {
"label": "Current Password",
"msg": {
"empty": "cannot be empty",
"length": "The length needs to be between 8 and 32",
"different": "The two entered passwords do not match"
"empty": "Current Password cannot be empty.",
"length": "The length needs to be between 8 and 32.",
"different": "The two entered passwords do not match."
}
},
"new_pass": {
@ -595,7 +596,7 @@
"title": "Your Answer",
"btn_name": "Post your answer",
"confirm_info": "you have answered, confirm to continue answer",
"empty": "cannot be empty"
"empty": "Answer cannot be empty."
}
},
"delete": {
@ -718,6 +719,14 @@
"x_answers": "answers",
"x_questions": "questions"
},
"page_404": {
"description": "Unfortunately, this page doesn't exist.",
"back_home": "Back to homepage"
},
"page_50X": {
"description": "The server encountered an error and could not complete your request.",
"back_home": "Back to homepage"
},
"admin": {
"admin_header": {
"title": "Admin"
@ -822,17 +831,17 @@
"page_title": "General",
"name": {
"label": "Site name",
"msg": "Site name cannot be empty",
"msg": "Site name cannot be empty.",
"text": "The name of this site, as used in the title tag."
},
"short_description": {
"label": "Short site description",
"msg": "Short site description cannot be empty",
"label": "Short site description (optional)",
"msg": "Short site description cannot be empty.",
"text": "Short description, as used in the title tag on homepage."
},
"description": {
"label": "Site description",
"msg": "Site description cannot be empty",
"label": "Site description (optional)",
"msg": "Site description cannot be empty.",
"text": "Describe this site in one sentence, as used in the meta description tag."
}
},
@ -840,19 +849,19 @@
"page_title": "Interface",
"logo": {
"label": "Logo",
"msg": "Site name cannot be empty",
"msg": "Site logo cannot be empty.",
"text": "You can upload your image or <1>reset</1> it to the site title text."
},
"theme": {
"label": "Theme",
"msg": "Theme cannot be empty",
"msg": "Theme cannot be empty.",
"text": "Select an existing theme."
},
"language": {
"label": "Interface language",
"msg": "Interface language cannot be empty",
"msg": "Interface language cannot be empty.",
"text": "User interface language. It will change when you refresh the page."
}
}
}
}
}

View File

@ -1,4 +1,4 @@
@import './variable.scss';
@import 'common/variable';
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-icons';
@ -60,6 +60,10 @@ a {
height: 24px;
}
.badge-tag:hover {
background: #9EC5FE;
}
.divide-line {
border-bottom: 1px solid rgba(33, 37, 41, 0.25);
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,3 +1,24 @@
const Index = () => <div>404 page</div>;
import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_404' });
return (
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
<div
className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=x=)
</div>
<div className="text-center mb-4">{t('description')}</div>
<div className="text-center">
<Button as={Link} to="/" variant="link">
{t('back_home')}
</Button>
</div>
</Container>
);
};
export default Index;

View File

@ -1,3 +1,24 @@
const Index = () => <div>50X page</div>;
import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
return (
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
<div
className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=T^T=)
</div>
<div className="text-center mb-3">{t('description')}</div>
<div className="text-center">
<Button as={Link} to="/" variant="link">
{t('back_home')}
</Button>
</div>
</Container>
);
};
export default Index;

View File

@ -18,14 +18,10 @@ import {
BaseUserCard,
Empty,
} from '@answer/components';
import { ADMIN_LIST_STATUS } from '@answer/common';
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
import { useEditStatusModal } from '@answer/hooks';
import * as Type from '@/services/types';
import {
useAnswerSearch,
changeAnswerStatus,
} from '@/services/answer-admin.api';
import { useAnswerSearch, changeAnswerStatus } from '@answer/api';
import * as Type from '@answer/common/interface';
import '../index.scss';

View File

@ -10,8 +10,8 @@ import {
Pagination,
} from '@answer/components';
import { useReportModal } from '@answer/hooks';
import * as Type from '@answer/services/types';
import { useFlagSearch } from '@answer/services/flag-admin.api';
import * as Type from '@answer/common/interface';
import { useFlagSearch } from '@answer/api';
import '../index.scss';
@ -97,7 +97,9 @@ const Flags: FC = () => {
<tr>
<th>{t('flagged')}</th>
<th style={{ width: '20%' }}>{t('created')}</th>
<th style={{ width: '20%' }}>{t('action')}</th>
{curFilter !== 'completed' ? (
<th style={{ width: '20%' }}>{t('action')}</th>
) : null}
</tr>
</thead>
<tbody className="align-middle">
@ -140,11 +142,13 @@ const Flags: FC = () => {
)}
</Stack>
</td>
<td>
<Button variant="link" onClick={() => handleReview(li)}>
{t('review')}
</Button>
</td>
{curFilter !== 'completed' ? (
<td>
<Button variant="link" onClick={() => handleReview(li)}>
{t('review')}
</Button>
</td>
) : null}
</tr>
);
})}

View File

@ -2,15 +2,10 @@ import React, { FC, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type * as Type from '@answer/services/types';
import type * as Type from '@answer/common/interface';
import { useToast } from '@answer/hooks';
import { siteInfoStore } from '@answer/stores';
import { FormDataType } from '@/common/interface';
import {
useGeneralSetting,
updateGeneralSetting,
} from '@/services/settings-admin.api';
import { useGeneralSetting, updateGeneralSetting } from '@answer/api';
import '../index.scss';
@ -22,7 +17,7 @@ const General: FC = () => {
const updateSiteInfo = siteInfoStore((state) => state.update);
const { data: setting } = useGeneralSetting();
const [formData, setFormData] = useState<FormDataType>({
const [formData, setFormData] = useState<Type.FormDataType>({
name: {
value: '',
isInvalid: false,
@ -41,7 +36,7 @@ const General: FC = () => {
});
const checkValidated = (): boolean => {
let ret = true;
const { name, short_description, description } = formData;
const { name } = formData;
if (!name.value) {
ret = false;
formData.name = {
@ -50,22 +45,6 @@ const General: FC = () => {
errorMsg: t('name.msg'),
};
}
if (!short_description.value) {
ret = false;
formData.short_description = {
value: '',
isInvalid: true,
errorMsg: t('short_description.msg'),
};
}
if (!description.value) {
ret = false;
formData.description = {
value: '',
isInvalid: true,
errorMsg: t('description.msg'),
};
}
setFormData({
...formData,
});
@ -104,7 +83,7 @@ const General: FC = () => {
if (!formData[fieldName]) {
return;
}
const fieldData: FormDataType = {
const fieldData: Type.FormDataType = {
[fieldName]: {
value: fieldValue,
isInvalid: false,

View File

@ -3,15 +3,18 @@ import { Form, Button, Image, Stack } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { useToast } from '@answer/hooks';
import * as Type from '@answer/services/types';
import { LangsType } from '@answer/services/types';
import { FormDataType } from '@answer/common/interface';
import { languages, uploadAvatar } from '@answer/services/api';
import {
LangsType,
FormDataType,
AdminSettingsInterface,
} from '@answer/common/interface';
import {
languages,
uploadAvatar,
updateInterfaceSetting,
useInterfaceSetting,
useThemeOptions,
} from '@answer/services/settings-admin.api';
} from '@answer/api';
import { interfaceStore } from '@answer/stores';
import { UploadImg } from '@answer/components';
@ -72,16 +75,8 @@ const Interface: FC = () => {
const checkValidated = (): boolean => {
let ret = true;
const { logo, theme, language } = formData;
const { theme, language } = formData;
const formCheckData = { ...formData };
if (!logo.value) {
ret = false;
formCheckData.logo = {
value: '',
isInvalid: true,
errorMsg: t('logo.msg'),
};
}
if (!theme.value) {
ret = false;
formCheckData.theme = {
@ -109,7 +104,7 @@ const Interface: FC = () => {
if (checkValidated() === false) {
return;
}
const reqParams: Type.AdminSettingsInterface = {
const reqParams: AdminSettingsInterface = {
logo: formData.logo.value,
theme: formData.theme.value,
language: formData.language.value,

View File

@ -18,15 +18,14 @@ import {
BaseUserCard,
Empty,
} from '@answer/components';
import { ADMIN_LIST_STATUS } from '@answer/common';
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
import { useEditStatusModal, useReportModal } from '@answer/hooks';
import { questionDelete } from '@answer/services/api';
import * as Type from '@/services/types';
import {
useQuestionSearch,
changeQuestionStatus,
} from '@/services/question-admin.api';
questionDelete,
} from '@answer/api';
import * as Type from '@answer/common/interface';
import '../index.scss';

View File

@ -10,8 +10,8 @@ import {
} from 'react-bootstrap';
import { AccordionNav } from '@answer/components';
import { ADMIN_NAV_MENUS } from '@answer/common/constants';
import { ADMIN_NAV_MENUS } from '@/pages/Admin/admin.constants';
import '../index.scss';
const UserOverview: FC = () => {

View File

@ -3,14 +3,14 @@ import { ButtonGroup, Button, Form, Table, Badge } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQueryUsers } from '@answer/services/question-admin.api';
import { useQueryUsers } from '@answer/api';
import {
Pagination,
FormatTime,
BaseUserCard,
Empty,
} from '@answer/components';
import * as Type from '@answer/services/types';
import * as Type from '@answer/common/interface';
import { useChangeModal } from '@answer/hooks';
import '../index.scss';

View File

@ -1,21 +0,0 @@
export const ADMIN_NAV_MENUS = [
{
name: 'dashboard',
children: [],
},
{
name: 'contents',
child: [{ name: 'questions' }, { name: 'answers' }],
},
{
name: 'users',
},
{
name: 'flags',
// badgeContent: 5,
},
{
name: 'settings',
child: [{ name: 'general' }, { name: 'interface' }],
},
];

View File

@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom';
import { AccordionNav, PageTitle } from '@answer/components';
import { ADMIN_NAV_MENUS } from '@/pages/Admin/admin.constants';
import { ADMIN_NAV_MENUS } from '@answer/common/constants';
import './index.scss';

View File

@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { SWRConfig } from 'swr';
import {
userInfoStore,
siteInfoStore,
@ -10,8 +12,8 @@ import {
toastStore,
} from '@answer/stores';
import { Header, AdminHeader, Footer, Toast } from '@answer/components';
import { useSiteSettings, useCheckUserStatus } from '@answer/api';
import { useSiteSettings } from '@/services/api';
import Storage from '@/utils/storage';
let isMounted = false;
@ -19,6 +21,7 @@ const Layout: FC = () => {
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
const { update: interfaceStoreUpdate } = interfaceStore();
const { data: siteSettings } = useSiteSettings();
const { data: userStatus } = useCheckUserStatus();
useEffect(() => {
if (siteSettings) {
siteStoreUpdate(siteSettings.general);
@ -34,8 +37,8 @@ const Layout: FC = () => {
};
if (!isMounted) {
isMounted = true;
const user = Storage.get('userInfo');
const lang = Storage.get('LANG');
const user = Storage.get('userInfo');
if (user) {
updateUser(user);
}
@ -44,6 +47,14 @@ const Layout: FC = () => {
}
}
if (userStatus?.status) {
const user = Storage.get('userInfo');
if (userStatus.status !== user.status) {
user.status = userStatus?.status;
updateUser(user);
}
}
return (
<>
<Helmet>
@ -51,13 +62,18 @@ const Layout: FC = () => {
<meta name="description" content={siteInfo.description} />
) : null}
</Helmet>
<Header />
<AdminHeader />
<div className="position-relative page-wrap">
<Outlet />
</div>
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
<Footer />
<SWRConfig
value={{
revalidateOnFocus: false,
}}>
<Header />
<AdminHeader />
<div className="position-relative page-wrap">
<Outlet />
</div>
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
<Footer />
</SWRConfig>
</>
);
};

View File

@ -14,8 +14,8 @@ import {
useQueryRevisions,
postAnswer,
useQueryQuestionByTitle,
} from '@answer/services/api';
import type * as Type from '@answer/services/types';
} from '@answer/api';
import type * as Type from '@answer/common/interface';
import SearchQuestion from './components/SearchQuestion';

View File

@ -1,21 +1,12 @@
import { memo, FC, useState } from 'react';
import { memo, FC } from 'react';
import { Alert } from 'react-bootstrap';
interface Props {
data;
}
const Index: FC<Props> = ({ data }) => {
const [show, setShow] = useState(Boolean(data.operation_description));
return (
<Alert
className="mb-4"
variant="info"
show={show}
dismissible
onClose={() => {
setShow(false);
}}>
<Alert className="mb-4" variant="info">
<div>
<strong>{data.operation_msg} </strong>
{data.operation_description}

View File

@ -10,13 +10,12 @@ import {
Comment,
FormatTime,
} from '@answer/components';
import { adoptAnswer } from '@answer/services/api';
import { acceptanceAnswer } from '@answer/api';
import { scrollTop } from '@answer/utils';
import { AnswerContent } from '@/services/types';
import { AnswerItem } from '@answer/common/interface';
interface Props {
data: AnswerContent;
data: AnswerItem;
/** router answer id */
aid?: string;
/** is author */
@ -36,9 +35,9 @@ const Index: FC<Props> = ({
});
const answerRef = useRef<HTMLDivElement>(null);
const acceptAnswer = () => {
adoptAnswer({
question_id: data?.adopted === 2 ? '0' : data?.question_id,
answer_id: data?.id,
acceptanceAnswer({
question_id: data.question_id,
answer_id: data.adopted === 2 ? '0' : data.id,
}).then(() => {
callback?.('');
});
@ -78,8 +77,8 @@ const Index: FC<Props> = ({
{data?.adopted === 2 && (
<Button
disabled={!isAuthor}
variant="success"
className="ms-3"
variant="outline-success"
className="ms-3 active opacity-100 bg-success text-white"
onClick={acceptAnswer}>
<Icon name="check-circle-fill" className="me-2" />
<span>{t('answers.btn_accepted')}</span>

View File

@ -12,7 +12,7 @@ import {
FormatTime,
} from '@answer/components';
import { formatCount } from '@answer/utils';
import { following } from '@answer/services/api';
import { following } from '@answer/api';
interface Props {
data: any;

View File

@ -3,7 +3,7 @@ import { Card, ListGroup } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useSimilarQuestion } from '@answer/services/question.api';
import { useSimilarQuestion } from '@answer/api';
import { Icon } from '@answer/components';
import { userInfoStore } from '@/stores';

View File

@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
import { marked } from 'marked';
import { Editor, Modal } from '@answer/components';
import { postAnswer } from '@answer/api';
import { FormDataType } from '@answer/common/interface';
import { postAnswer } from '@answer/services/api';
interface Props {
visible?: boolean;

View File

@ -2,16 +2,16 @@ import { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import { useParams, useSearchParams } from 'react-router-dom';
import { questionDetail, getAnswers } from '@answer/services/api';
import { questionDetail, getAnswers } from '@answer/api';
import { Pagination, PageTitle } from '@answer/components';
import type {
AnswerRes,
QuDetailRes,
AnswerContent,
} from '@answer/services/types';
import { userInfoStore } from '@answer/stores';
import { scrollTop } from '@answer/utils';
import { usePageUsers } from '@answer/hooks';
import type {
ListResult,
QuDetailRes,
AnswerItem,
} from '@answer/common/interface';
import {
Question,
@ -31,7 +31,7 @@ const Index = () => {
const page = Number(urlSearch.get('page') || 0);
const order = urlSearch.get('order') || '';
const [question, setQuestion] = useState<QuDetailRes | null>(null);
const [answers, setAnswers] = useState<AnswerRes>({
const [answers, setAnswers] = useState<ListResult<AnswerItem>>({
count: -1,
list: [],
});
@ -82,7 +82,7 @@ const Index = () => {
requestAnswers();
};
const writeAnswerCallback = (obj: AnswerContent) => {
const writeAnswerCallback = (obj: AnswerItem) => {
setAnswers({
count: answers.count + 1,
list: [...answers.list, obj],

View File

@ -11,8 +11,8 @@ import {
useQueryAnswerInfo,
modifyAnswer,
useQueryRevisions,
} from '@answer/services/api';
import type * as Type from '@answer/services/types';
} from '@answer/api';
import type * as Type from '@answer/common/interface';
import './index.scss';

View File

@ -3,7 +3,7 @@ import { useSearchParams, Link } from 'react-router-dom';
import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { following } from '@answer/services/api';
import { following } from '@answer/api';
import { isLogin } from '@answer/utils';
interface Props {

View File

@ -2,7 +2,7 @@ import { memo, FC } from 'react';
import { ListGroupItem, Badge } from 'react-bootstrap';
import { Icon, Tag, FormatTime } from '@answer/components';
import type { SearchResItem } from '@answer/services/types';
import type { SearchResItem } from '@answer/common/interface';
import { formatCount } from '@answer/utils';
interface Props {

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { Pagination, PageTitle } from '@answer/components';
import { useSearch } from '@answer/services/search.api';
import { useSearch } from '@answer/api';
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';

View File

@ -1,100 +0,0 @@
import React, { useEffect, useState, FormEvent } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import en from 'dayjs/locale/en';
import zh from 'dayjs/locale/zh-cn';
import { languages } from '@answer/services/api';
import type { FormDataType } from '@answer/common/interface';
import type { LangsType } from '@answer/services/types';
import { useToast } from '@answer/hooks';
import Storage from '@answer/utils/storage';
const Index = () => {
const { t, i18n } = useTranslation('translation', {
keyPrefix: 'settings.interface',
});
const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>();
const [formData, setFormData] = useState<FormDataType>({
lang: {
value: true,
isInvalid: false,
errorMsg: '',
},
});
const getLangs = async () => {
const res: LangsType[] = await languages();
setLangs(res);
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
Storage.set('LANG', formData.lang.value);
dayjs.locale(formData.lang.value === 'en_US' ? en : zh);
i18n.changeLanguage(formData.lang.value);
toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
};
useEffect(() => {
getLangs();
const lang = Storage.get('LANG');
if (lang) {
setFormData({
lang: {
value: lang,
isInvalid: false,
errorMsg: '',
},
});
}
}, []);
return (
<>
<h4 className="mb-3">{t('interface', { keyPrefix: 'settings.nav' })}</h4>
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="emailSend" className="mb-3">
<Form.Label>{t('lang.label')}</Form.Label>
<Form.Select
value={formData.lang.value}
isInvalid={formData.lang.isInvalid}
onChange={(e) => {
setFormData({
lang: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}>
{langs?.map((item) => {
return (
<option value={item.value} key={item.value}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('lang.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.lang.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
</>
);
};
export default React.memo(Index);

View File

@ -3,13 +3,12 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import * as Type from '@answer/services/types';
import * as Type from '@answer/common/interface';
import { PageTitle, FollowingTags } from '@answer/components';
import { useTagInfo, useFollow } from '@answer/api';
import QuestionList from '@/components/Questions';
import HotQuestions from '@/components/HotQuestions';
import { useTagInfo } from '@/services/tag.api';
import { useFollow } from '@/services/activity.api';
const Questions: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
@ -50,7 +49,7 @@ const Questions: FC = () => {
}, [tagResp, followResp]);
let pageTitle = '';
if (tagInfo) {
pageTitle = `${tagInfo.display_name} ${t('questions', {
pageTitle = `'${tagInfo.display_name}' ${t('questions', {
keyPrefix: 'page_title',
})}`;
}

View File

@ -7,8 +7,7 @@ import dayjs from 'dayjs';
import classNames from 'classnames';
import { Editor, EditorRef, PageTitle } from '@answer/components';
import { useQueryRevisions } from '@answer/services/api';
import { useTagInfo, modifyTag } from '@answer/services/tag.api';
import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api';
import { userInfoStore } from '@answer/stores';
interface FormDataItem {

View File

@ -17,7 +17,7 @@ import {
useQuerySynonymsTags,
saveSynonymsTags,
deleteTag,
} from '@answer/services/tag.api';
} from '@answer/api';
const TagIntroduction = () => {
const [isEdit, setEditState] = useState(false);
@ -84,7 +84,7 @@ const TagIntroduction = () => {
let pageTitle = '';
if (tagInfo) {
pageTitle = `${tagInfo.display_name} ${t('tag_wiki', {
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
keyPrefix: 'page_title',
})}`;
}

View File

@ -11,7 +11,7 @@ import {
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQueryTags, following } from '@answer/services/api';
import { useQueryTags, following } from '@answer/api';
import { Tag, Pagination, PageTitle } from '@answer/components';
import { formatCount } from '@answer/utils';

View File

@ -2,10 +2,14 @@ import { FC, memo, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { passRetrieve, checkImgCode } from '@answer/services/api';
import type { FormDataType } from '@answer/common/interface';
import type { ImgCodeRes, PssRetReq } from '@answer/services/types';
import { PicAuthCodeModal } from '@answer/components/Modal';
import { passRetrieve, checkImgCode } from '@answer/api';
import type {
ImgCodeRes,
PssRetReq,
FormDataType,
} from '@answer/common/interface';
import { PicAuthCodeModal } from '@/components/Modal';
interface IProps {
visible: boolean;

View File

@ -1,7 +1,7 @@
import { FC, memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { accountActivate } from '@answer/services/api';
import { accountActivate } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import { getQueryString } from '@answer/utils';

View File

@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { changeEmailVerify } from '@answer/services/api';
import { changeEmailVerify } from '@answer/api';
import { PageTitle } from '@/components';

View File

@ -3,14 +3,18 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import type { FormDataType } from '@answer/common/interface';
import { login, checkImgCode } from '@answer/services/api';
import type { LoginReqParams, ImgCodeRes } from '@answer/services/types';
import { login, checkImgCode } from '@answer/api';
import type {
LoginReqParams,
ImgCodeRes,
FormDataType,
} from '@answer/common/interface';
import { PageTitle, Unactivate } from '@answer/components';
import { PicAuthCodeModal } from '@answer/components/Modal';
import { userInfoStore } from '@answer/stores';
import { isLogin, getQueryString } from '@answer/utils';
import Storage from '@answer/utils/storage';
import { PicAuthCodeModal } from '@/components/Modal';
import Storage from '@/utils/storage';
const Index: React.FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'login' });

View File

@ -2,6 +2,9 @@ import { ListGroup } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { Empty } from '@answer/components';
import './index.scss';
@ -9,24 +12,33 @@ const Achievements = ({ data, handleReadNotification }) => {
if (!data) {
return null;
}
if (isEmpty(data)) {
return <Empty />;
}
return (
<ListGroup
className="border-top border-bottom achievement-wrap"
variant="flush">
{data.map((item) => {
const { comment, question, answer } =
item?.object_info?.object_map || {};
let url = '';
switch (item.object_info.object_type) {
case 'question':
url = `/questions/${item.object_info.object_id}`;
break;
case 'answer':
url = `/questions/${item.object_info?.object_map?.question}/${item.object_info.object_id}`;
url = `/questions/${question}/${item.object_info.object_id}`;
break;
case 'comment':
url = `/questions/${question}/${answer}?commentId=${comment}`;
break;
default:
url = '';
}
return (
<ListGroup.Item
key={item.id}
className={classNames('d-flex', !item.is_read && 'warning')}>
{item.rank > 0 && (
<div className="text-success num text-end">{`+${item.rank}`}</div>

View File

@ -2,17 +2,22 @@ import { ListGroup } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { FormatTime } from '@answer/components';
import { FormatTime, Empty } from '@answer/components';
const Inbox = ({ data, handleReadNotification }) => {
if (!data) {
return null;
}
if (isEmpty(data)) {
return <Empty />;
}
return (
<ListGroup className="border-top border-bottom" variant="flush">
{data.map((item) => {
const { comment, question, answer } = item.object_info.object_map;
const { comment, question, answer } =
item?.object_info?.object_map || {};
let url = '';
switch (item.object_info.object_type) {
case 'question':

View File

@ -8,13 +8,12 @@ import {
clearUnReadNotification,
clearNotificationRedDot,
readNotification,
} from '@answer/services/notification.api';
} from '@answer/api';
import { PageTitle } from '@answer/components';
import Inbox from './components/Inbox';
import Achievements from './components/Achievements';
import { PageTitle } from '@/components';
const PAGE_SIZE = 10;
const Notifications = () => {
@ -45,7 +44,10 @@ const Notifications = () => {
}, [data]);
const navigate = useNavigate();
const handleTypeChange = (val) => {
const handleTypeChange = (evt, val) => {
evt.preventDefault();
setPage(1);
setNotificationData([]);
navigate(`/users/notifications/${val}`);
};
@ -72,19 +74,24 @@ const Notifications = () => {
<div className="d-flex justify-content-between mb-3">
<ButtonGroup size="sm">
<Button
as="a"
href="/users/notifications/inbox"
variant="outline-secondary"
active={type === 'inbox'}
onClick={() => handleTypeChange('inbox')}>
onClick={(evt) => handleTypeChange(evt, 'inbox')}>
{t('inbox')}
</Button>
<Button
as="a"
href="/users/notifications/achievement"
variant="outline-secondary"
active={type === 'achievement'}
onClick={() => handleTypeChange('achievement')}>
onClick={(evt) => handleTypeChange(evt, 'achievement')}>
{t('achievement')}
</Button>
</ButtonGroup>
<Button
size="sm"
variant="outline-secondary"
onClick={handleUnreadNotification}>
{t('all_read')}

View File

@ -3,12 +3,12 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@answer/common/interface';
import { passRetrieveSet } from '@answer/services/api';
import { passRetrieveSet } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import Storage from '@answer/utils/storage';
import { getQueryString, isLogin } from '@answer/utils';
import type { FormDataType } from '@answer/common/interface';
import Storage from '@/utils/storage';
import { PageTitle } from '@/components';
const Index: React.FC = () => {

View File

@ -3,7 +3,7 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Avatar, Icon } from '@answer/components';
import type { UserInfoRes } from '@answer/services/types';
import type { UserInfoRes } from '@answer/common/interface';
interface Props {
data: UserInfoRes;

View File

@ -9,7 +9,7 @@ import {
usePersonalInfoByName,
usePersonalTop,
usePersonalListByTabName,
} from '@answer/services/personal.api';
} from '@answer/api';
import {
UserInfo,

View File

@ -3,9 +3,10 @@ import { Form, Button, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { register } from '@answer/services/api';
import { register } from '@answer/api';
import type { FormDataType } from '@answer/common/interface';
import userStore from '@answer/stores/userInfo';
import userStore from '@/stores/userInfo';
interface Props {
callback: () => void;

View File

@ -2,9 +2,8 @@ import React, { FC, FormEvent, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@answer/common/interface';
import type * as Type from '@answer/services/types';
import { getUserInfo, changeEmail } from '@answer/services/api';
import type * as Type from '@answer/common/interface';
import { getUserInfo, changeEmail } from '@answer/api';
import { useToast } from '@answer/hooks';
const reg = /(?<=.{2}).+(?=@)/gi;
@ -14,7 +13,7 @@ const Index: FC = () => {
keyPrefix: 'settings.account',
});
const [step, setStep] = useState(1);
const [formData, setFormData] = useState<FormDataType>({
const [formData, setFormData] = useState<Type.FormDataType>({
e_mail: {
value: '',
isInvalid: false,
@ -29,7 +28,7 @@ const Index: FC = () => {
});
}, []);
const handleChange = (params: FormDataType) => {
const handleChange = (params: Type.FormDataType) => {
setFormData({ ...formData, ...params });
};

View File

@ -2,9 +2,9 @@ import React, { FC, FormEvent, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@answer/common/interface';
import { modifyPassword } from '@answer/services/api';
import { modifyPassword } from '@answer/api';
import { useToast } from '@answer/hooks';
import type { FormDataType } from '@answer/common/interface';
const Index: FC = () => {
const { t } = useTranslation('translation', {

View File

@ -1,14 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ModifyEmail from './components/ModifyEmail';
import ModifyPassword from './components/ModifyPass';
const Index = () => {
const { t } = useTranslation();
return (
<>
<h4 className="mb-3">{t('settings.nav.account')}</h4>
<ModifyEmail />
<ModifyPassword />
</>

View File

@ -0,0 +1,97 @@
import React, { useEffect, useState, FormEvent } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import en from 'dayjs/locale/en';
import zh from 'dayjs/locale/zh-cn';
import { languages } from '@answer/api';
import type { LangsType, FormDataType } from '@answer/common/interface';
import { useToast } from '@answer/hooks';
import Storage from '@/utils/storage';
const Index = () => {
const { t, i18n } = useTranslation('translation', {
keyPrefix: 'settings.interface',
});
const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>();
const [formData, setFormData] = useState<FormDataType>({
lang: {
value: true,
isInvalid: false,
errorMsg: '',
},
});
const getLangs = async () => {
const res: LangsType[] = await languages();
setLangs(res);
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
Storage.set('LANG', formData.lang.value);
dayjs.locale(formData.lang.value === 'en_US' ? en : zh);
i18n.changeLanguage(formData.lang.value);
toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
};
useEffect(() => {
getLangs();
const lang = Storage.get('LANG');
if (lang) {
setFormData({
lang: {
value: lang,
isInvalid: false,
errorMsg: '',
},
});
}
}, []);
return (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="emailSend" className="mb-3">
<Form.Label>{t('lang.label')}</Form.Label>
<Form.Select
value={formData.lang.value}
isInvalid={formData.lang.isInvalid}
onChange={(e) => {
setFormData({
lang: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}>
{langs?.map((item) => {
return (
<option value={item.value} key={item.value}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('lang.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.lang.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
);
};
export default React.memo(Index);

Some files were not shown because too many files have changed in this diff Show More