mirror of https://gitee.com/answerdev/answer.git
feat(navigation): guard route done
This commit is contained in:
parent
1ba76183fe
commit
a801ff6cda
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
extends: ['@commitlint/routes-conventional'],
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import router from '@/router';
|
||||
import { routes, createBrowserRouter } from '@/router';
|
||||
|
||||
function App() {
|
||||
const router = createBrowserRouter(routes);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export const LOGIN_NEED_BACK = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/password-reset',
|
||||
];
|
||||
export const DEFAULT_LANG = 'en_US';
|
||||
export const CURRENT_LANG_STORAGE_KEY = '_a_lang__';
|
||||
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
|
|
|
@ -108,7 +108,7 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
is_admin?: true;
|
||||
is_admin?: boolean;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
|
|
|
@ -5,11 +5,12 @@ import { useTranslation } from 'react-i18next';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { bookmark, postVote } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { useToast } from '@answer/hooks';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
import { bookmark, postVote } from '@/services';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
data: {
|
||||
|
@ -32,7 +33,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
state: data?.collected,
|
||||
count: data?.collectCount,
|
||||
});
|
||||
const { username = '' } = userInfoStore((state) => state.user);
|
||||
const { username = '' } = loggedUserInfoStore((state) => state.user);
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
|
@ -48,7 +49,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
}, []);
|
||||
|
||||
const handleVote = (type: 'up' | 'down') => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -84,7 +85,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
};
|
||||
|
||||
const handleBookmark = () => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
bookmark({
|
||||
|
|
|
@ -8,18 +8,20 @@ import { unionBy } from 'lodash';
|
|||
import { marked } from 'marked';
|
||||
|
||||
import * as Types from '@answer/common/interface';
|
||||
import { Modal } from '@answer/components';
|
||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
||||
import { matchedUsers, parseUserInfo } from '@answer/utils';
|
||||
|
||||
import { Form, ActionBar, Reply } from './components';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
import {
|
||||
useQueryComments,
|
||||
addComment,
|
||||
deleteComment,
|
||||
updateComment,
|
||||
postVote,
|
||||
} from '@answer/api';
|
||||
import { Modal } from '@answer/components';
|
||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
||||
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
|
||||
|
||||
import { Form, ActionBar, Reply } from './components';
|
||||
} from '@/services';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
@ -163,7 +165,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleVote = (id, is_cancel) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -189,7 +191,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleAction = ({ action }, item) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (action === 'report') {
|
||||
|
|
|
@ -3,10 +3,11 @@ import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal as AnswerModal } from '@answer/components';
|
||||
import { uploadImage } from '@answer/api';
|
||||
import ToolItem from '../toolItem';
|
||||
import { IEditorContext } from '../types';
|
||||
|
||||
import { uploadImage } from '@/services';
|
||||
|
||||
const Image: FC<IEditorContext> = ({ editor }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next';
|
|||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { TagSelector, Tag } from '@answer/components';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { useFollowingTags, followTags } from '@answer/api';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
import { useFollowingTags, followTags } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
@ -32,7 +33,7 @@ const Index: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
if (!isLogin()) {
|
||||
if (!tryNormalLogged()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,17 +17,22 @@ import {
|
|||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@answer/api';
|
||||
import Storage from '@answer/utils/storage';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
interfaceStore,
|
||||
} from '@answer/stores';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Header: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, clear } = userInfoStore();
|
||||
const { user, clear } = loggedUserInfoStore();
|
||||
const { t } = useTranslation();
|
||||
const [urlSearch] = useSearchParams();
|
||||
const q = urlSearch.get('q');
|
||||
|
@ -42,9 +47,8 @@ const Header: FC = () => {
|
|||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
Storage.remove('token');
|
||||
clear();
|
||||
navigate('/');
|
||||
navigate(RouteAlias.home);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,9 +3,10 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useHotQuestions } from '@answer/api';
|
||||
import { Icon } from '@answer/components';
|
||||
|
||||
import { useHotQuestions } from '@/services';
|
||||
|
||||
const HotQuestions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const [questions, setQuestions] = useState<any>([]);
|
||||
|
|
|
@ -9,6 +9,9 @@ import type {
|
|||
ImgCodeRes,
|
||||
} from '@answer/common/interface';
|
||||
|
||||
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
interface IProps {
|
||||
/** control visible */
|
||||
visible: boolean;
|
||||
|
@ -55,7 +58,7 @@ const Index: React.FC<IProps> = ({
|
|||
placeholder={t('placeholder')}
|
||||
isInvalid={captcha.isInvalid}
|
||||
onChange={(e) => {
|
||||
localStorage.setItem('captchaCode', e.target.value);
|
||||
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||
handleCaptcha({
|
||||
captcha_code: {
|
||||
value: e.target.value,
|
||||
|
|
|
@ -5,10 +5,11 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { Modal } from '@answer/components';
|
||||
import { useReportModal, useToast } from '@answer/hooks';
|
||||
import { deleteQuestion, deleteAnswer } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import Share from '../Share';
|
||||
|
||||
import { deleteQuestion, deleteAnswer } from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
qid: string;
|
||||
|
@ -98,7 +99,7 @@ const Index: FC<IProps> = ({
|
|||
};
|
||||
|
||||
const handleAction = (action) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap';
|
|||
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQuestionList } from '@answer/api';
|
||||
import { useQuestionList } from '@/services';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import {
|
||||
Icon,
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { FacebookShareButton, TwitterShareButton } from 'next-share';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
|
@ -15,7 +15,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
const Index: FC<IProps> = ({ type, qid, aid, title }) => {
|
||||
const user = userInfoStore((state) => state.user);
|
||||
const user = loggedUserInfoStore((state) => state.user);
|
||||
const [show, setShow] = useState(false);
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
const [canSystemShare, setSystemShareState] = useState(false);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { marked } from 'marked';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { useTagModal } from '@answer/hooks';
|
||||
import { queryTags } from '@answer/api';
|
||||
import { queryTags } from '@/services';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import './index.scss';
|
||||
|
|
|
@ -2,14 +2,17 @@ import React, { useState, useEffect } from 'react';
|
|||
import { Button, Col } from 'react-bootstrap';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { resendEmail, checkImgCode } from '@answer/api';
|
||||
import { resendEmail, checkImgCode } from '@/services';
|
||||
import { PicAuthCodeModal } from '@answer/components/Modal';
|
||||
import type {
|
||||
ImgCodeRes,
|
||||
ImgCodeReq,
|
||||
FormDataType,
|
||||
} from '@answer/common/interface';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
|
||||
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
|
@ -19,7 +22,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
|||
const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
|
||||
const [isSuccess, setSuccess] = useState(false);
|
||||
const [showModal, setModalState] = useState(false);
|
||||
const { e_mail } = userInfoStore((state) => state.user);
|
||||
const { e_mail } = loggedUserInfoStore((state) => state.user);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
captcha_code: {
|
||||
value: '',
|
||||
|
@ -47,7 +50,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
|||
}
|
||||
let obj: ImgCodeReq = {};
|
||||
if (imgCode.verify) {
|
||||
const code = localStorage.getItem('captchaCode') || '';
|
||||
const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
|
||||
obj = {
|
||||
captcha_code: code,
|
||||
captcha_id: imgCode.captcha_id,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { changeUserStatus } from '@answer/api';
|
||||
import { changeUserStatus } from '@/services';
|
||||
import { Modal as AnswerModal } from '@answer/components';
|
||||
|
||||
const div = document.createElement('div');
|
||||
|
|
|
@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { reportList, postReport, closeQuestion, putReport } from '@answer/api';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import { reportList, postReport, closeQuestion, putReport } from '@/services';
|
||||
|
||||
interface Params {
|
||||
isBackend?: boolean;
|
||||
type: Type.ReportType;
|
||||
|
|
|
@ -6,6 +6,8 @@ import Backend from 'i18next-http-backend';
|
|||
import en from './locales/en.json';
|
||||
import zh from './locales/zh_CN.json';
|
||||
|
||||
import { DEFAULT_LANG } from '@/common/constants';
|
||||
|
||||
i18next
|
||||
// load translation using http
|
||||
.use(Backend)
|
||||
|
@ -21,7 +23,7 @@ i18next
|
|||
},
|
||||
},
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
fallbackLng: process.env.REACT_APP_LANG || 'en_US',
|
||||
fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
|
|
@ -3,14 +3,26 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
|
||||
import { pullLoggedUser } from '@/utils/guards';
|
||||
|
||||
import './i18n/init';
|
||||
import './index.scss';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
async function bootstrapApp() {
|
||||
/**
|
||||
* NOTICE: must pre init logged user info for router
|
||||
*/
|
||||
await pullLoggedUser();
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
bootstrapApp();
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@answer/components';
|
||||
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
|
||||
import { useEditStatusModal } from '@answer/hooks';
|
||||
import { useAnswerSearch, changeAnswerStatus } from '@answer/api';
|
||||
import { useAnswerSearch, changeAnswerStatus } from '@/services';
|
||||
import * as Type from '@answer/common/interface';
|
||||
|
||||
import '../index.scss';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@answer/components';
|
||||
import { useReportModal } from '@answer/hooks';
|
||||
import * as Type from '@answer/common/interface';
|
||||
import { useFlagSearch } from '@answer/api';
|
||||
import { useFlagSearch } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import type * as Type from '@answer/common/interface';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { siteInfoStore } from '@answer/stores';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@answer/api';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
updateInterfaceSetting,
|
||||
useInterfaceSetting,
|
||||
useThemeOptions,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
import { interfaceStore } from '@answer/stores';
|
||||
import { UploadImg } from '@answer/components';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
useQuestionSearch,
|
||||
changeQuestionStatus,
|
||||
deleteQuestion,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
import * as Type from '@answer/common/interface';
|
||||
|
||||
import '../index.scss';
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Button, Form, Table, Badge } from 'react-bootstrap';
|
|||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQueryUsers } from '@answer/api';
|
||||
import { useQueryUsers } from '@/services';
|
||||
import {
|
||||
Pagination,
|
||||
FormatTime,
|
||||
|
|
|
@ -1,60 +1,43 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { FC, useEffect, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import {
|
||||
userInfoStore,
|
||||
siteInfoStore,
|
||||
interfaceStore,
|
||||
toastStore,
|
||||
} from '@answer/stores';
|
||||
import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores';
|
||||
import { Header, AdminHeader, Footer, Toast } from '@answer/components';
|
||||
import { useSiteSettings, useCheckUserStatus } from '@answer/api';
|
||||
import { useSiteSettings } from '@/services';
|
||||
|
||||
import Storage from '@/utils/storage';
|
||||
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
let isMounted = false;
|
||||
const Layout: FC = () => {
|
||||
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
|
||||
const { update: interfaceStoreUpdate } = interfaceStore();
|
||||
const { data: siteSettings } = useSiteSettings();
|
||||
const { data: userStatus } = useCheckUserStatus();
|
||||
useEffect(() => {
|
||||
if (siteSettings) {
|
||||
siteStoreUpdate(siteSettings.general);
|
||||
interfaceStoreUpdate(siteSettings.interface);
|
||||
}
|
||||
}, [siteSettings]);
|
||||
const updateUser = userInfoStore((state) => state.update);
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (siteSettings) {
|
||||
siteStoreUpdate(siteSettings.general);
|
||||
interfaceStoreUpdate(siteSettings.interface);
|
||||
}
|
||||
}, [siteSettings]);
|
||||
if (!isMounted) {
|
||||
isMounted = true;
|
||||
const lang = Storage.get('LANG');
|
||||
const user = Storage.get('userInfo');
|
||||
if (user) {
|
||||
updateUser(user);
|
||||
}
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||
if (lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
}
|
||||
}
|
||||
|
||||
if (userStatus?.status) {
|
||||
const user = Storage.get('userInfo');
|
||||
if (userStatus.status !== user.status) {
|
||||
user.status = userStatus?.status;
|
||||
updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
|
@ -76,4 +59,4 @@ const Layout: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
export default memo(Layout);
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
useQueryRevisions,
|
||||
postAnswer,
|
||||
useQueryQuestionByTitle,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import SearchQuestion from './components/SearchQuestion';
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@answer/components';
|
||||
import { acceptanceAnswer } from '@answer/api';
|
||||
import { acceptanceAnswer } from '@/services';
|
||||
import { scrollTop } from '@answer/utils';
|
||||
import { AnswerItem } from '@answer/common/interface';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
htmlRender,
|
||||
} from '@answer/components';
|
||||
import { formatCount } from '@answer/utils';
|
||||
import { following } from '@answer/api';
|
||||
import { following } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
|
@ -3,16 +3,16 @@ import { Card, ListGroup } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSimilarQuestion } from '@answer/api';
|
||||
import { useSimilarQuestion } from '@/services';
|
||||
import { Icon } from '@answer/components';
|
||||
|
||||
import { userInfoStore } from '@/stores';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ id }) => {
|
||||
const { user } = userInfoStore();
|
||||
const { user } = loggedUserInfoStore();
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'related_question',
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import { marked } from 'marked';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { Editor, Modal } from '@answer/components';
|
||||
import { postAnswer } from '@answer/api';
|
||||
import { postAnswer } from '@/services';
|
||||
import { FormDataType } from '@answer/common/interface';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
|
|||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { questionDetail, getAnswers } from '@answer/api';
|
||||
import { questionDetail, getAnswers } from '@/services';
|
||||
import { Pagination, PageTitle } from '@answer/components';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { scrollTop } from '@answer/utils';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import type {
|
||||
|
@ -37,7 +37,7 @@ const Index = () => {
|
|||
list: [],
|
||||
});
|
||||
const { setUsers } = usePageUsers();
|
||||
const userInfo = userInfoStore((state) => state.user);
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const requestAnswers = async () => {
|
||||
const res = await getAnswers({
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
useQueryAnswerInfo,
|
||||
modifyAnswer,
|
||||
useQueryRevisions,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import './index.scss';
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom';
|
|||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { following } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { following } from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
|
||||
interface Props {
|
||||
data;
|
||||
|
@ -20,7 +20,7 @@ const Index: FC<Props> = ({ data }) => {
|
|||
const [followed, setFollowed] = useState(data?.is_follower);
|
||||
|
||||
const follow = () => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Pagination, PageTitle } from '@answer/components';
|
||||
import { useSearch } from '@answer/api';
|
||||
import { useSearch } from '@/services';
|
||||
|
||||
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import * as Type from '@answer/common/interface';
|
||||
import { PageTitle, FollowingTags } from '@answer/components';
|
||||
import { useTagInfo, useFollow } from '@answer/api';
|
||||
import { useTagInfo, useFollow } from '@/services';
|
||||
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
|
|
|
@ -7,8 +7,8 @@ import dayjs from 'dayjs';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { Editor, EditorRef, PageTitle } from '@answer/components';
|
||||
import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
interface FormDataItem {
|
||||
|
@ -40,7 +40,7 @@ const initFormData = {
|
|||
},
|
||||
};
|
||||
const Ask = () => {
|
||||
const { is_admin = false } = userInfoStore((state) => state.user);
|
||||
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||
|
||||
const { tagId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
useQuerySynonymsTags,
|
||||
saveSynonymsTags,
|
||||
deleteTag,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
|
||||
const TagIntroduction = () => {
|
||||
const [isEdit, setEditState] = useState(false);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
|||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQueryTags, following } from '@answer/api';
|
||||
import { useQueryTags, following } from '@/services';
|
||||
import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components';
|
||||
import { formatCount } from '@answer/utils';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo, useEffect, useState } from 'react';
|
|||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { resetPassword, checkImgCode } from '@answer/api';
|
||||
import { resetPassword, checkImgCode } from '@/services';
|
||||
import type {
|
||||
ImgCodeRes,
|
||||
PasswordResetReq,
|
||||
|
|
|
@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react';
|
|||
import { Container, Col } from 'react-bootstrap';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { isLogin } from '@answer/utils';
|
||||
|
||||
import SendEmail from './components/sendEmail';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
|
@ -19,7 +18,7 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
isLogin();
|
||||
tryNormalLogged();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { FC, memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { activateAccount } from '@answer/api';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { activateAccount } from '@/services';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { getQueryString } from '@answer/utils';
|
||||
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||
const updateUser = userInfoStore((state) => state.update);
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
useEffect(() => {
|
||||
const code = getQueryString('code');
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { changeEmailVerify } from '@answer/api';
|
||||
import { changeEmailVerify } from '@/services';
|
||||
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
import React, { FormEvent, useState, useEffect } from 'react';
|
||||
import { Container, Form, Button, Col } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { login, checkImgCode } from '@answer/api';
|
||||
import type {
|
||||
LoginReqParams,
|
||||
ImgCodeRes,
|
||||
FormDataType,
|
||||
} from '@answer/common/interface';
|
||||
import { PageTitle, Unactivate } from '@answer/components';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { isLogin, getQueryString } from '@answer/utils';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { getQueryString } from '@answer/utils';
|
||||
|
||||
import { login, checkImgCode } from '@/services';
|
||||
import { deriveUserStat, tryNormalLogged } from '@/utils/guards';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const navigate = useNavigate();
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const updateUser = userInfoStore((state) => state.update);
|
||||
const storeUser = userInfoStore((state) => state.user);
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const storeUser = loggedUserInfoStore((state) => state.user);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
e_mail: {
|
||||
value: '',
|
||||
|
@ -102,15 +106,16 @@ const Index: React.FC = () => {
|
|||
login(params)
|
||||
.then((res) => {
|
||||
updateUser(res);
|
||||
if (res.mail_status === 2) {
|
||||
const userStat = deriveUserStat();
|
||||
if (!userStat.isActivated) {
|
||||
// inactive
|
||||
setStep(2);
|
||||
setRefresh((pre) => pre + 1);
|
||||
}
|
||||
if (res.mail_status === 1) {
|
||||
const path = Storage.get('ANSWER_PATH') || '/';
|
||||
Storage.remove('ANSWER_PATH');
|
||||
window.location.replace(path);
|
||||
} else {
|
||||
const path =
|
||||
Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
navigate(path, { replace: true });
|
||||
}
|
||||
|
||||
setModalState(false);
|
||||
|
@ -154,7 +159,7 @@ const Index: React.FC = () => {
|
|||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
||||
setStep(2);
|
||||
} else {
|
||||
isLogin();
|
||||
tryNormalLogged();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
clearUnreadNotification,
|
||||
clearNotificationStatus,
|
||||
readNotification,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
import { PageTitle } from '@answer/components';
|
||||
|
||||
import Inbox from './components/Inbox';
|
||||
|
|
|
@ -3,19 +3,19 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { replacementPassword } from '@answer/api';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { getQueryString, isLogin } from '@answer/utils';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { getQueryString } from '@answer/utils';
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
|
||||
import Storage from '@/utils/storage';
|
||||
import { replacementPassword } from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const clearUser = userInfoStore((state) => state.clear);
|
||||
const clearUser = loggedUserInfoStore((state) => state.clear);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
pass: {
|
||||
value: '',
|
||||
|
@ -105,7 +105,6 @@ const Index: React.FC = () => {
|
|||
.then(() => {
|
||||
// clear login information then to login page
|
||||
clearUser();
|
||||
Storage.remove('token');
|
||||
setStep(2);
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -118,7 +117,7 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
isLogin();
|
||||
tryNormalLogged();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import {
|
||||
usePersonalInfoByName,
|
||||
usePersonalTop,
|
||||
usePersonalListByTabName,
|
||||
} from '@answer/api';
|
||||
} from '@/services';
|
||||
|
||||
import {
|
||||
UserInfo,
|
||||
|
@ -30,7 +30,7 @@ const Personal: FC = () => {
|
|||
const page = searchParams.get('page') || 1;
|
||||
const order = searchParams.get('order') || 'newest';
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||
const sessionUser = userInfoStore((state) => state.user);
|
||||
const sessionUser = loggedUserInfoStore((state) => state.user);
|
||||
const isSelf = sessionUser?.username === username;
|
||||
|
||||
const { data: userInfo } = usePersonalInfoByName(username);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Form, Button, Col } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { register } from '@answer/api';
|
||||
import { register } from '@/services';
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
|
||||
import userStore from '@/stores/userInfo';
|
||||
|
|
|
@ -3,10 +3,11 @@ import { Container } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageTitle, Unactivate } from '@answer/components';
|
||||
import { isLogin } from '@answer/utils';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const [showForm, setShowForm] = useState(true);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
|
@ -16,7 +17,7 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
isLogin();
|
||||
tryNormalLogged();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Form, Button } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@answer/common/interface';
|
||||
import { getUserInfo, changeEmail } from '@answer/api';
|
||||
import { getLoggedUserInfo, changeEmail } from '@/services';
|
||||
import { useToast } from '@answer/hooks';
|
||||
|
||||
const reg = /(?<=.{2}).+(?=@)/gi;
|
||||
|
@ -23,7 +23,7 @@ const Index: FC = () => {
|
|||
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
|
||||
const toast = useToast();
|
||||
useEffect(() => {
|
||||
getUserInfo().then((resp) => {
|
||||
getLoggedUserInfo().then((resp) => {
|
||||
setUserInfo(resp);
|
||||
});
|
||||
}, []);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, FormEvent, useState } from 'react';
|
|||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { modifyPassword } from '@answer/api';
|
||||
import { modifyPassword } from '@/services';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
|
||||
|
|
|
@ -6,10 +6,11 @@ import dayjs from 'dayjs';
|
|||
import en from 'dayjs/locale/en';
|
||||
import zh from 'dayjs/locale/zh-cn';
|
||||
|
||||
import { languages } from '@answer/api';
|
||||
import { languages } from '@/services';
|
||||
import type { LangsType, FormDataType } from '@answer/common/interface';
|
||||
import { useToast } from '@answer/hooks';
|
||||
|
||||
import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
const Index = () => {
|
||||
|
@ -34,8 +35,8 @@ const Index = () => {
|
|||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
Storage.set('LANG', formData.lang.value);
|
||||
dayjs.locale(formData.lang.value === 'en_US' ? en : zh);
|
||||
Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value);
|
||||
dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh);
|
||||
i18n.changeLanguage(formData.lang.value);
|
||||
toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
|
@ -45,7 +46,7 @@ const Index = () => {
|
|||
|
||||
useEffect(() => {
|
||||
getLangs();
|
||||
const lang = Storage.get('LANG');
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||
if (lang) {
|
||||
setFormData({
|
||||
lang: {
|
||||
|
@ -60,7 +61,6 @@ const Index = () => {
|
|||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="emailSend" className="mb-3">
|
||||
<Form.Label>{t('lang.label')}</Form.Label>
|
||||
|
||||
<Form.Select
|
||||
value={formData.lang.value}
|
||||
isInvalid={formData.lang.isInvalid}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Form, Button } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
import { setNotice, getUserInfo } from '@answer/api';
|
||||
import { setNotice, getLoggedUserInfo } from '@/services';
|
||||
import { useToast } from '@answer/hooks';
|
||||
|
||||
const Index = () => {
|
||||
|
@ -20,7 +20,7 @@ const Index = () => {
|
|||
});
|
||||
|
||||
const getProfile = () => {
|
||||
getUserInfo().then((res) => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
setFormData({
|
||||
notice_switch: {
|
||||
value: res.notice_status === 1,
|
||||
|
|
|
@ -4,10 +4,10 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api';
|
||||
import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services';
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
import { UploadImg, Avatar } from '@answer/components';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
import { useToast } from '@answer/hooks';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
|
@ -15,7 +15,7 @@ const Index: React.FC = () => {
|
|||
keyPrefix: 'settings.profile',
|
||||
});
|
||||
const toast = useToast();
|
||||
const { user, update } = userInfoStore();
|
||||
const { user, update } = loggedUserInfoStore();
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
display_name: {
|
||||
value: '',
|
||||
|
@ -164,7 +164,7 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
const getProfile = () => {
|
||||
getUserInfo().then((res) => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.username.value = res.username;
|
||||
formData.bio.value = res.bio;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { getUserInfo } from '@answer/api';
|
||||
import { getLoggedUserInfo } from '@/services';
|
||||
import type { FormDataType } from '@answer/common/interface';
|
||||
|
||||
import Nav from './components/Nav';
|
||||
|
@ -43,7 +43,7 @@ const Index: React.FC = () => {
|
|||
},
|
||||
});
|
||||
const getProfile = () => {
|
||||
getUserInfo().then((res) => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore } from '@answer/stores';
|
||||
|
||||
import { PageTitle } from '@/components';
|
||||
|
||||
const Suspended = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'suspended' });
|
||||
const userInfo = userInfoStore((state) => state.user);
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
|
||||
if (userInfo.status !== 'forbidden') {
|
||||
window.location.replace('/');
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export const RouteAlias = {
|
||||
home: '/',
|
||||
login: '/users/login',
|
||||
register: '/users/register',
|
||||
activation: '/users/login?status=inactive',
|
||||
activationFailed: '/users/account-activation/failed',
|
||||
suspended: '/users/account-suspended',
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
pullLoggedUser,
|
||||
isLoggedAndNormal,
|
||||
isAdminLogged,
|
||||
isLogged,
|
||||
isNotLogged,
|
||||
isNotLoggedOrNormal,
|
||||
isLoggedAndInactive,
|
||||
isLoggedAndSuspended,
|
||||
isNotLoggedOrInactive,
|
||||
} from '@/utils/guards';
|
||||
|
||||
const RouteGuarder = {
|
||||
base: async () => {
|
||||
return isNotLoggedOrNormal();
|
||||
},
|
||||
logged: async () => {
|
||||
return isLogged();
|
||||
},
|
||||
notLogged: async () => {
|
||||
return isNotLogged();
|
||||
},
|
||||
notLoggedOrInactive: async () => {
|
||||
return isNotLoggedOrInactive();
|
||||
},
|
||||
loggedAndNormal: async () => {
|
||||
await pullLoggedUser(true);
|
||||
return isLoggedAndNormal();
|
||||
},
|
||||
loggedAndInactive: async () => {
|
||||
return isLoggedAndInactive();
|
||||
},
|
||||
loggedAndSuspended: async () => {
|
||||
return isLoggedAndSuspended();
|
||||
},
|
||||
adminLogged: async () => {
|
||||
await pullLoggedUser(true);
|
||||
return isAdminLogged();
|
||||
},
|
||||
};
|
||||
|
||||
export default RouteGuarder;
|
|
@ -1,14 +1,13 @@
|
|||
import React, { Suspense, lazy } from 'react';
|
||||
import { RouteObject, createBrowserRouter } from 'react-router-dom';
|
||||
import { RouteObject, createBrowserRouter, redirect } from 'react-router-dom';
|
||||
|
||||
import Layout from '@answer/pages/Layout';
|
||||
|
||||
import routeConfig, { RouteNode } from '@/router/route-config';
|
||||
import RouteRules from '@/router/route-rules';
|
||||
import Layout from '@/pages/Layout';
|
||||
import baseRoutes, { RouteNode } from '@/router/routes';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
|
||||
const routes: RouteObject[] = [];
|
||||
|
||||
const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
||||
const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
||||
routeNodes.forEach((rn) => {
|
||||
if (rn.path === '/') {
|
||||
rn.element = <Layout />;
|
||||
|
@ -18,40 +17,37 @@ const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
|||
* ref: https://webpack.js.org/api/module-methods/#import-1
|
||||
*/
|
||||
rn.page = rn.page.replace('pages/', '');
|
||||
const Control = lazy(() => import(`@/pages/${rn.page}`));
|
||||
const Ctrl = lazy(() => import(`@/pages/${rn.page}`));
|
||||
rn.element = (
|
||||
<Suspense>
|
||||
<Control />
|
||||
<Ctrl />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
root.push(rn);
|
||||
if (Array.isArray(rn.rules)) {
|
||||
const ruleFunc: Function[] = [];
|
||||
if (typeof rn.loader === 'function') {
|
||||
ruleFunc.push(rn.loader);
|
||||
}
|
||||
rn.rules.forEach((ruleKey) => {
|
||||
const func = RouteRules[ruleKey];
|
||||
if (typeof func === 'function') {
|
||||
ruleFunc.push(func);
|
||||
if (rn.guard) {
|
||||
const { guard } = rn;
|
||||
const loaderRef = rn.loader;
|
||||
rn.loader = async (args) => {
|
||||
const gr = await guard(args);
|
||||
if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) {
|
||||
return redirect(gr.redirect);
|
||||
}
|
||||
});
|
||||
rn.loader = ({ params }) => {
|
||||
ruleFunc.forEach((func) => {
|
||||
func(params);
|
||||
});
|
||||
let ret;
|
||||
if (typeof loaderRef === 'function') {
|
||||
ret = await loaderRef(args);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
const children = Array.isArray(rn.children) ? rn.children : null;
|
||||
if (children) {
|
||||
rn.children = [];
|
||||
routeGen(children, rn.children);
|
||||
routeWrapper(children, rn.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
routeGen(routeConfig, routes);
|
||||
routeWrapper(baseRoutes, routes);
|
||||
|
||||
const router = createBrowserRouter(routes);
|
||||
export default router;
|
||||
export { routes, createBrowserRouter };
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { isLogin } from '@answer/utils';
|
||||
|
||||
const RouteRules = {
|
||||
isLoginAndNormal: () => {
|
||||
return isLogin(true);
|
||||
},
|
||||
};
|
||||
|
||||
export default RouteRules;
|
|
@ -1,14 +1,18 @@
|
|||
import { RouteObject } from 'react-router-dom';
|
||||
|
||||
import RouteGuarder from '@/router/guarder';
|
||||
|
||||
export interface RouteNode extends RouteObject {
|
||||
page: string;
|
||||
children?: RouteNode[];
|
||||
rules?: string[];
|
||||
guard?: Function;
|
||||
}
|
||||
const routeConfig: RouteNode[] = [
|
||||
|
||||
const routes: RouteNode[] = [
|
||||
{
|
||||
path: '/',
|
||||
page: 'pages/Layout',
|
||||
guard: RouteGuarder.base,
|
||||
children: [
|
||||
// question and answer
|
||||
{
|
||||
|
@ -31,12 +35,12 @@ const routeConfig: RouteNode[] = [
|
|||
{
|
||||
path: 'questions/ask',
|
||||
page: 'pages/Questions/Ask',
|
||||
rules: ['isLoginAndNormal'],
|
||||
guard: RouteGuarder.loggedAndNormal,
|
||||
},
|
||||
{
|
||||
path: 'posts/:qid/edit',
|
||||
page: 'pages/Questions/Ask',
|
||||
rules: ['isLoginAndNormal'],
|
||||
guard: RouteGuarder.loggedAndNormal,
|
||||
},
|
||||
{
|
||||
path: 'posts/:qid/:aid/edit',
|
||||
|
@ -105,18 +109,22 @@ const routeConfig: RouteNode[] = [
|
|||
{
|
||||
path: 'users/login',
|
||||
page: 'pages/Users/Login',
|
||||
guard: RouteGuarder.notLoggedOrInactive,
|
||||
},
|
||||
{
|
||||
path: 'users/register',
|
||||
page: 'pages/Users/Register',
|
||||
guard: RouteGuarder.notLogged,
|
||||
},
|
||||
{
|
||||
path: 'users/account-recovery',
|
||||
page: 'pages/Users/AccountForgot',
|
||||
guard: RouteGuarder.loggedAndNormal,
|
||||
},
|
||||
{
|
||||
path: 'users/password-reset',
|
||||
page: 'pages/Users/PasswordReset',
|
||||
guard: RouteGuarder.loggedAndNormal,
|
||||
},
|
||||
{
|
||||
path: 'users/account-activation',
|
||||
|
@ -142,6 +150,7 @@ const routeConfig: RouteNode[] = [
|
|||
{
|
||||
path: 'admin',
|
||||
page: 'pages/Admin',
|
||||
guard: RouteGuarder.adminLogged,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
|
@ -192,4 +201,4 @@ const routeConfig: RouteNode[] = [
|
|||
],
|
||||
},
|
||||
];
|
||||
export default routeConfig;
|
||||
export default routes;
|
|
@ -1,6 +1,5 @@
|
|||
export * from './activity';
|
||||
export * from './personal';
|
||||
export * from './user';
|
||||
export * from './notification';
|
||||
export * from './question';
|
||||
export * from './search';
|
||||
|
|
|
@ -2,9 +2,10 @@ import useSWR from 'swr';
|
|||
import qs from 'qs';
|
||||
|
||||
import request from '@answer/utils/request';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
|
||||
export const useQueryNotifications = (params) => {
|
||||
const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
|
@ -33,7 +34,7 @@ export const useQueryNotificationStatus = () => {
|
|||
const apiUrl = '/answer/api/v1/notification/status';
|
||||
|
||||
return useSWR<{ inbox: number; achievement: number }>(
|
||||
isLogin() ? apiUrl : null,
|
||||
tryNormalLogged() ? apiUrl : null,
|
||||
request.instance.get,
|
||||
{
|
||||
refreshInterval: 3000,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@answer/utils/request';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import type * as Type from '@answer/common/interface';
|
||||
|
||||
import { tryNormalLogged } from '@/utils/guards';
|
||||
|
||||
export const deleteTag = (id) => {
|
||||
return request.delete('/answer/api/v1/tag', {
|
||||
tag_id: id,
|
||||
|
@ -24,7 +25,7 @@ export const saveSynonymsTags = (params) => {
|
|||
|
||||
export const useFollowingTags = () => {
|
||||
let apiUrl = '';
|
||||
if (isLogin()) {
|
||||
if (tryNormalLogged()) {
|
||||
apiUrl = '/answer/api/v1/tags/following';
|
||||
}
|
||||
const { data, error, mutate } = useSWR<any[]>(apiUrl, request.instance.get);
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@answer/utils/request';
|
||||
|
||||
export const useCheckUserStatus = () => {
|
||||
const apiUrl = '/answer/api/v1/user/status';
|
||||
const hasToken = localStorage.getItem('token');
|
||||
const { data, error } = useSWR<{ status: string }, Error>(
|
||||
hasToken ? apiUrl : null,
|
||||
request.instance.get,
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !data && !error,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -115,7 +115,7 @@ export const resendEmail = (params?: Type.ImgCodeReq) => {
|
|||
* @description get login userinfo
|
||||
* @returns {UserInfo}
|
||||
*/
|
||||
export const getUserInfo = () => {
|
||||
export const getLoggedUserInfo = () => {
|
||||
return request.get<Type.UserInfoRes>('/answer/api/v1/user/info');
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import toastStore from './toast';
|
||||
import userInfoStore from './userInfo';
|
||||
import loggedUserInfoStore from './userInfo';
|
||||
import globalStore from './global';
|
||||
import siteInfoStore from './siteInfo';
|
||||
import interfaceStore from './interface';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
userInfoStore,
|
||||
loggedUserInfoStore,
|
||||
globalStore,
|
||||
siteInfoStore,
|
||||
interfaceStore,
|
||||
|
|
|
@ -3,6 +3,11 @@ import create from 'zustand';
|
|||
import type { UserInfoRes } from '@answer/common/interface';
|
||||
import Storage from '@answer/utils/storage';
|
||||
|
||||
import {
|
||||
LOGGED_USER_STORAGE_KEY,
|
||||
LOGGED_TOKEN_STORAGE_KEY,
|
||||
} from '@/common/constants';
|
||||
|
||||
interface UserInfoStore {
|
||||
user: UserInfoRes;
|
||||
update: (params: UserInfoRes) => void;
|
||||
|
@ -19,23 +24,23 @@ const initUser: UserInfoRes = {
|
|||
location: '',
|
||||
website: '',
|
||||
status: '',
|
||||
mail_status: 0,
|
||||
mail_status: 1,
|
||||
};
|
||||
|
||||
const userInfoStore = create<UserInfoStore>((set) => ({
|
||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
user: initUser,
|
||||
update: (params) =>
|
||||
set(() => {
|
||||
Storage.set('token', params.access_token);
|
||||
Storage.set('userInfo', params);
|
||||
Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token);
|
||||
Storage.set(LOGGED_USER_STORAGE_KEY, params);
|
||||
return { user: params };
|
||||
}),
|
||||
clear: () =>
|
||||
set(() => {
|
||||
// Storage.remove('token');
|
||||
Storage.remove('userInfo');
|
||||
Storage.remove(LOGGED_TOKEN_STORAGE_KEY);
|
||||
Storage.remove(LOGGED_USER_STORAGE_KEY);
|
||||
return { user: initUser };
|
||||
}),
|
||||
}));
|
||||
|
||||
export default userInfoStore;
|
||||
export default loggedUserInfoStore;
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
function getQueryString(name: string): string {
|
||||
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`);
|
||||
const r = window.location.search.substr(1).match(reg);
|
||||
if (r != null) return unescape(r[2]);
|
||||
return '';
|
||||
}
|
||||
|
||||
function thousandthDivision(num) {
|
||||
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
||||
return `${num}`.replace(reg, '$&,');
|
||||
}
|
||||
|
||||
function formatCount($num: number): string {
|
||||
let res = String($num);
|
||||
if (!Number.isFinite($num)) {
|
||||
res = '0';
|
||||
} else if ($num < 10000) {
|
||||
res = thousandthDivision($num);
|
||||
} else if ($num < 1000000) {
|
||||
res = `${Math.round($num / 100) / 10}k`;
|
||||
} else if ($num >= 1000000) {
|
||||
res = `${Math.round($num / 100000) / 10}m`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function scrollTop(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const offset = 120;
|
||||
const bodyRect = document.body.getBoundingClientRect().top;
|
||||
const elementRect = element.getBoundingClientRect().top;
|
||||
const elementPosition = elementRect - bodyRect;
|
||||
const offsetPosition = elementPosition - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract user info from markdown
|
||||
* @param markdown string
|
||||
* @returns Array<{displayName: string, userName: string}>
|
||||
*/
|
||||
function matchedUsers(markdown) {
|
||||
const globalReg = /\B@([\w|]+)/g;
|
||||
const reg = /\B@([\w\\_\\.]+)/;
|
||||
|
||||
const users = markdown.match(globalReg);
|
||||
if (!users) {
|
||||
return [];
|
||||
}
|
||||
return users.map((user) => {
|
||||
const matched = user.match(reg);
|
||||
return {
|
||||
userName: matched[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify user information from markdown
|
||||
* @param markdown string
|
||||
* @returns string
|
||||
*/
|
||||
function parseUserInfo(markdown) {
|
||||
const globalReg = /\B@([\w\\_\\.\\-]+)/g;
|
||||
return markdown.replace(globalReg, '[@$1](/u/$1)');
|
||||
}
|
||||
|
||||
export {
|
||||
getQueryString,
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
scrollTop,
|
||||
matchedUsers,
|
||||
parseUserInfo,
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
const differentCurrent = (target: string, base?: string) => {
|
||||
base ||= window.location.origin;
|
||||
const targetUrl = new URL(target, base);
|
||||
return targetUrl.toString() !== window.location.href;
|
||||
};
|
||||
|
||||
/**
|
||||
* only navigate if not same as current url
|
||||
* @param pathname
|
||||
* @param callback
|
||||
*/
|
||||
const navigate = (pathname: string, callback: Function) => {
|
||||
if (differentCurrent(pathname)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* auto navigate to login page with redirect info
|
||||
*/
|
||||
const navigateToLogin = () => {
|
||||
const { pathname } = window.location;
|
||||
if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) {
|
||||
const redirectUrl = window.location.href;
|
||||
Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);
|
||||
}
|
||||
navigate(RouteAlias.login, () => {
|
||||
window.location.replace(RouteAlias.login);
|
||||
});
|
||||
};
|
||||
|
||||
export const floppyNavigation = {
|
||||
differentCurrent,
|
||||
navigate,
|
||||
navigateToLogin,
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
import { getLoggedUserInfo } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
||||
import { floppyNavigation } from '@/utils/floppyNavigation';
|
||||
|
||||
type UserStat = {
|
||||
isLogged: boolean;
|
||||
isActivated: boolean;
|
||||
isSuspended: boolean;
|
||||
isNormal: boolean;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
export const deriveUserStat = (): UserStat => {
|
||||
const stat: UserStat = {
|
||||
isLogged: false,
|
||||
isActivated: false,
|
||||
isSuspended: false,
|
||||
isNormal: false,
|
||||
isAdmin: false,
|
||||
};
|
||||
const { user } = loggedUserInfoStore.getState();
|
||||
if (user.id && user.username) {
|
||||
stat.isLogged = true;
|
||||
}
|
||||
if (stat.isLogged && user.mail_status === 1) {
|
||||
stat.isActivated = true;
|
||||
}
|
||||
if (stat.isLogged && user.status === 'forbidden') {
|
||||
stat.isSuspended = true;
|
||||
}
|
||||
if (stat.isLogged && stat.isActivated && !stat.isSuspended) {
|
||||
stat.isNormal = true;
|
||||
}
|
||||
if (stat.isNormal && user.is_admin === true) {
|
||||
stat.isAdmin = true;
|
||||
}
|
||||
|
||||
return stat;
|
||||
};
|
||||
|
||||
type GuardResult = {
|
||||
ok: boolean;
|
||||
redirect?: string;
|
||||
};
|
||||
let pullLock = false;
|
||||
let dedupeTimestamp = 0;
|
||||
export const pullLoggedUser = async (forceRePull = false) => {
|
||||
// only pull once if not force re-pull
|
||||
if (pullLock && !forceRePull) {
|
||||
return;
|
||||
}
|
||||
// dedupe pull requests in this time span in 10 seconds
|
||||
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
||||
return;
|
||||
}
|
||||
dedupeTimestamp = Date.now();
|
||||
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
||||
dedupeTimestamp = 0;
|
||||
if (!deriveUserStat().isLogged) {
|
||||
// load fallback userInfo from local storage
|
||||
const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY);
|
||||
if (storageLoggedUserInfo) {
|
||||
loggedUserInfoStore.getState().update(storageLoggedUserInfo);
|
||||
}
|
||||
}
|
||||
console.error(ex);
|
||||
});
|
||||
if (loggedUserInfo) {
|
||||
pullLock = true;
|
||||
loggedUserInfoStore.getState().update(loggedUserInfo);
|
||||
}
|
||||
};
|
||||
|
||||
export const isLogged = () => {
|
||||
const ret: GuardResult = { ok: true, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (!userStat.isLogged) {
|
||||
ret.ok = false;
|
||||
ret.redirect = RouteAlias.login;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isNotLogged = () => {
|
||||
const ret: GuardResult = { ok: true, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (userStat.isLogged) {
|
||||
ret.ok = false;
|
||||
ret.redirect = RouteAlias.home;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isLoggedAndInactive = () => {
|
||||
const ret: GuardResult = { ok: false, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (!userStat.isActivated) {
|
||||
ret.ok = true;
|
||||
ret.redirect = RouteAlias.activation;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isLoggedAndSuspended = () => {
|
||||
const ret: GuardResult = { ok: false, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (userStat.isSuspended) {
|
||||
ret.redirect = RouteAlias.suspended;
|
||||
ret.ok = true;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isLoggedAndNormal = () => {
|
||||
const ret: GuardResult = { ok: false, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (userStat.isNormal) {
|
||||
ret.ok = true;
|
||||
} else if (!userStat.isActivated) {
|
||||
ret.redirect = RouteAlias.activation;
|
||||
} else if (!userStat.isSuspended) {
|
||||
ret.redirect = RouteAlias.suspended;
|
||||
} else if (!userStat.isLogged) {
|
||||
ret.redirect = RouteAlias.login;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isNotLoggedOrNormal = () => {
|
||||
const ret: GuardResult = { ok: true, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
const gr = isLoggedAndNormal();
|
||||
if (!gr.ok && userStat.isLogged) {
|
||||
ret.ok = false;
|
||||
ret.redirect = gr.redirect;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isNotLoggedOrInactive = () => {
|
||||
const ret: GuardResult = { ok: true, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (userStat.isLogged || userStat.isActivated) {
|
||||
ret.ok = false;
|
||||
ret.redirect = RouteAlias.home;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const isAdminLogged = () => {
|
||||
const ret: GuardResult = { ok: true, redirect: undefined };
|
||||
const userStat = deriveUserStat();
|
||||
if (!userStat.isAdmin) {
|
||||
ret.redirect = RouteAlias.home;
|
||||
ret.ok = false;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* try user was logged and all state ok
|
||||
* @param autoLogin
|
||||
*/
|
||||
export const tryNormalLogged = (autoLogin: boolean = false) => {
|
||||
const gr = isLoggedAndNormal();
|
||||
if (gr.ok) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gr.redirect === RouteAlias.login && autoLogin) {
|
||||
floppyNavigation.navigateToLogin();
|
||||
} else if (gr.redirect) {
|
||||
floppyNavigation.navigate(gr.redirect, () => {
|
||||
// @ts-ignore
|
||||
window.location.replace(gr.redirect);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -1,114 +1,6 @@
|
|||
import { LOGIN_NEED_BACK } from '@answer/common/constants';
|
||||
export * from './common';
|
||||
export * as guards from './guards';
|
||||
|
||||
import Storage from './storage';
|
||||
|
||||
function getQueryString(name: string): string {
|
||||
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`);
|
||||
const r = window.location.search.substr(1).match(reg);
|
||||
if (r != null) return unescape(r[2]);
|
||||
return '';
|
||||
}
|
||||
|
||||
function thousandthDivision(num) {
|
||||
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
||||
return `${num}`.replace(reg, '$&,');
|
||||
}
|
||||
|
||||
function formatCount($num: number): string {
|
||||
let res = String($num);
|
||||
if (!Number.isFinite($num)) {
|
||||
res = '0';
|
||||
} else if ($num < 10000) {
|
||||
res = thousandthDivision($num);
|
||||
} else if ($num < 1000000) {
|
||||
res = `${Math.round($num / 100) / 10}k`;
|
||||
} else if ($num >= 1000000) {
|
||||
res = `${Math.round($num / 100000) / 10}m`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function isLogin(needToLogin?: boolean): boolean {
|
||||
const user = Storage.get('userInfo');
|
||||
const path = window.location.pathname;
|
||||
|
||||
// User deleted or suspended
|
||||
if (user.username && user.status === 'forbidden') {
|
||||
if (path !== '/users/account-suspended') {
|
||||
window.location.pathname = '/users/account-suspended';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// login and active
|
||||
if (user.username && user.mail_status === 1) {
|
||||
if (LOGIN_NEED_BACK.includes(path)) {
|
||||
window.location.replace('/');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// un login or inactivated
|
||||
if ((!user.username || user.mail_status === 2) && needToLogin) {
|
||||
Storage.set('ANSWER_PATH', path);
|
||||
window.location.href = '/users/login';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function scrollTop(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const offset = 120;
|
||||
const bodyRect = document.body.getBoundingClientRect().top;
|
||||
const elementRect = element.getBoundingClientRect().top;
|
||||
const elementPosition = elementRect - bodyRect;
|
||||
const offsetPosition = elementPosition - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract user info from markdown
|
||||
* @param markdown string
|
||||
* @returns Array<{displayName: string, userName: string}>
|
||||
*/
|
||||
function matchedUsers(markdown) {
|
||||
const globalReg = /\B@([\w|]+)/g;
|
||||
const reg = /\B@([\w\\_\\.]+)/;
|
||||
|
||||
const users = markdown.match(globalReg);
|
||||
if (!users) {
|
||||
return [];
|
||||
}
|
||||
return users.map((user) => {
|
||||
const matched = user.match(reg);
|
||||
return {
|
||||
userName: matched[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify user infromation from markdown
|
||||
* @param markdown string
|
||||
* @returns string
|
||||
*/
|
||||
function parseUserInfo(markdown) {
|
||||
const globalReg = /\B@([\w\\_\\.\\-]+)/g;
|
||||
return markdown.replace(globalReg, '[@$1](/u/$1)');
|
||||
}
|
||||
|
||||
export {
|
||||
getQueryString,
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
isLogin,
|
||||
scrollTop,
|
||||
matchedUsers,
|
||||
parseUserInfo,
|
||||
};
|
||||
export { default as request } from './request';
|
||||
export { default as Storage } from './storage';
|
||||
export { floppyNavigation } from './floppyNavigation';
|
||||
|
|
|
@ -2,9 +2,17 @@ import axios, { AxiosResponse } from 'axios';
|
|||
import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||
|
||||
import { Modal } from '@answer/components';
|
||||
import { userInfoStore, toastStore } from '@answer/stores';
|
||||
import { loggedUserInfoStore, toastStore } from '@answer/stores';
|
||||
|
||||
import Storage from './storage';
|
||||
import { floppyNavigation } from './floppyNavigation';
|
||||
|
||||
import {
|
||||
LOGGED_TOKEN_STORAGE_KEY,
|
||||
CURRENT_LANG_STORAGE_KEY,
|
||||
DEFAULT_LANG,
|
||||
} from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
const API = {
|
||||
development: '',
|
||||
|
@ -25,12 +33,11 @@ class Request {
|
|||
|
||||
constructor(config: AxiosRequestConfig) {
|
||||
this.instance = axios.create(config);
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(requestConfig: AxiosRequestConfig) => {
|
||||
const token = Storage.get('token') || '';
|
||||
const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || '';
|
||||
// default lang en_US
|
||||
const lang = Storage.get('LANG') || 'en_US';
|
||||
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG;
|
||||
requestConfig.headers = {
|
||||
Authorization: token,
|
||||
'Accept-Language': lang,
|
||||
|
@ -54,23 +61,23 @@ class Request {
|
|||
return data;
|
||||
},
|
||||
(error) => {
|
||||
const { status, data, msg } = error.response;
|
||||
const { data: realData, msg: realMsg = '' } = data;
|
||||
const { status, data: respData, msg: respMsg } = error.response;
|
||||
const { data, msg = '' } = respData;
|
||||
if (status === 400) {
|
||||
// show error message
|
||||
if (realData instanceof Object && realData.err_type) {
|
||||
if (realData.err_type === 'toast') {
|
||||
if (data instanceof Object && data.err_type) {
|
||||
if (data.err_type === 'toast') {
|
||||
// toast error message
|
||||
toastStore.getState().show({
|
||||
msg: realMsg,
|
||||
msg,
|
||||
variant: 'danger',
|
||||
});
|
||||
}
|
||||
|
||||
if (realData.type === 'modal') {
|
||||
if (data.type === 'modal') {
|
||||
// modal error message
|
||||
Modal.confirm({
|
||||
content: realMsg,
|
||||
content: msg,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -78,63 +85,56 @@ class Request {
|
|||
}
|
||||
|
||||
if (
|
||||
realData instanceof Object &&
|
||||
Object.keys(realData).length > 0 &&
|
||||
realData.key
|
||||
data instanceof Object &&
|
||||
Object.keys(data).length > 0 &&
|
||||
data.key
|
||||
) {
|
||||
// handle form error
|
||||
return Promise.reject({ ...realData, isError: true });
|
||||
return Promise.reject({ ...data, isError: true });
|
||||
}
|
||||
|
||||
if (!realData || Object.keys(realData).length <= 0) {
|
||||
if (!data || Object.keys(data).length <= 0) {
|
||||
// default error msg will show modal
|
||||
Modal.confirm({
|
||||
content: realMsg,
|
||||
content: msg,
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
}
|
||||
// 401: Re-login required
|
||||
if (status === 401) {
|
||||
// clear userinfo
|
||||
loggedUserInfoStore.getState().clear();
|
||||
floppyNavigation.navigateToLogin();
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (status === 403) {
|
||||
// Permission interception
|
||||
if (data?.type === 'url_expired') {
|
||||
// url expired
|
||||
floppyNavigation.navigate(RouteAlias.activationFailed, () => {
|
||||
window.location.replace(RouteAlias.activationFailed);
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
if (data?.type === 'inactive') {
|
||||
// inactivated
|
||||
floppyNavigation.navigate(RouteAlias.activation, () => {
|
||||
window.location.href = RouteAlias.activation;
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (data?.type === 'suspended') {
|
||||
floppyNavigation.navigate(RouteAlias.suspended, () => {
|
||||
window.location.replace(RouteAlias.suspended);
|
||||
});
|
||||
return Promise.reject(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 401) {
|
||||
// clear userinfo;
|
||||
Storage.remove('token');
|
||||
userInfoStore.getState().clear();
|
||||
// need login
|
||||
const { pathname } = window.location;
|
||||
if (pathname !== '/users/login' && pathname !== '/users/register') {
|
||||
Storage.set('ANSWER_PATH', window.location.pathname);
|
||||
}
|
||||
window.location.href = '/users/login';
|
||||
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
// Permission interception
|
||||
|
||||
if (realData?.type === 'inactive') {
|
||||
// inactivated
|
||||
window.location.href = '/users/login?status=inactive';
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (realData?.type === 'url_expired') {
|
||||
// url expired
|
||||
window.location.href = '/users/account-activation/failed';
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
if (realData?.type === 'suspended') {
|
||||
if (window.location.pathname !== '/users/account-suspended') {
|
||||
window.location.href = '/users/account-suspended';
|
||||
}
|
||||
|
||||
return Promise.reject(false);
|
||||
}
|
||||
}
|
||||
|
||||
toastStore.getState().show({
|
||||
msg: `statusCode: ${status}; ${msg || ''}`,
|
||||
msg: `statusCode: ${status}; ${respMsg || ''}`,
|
||||
variant: 'danger',
|
||||
});
|
||||
return Promise.reject(false);
|
||||
|
@ -178,6 +178,4 @@ class Request {
|
|||
}
|
||||
}
|
||||
|
||||
// export const Request;
|
||||
|
||||
export default new Request(baseConfig);
|
||||
|
|
|
@ -3,13 +3,12 @@ const Storage = {
|
|||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
return v;
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return undefined;
|
||||
},
|
||||
set: (key: string, value: any): void => {
|
||||
if (typeof value === 'string') {
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
"@answer/components/*": ["src/components/*"],
|
||||
"@answer/stores": ["src/stores"],
|
||||
"@answer/stores/*": ["src/stores/*"],
|
||||
"@answer/api": ["src/services/api.ts"],
|
||||
"@answer/services/*": ["src/services/*"],
|
||||
"@answer/hooks": ["src/hooks"],
|
||||
"@answer/common": ["src/common"],
|
||||
|
|
Loading…
Reference in New Issue