Merge branch 'ui' into 'main'
Ui See merge request opensource/answer!14
This commit is contained in:
commit
5f47064b3c
|
@ -6,6 +6,7 @@
|
|||
.DS_Store
|
||||
._*
|
||||
/.idea
|
||||
/.fleet
|
||||
/.vscode/*.log
|
||||
/cmd/answer/*.sh
|
||||
/cmd/logs
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"eslint.workingDirectories": [
|
||||
"ui"
|
||||
]
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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' }],
|
||||
},
|
||||
];
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -90,7 +90,7 @@ const Editor = ({
|
|||
return;
|
||||
}
|
||||
if (editor.getValue() !== value) {
|
||||
// editor.setValue(value);
|
||||
editor.setValue(value);
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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: '',
|
||||
});
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 don’t 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 you’re 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' }],
|
||||
},
|
||||
];
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
@ -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}
|
|
@ -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>
|
|
@ -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;
|
|
@ -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';
|
|
@ -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;
|
|
@ -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],
|
|
@ -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';
|
||||
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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',
|
||||
})}`;
|
||||
}
|
|
@ -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 {
|
|
@ -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',
|
||||
})}`;
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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' });
|
|
@ -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>
|
|
@ -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':
|
|
@ -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')}
|
|
@ -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 = () => {
|
|
@ -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;
|
|
@ -9,7 +9,7 @@ import {
|
|||
usePersonalInfoByName,
|
||||
usePersonalTop,
|
||||
usePersonalListByTabName,
|
||||
} from '@answer/services/personal.api';
|
||||
} from '@answer/api';
|
||||
|
||||
import {
|
||||
UserInfo,
|
|
@ -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;
|
|
@ -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 });
|
||||
};
|
||||
|
|
@ -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', {
|
|
@ -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 />
|
||||
</>
|
|
@ -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
Loading…
Reference in New Issue