mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/1.0.8/ui' into feat/ui-1.1.0
This commit is contained in:
commit
0c3efb66ab
|
@ -304,6 +304,7 @@ ui:
|
|||
oauth_callback: Processing
|
||||
http_404: HTTP Error 404
|
||||
http_50X: HTTP Error 500
|
||||
http_403: HTTP Error 403
|
||||
notifications:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
|
@ -526,6 +527,7 @@ ui:
|
|||
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.
|
||||
tip_vote: It adds something useful to the post
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
|
@ -802,6 +804,11 @@ ui:
|
|||
answered: answered
|
||||
closed_in: Closed in
|
||||
show_exist: Show existing question.
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
title: Answers
|
||||
score: Score
|
||||
|
@ -819,10 +826,19 @@ ui:
|
|||
edit link to refine and improve your existing answer, instead.</p>
|
||||
empty: Answer cannot be empty.
|
||||
characters: content must be at least 6 characters in length.
|
||||
tips:
|
||||
header_1: Thanks for your answer
|
||||
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
|
||||
li1_2: Back up any statements you make with references or personal experience.
|
||||
header_2: But <strong>avoid</strong> ...
|
||||
li2_1: Asking for help, seeking clarification, or responding to other answers.
|
||||
|
||||
reopen:
|
||||
confirm_btn: Reopen
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
success: This post has been reopened
|
||||
|
||||
delete:
|
||||
title: Delete this post
|
||||
question: >-
|
||||
|
@ -1047,13 +1063,11 @@ ui:
|
|||
votes: votes
|
||||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
page_error:
|
||||
http_error: HTTP Error {{ code }}
|
||||
desc_403: You don’t have permission to access this page.
|
||||
desc_404: Unfortunately, this page doesn't exist.
|
||||
desc_50X: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
desc: "We are under maintenance, we'll be back soon."
|
||||
|
@ -1467,8 +1481,8 @@ ui:
|
|||
no_data: "We couldn't find anything."
|
||||
users:
|
||||
title: Users
|
||||
users_with_the_most_reputation: Users with the highest reputation scores
|
||||
users_with_the_most_vote: Users who voted the most
|
||||
users_with_the_most_reputation: Users with the highest reputation scores this week
|
||||
users_with_the_most_vote: Users who voted the most this week
|
||||
staffs: Our community staff
|
||||
reputation: reputation
|
||||
votes: votes
|
||||
|
|
|
@ -979,13 +979,11 @@ ui:
|
|||
votes: 个点赞
|
||||
answers: 个回答
|
||||
accepted: 已被采纳
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "很抱歉,此页面不存在。"
|
||||
back_home: 回到主页
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: 服务器遇到了一个错误,无法完成你的请求。
|
||||
page_error:
|
||||
http_error: HTTP Error {{ code }}
|
||||
desc_403: 你无权访问此页面。
|
||||
desc_404: 很抱歉,此页面不存在。
|
||||
desc_50X: 服务器遇到了一个错误,无法完成你的请求。
|
||||
back_home: 回到主页
|
||||
page_maintenance:
|
||||
desc: "我们正在进行维护,我们将很快回来。"
|
||||
|
@ -1366,8 +1364,8 @@ ui:
|
|||
no_data: "空空如也"
|
||||
users:
|
||||
title: 用户
|
||||
users_with_the_most_reputation: 信誉积分最高的用户
|
||||
users_with_the_most_vote: 投票最多的用户
|
||||
users_with_the_most_reputation: 本周信誉积分最高的用户
|
||||
users_with_the_most_vote: 本周投票最多的用户
|
||||
staffs: 我们的社区工作人员
|
||||
reputation: 声望值
|
||||
votes: 投票
|
||||
|
|
|
@ -977,13 +977,11 @@ ui:
|
|||
votes: 得票
|
||||
answers: 回答
|
||||
accepted: 已採納
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "很抱歉,此頁面不存在。"
|
||||
back_home: 回到首頁
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: 伺服器遇到了一個錯誤,無法完成你的請求。
|
||||
page_error:
|
||||
http_error: HTTP Error {{ code }}
|
||||
desc_403: 你无权访问此頁面。
|
||||
desc_404: 很抱歉,此頁面不存在。
|
||||
desc_50X: 伺服器遇到了一個錯誤,無法完成你的請求。
|
||||
back_home: 回到首頁
|
||||
page_maintenance:
|
||||
desc: "我們正在維護中,很快就會回來。"
|
||||
|
|
|
@ -22,9 +22,7 @@
|
|||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"dompurify": "^2.4.3",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"html-react-parser": "^3.0.8",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -84,7 +82,7 @@
|
|||
"react-app-rewired": "^2.2.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.54.4",
|
||||
"typescript": "^4.8.3",
|
||||
"typescript": "^4.9.5",
|
||||
"yaml-loader": "^0.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@7.9.5",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -86,6 +86,9 @@
|
|||
{
|
||||
name: 'Safari',
|
||||
version: '15'
|
||||
},
|
||||
{
|
||||
name: 'IE',
|
||||
}
|
||||
];
|
||||
function getBrowerTypeAndVersion(){
|
||||
|
@ -95,6 +98,7 @@
|
|||
};
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var s;
|
||||
((ua.indexOf("compatible") > -1 && ua.indexOf("MSIE") > -1) || (ua.indexOf('Trident') > -1 && ua.indexOf("rv:11.0") > -1)) ? brower = { name: 'IE', version: '' } :
|
||||
(s = ua.match(/edge\/([\d\.]+)/)) ? brower = { name: 'Edge', version: s[1] } :
|
||||
(s = ua.match(/firefox\/([\d\.]+)/)) ? brower = { name: 'Firefox', version: s[1] } :
|
||||
(s = ua.match(/chrome\/([\d\.]+)/)) ? brower = { name: 'Chrome', version: s[1] } :
|
||||
|
@ -126,16 +130,24 @@
|
|||
}
|
||||
|
||||
const browerInfo = getBrowerTypeAndVersion();
|
||||
const notSupport = defaultList.some(item => {
|
||||
if (item.name === browerInfo.name) {
|
||||
return compareVersion(browerInfo.version, item.version) === -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (notSupport) {
|
||||
|
||||
if (browerInfo.name === 'IE') {
|
||||
const div = document.getElementById('protect-brower');
|
||||
div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'
|
||||
div.innerText = 'Sorry, this site does not support Internet Explorer. In order to avoid affecting the normal use of our features, please use a more modern browser such as Edge, Firefox, Chrome, or Safari.'
|
||||
} else {
|
||||
const notSupport = defaultList.some(item => {
|
||||
if (item.name === browerInfo.name) {
|
||||
return compareVersion(browerInfo.version, item.version) === -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (notSupport) {
|
||||
const div = document.getElementById('protect-brower');
|
||||
div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -58,10 +58,13 @@ export interface QuestionParams {
|
|||
title: string;
|
||||
url_title?: string;
|
||||
content: string;
|
||||
html?: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export interface QuestionWithAnswer extends QuestionParams {
|
||||
answer_content: string;
|
||||
}
|
||||
|
||||
export interface ListResult<T = any> {
|
||||
count: number;
|
||||
list: T[];
|
||||
|
|
|
@ -12,6 +12,7 @@ import { bookmark, postVote } from '@/services';
|
|||
|
||||
interface Props {
|
||||
className?: string;
|
||||
source: 'question' | 'answer';
|
||||
data: {
|
||||
id: string;
|
||||
votesCount: number;
|
||||
|
@ -24,7 +25,7 @@ interface Props {
|
|||
};
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ className, data }) => {
|
||||
const Index: FC<Props> = ({ className, data, source }) => {
|
||||
const [votes, setVotes] = useState(0);
|
||||
const [like, setLike] = useState(false);
|
||||
const [hate, setHated] = useState(false);
|
||||
|
@ -102,6 +103,11 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
<div className={classNames(className)}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
title={
|
||||
source === 'question'
|
||||
? t('question_detail.question_useful')
|
||||
: t('question_detail.answer_useful')
|
||||
}
|
||||
variant="outline-secondary"
|
||||
active={like}
|
||||
onClick={() => handleVote('up')}>
|
||||
|
@ -111,6 +117,11 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
{votes}
|
||||
</Button>
|
||||
<Button
|
||||
title={
|
||||
source === 'question'
|
||||
? t('question_detail.question_un_useful')
|
||||
: t('question_detail.answer_un_useful')
|
||||
}
|
||||
variant="outline-secondary"
|
||||
active={hate}
|
||||
onClick={() => handleVote('down')}>
|
||||
|
|
|
@ -32,6 +32,7 @@ const ActionBar = ({
|
|||
<span className="mx-1">•</span>
|
||||
<FormatTime time={createdAt} className="me-3" />
|
||||
<Button
|
||||
title={t('tip_vote')}
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={`me-3 btn-no-border p-0 ${isVote ? '' : 'link-secondary'}`}
|
||||
|
|
|
@ -32,7 +32,7 @@ const ActivateScriptNodes = (el, part) => {
|
|||
scriptList.push(node);
|
||||
}
|
||||
}
|
||||
scriptList.forEach((so) => {
|
||||
scriptList?.forEach((so) => {
|
||||
const script = document.createElement('script');
|
||||
script.text = so.text;
|
||||
for (let i = 0; i < so.attributes.length; i += 1) {
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from 'react';
|
||||
|
||||
import { markdownToHtml } from '@/services';
|
||||
import { htmlToReact } from '@/utils';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
|
@ -51,9 +50,9 @@ const Index = ({ value }, ref) => {
|
|||
return (
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
|
||||
{htmlToReact(html)}
|
||||
</div>
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -80,6 +80,15 @@ export function createEditorUtils(
|
|||
|
||||
export function htmlRender(el: HTMLElement | null) {
|
||||
if (!el) return;
|
||||
// Replace all br tags with newlines
|
||||
// Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors.
|
||||
el.querySelectorAll('p').forEach((p) => {
|
||||
if (p.innerHTML.startsWith('$$') && p.innerHTML.endsWith('$$')) {
|
||||
const str = p.innerHTML.replace(/<br>/g, '\n');
|
||||
p.innerHTML = str;
|
||||
}
|
||||
});
|
||||
|
||||
import('mermaid').then(({ default: mermaid }) => {
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
|
||||
|
@ -99,6 +108,7 @@ export function htmlRender(el: HTMLElement | null) {
|
|||
render(el, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$$<br>', right: '<br>$$', display: true },
|
||||
{
|
||||
left: '\\begin{equation}',
|
||||
right: '\\end{equation}',
|
||||
|
@ -114,8 +124,31 @@ export function htmlRender(el: HTMLElement | null) {
|
|||
},
|
||||
);
|
||||
|
||||
// remove change table style to htmlToReact function
|
||||
/**
|
||||
* @description: You modify the DOM with other scripts after React has rendered the DOM. This way, on the next render cycle (re-render), React cannot find the DOM node it rendered before, because it has been modified or removed by other scripts.
|
||||
*/
|
||||
// change table style
|
||||
|
||||
el.querySelectorAll('table').forEach((table) => {
|
||||
if (
|
||||
(table.parentNode as HTMLDivElement)?.classList.contains(
|
||||
'table-responsive',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
table.classList.add('table', 'table-bordered');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'table-responsive';
|
||||
table.parentNode?.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
});
|
||||
|
||||
// add rel nofollow for link not inlcludes domain
|
||||
el.querySelectorAll('a').forEach((a) => {
|
||||
const base = window.location.origin;
|
||||
const targetUrl = new URL(a.href, base);
|
||||
|
||||
if (targetUrl.origin !== base) {
|
||||
a.rel = 'nofollow';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { memo, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = ({ httpCode = '', errMsg = '' }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
const pageWrap = document.querySelector('.page-wrap') as HTMLElement;
|
||||
if (pageWrap) {
|
||||
pageWrap.style.display = 'contents';
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (pageWrap) {
|
||||
pageWrap.style.display = 'block';
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePageTags({
|
||||
title: t(`http_${httpCode}`, { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center">
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
||||
<div className="text-center mb-3 fs-5">
|
||||
{errMsg || t(`desc_${httpCode}`)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Link to="/" className="btn btn-link">
|
||||
{t('back_home')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -49,7 +49,7 @@ const Index = ({
|
|||
cancelBtnVariant={cancelBtnVariant}
|
||||
confirmBtnVariant={confirmBtnVariant}
|
||||
{...props}>
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<p dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</Modal>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ const Index: FC<IProps> = ({
|
|||
if (type === 'question') {
|
||||
Modal.confirm({
|
||||
title: t('title'),
|
||||
content: hasAnswer ? `<p>${t('question')}</p>` : `<p>${t('other')}</p>`,
|
||||
content: hasAnswer ? t('question') : t('other'),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
@ -90,7 +90,7 @@ const Index: FC<IProps> = ({
|
|||
if (type === 'answer' && aid) {
|
||||
Modal.confirm({
|
||||
title: t('title'),
|
||||
content: isAccepted ? t('answer_accepted') : `<p>${t('other')}</p>`,
|
||||
content: isAccepted ? t('answer_accepted') : t('other'),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
@ -128,6 +128,7 @@ const Index: FC<IProps> = ({
|
|||
title: t('title', { keyPrefix: 'question_detail.reopen' }),
|
||||
content: t('content', { keyPrefix: 'question_detail.reopen' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.reopen' }),
|
||||
onConfirm: () => {
|
||||
reopenQuestion({
|
||||
question_id: qid,
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
} from '@/components';
|
||||
|
||||
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
||||
'newest',
|
||||
'active',
|
||||
'newest',
|
||||
'frequent',
|
||||
'score',
|
||||
'unanswered',
|
||||
|
|
|
@ -30,7 +30,9 @@ const Index: FC<IProps> = ({
|
|||
data.recommend && 'badge-tag-required',
|
||||
className,
|
||||
)}>
|
||||
<span className={textClassName}>{data.display_name}</span>
|
||||
<span className={textClassName}>
|
||||
{data.display_name || data.slug_name}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,6 +36,7 @@ import WelcomeTitle from './WelcomeTitle';
|
|||
import Counts from './Counts';
|
||||
import QuestionList from './QuestionList';
|
||||
import HotQuestions from './HotQuestions';
|
||||
import HttpErrorContent from './HttpErrorContent';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -78,5 +79,6 @@ export {
|
|||
Counts,
|
||||
QuestionList,
|
||||
HotQuestions,
|
||||
HttpErrorContent,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -218,6 +218,9 @@ img:not(a img, img.broken) {
|
|||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
p {
|
||||
> code {
|
||||
background-color: #e9ecef;
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Container, Button } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_404' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
const pageWrap = document.querySelector('.page-wrap');
|
||||
pageWrap.style.display = 'contents';
|
||||
|
||||
return () => {
|
||||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePageTags({
|
||||
title: t('http_404', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<h4 className="text-center">{t('http_error')}</h4>
|
||||
<div className="text-center mb-3 fs-5">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,7 @@
|
|||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <HttpErrorContent httpCode="404" />;
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,45 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Container, Button } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
const pageWrap = document.querySelector('.page-wrap');
|
||||
pageWrap.style.display = 'contents';
|
||||
|
||||
return () => {
|
||||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePageTags({
|
||||
title: t('http_50X', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=T^T=)
|
||||
</div>
|
||||
|
||||
<h4 className="text-center">{t('http_error')}</h4>
|
||||
<div className="text-center mb-3 fs-5">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,7 @@
|
|||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <HttpErrorContent httpCode="50X" />;
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -58,7 +58,7 @@ const Answers: FC = () => {
|
|||
content:
|
||||
item.accepted === 2
|
||||
? t('answer_accepted', { keyPrefix: 'delete' })
|
||||
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
|
||||
: t('other', { keyPrefix: 'delete' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
|
|
@ -67,8 +67,8 @@ const Questions: FC = () => {
|
|||
title: t('title', { keyPrefix: 'delete' }),
|
||||
content:
|
||||
item.answer_count > 0
|
||||
? `<p>${t('question', { keyPrefix: 'delete' })}</p>`
|
||||
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
|
||||
? t('question', { keyPrefix: 'delete' })
|
||||
: t('other', { keyPrefix: 'delete' }),
|
||||
cancelBtnVariant: 'link',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
|
|
|
@ -19,14 +19,14 @@ const Index: FC = () => {
|
|||
type: 'number',
|
||||
title: t('permalink.label'),
|
||||
description: t('permalink.text'),
|
||||
enum: [1, 2, 3, 4],
|
||||
enum: [4, 3, 2, 1],
|
||||
enumNames: [
|
||||
'/questions/10010000000000001/post-title',
|
||||
'/questions/10010000000000001',
|
||||
'/questions/D1D1/post-title',
|
||||
'/questions/D1D1',
|
||||
'/questions/D1D1/post-title',
|
||||
'/questions/10010000000000001',
|
||||
'/questions/10010000000000001/post-title',
|
||||
],
|
||||
default: 1,
|
||||
default: 4,
|
||||
},
|
||||
robots: {
|
||||
type: 'string',
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async';
|
|||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { toastStore, loginToContinueStore, errorCode } from '@/stores';
|
||||
import { toastStore, loginToContinueStore, errorCodeStore } from '@/stores';
|
||||
import {
|
||||
Header,
|
||||
Footer,
|
||||
|
@ -12,11 +12,10 @@ import {
|
|||
Customize,
|
||||
CustomizeTheme,
|
||||
PageTags,
|
||||
HttpErrorContent,
|
||||
} from '@/components';
|
||||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
import { useImgViewer } from '@/hooks';
|
||||
import Component404 from '@/pages/404';
|
||||
import Component50X from '@/pages/50X';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const location = useLocation();
|
||||
|
@ -24,8 +23,7 @@ const Layout: FC = () => {
|
|||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
const { code: httpStatusCode, reset: httpStatusReset } = errorCode();
|
||||
|
||||
const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore();
|
||||
const imgViewer = useImgViewer();
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
|
||||
|
@ -45,10 +43,8 @@ const Layout: FC = () => {
|
|||
<div
|
||||
className="position-relative page-wrap"
|
||||
onClick={imgViewer.checkClickForImgView}>
|
||||
{httpStatusCode === '404' ? (
|
||||
<Component404 />
|
||||
) : httpStatusCode === '50X' ? (
|
||||
<Component50X />
|
||||
{httpStatusCode ? (
|
||||
<HttpErrorContent httpCode={httpStatusCode} />
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { useLegalPrivacy } from '@/services';
|
||||
import { htmlRender } from '@/components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||
|
@ -12,6 +13,15 @@ const Index: FC = () => {
|
|||
const { data: privacy } = useLegalPrivacy();
|
||||
const contentText = privacy?.privacy_policy_original_text;
|
||||
let matchUrl: URL | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const fmt = document.querySelector('.fmt') as HTMLElement;
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
htmlRender(fmt);
|
||||
}, [privacy?.privacy_policy_parsed_text]);
|
||||
|
||||
try {
|
||||
if (contentText) {
|
||||
matchUrl = new URL(contentText);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { useLegalTos } from '@/services';
|
||||
import { htmlRender } from '@/components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||
|
@ -12,6 +13,15 @@ const Index: FC = () => {
|
|||
const { data: tos } = useLegalTos();
|
||||
const contentText = tos?.terms_of_service_original_text;
|
||||
let matchUrl: URL | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const fmt = document.querySelector('.fmt') as HTMLElement;
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
htmlRender(fmt);
|
||||
}, [tos?.terms_of_service_parsed_text]);
|
||||
|
||||
try {
|
||||
if (contentText) {
|
||||
matchUrl = new URL(contentText);
|
||||
|
@ -22,8 +32,9 @@ const Index: FC = () => {
|
|||
window.location.replace(matchUrl.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="mb-4">{t('tos')}</h3>
|
||||
<div
|
||||
className="fmt"
|
||||
|
@ -31,7 +42,7 @@ const Index: FC = () => {
|
|||
__html: tos?.terms_of_service_parsed_text || '',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -16,9 +16,10 @@ import {
|
|||
questionDetail,
|
||||
modifyQuestion,
|
||||
useQueryRevisions,
|
||||
postAnswer,
|
||||
// postAnswer,
|
||||
useQueryQuestionByTitle,
|
||||
getTagsBySlugName,
|
||||
saveQuestionWidthAnaser,
|
||||
} from '@/services';
|
||||
import { handleFormError, SaveDraft, storageExpires } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
@ -29,7 +30,7 @@ interface FormDataItem {
|
|||
title: Type.FormValue<string>;
|
||||
tags: Type.FormValue<Type.Tag[]>;
|
||||
content: Type.FormValue<string>;
|
||||
answer: Type.FormValue<string>;
|
||||
answer_content: Type.FormValue<string>;
|
||||
edit_summary: Type.FormValue<string>;
|
||||
}
|
||||
|
||||
|
@ -52,7 +53,7 @@ const Ask = () => {
|
|||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
answer: {
|
||||
answer_content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
|
@ -92,7 +93,7 @@ const Ask = () => {
|
|||
return;
|
||||
}
|
||||
getTagsBySlugName(queryTags).then((tags) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
// eslint-disable-next-line
|
||||
handleTagsChange(tags);
|
||||
});
|
||||
};
|
||||
|
@ -116,8 +117,8 @@ const Ask = () => {
|
|||
formData.title.value = draft.title;
|
||||
formData.content.value = draft.content;
|
||||
formData.tags.value = draft.tags;
|
||||
formData.answer.value = draft.answer;
|
||||
setCheckState(Boolean(draft.answer));
|
||||
formData.answer_content.value = draft.answer_content;
|
||||
setCheckState(Boolean(draft.answer_content));
|
||||
setHasDraft(true);
|
||||
setFormData({ ...formData });
|
||||
} else {
|
||||
|
@ -131,7 +132,7 @@ const Ask = () => {
|
|||
}, [qid]);
|
||||
|
||||
useEffect(() => {
|
||||
const { title, tags, content, answer } = formData;
|
||||
const { title, tags, content, answer_content } = formData;
|
||||
const { title: editTitle, tags: editTags, content: editContent } = immData;
|
||||
|
||||
// edited
|
||||
|
@ -151,14 +152,19 @@ const Ask = () => {
|
|||
return;
|
||||
}
|
||||
// write
|
||||
if (title.value || tags.value.length > 0 || content.value || answer.value) {
|
||||
if (
|
||||
title.value ||
|
||||
tags.value.length > 0 ||
|
||||
content.value ||
|
||||
answer_content.value
|
||||
) {
|
||||
// save draft
|
||||
saveDraft.save({
|
||||
params: {
|
||||
title: title.value,
|
||||
tags: tags.value,
|
||||
content: content.value,
|
||||
answer: answer.value,
|
||||
answer_content: answer_content.value,
|
||||
},
|
||||
callback: () => setHasDraft(true),
|
||||
});
|
||||
|
@ -215,7 +221,7 @@ const Ask = () => {
|
|||
const handleAnswerChange = (value: string) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
answer: { ...formData.answer, value, errorMsg: '' },
|
||||
answer_content: { ...formData.answer_content, value, errorMsg: '' },
|
||||
});
|
||||
|
||||
const handleSummaryChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
|
@ -263,31 +269,30 @@ const Ask = () => {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
const res = await saveQuestion(params).catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
let res;
|
||||
if (checked) {
|
||||
res = await saveQuestionWidthAnaser({
|
||||
...params,
|
||||
answer_content: formData.answer_content.value,
|
||||
}).catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res = await saveQuestion(params).catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const id = res?.id;
|
||||
const id = res?.id || res?.question?.id;
|
||||
if (id) {
|
||||
if (checked) {
|
||||
postAnswer({
|
||||
question_id: id,
|
||||
content: formData.answer.value,
|
||||
})
|
||||
.then(() => {
|
||||
navigate(pathFactory.questionLanding(id, params.url_title));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData, [
|
||||
{ from: 'content', to: 'answer' },
|
||||
]);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
navigate(pathFactory.questionLanding(id, res?.question?.url_title));
|
||||
} else {
|
||||
navigate(pathFactory.questionLanding(id));
|
||||
}
|
||||
|
@ -448,7 +453,7 @@ const Ask = () => {
|
|||
<Form.Group controlId="answer" className="mt-4">
|
||||
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
||||
<Editor
|
||||
value={formData.answer.value}
|
||||
value={formData.answer_content.value}
|
||||
onChange={handleAnswerChange}
|
||||
ref={editorRef2}
|
||||
className={classNames(
|
||||
|
@ -464,11 +469,11 @@ const Ask = () => {
|
|||
/>
|
||||
<Form.Control
|
||||
type="text"
|
||||
isInvalid={formData.answer.isInvalid}
|
||||
isInvalid={formData.answer_content.isInvalid}
|
||||
hidden
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.answer.errorMsg}
|
||||
{formData.answer_content.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, FC, useEffect, useRef } from 'react';
|
||||
import { Button, Alert } from 'react-bootstrap';
|
||||
import { Button, Alert, Badge } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
@ -20,8 +20,7 @@ interface Props {
|
|||
data: AnswerItem;
|
||||
/** router answer id */
|
||||
aid?: string;
|
||||
/** is author */
|
||||
isAuthor: boolean;
|
||||
canAccept: boolean;
|
||||
questionTitle: string;
|
||||
slugTitle: string;
|
||||
isLogged: boolean;
|
||||
|
@ -30,11 +29,11 @@ interface Props {
|
|||
const Index: FC<Props> = ({
|
||||
aid,
|
||||
data,
|
||||
isAuthor,
|
||||
isLogged,
|
||||
questionTitle = '',
|
||||
slugTitle,
|
||||
callback,
|
||||
canAccept = false,
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'question_detail',
|
||||
|
@ -77,12 +76,21 @@ const Index: FC<Props> = ({
|
|||
{t('post_deleted', { keyPrefix: 'messages' })}
|
||||
</Alert>
|
||||
)}
|
||||
{data?.accepted === 2 && (
|
||||
<div className="mb-3 lh-1">
|
||||
<Badge bg="success" pill>
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
Best answer
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<article
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<Actions
|
||||
source="answer"
|
||||
data={{
|
||||
id: data?.id,
|
||||
isHate: data?.vote_status === 'vote_down',
|
||||
|
@ -95,24 +103,17 @@ const Index: FC<Props> = ({
|
|||
}}
|
||||
/>
|
||||
|
||||
{data?.accepted === 2 && (
|
||||
{canAccept && (
|
||||
<Button
|
||||
disabled={!isAuthor}
|
||||
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>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isAuthor && data.accepted === 1 && (
|
||||
<Button
|
||||
variant="outline-success"
|
||||
variant={data.accepted === 2 ? 'success' : 'outline-success'}
|
||||
className="ms-3"
|
||||
onClick={acceptAnswer}>
|
||||
<Icon name="check-circle-fill" className="me-2" />
|
||||
<span>{t('answers.btn_accept')}</span>
|
||||
<span>
|
||||
{data.accepted === 2
|
||||
? t('answers.btn_accepted')
|
||||
: t('answers.btn_accept')}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -108,12 +108,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
</div>
|
||||
<article
|
||||
ref={ref}
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt text-break text-wrap mt-4"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
|
||||
<Actions
|
||||
className="mt-4"
|
||||
source="question"
|
||||
data={{
|
||||
id: data?.id,
|
||||
isHate: data?.vote_status === 'vote_down',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { memo, useState, FC, useEffect } from 'react';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Alert } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import classNames from 'classnames';
|
||||
|
@ -18,6 +18,7 @@ interface Props {
|
|||
/** question id */
|
||||
qid: string;
|
||||
answered?: boolean;
|
||||
loggedUserRank: number;
|
||||
};
|
||||
callback?: (obj) => void;
|
||||
}
|
||||
|
@ -39,6 +40,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
const [showTips, setShowTips] = useState(data.loggedUserRank < 100);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: Boolean(formData.content.value),
|
||||
|
@ -212,29 +214,58 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
</div>
|
||||
)}
|
||||
{showEditor && (
|
||||
<Editor
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
value: val,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Editor
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
value: val,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
variant="warning"
|
||||
show={data.loggedUserRank < 100 && showTips}
|
||||
onClose={() => setShowTips(false)}
|
||||
dismissible
|
||||
className="mt-3">
|
||||
<p>{t('tips.header_1')}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey="question_detail.write_answer.tips.li1_1"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>{t('tips.li1_2')}</li>
|
||||
</ul>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="question_detail.write_answer.tips.header_2"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
<ul className="mb-0">
|
||||
<li>{t('tips.li2_1')}</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
|
|
|
@ -56,7 +56,9 @@ const Index = () => {
|
|||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const isAdmin = userInfo?.role_id === 2;
|
||||
const isModerator = userInfo?.role_id === 3;
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const loggedUserRank = userInfo?.rank;
|
||||
const { state: locationState } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -221,7 +223,7 @@ const Index = () => {
|
|||
data={item}
|
||||
questionTitle={question?.title || ''}
|
||||
slugTitle={question?.url_title}
|
||||
isAuthor={isAuthor}
|
||||
canAccept={isAuthor || isAdmin || isModerator}
|
||||
callback={initPage}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
|
@ -247,6 +249,7 @@ const Index = () => {
|
|||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
loggedUserRank,
|
||||
}}
|
||||
callback={writeAnswerCallback}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { handleFormError } from '@/utils';
|
||||
import { handleFormError, scrollToDocTop } from '@/utils';
|
||||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Editor, EditorRef, Icon } from '@/components';
|
||||
import { Editor, EditorRef, Icon, htmlRender } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
useQueryAnswerInfo,
|
||||
|
@ -23,31 +23,47 @@ interface FormDataItem {
|
|||
content: Type.FormValue<string>;
|
||||
description: Type.FormValue<string>;
|
||||
}
|
||||
const initFormData = {
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const { aid = '', qid = '' } = useParams();
|
||||
const [focusType, setForceType] = useState('');
|
||||
useLayoutEffect(() => {
|
||||
scrollToDocTop();
|
||||
}, []);
|
||||
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'edit_answer' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const initFormData = {
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { data } = useQueryAnswerInfo(aid);
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData, setImmData] = useState(initFormData);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
|
||||
initFormData.content.value = data?.info.content || '';
|
||||
useLayoutEffect(() => {
|
||||
if (data?.info?.content) {
|
||||
setFormData({
|
||||
...formData,
|
||||
content: {
|
||||
value: data.info.content,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [data?.info?.content]);
|
||||
|
||||
const { data: revisions = [] } = useQueryRevisions(aid);
|
||||
|
||||
|
@ -57,6 +73,13 @@ const Index = () => {
|
|||
|
||||
const questionContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!questionContentRef?.current) {
|
||||
return;
|
||||
}
|
||||
htmlRender(questionContentRef.current);
|
||||
}, [questionContentRef]);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: contentChanged,
|
||||
});
|
||||
|
@ -147,9 +170,11 @@ const Index = () => {
|
|||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
if (revision?.content) {
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
};
|
||||
|
||||
const backPage = () => {
|
||||
|
@ -192,7 +217,7 @@ const Index = () => {
|
|||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="revision" className="mb-3">
|
||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||
<Form.Select onChange={handleSelectedRevision}>
|
||||
<Form.Select onChange={handleSelectedRevision} defaultValue={0}>
|
||||
{revisions.map(({ create_at, reason, user_info }, index) => {
|
||||
const date = dayjs(create_at * 1000)
|
||||
.tz()
|
||||
|
|
|
@ -15,7 +15,7 @@ const Questions: FC = () => {
|
|||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const curOrder = urlSearchParams.get('order') || 'newest';
|
||||
const curOrder = urlSearchParams.get('order') || 'active';
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: 20,
|
||||
page: curPage,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { QueryGroup } from '@/components';
|
||||
|
||||
const sortBtns = ['relevance', 'newest', 'active', 'score'];
|
||||
const sortBtns = ['active', 'newest', 'relevance', 'score'];
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
|
|
|
@ -21,7 +21,7 @@ const Index = () => {
|
|||
const [searchParams] = useSearchParams();
|
||||
const page = searchParams.get('page') || 1;
|
||||
const q = searchParams.get('q') || '';
|
||||
const order = searchParams.get('order') || 'relevance';
|
||||
const order = searchParams.get('order') || 'active';
|
||||
|
||||
const { data, isLoading } = useSearch({
|
||||
q,
|
||||
|
|
|
@ -28,7 +28,7 @@ const Questions: FC = () => {
|
|||
const routeParams = useParams();
|
||||
const curTagName = routeParams.tagName || '';
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curOrder = urlSearchParams.get('order') || 'newest';
|
||||
const curOrder = urlSearchParams.get('order') || 'active';
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: 20,
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Tag, TagSelector, FormatTime, Modal } from '@/components';
|
||||
import { Tag, TagSelector, FormatTime, Modal, htmlRender } from '@/components';
|
||||
import {
|
||||
useTagInfo,
|
||||
useQuerySynonymsTags,
|
||||
|
@ -44,6 +44,15 @@ const TagIntroduction = () => {
|
|||
});
|
||||
}
|
||||
}, [locationState]);
|
||||
|
||||
useEffect(() => {
|
||||
const fmt = document.querySelector('.content.fmt') as HTMLElement;
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
htmlRender(fmt);
|
||||
}, [tagInfo?.parsed_text]);
|
||||
|
||||
if (!tagInfo) {
|
||||
return null;
|
||||
}
|
||||
|
@ -108,7 +117,9 @@ const TagIntroduction = () => {
|
|||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||
confirmBtnVariant: 'danger',
|
||||
onConfirm: () => {
|
||||
deleteTag(tagInfo.tag_id);
|
||||
deleteTag(tagInfo.tag_id).then(() => {
|
||||
navigate('/tags', { replace: true });
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -143,7 +154,7 @@ const TagIntroduction = () => {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="content text-break"
|
||||
className="content text-break fmt"
|
||||
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
|
@ -204,7 +215,8 @@ const TagIntroduction = () => {
|
|||
data={{
|
||||
slug_name: tagName || '',
|
||||
main_tag_slug_name: '',
|
||||
display_name: '',
|
||||
display_name:
|
||||
tagInfo?.display_name || tagInfo?.slug_name || '',
|
||||
recommend: false,
|
||||
reserved: false,
|
||||
}}
|
||||
|
|
|
@ -27,7 +27,7 @@ const Tags = () => {
|
|||
const { role_id } = loggedUserInfoStore((_) => _.user);
|
||||
|
||||
const page = Number(urlSearch.get('page')) || 1;
|
||||
const sort = urlSearch.get('sort');
|
||||
const sort = urlSearch.get('sort') || sortBtns[0];
|
||||
|
||||
const pageSize = 20;
|
||||
const {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { loggedUserInfoStore, siteInfoStore } from '@/stores';
|
||||
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
|
||||
import { changeEmailVerify } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||
|
@ -20,12 +20,12 @@ const Index: FC = () => {
|
|||
if (code) {
|
||||
// do
|
||||
changeEmailVerify({ code })
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
setStep('success');
|
||||
getLoggedUserInfo().then((res) => {
|
||||
if (res?.access_token) {
|
||||
// update user info
|
||||
updateUser(res);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setStep('invalid');
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Error50X from '@/pages/50X';
|
||||
// import Page404 from '@/pages/404';
|
||||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <Error50X />;
|
||||
const Index = ({ errCode = '50X', errMsg = '' }) => {
|
||||
return <HttpErrorContent httpCode={errCode} errMsg={errMsg} />;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
|
@ -1,41 +1,53 @@
|
|||
import { FC, ReactNode, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { TGuardFunc } from '@/utils/guard';
|
||||
|
||||
const Index: FC<{
|
||||
import RouteErrorBoundary from './RouteErrorBoundary';
|
||||
|
||||
const RouteGuard: FC<{
|
||||
children: ReactNode;
|
||||
onEnter?: TGuardFunc;
|
||||
onEnter: TGuardFunc;
|
||||
path?: string;
|
||||
}> = ({
|
||||
children,
|
||||
onEnter,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
path,
|
||||
}) => {
|
||||
page?: string;
|
||||
}> = ({ children, onEnter, path, page }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const callGuards = () => {
|
||||
if (onEnter) {
|
||||
const gr = onEnter();
|
||||
const redirectUrl = gr.redirect;
|
||||
if (redirectUrl) {
|
||||
floppyNavigation.navigate(redirectUrl, () => {
|
||||
navigate(redirectUrl, { replace: true });
|
||||
});
|
||||
}
|
||||
const loaderData = useLoaderData();
|
||||
const gr = onEnter({
|
||||
loaderData,
|
||||
path,
|
||||
page,
|
||||
});
|
||||
|
||||
let guardError;
|
||||
const errCode = gr.error?.code;
|
||||
if (errCode === '403' || errCode === '404' || errCode === '50X') {
|
||||
guardError = {
|
||||
code: errCode,
|
||||
msg: gr.error?.msg,
|
||||
};
|
||||
}
|
||||
const handleGuardRedirect = () => {
|
||||
const redirectUrl = gr.redirect;
|
||||
if (redirectUrl) {
|
||||
floppyNavigation.navigate(redirectUrl, () => {
|
||||
navigate(redirectUrl, { replace: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
callGuards();
|
||||
handleGuardRedirect();
|
||||
}, [location]);
|
||||
return (
|
||||
<>
|
||||
{/* Route Guard */}
|
||||
{children}
|
||||
{gr.ok ? children : null}
|
||||
{!gr.ok && guardError ? (
|
||||
<RouteErrorBoundary errCode={guardError.code} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default RouteGuard;
|
||||
|
|
|
@ -13,7 +13,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
|||
routeNodes.forEach((rn) => {
|
||||
if (rn.page === 'pages/Layout') {
|
||||
rn.element = rn.guard ? (
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path}>
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>
|
||||
<Layout />
|
||||
</RouteGuard>
|
||||
) : (
|
||||
|
@ -30,7 +30,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
|||
rn.element = (
|
||||
<Suspense>
|
||||
{rn.guard ? (
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path}>
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>
|
||||
<Ctrl />
|
||||
</RouteGuard>
|
||||
) : (
|
||||
|
|
|
@ -19,6 +19,9 @@ const tagEdit = (tagId: string) => {
|
|||
};
|
||||
const questionLanding = (questionId: string, slugTitle: string = '') => {
|
||||
const { seo } = seoSettingStore.getState();
|
||||
if (!questionId) {
|
||||
return slugTitle ? `/questions/null/${slugTitle}` : '/questions/null';
|
||||
}
|
||||
// @ts-ignore
|
||||
if (/[13]/.test(seo.permalink) && slugTitle) {
|
||||
return urlcat('/questions/:questionId/:slugPermalink', {
|
||||
|
|
|
@ -2,6 +2,8 @@ import type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom';
|
|||
|
||||
import { guard } from '@/utils';
|
||||
import type { TGuardFunc } from '@/utils/guard';
|
||||
import { editCheck } from '@/services';
|
||||
import { isEditable } from '@/utils/guard';
|
||||
|
||||
type IndexRouteNode = Omit<IndexRouteObject, 'children'>;
|
||||
type NonIndexRouteNode = Omit<NonIndexRouteObject, 'children'>;
|
||||
|
@ -74,6 +76,13 @@ const routes: RouteNode[] = [
|
|||
{
|
||||
path: 'posts/:qid/:aid/edit',
|
||||
page: 'pages/Questions/EditAnswer',
|
||||
loader: async ({ params }) => {
|
||||
const ret = await editCheck(params.aid as string, true);
|
||||
return ret;
|
||||
},
|
||||
guard: (args) => {
|
||||
return isEditable(args);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
|
|
|
@ -34,7 +34,7 @@ export const useQueryNotificationStatus = () => {
|
|||
|
||||
return useSWR<Type.NotificationStatus>(
|
||||
tryLoggedAndActivated().ok ? apiUrl : null,
|
||||
request.instance.get,
|
||||
(url) => request.get(url, { ignoreError: '50X' }),
|
||||
{
|
||||
refreshInterval: 3000,
|
||||
},
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import request from '@/utils/request';
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
export const editCheck = (id: string) => {
|
||||
export const editCheck = (id: string, passingError: boolean = false) => {
|
||||
const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`;
|
||||
return request.get(apiUrl);
|
||||
return request.get(apiUrl, {
|
||||
passingError,
|
||||
});
|
||||
};
|
||||
|
||||
export const revisionAudit = (id: string, operation: 'approve' | 'reject') => {
|
||||
|
|
|
@ -274,3 +274,7 @@ export const markdownToHtml = (content: string) => {
|
|||
const apiUrl = '/answer/api/v1/post/render';
|
||||
return request.post(apiUrl, { content });
|
||||
};
|
||||
|
||||
export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => {
|
||||
return request.post('/answer/api/v1/question/answer', params);
|
||||
};
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import create from 'zustand';
|
||||
|
||||
type codeType = '404' | '50X' | '';
|
||||
type codeType = '403' | '404' | '50X' | '';
|
||||
|
||||
interface NotFoundType {
|
||||
interface ErrorCodeType {
|
||||
code: codeType;
|
||||
update: (code: codeType) => void;
|
||||
msg: string;
|
||||
update: (code: codeType, msg?: string) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const notFound = create<NotFoundType>((set) => ({
|
||||
const Index = create<ErrorCodeType>((set) => ({
|
||||
code: '',
|
||||
update: (code: codeType) => {
|
||||
msg: '',
|
||||
update: (code: codeType, msg: string = '') => {
|
||||
set(() => {
|
||||
return { code };
|
||||
return { code, msg };
|
||||
});
|
||||
},
|
||||
reset: () => {
|
||||
set(() => {
|
||||
return { code: '' };
|
||||
return { code: '', msg: '' };
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
export default notFound;
|
||||
export default Index;
|
||||
|
|
|
@ -10,7 +10,7 @@ import pageTagStore from './pageTags';
|
|||
import customizeStore from './customize';
|
||||
import themeSettingStore from './themeSetting';
|
||||
import loginToContinueStore from './loginToContinue';
|
||||
import errorCode from './errorCode';
|
||||
import errorCodeStore from './errorCode';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
|
@ -24,5 +24,5 @@ export {
|
|||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
errorCode,
|
||||
errorCodeStore,
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ const initUser: UserInfoRes = {
|
|||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
user: initUser,
|
||||
update: (params) => {
|
||||
if (!params.language) {
|
||||
if (!params?.language) {
|
||||
params.language = 'Default';
|
||||
}
|
||||
set(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@ interface IProps {
|
|||
update: (params: AdminSettingsSeo) => void;
|
||||
}
|
||||
|
||||
const siteInfo = create<IProps>((set) => ({
|
||||
const Index = create<IProps>((set) => ({
|
||||
seo: {
|
||||
robots: '',
|
||||
permalink: 1,
|
||||
|
@ -25,4 +25,4 @@ const siteInfo = create<IProps>((set) => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
export default siteInfo;
|
||||
export default Index;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import i18next from 'i18next';
|
||||
import parse from 'html-react-parser';
|
||||
import * as DOMPurify from 'dompurify';
|
||||
|
||||
const Diff = require('diff');
|
||||
|
||||
|
@ -235,32 +233,6 @@ function diffText(newText: string, oldText?: string): string {
|
|||
return result.join('');
|
||||
}
|
||||
|
||||
function htmlToReact(html: string) {
|
||||
const cleanedHtml = DOMPurify.sanitize(html, {
|
||||
USE_PROFILES: { html: true },
|
||||
});
|
||||
|
||||
const ele = document.createElement('div');
|
||||
ele.innerHTML = cleanedHtml;
|
||||
|
||||
ele.querySelectorAll('table').forEach((table) => {
|
||||
if (
|
||||
(!table || (table.parentNode as HTMLDivElement))?.classList.contains(
|
||||
'table-responsive',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
table.classList.add('table', 'table-bordered');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'table-responsive';
|
||||
table.parentNode?.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
});
|
||||
return parse(ele.innerHTML);
|
||||
}
|
||||
|
||||
function base64ToSvg(base64: string) {
|
||||
// base64 to svg xml
|
||||
const svgxml = atob(base64);
|
||||
|
@ -296,6 +268,5 @@ export {
|
|||
labelStyle,
|
||||
handleFormError,
|
||||
diffText,
|
||||
htmlToReact,
|
||||
base64ToSvg,
|
||||
};
|
||||
|
|
|
@ -30,8 +30,16 @@ type TLoginState = {
|
|||
export type TGuardResult = {
|
||||
ok: boolean;
|
||||
redirect?: string;
|
||||
error?: {
|
||||
code?: number | string;
|
||||
msg?: string;
|
||||
};
|
||||
};
|
||||
export type TGuardFunc = () => TGuardResult;
|
||||
export type TGuardFunc = (args: {
|
||||
loaderData?: any;
|
||||
path?: string;
|
||||
page?: string;
|
||||
}) => TGuardResult;
|
||||
|
||||
export const deriveLoginState = (): TLoginState => {
|
||||
const ls: TLoginState = {
|
||||
|
@ -173,7 +181,11 @@ export const admin = () => {
|
|||
const us = deriveLoginState();
|
||||
if (gr.ok && !us.isAdmin) {
|
||||
gr.ok = false;
|
||||
gr.redirect = RouteAlias.home;
|
||||
gr.error = {
|
||||
code: '403',
|
||||
msg: '',
|
||||
};
|
||||
gr.redirect = '';
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
@ -183,7 +195,24 @@ export const isAdminOrModerator = () => {
|
|||
const us = deriveLoginState();
|
||||
if (gr.ok && !us.isAdmin && !us.isModerator) {
|
||||
gr.ok = false;
|
||||
gr.redirect = RouteAlias.home;
|
||||
gr.error = {
|
||||
code: '403',
|
||||
msg: '',
|
||||
};
|
||||
gr.redirect = '';
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
||||
export const isEditable = (args) => {
|
||||
const loaderData = args?.loaderData || {};
|
||||
const gr: TGuardResult = { ok: true };
|
||||
if (loaderData.code === 400) {
|
||||
gr.ok = false;
|
||||
gr.error = {
|
||||
code: '403',
|
||||
msg: loaderData.msg,
|
||||
};
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios';
|
|||
import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||
|
||||
import { Modal } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore, errorCode } from '@/stores';
|
||||
import { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { getCurrentLang } from '@/utils/localize';
|
||||
|
@ -16,8 +16,12 @@ const baseConfig = {
|
|||
withCredentials: true,
|
||||
};
|
||||
|
||||
interface APIconfig extends AxiosRequestConfig {
|
||||
allow404: boolean;
|
||||
interface ApiConfig extends AxiosRequestConfig {
|
||||
// Configure whether to allow takeover of 404 errors
|
||||
allow404?: boolean;
|
||||
ignoreError?: '403' | '50X';
|
||||
// Configure whether to pass errors directly
|
||||
passingError?: boolean;
|
||||
}
|
||||
|
||||
class Request {
|
||||
|
@ -52,14 +56,17 @@ class Request {
|
|||
return data;
|
||||
},
|
||||
(error) => {
|
||||
const { status, data: respData } = error.response || {};
|
||||
const { data = {}, msg = '', reason = '' } = respData || {};
|
||||
|
||||
console.log('response error:', error);
|
||||
|
||||
const {
|
||||
status,
|
||||
data: errModel,
|
||||
config: errConfig,
|
||||
} = error.response || {};
|
||||
const { data = {}, msg = '' } = errModel || {};
|
||||
if (status === 400) {
|
||||
// show error message
|
||||
if (data instanceof Object && data.err_type) {
|
||||
if (data?.err_type && errConfig?.passingError) {
|
||||
return errModel;
|
||||
}
|
||||
if (data?.err_type) {
|
||||
if (data.err_type === 'toast') {
|
||||
// toast error message
|
||||
toastStore.getState().show({
|
||||
|
@ -90,7 +97,6 @@ class Request {
|
|||
return Promise.reject({
|
||||
code: status,
|
||||
msg,
|
||||
reason,
|
||||
isError: true,
|
||||
list: data,
|
||||
});
|
||||
|
@ -107,14 +113,13 @@ class Request {
|
|||
// 401: Re-login required
|
||||
if (status === 401) {
|
||||
// clear userinfo
|
||||
errorCode.getState().reset();
|
||||
errorCodeStore.getState().reset();
|
||||
loggedUserInfoStore.getState().clear();
|
||||
floppyNavigation.navigateToLogin();
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (status === 403) {
|
||||
// Permission interception
|
||||
errorCode.getState().reset();
|
||||
if (data?.type === 'url_expired') {
|
||||
// url expired
|
||||
floppyNavigation.navigate(RouteAlias.activationFailed, () => {
|
||||
|
@ -137,6 +142,14 @@ class Request {
|
|||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (isIgnoredPath(IGNORE_PATH_LIST)) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (error.config?.url.includes('/admin/api')) {
|
||||
errorCodeStore.getState().update('403');
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
toastStore.getState().show({
|
||||
msg,
|
||||
|
@ -150,14 +163,18 @@ class Request {
|
|||
if (isIgnoredPath(IGNORE_PATH_LIST)) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
errorCode.getState().update('404');
|
||||
errorCodeStore.getState().update('404');
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (status >= 500) {
|
||||
if (isIgnoredPath(IGNORE_PATH_LIST)) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
errorCode.getState().update('50X');
|
||||
|
||||
if (error.config?.ignoreError !== '50X') {
|
||||
errorCodeStore.getState().update('50X');
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Request failed with status code ${status}, ${msg || ''}`,
|
||||
);
|
||||
|
@ -171,7 +188,7 @@ class Request {
|
|||
return this.instance.request(config);
|
||||
}
|
||||
|
||||
public get<T = any>(url: string, config?: APIconfig): Promise<T> {
|
||||
public get<T = any>(url: string, config?: ApiConfig): Promise<T> {
|
||||
return this.instance.get(url, config);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export type QuestionDraft = {
|
|||
title: string;
|
||||
content: string;
|
||||
tags: any[];
|
||||
answer: string;
|
||||
answer_content: string;
|
||||
};
|
||||
callback?: () => void;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noImplicitAny": false,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
|
Loading…
Reference in New Issue