Merge branch 'ui' into 'main'

Ui

See merge request opensource/answer!19
This commit is contained in:
Li Shuailing 2022-09-29 08:42:20 +00:00
commit 8287915659
44 changed files with 206 additions and 362 deletions

View File

@ -26,6 +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',
changeOrigin: true,
secure: false,

View File

@ -46,7 +46,7 @@
"react": "^18.2.0",
"react-bootstrap": "^2.5.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-helmet-async": "^1.3.0",
"react-i18next": "^11.18.3",
"react-router-dom": "^6.4.0",
"swr": "^1.3.0",
@ -108,4 +108,4 @@
"pnpm": ">=7"
},
"license": "MIT"
}
}

View File

@ -67,7 +67,7 @@ specifiers:
react-app-rewired: ^2.2.1
react-bootstrap: ^2.5.0
react-dom: ^18.2.0
react-helmet: ^6.1.0
react-helmet-async: ^1.3.0
react-i18next: ^11.18.3
react-router-dom: ^6.4.0
react-scripts: 5.0.1
@ -102,7 +102,7 @@ dependencies:
react: 18.2.0
react-bootstrap: 2.5.0_7ey2zzynotv32rpkwno45fsx4e
react-dom: 18.2.0_react@18.2.0
react-helmet: 6.1.0_react@18.2.0
react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y
react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
swr: 1.3.0_react@18.2.0
@ -9581,16 +9581,19 @@ packages:
resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==}
dev: false
/react-helmet/6.1.0_react@18.2.0:
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
/react-helmet-async/1.3.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==}
peerDependencies:
react: '>=16.3.0'
react: ^16.6.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0
dependencies:
object-assign: 4.1.1
'@babel/runtime': 7.19.0
invariant: 2.2.4
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0
react-side-effect: 2.1.2_react@18.2.0
shallowequal: 1.1.0
dev: false
/react-i18next/11.18.6_ulhmqqxshznzmtuvahdi5nasbq:
@ -9747,14 +9750,6 @@ packages:
- webpack-hot-middleware
- webpack-plugin-serve
/react-side-effect/2.1.2_react@18.2.0:
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@ -10257,6 +10252,10 @@ packages:
/setprototypeof/1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
/shallowequal/1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: false
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}

View File

@ -80,7 +80,7 @@ export interface RegisterReqParams extends LoginReqParams {
name: string;
}
export interface ModifyPassReq {
export interface ModifyPasswordReq {
old_pass: string;
pass: string;
}
@ -137,7 +137,7 @@ export interface ImgCodeRes {
verify: boolean;
}
export interface PssRetReq extends ImgCodeReq {
export interface PasswordResetReq extends ImgCodeReq {
e_mail: string;
}
@ -145,11 +145,11 @@ export interface CheckImgReq {
action: 'login' | 'e_mail' | 'find_pass';
}
export interface NoticeSetReq {
export interface SetNoticeReq {
notice_switch: boolean;
}
export interface QuDetailRes {
export interface QuestionDetailRes {
id: string;
title: string;
content: string;

View File

@ -5,23 +5,35 @@ import { Avatar } from '@answer/components';
interface Props {
data: any;
showAvatar?: boolean;
avatarSize?: string;
className?: string;
}
const Index: FC<Props> = ({
data,
showAvatar = true,
avatarSize = '20px',
className = 'fs-14',
}) => {
return (
<div className={`text-secondary ${className}`}>
<Link to={`/users/${data?.username}`}>
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
</Link>
<Link to={`/users/${data?.username}`} className="me-1 text-break">
{data?.display_name}
</Link>
{data.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`}>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
)}
<span className="me-1 text-break">{data?.display_name}</span>
</Link>
) : (
<>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
)}
<span className="me-1 text-break">{data?.display_name}</span>
</>
)}
<span className="fw-bold">{data?.rank}</span>
</div>
);

View File

@ -17,15 +17,20 @@ const ActionBar = ({
onReply,
onVote,
onAction,
userStatus = '',
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
return (
<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 text-secondary"></span>
<FormatTime time={createdAt} className="text-secondary me-3" />
<div className="d-flex align-items-center text-secondary">
{userStatus !== 'deleted' ? (
<Link to={`/users/${username}`}>{nickName}</Link>
) : (
<span>{nickName}</span>
)}
<span className="mx-1"></span>
<FormatTime time={createdAt} className="me-3" />
<Button
variant="link"
size="sm"

View File

@ -36,7 +36,7 @@ const Form = ({
<Mentions pageUsers={pageUsers.getUsers()}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="text-muted fs-14">{t(`tip_${mode}`)}</div>
<div className="form-text">{t(`tip_${mode}`)}</div>
</div>
{type === 'edit' ? (
<div className="d-flex flex-column">

View File

@ -22,7 +22,7 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
<Mentions pageUsers={pageUsers.getUsers()}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="text-muted fs-14">{t(`tip_${mode}`)}</div>
<div className="form-text">{t(`tip_${mode}`)}</div>
</div>
<div className="d-flex flex-column">
<Button

View File

@ -269,6 +269,7 @@ const Comment = ({ objectId, mode }) => {
voteCount={item.vote_count}
isVote={item.is_vote}
memberActions={item.member_actions}
userStatus={item.user_status}
onReply={() => {
handleReply(item.comment_id);
}}

View File

@ -14,7 +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, useQueryNotificationRedDot } from '@answer/api';
import { logout, useQueryNotificationStatus } from '@answer/api';
import Storage from '@answer/utils/storage';
import './index.scss';
@ -28,7 +28,7 @@ const Header: FC = () => {
const [searchStr, setSearch] = useState('');
const siteInfo = siteInfoStore((state) => state.siteInfo);
const { interface: interfaceInfo } = interfaceStore();
const { data: redDot } = useQueryNotificationRedDot();
const { data: redDot } = useQueryNotificationStatus();
const handleInput = (val) => {
setSearch(val);
};

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/api';
import { deleteQuestion, deleteAnswer } from '@answer/api';
import { isLogin } from '@answer/utils';
import Share from '../Share';
@ -61,7 +61,7 @@ const Index: FC<IProps> = ({
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
questionDelete({
deleteQuestion({
id: qid,
}).then(() => {
toast.onShow({
@ -82,7 +82,7 @@ const Index: FC<IProps> = ({
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
answerDelete({
deleteAnswer({
id: aid,
}).then(() => {
// refersh page

View File

@ -1,4 +1,4 @@
import { FC, memo } from 'react';
import { FC } from 'react';
import { Pagination } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
@ -47,6 +47,7 @@ const PageItem = ({ page, currentPage, path }: PageItemProps) => {
href={path}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(path);
window.scrollTo(0, 0);
}}>
@ -111,7 +112,7 @@ const Index: FC<Props> = ({
)}
{currentPage === 4 && totalPage > 6 && (
<PageItem
key="6"
key="page6"
page={6}
currentPage={currentPage}
path={handleParams(6)}
@ -133,13 +134,13 @@ const Index: FC<Props> = ({
{currentPage >= 5 && (
<>
<PageItem
key="prev2"
key={realPage - 2}
page={realPage - 2}
currentPage={currentPage}
path={handleParams(realPage - 2)}
/>
<PageItem
key="prev1"
key={realPage - 1}
page={realPage - 1}
currentPage={currentPage}
path={handleParams(realPage - 1)}
@ -194,4 +195,4 @@ const Index: FC<Props> = ({
);
};
export default memo(Index);
export default Index;

View File

@ -5,7 +5,14 @@ import { useTranslation } from 'react-i18next';
import { useQuestionList } from '@answer/api';
import type * as Type from '@answer/common/interface';
import { Icon, Tag, Pagination, FormatTime, Empty } from '@answer/components';
import {
Icon,
Tag,
Pagination,
FormatTime,
Empty,
BaseUserCard,
} from '@answer/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
'newest',
@ -25,12 +32,11 @@ const QuestionLastUpdate = ({ q }) => {
// question answered
return (
<>
<a
className="p-0"
href={`/users/${q.last_answered_user_info.username}`}>
{q.last_answered_user_info.display_name}
</a>
<span className="fw-bold px-1">{q.last_answered_user_info.rank}</span>
<BaseUserCard
data={q.last_answered_user_info}
showAvatar={false}
className="me-1"
/>
<FormatTime
time={q.update_time}
@ -45,10 +51,11 @@ const QuestionLastUpdate = ({ q }) => {
// question modified
return (
<>
<a className="p-0" href={`/users/${q.update_user_info.username}`}>
{q.update_user_info.display_name}
</a>
<span className="fw-bold px-1">{q.update_user_info.rank}</span>
<BaseUserCard
data={q.update_user_info}
showAvatar={false}
className="me-1"
/>
<FormatTime
time={q.edit_time}
@ -62,10 +69,7 @@ const QuestionLastUpdate = ({ q }) => {
// default: asked
return (
<>
<a className="p-0" href={`/users/${q.user_info.username}`}>
{q.user_info.display_name}
</a>
<strong className="px-1">{q.user_info.rank}</strong>
<BaseUserCard data={q.user_info} showAvatar={false} className="me-1" />
<FormatTime
time={q.create_time}

View File

@ -50,7 +50,7 @@ const Index: FC<IProps> = ({ type, qid, aid, title }) => {
});
};
useEffect(() => {
if (window.navigator?.canShare?.({ text: '111' })) {
if (window.navigator?.canShare?.({ text: 'can_share' })) {
setSystemShareState(true);
}
}, []);

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Button, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { emailReSend, checkImgCode } from '@answer/api';
import { resendEmail, checkImgCode } from '@answer/api';
import { PicAuthCodeModal } from '@answer/components/Modal';
import type {
ImgCodeRes,
@ -53,7 +53,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
captcha_id: imgCode.captcha_id,
};
}
emailReSend(obj)
resendEmail(obj)
.then(() => {
setSuccess(true);
setModalState(false);

View File

@ -12,14 +12,22 @@ interface Props {
const Index: FC<Props> = ({ data, time, preFix }) => {
return (
<div className="d-flex">
<Link to={`/users/${data?.username}`}>
{data?.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`}>
<Avatar avatar={data?.avatar} size="40px" className="me-2" />
</Link>
) : (
<Avatar avatar={data?.avatar} size="40px" className="me-2" />
</Link>
)}
<div className="fs-14 text-secondary">
<div>
<Link to={`/users/${data?.username}`} className="me-1 text-break">
{data?.display_name}
</Link>
{data?.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`} className="me-1 text-break">
{data?.display_name}
</Link>
) : (
<span className="me-1 text-break">{data?.display_name}</span>
)}
<span className="fw-bold">{data?.rank}</span>
</div>
{time && <FormatTime time={time} preFix={preFix} />}

View File

@ -848,7 +848,7 @@
"interface": {
"page_title": "Interface",
"logo": {
"label": "Logo",
"label": "Logo (optional)",
"msg": "Site logo cannot be empty.",
"text": "You can upload your image or <1>reset</1> it to the site title text."
},

View File

@ -30,7 +30,7 @@ const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
const Answers: FC = () => {
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('status') || answerFilterItems[0];
const pageSize = 20;
const PAGE_SIZE = 20;
const curPage = Number(urlSearchParams.get('page')) || 1;
const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });
@ -39,7 +39,7 @@ const Answers: FC = () => {
isLoading,
mutate: refreshList,
} = useAnswerSearch({
page_size: pageSize,
page_size: PAGE_SIZE,
page: curPage,
status: curFilter as Type.AdminContentsFilterBy,
});
@ -189,7 +189,7 @@ const Answers: FC = () => {
<Pagination
currentPage={curPage}
totalSize={count}
pageSize={pageSize}
pageSize={PAGE_SIZE}
/>
</div>
</>

View File

@ -23,14 +23,14 @@ const Flags: FC = () => {
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('status') || flagFilterKeys[0];
const curType = urlSearchParams.get('type') || flagTypeKeys[0];
const pageSize = 20;
const PAGE_SIZE = 20;
const curPage = Number(urlSearchParams.get('page')) || 1;
const {
data: listData,
isLoading,
mutate: refreshList,
} = useFlagSearch({
page_size: pageSize,
page_size: PAGE_SIZE,
page: curPage,
status: curFilter as Type.FlagStatus,
object_type: curType as Type.FlagType,
@ -159,7 +159,7 @@ const Flags: FC = () => {
<Pagination
currentPage={curPage}
totalSize={count}
pageSize={pageSize}
pageSize={PAGE_SIZE}
/>
</div>
</>

View File

@ -23,7 +23,7 @@ import { useEditStatusModal, useReportModal } from '@answer/hooks';
import {
useQuestionSearch,
changeQuestionStatus,
questionDelete,
deleteQuestion,
} from '@answer/api';
import * as Type from '@answer/common/interface';
@ -35,7 +35,7 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [
'deleted',
];
const pageSize = 20;
const PAGE_SIZE = 20;
const Questions: FC = () => {
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('status') || questionFilterItems[0];
@ -47,7 +47,7 @@ const Questions: FC = () => {
isLoading,
mutate: refreshList,
} = useQuestionSearch({
page_size: pageSize,
page_size: PAGE_SIZE,
page: curPage,
status: curFilter as Type.AdminContentsFilterBy,
});
@ -89,7 +89,7 @@ const Questions: FC = () => {
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
questionDelete({
deleteQuestion({
id,
}).then(() => {
refreshList();
@ -207,7 +207,7 @@ const Questions: FC = () => {
<Pagination
currentPage={curPage}
totalSize={count}
pageSize={pageSize}
pageSize={PAGE_SIZE}
/>
</div>
</>

View File

@ -1,140 +0,0 @@
import { FC } from 'react';
import {
Container,
Row,
Col,
Button,
Table,
Figure,
Stack,
} from 'react-bootstrap';
import { AccordionNav } from '@answer/components';
import { ADMIN_NAV_MENUS } from '@answer/common/constants';
import '../index.scss';
const UserOverview: FC = () => {
return (
<Container className="admin-container">
<Row>
<Col lg={2}>
<AccordionNav menus={ADMIN_NAV_MENUS} />
</Col>
<Col lg={10}>
<Button variant="outline-secondary" size="sm">
Back
</Button>
<h5 className="mb-3 mt-4">Profile</h5>
<Table className="mb-5">
<tbody className="align-middle">
<tr>
<td>ID</td>
<td>1030000000091295</td>
<td />
</tr>
<tr>
<td>Display name</td>
<td>Jim Green</td>
<td />
</tr>
<tr>
<td>username</td>
<td>jimgreen</td>
<td>
<Button variant="link" size="sm">
Edit
</Button>
</td>
</tr>
<tr>
<td>Profile image</td>
<td>
<Figure.Image
width={48}
height={48}
className="rounded-1 m-0"
src="https://gw.alicdn.com/bao/uploaded/i4/1607723262/O1CN01JJCGVD1Zy2jryOhDc_!!1607723262.jpg"
/>
</td>
<td />
</tr>
</tbody>
</Table>
<h5 className="mb-3 mt-4">Permissions</h5>
<Table className="mb-5">
<tbody className="align-middle">
<tr>
<td>Activated</td>
<td>No</td>
<td>
<Button size="sm" variant="link">
Activate
</Button>
</td>
</tr>
<tr>
<td>Admin?</td>
<td>No</td>
<td>
<Button size="sm" variant="link">
Grant
</Button>
</td>
</tr>
<tr>
<td>Suspended?</td>
<td>No</td>
<td>
<Stack direction="horizontal" gap={1}>
<Button size="sm" variant="link" className="text-danger">
Suspend
</Button>
<div className="text-secondary text-nowrap">
A suspended user can't log in
</div>
</Stack>
</td>
</tr>
</tbody>
</Table>
<h5 className="mb-3 mt-4">Activity</h5>
<Table className="mb-5">
<tbody className="align-middle">
<tr>
<td>Reputation</td>
<td>1805</td>
</tr>
<tr>
<td>Answers</td>
<td>30</td>
</tr>
<tr>
<td>Questions</td>
<td>10</td>
</tr>
<tr>
<td>Created</td>
<td>Sep 1, 2022 at 16:00</td>
</tr>
<tr>
<td>Registration IP address</td>
<td>11.22.33.44</td>
</tr>
<tr>
<td>Seen</td>
<td>Sep 6, 2022 at 09:35</td>
</tr>
<tr>
<td>Last IP address</td>
<td>11.22.33.44</td>
</tr>
</tbody>
</Table>
</Col>
</Row>
</Container>
);
};
export default UserOverview;

View File

@ -1,7 +1,7 @@
import { FC, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr';
@ -56,11 +56,9 @@ const Layout: FC = () => {
}
return (
<>
<HelmetProvider>
<Helmet>
{siteInfo ? (
<meta name="description" content={siteInfo.description} />
) : null}
{siteInfo && <meta name="description" content={siteInfo.description} />}
</Helmet>
<SWRConfig
value={{
@ -74,7 +72,7 @@ const Layout: FC = () => {
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
<Footer />
</SWRConfig>
</>
</HelmetProvider>
);
};

View File

@ -20,49 +20,26 @@ import type * as Type from '@answer/common/interface';
import SearchQuestion from './components/SearchQuestion';
interface FormDataItem {
title: {
value: string;
isInvalid: boolean;
errorMsg: string;
focus?: boolean;
};
tags: {
value: Type.Tag[];
isInvalid: boolean;
errorMsg: string;
focus?: boolean;
};
content: {
value: string;
isInvalid: boolean;
errorMsg: string;
focus?: boolean;
};
answer: {
value: string;
isInvalid: boolean;
errorMsg: string;
focus?: boolean;
};
title: Type.FormValue<string>;
tags: Type.FormValue<Type.Tag[]>;
content: Type.FormValue<string>;
answer: Type.FormValue<string>;
}
const initFormData = {
title: {
value: '',
isInvalid: false,
errorMsg: '',
focus: false,
},
tags: {
value: [],
isInvalid: false,
errorMsg: '',
focus: false,
},
content: {
value: '',
isInvalid: false,
errorMsg: '',
focus: false,
},
answer: {
value: '',

View File

@ -1,7 +1,7 @@
import { memo, FC } from 'react';
import { ButtonGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
interface Props {
count: number;
@ -11,6 +11,7 @@ const Index: FC<Props> = ({ count = 0, order = 'default' }) => {
const { t } = useTranslation('translation', {
keyPrefix: 'question_detail.answers',
});
const location = useLocation();
return (
<div
className="d-flex align-items-center justify-content-between mt-5 mb-3"
@ -20,14 +21,14 @@ const Index: FC<Props> = ({ count = 0, order = 'default' }) => {
</h5>
<ButtonGroup size="sm">
<Link
to={`${window.location.pathname}?order=default`}
to={`${location.pathname}?order=default`}
className={`btn btn-outline-secondary ${
order !== 'updated' ? 'active' : ''
}`}>
{t('score')}
</Link>
<Link
to={`${window.location.pathname}?order=updated`}
to={`${location.pathname}?order=updated`}
className={`btn btn-outline-secondary ${
order === 'updated' ? 'active' : ''
}`}>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import { useParams, useSearchParams } from 'react-router-dom';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import { questionDetail, getAnswers } from '@answer/api';
import { Pagination, PageTitle } from '@answer/components';
@ -9,7 +9,7 @@ import { scrollTop } from '@answer/utils';
import { usePageUsers } from '@answer/hooks';
import type {
ListResult,
QuDetailRes,
QuestionDetailRes,
AnswerItem,
} from '@answer/common/interface';
@ -25,12 +25,13 @@ import {
import './index.scss';
const Index = () => {
const navigate = useNavigate();
const { qid = '', aid = '' } = useParams();
const [urlSearch] = useSearchParams();
const page = Number(urlSearch.get('page') || 0);
const order = urlSearch.get('order') || '';
const [question, setQuestion] = useState<QuDetailRes | null>(null);
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
const [answers, setAnswers] = useState<ListResult<AnswerItem>>({
count: -1,
list: [],
@ -75,7 +76,7 @@ const Index = () => {
const initPage = (type: string) => {
if (type === 'delete_question') {
setTimeout(() => {
window.history.back();
navigate(-1);
}, 1000);
return;
}

View File

@ -17,16 +17,8 @@ import type * as Type from '@answer/common/interface';
import './index.scss';
interface FormDataItem {
answer: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
description: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
answer: Type.FormValue<string>;
description: Type.FormValue<string>;
}
const initFormData = {
answer: {
@ -111,7 +103,7 @@ const Ask = () => {
id: aid,
};
modifyAnswer(params).then(() => {
window.location.href = `/questions/${qid}/${aid}`;
navigate(`/questions/${qid}/${aid}`);
});
};

View File

@ -1,6 +1,6 @@
import { FC, memo } from 'react';
import { ListGroupItem, ButtonGroup, Button } from 'react-bootstrap';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const sortBtns = [
@ -21,6 +21,7 @@ interface Props {
}
const Index: FC<Props> = ({ sort, count = 0 }) => {
const [searchParams] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const { t } = useTranslation('translation', {
@ -28,7 +29,7 @@ const Index: FC<Props> = ({ sort, count = 0 }) => {
});
const handleParams = (order): string => {
const basePath = window.location.pathname;
const basePath = location.pathname;
searchParams.delete('page');
searchParams.set('order', order);
const searchStr = searchParams.toString();

View File

@ -1,9 +1,8 @@
import { memo, FC } from 'react';
import { ListGroupItem, Badge } from 'react-bootstrap';
import { Icon, Tag, FormatTime } from '@answer/components';
import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components';
import type { SearchResItem } from '@answer/common/interface';
import { formatCount } from '@answer/utils';
interface Props {
data: SearchResItem;
@ -28,14 +27,8 @@ const Index: FC<Props> = ({ data }) => {
</a>
</div>
<div className="d-flex flex-wrap align-items-center fs-14 text-secondary mb-2">
<a href={`/users/${data.object?.user_info?.username}`}>
{data.object?.user_info?.display_name}
</a>
{data.object?.user_info?.rank > 0 && (
<span className="fw-bold ms-1">
{formatCount(data.object.user_info.rank)}
</span>
)}
<BaseUserCard data={data.object?.user_info} showAvatar={false} />
<span className="split-dot" />
<FormatTime
time={data.object?.created_at}

View File

@ -9,28 +9,13 @@ import classNames from 'classnames';
import { Editor, EditorRef, PageTitle } from '@answer/components';
import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import type * as Type from '@answer/common/interface';
interface FormDataItem {
displayName: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
slugName: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
description: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
editSummary: {
value: string;
isInvalid: boolean;
errorMsg: string;
};
displayName: Type.FormValue<string>;
slugName: Type.FormValue<string>;
description: Type.FormValue<string>;
editSummary: Type.FormValue<string>;
}
const initFormData = {
displayName: {

View File

@ -2,10 +2,10 @@ import { FC, memo, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { passRetrieve, checkImgCode } from '@answer/api';
import { resetPassword, checkImgCode } from '@answer/api';
import type {
ImgCodeRes,
PssRetReq,
PasswordResetReq,
FormDataType,
} from '@answer/common/interface';
@ -70,7 +70,7 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
if (e) {
e.preventDefault();
}
const params: PssRetReq = {
const params: PasswordResetReq = {
e_mail: formData.e_mail.value,
};
if (imgCode.verify) {
@ -78,7 +78,7 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
params.captcha_id = imgCode.captcha_id;
}
passRetrieve(params)
resetPassword(params)
.then(() => {
callback?.(2, formData.e_mail.value);
setModalState(false);

View File

@ -1,7 +1,7 @@
import { FC, memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { accountActivate } from '@answer/api';
import { activateAccount } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import { getQueryString } from '@answer/utils';
@ -14,10 +14,10 @@ const Index: FC = () => {
const code = getQueryString('code');
if (code) {
accountActivate(encodeURIComponent(code)).then((res) => {
activateAccount(encodeURIComponent(code)).then((res) => {
updateUser(res);
setTimeout(() => {
window.location.href = '/users/account-activation/success';
window.location.replace('/users/account-activation/success');
}, 0);
});
}

View File

@ -108,13 +108,9 @@ const Index: React.FC = () => {
setRefresh((pre) => pre + 1);
}
if (res.mail_status === 1) {
const path = Storage.get('ANSWER_PATH');
const path = Storage.get('ANSWER_PATH') || '/';
Storage.remove('ANSWER_PATH');
if (path) {
window.location.href = path;
} else {
window.location.href = '/';
}
window.location.replace(path);
}
setModalState(false);

View File

@ -37,9 +37,13 @@ const Inbox = ({ data, handleReadNotification }) => {
key={item.id}
className={classNames('py-3', !item.is_read && 'warning')}>
<div>
<Link to={`/users/${item.user_info.username}`}>
{item.user_info.display_name}
</Link>{' '}
{item.user_info.status !== 'deleted' ? (
<Link to={`/users/${item.user_info.username}`}>
{item.user_info.display_name}{' '}
</Link>
) : (
<span>{item.user_info.display_name} </span>
)}
{item.notification_action}{' '}
<Link to={url} onClick={() => handleReadNotification(item.id)}>
{item.object_info.title}

View File

@ -5,8 +5,8 @@ import { useParams, useNavigate } from 'react-router-dom';
import {
useQueryNotifications,
clearUnReadNotification,
clearNotificationRedDot,
clearUnreadNotification,
clearNotificationStatus,
readNotification,
} from '@answer/api';
import { PageTitle } from '@answer/components';
@ -29,7 +29,7 @@ const Notifications = () => {
});
useEffect(() => {
clearNotificationRedDot(type);
clearNotificationStatus(type);
}, []);
useEffect(() => {
@ -56,7 +56,7 @@ const Notifications = () => {
};
const handleUnreadNotification = async () => {
await clearUnReadNotification(type);
await clearUnreadNotification(type);
mutate();
};

View File

@ -3,7 +3,7 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { passRetrieveSet } from '@answer/api';
import { replacementPassword } from '@answer/api';
import { userInfoStore } from '@answer/stores';
import { getQueryString, isLogin } from '@answer/utils';
import type { FormDataType } from '@answer/common/interface';
@ -98,7 +98,7 @@ const Index: React.FC = () => {
console.error('code is required');
return;
}
passRetrieveSet({
replacementPassword({
code: encodeURIComponent(code),
pass: formData.pass.value,
})

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag } from '@answer/components';
import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components';
interface Props {
visible: boolean;
@ -31,15 +31,10 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
</h6>
<div className="d-flex align-items-center fs-14 text-secondary mb-2">
{tabName === 'bookmarks' && (
<div className="d-flex">
<a
href={`/users/${item.user_info?.username}`}
className="me-1">
{item.user_info?.display_name}
</a>
<strong>{item.user_info?.rank}</strong>
<>
<BaseUserCard data={item.user_info} showAvatar={false} />
<span className="split-dot" />
</div>
</>
)}
<FormatTime
time={item.create_time}

View File

@ -1,6 +1,6 @@
import { FC, memo } from 'react';
import { ButtonGroup, Button } from 'react-bootstrap';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const sortBtns = [
@ -26,10 +26,11 @@ const Index: FC<Props> = ({
}) => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
const handleParams = (order): string => {
const basePath = window.location.pathname;
const basePath = location.pathname;
searchParams.delete('page');
searchParams.set('order', order);
const searchStr = searchParams.toString();

View File

@ -1,6 +1,7 @@
import { FC, memo } from 'react';
import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Avatar, Icon } from '@answer/components';
import type { UserInfoRes } from '@answer/common/interface';
@ -16,14 +17,26 @@ const Index: FC<Props> = ({ data }) => {
}
return (
<div className="d-flex mb-4">
<a href={`/users/${data.username}`}>
{data?.status !== 'deleted' ? (
<Link to={`/users/${data.username}`} reloadDocument>
<Avatar avatar={data.avatar} size="160px" />
</Link>
) : (
<Avatar avatar={data.avatar} size="160px" />
</a>
)}
<div className="ms-4">
<div className="d-flex align-items-center mb-2">
<a href={`/users/${data.username}`} className="text-body h3 mb-0">
{data.display_name}
</a>
{data?.status !== 'deleted' ? (
<Link
to={`/users/${data.username}`}
className="text-body h3 mb-0"
reloadDocument>
{data.display_name}
</Link>
) : (
<span className="text-body h3 mb-0">{data.display_name}</span>
)}
{data?.is_admin && (
<div className="ms-2">
<OverlayTrigger

View File

@ -3,7 +3,7 @@ import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@answer/common/interface';
import { noticeSet, getUserInfo } from '@answer/api';
import { setNotice, getUserInfo } from '@answer/api';
import { useToast } from '@answer/hooks';
const Index = () => {
@ -34,7 +34,7 @@ const Index = () => {
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
noticeSet({
setNotice({
notice_switch: formData.notice_switch.value,
}).then(() => {
toast.onShow({

View File

@ -9,7 +9,7 @@ const Suspended = () => {
const userInfo = userInfoStore((state) => state.user);
if (userInfo.status !== 'forbidden') {
window.location.href = '/';
window.location.replace('/');
return null;
}

View File

@ -29,7 +29,7 @@ export const readNotification = (id) => {
});
};
export const useQueryNotificationRedDot = () => {
export const useQueryNotificationStatus = () => {
const apiUrl = '/answer/api/v1/notification/status';
return useSWR<{ inbox: number; achievement: number }>(
@ -41,13 +41,13 @@ export const useQueryNotificationRedDot = () => {
);
};
export const clearNotificationRedDot = (type) => {
export const clearNotificationStatus = (type) => {
return request.instance.put('/answer/api/v1/notification/status', {
type,
});
};
export const clearUnReadNotification = (type) => {
export const clearUnreadNotification = (type) => {
return request.instance.put('/answer/api/v1/notification/read/state/all', {
type,
});

View File

@ -96,11 +96,11 @@ export const logout = () => {
return request.get('/answer/api/v1/user/logout');
};
export const emailVerify = (code: string) => {
export const verifyEmail = (code: string) => {
return request.get(`/answer/api/v1/email/verify?code=${code}`);
};
export const emailReSend = (params?: Type.ImgCodeReq) => {
export const resendEmail = (params?: Type.ImgCodeReq) => {
params = qs.parse(
qs.stringify(params, {
skipNulls: true,
@ -119,7 +119,7 @@ export const getUserInfo = () => {
return request.get<Type.UserInfoRes>('/answer/api/v1/user/info');
};
export const modifyPassword = (params: Type.ModifyPassReq) => {
export const modifyPassword = (params: Type.ModifyPasswordReq) => {
return request.post('/answer/api/v1/user/password/modify', params);
};
@ -131,15 +131,15 @@ export const uploadAvatar = (params: Type.AvatarUploadReq) => {
return request.post('/answer/api/v1/user/avatar/upload', params);
};
export const passRetrieve = (params: Type.PssRetReq) => {
export const resetPassword = (params: Type.PasswordResetReq) => {
return request.post('/answer/api/v1/user/password/reset', params);
};
export const passRetrieveSet = (params: { code: string; pass: string }) => {
export const replacementPassword = (params: { code: string; pass: string }) => {
return request.post('/answer/api/v1/user/password/replacement', params);
};
export const accountActivate = (code: string) => {
export const activateAccount = (code: string) => {
return request.post(`/answer/api/v1/user/email/verification`, { code });
};
@ -149,7 +149,7 @@ export const checkImgCode = (params: Type.CheckImgReq) => {
);
};
export const noticeSet = (params: Type.NoticeSetReq) => {
export const setNotice = (params: Type.SetNoticeReq) => {
return request.post('/answer/api/v1/user/notice/set', params);
};
@ -158,7 +158,9 @@ export const saveQuestion = (params: Type.QuestionParams) => {
};
export const questionDetail = (id: string) => {
return request.get<Type.QuDetailRes>(`/answer/api/v1/question/info?id=${id}`);
return request.get<Type.QuestionDetailRes>(
`/answer/api/v1/question/info?id=${id}`,
);
};
export const langConfig = () => {
@ -227,11 +229,11 @@ export const postReport = (params: {
return request.post('/answer/api/v1/report', params);
};
export const questionDelete = (params: { id: string }) => {
export const deleteQuestion = (params: { id: string }) => {
return request.delete('/answer/api/v1/question', params);
};
export const answerDelete = (params: { id: string }) => {
export const deleteAnswer = (params: { id: string }) => {
return request.delete('/answer/api/v1/answer', params);
};

View File

@ -34,7 +34,7 @@ function isLogin(needToLogin?: boolean): boolean {
// login and active
if (user.username && user.mail_status === 1) {
if (LOGIN_NEED_BACK.includes(path)) {
window.location.href = '/';
window.location.replace('/');
}
return true;
}

View File

@ -6,12 +6,6 @@ import { userInfoStore, toastStore } from '@answer/stores';
import Storage from './storage';
// type Result<T> = {
// code: number;
// msg: string;
// data: T;
// };
const API = {
development: '',
production: '',