diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 877a0039..98edc408 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { position: 'before', }, { - pattern: '@answer/**', + pattern: '@/**', group: 'internal', }, { diff --git a/ui/commitlint.config.js b/ui/commitlint.config.js index 84dcb122..4944db0e 100644 --- a/ui/commitlint.config.js +++ b/ui/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'], + extends: ['@commitlint/routes-conventional'], }; diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 06464361..ac52fa63 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -8,13 +8,6 @@ module.exports = { config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), - '@answer/pages': path.resolve(__dirname, 'src/pages'), - '@answer/components': path.resolve(__dirname, 'src/components'), - '@answer/stores': path.resolve(__dirname, 'src/stores'), - '@answer/hooks': path.resolve(__dirname, 'src/hooks'), - '@answer/utils': path.resolve(__dirname, 'src/utils'), - '@answer/common': path.resolve(__dirname, 'src/common'), - '@answer/api': path.resolve(__dirname, 'src/services/api'), }; return config; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d4f6925..878ca1ab 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -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 ; } diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 3e577515..b700ad54 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -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; @@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [ child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; +// timezones +export const TIMEZONES = [ + { + label: 'UTC-12', + value: 'UTC-12', + }, + { + label: 'UTC-11:30', + value: 'UTC-11.5', + }, + { + label: 'UTC-11', + value: 'UTC-11', + }, + { + label: 'UTC-10:30', + value: 'UTC-10.5', + }, + { + label: 'UTC-10', + value: 'UTC-10', + }, + { + label: 'UTC-9:30', + value: 'UTC-9.5', + }, + { + label: 'UTC-9', + value: 'UTC-9', + }, + { + label: 'UTC-8:30', + value: 'UTC-8.5', + }, + { + label: 'UTC-8', + value: 'UTC-8', + }, + { + label: 'UTC-7:30', + value: 'UTC-7.5', + }, + { + label: 'UTC-7', + value: 'UTC-7', + }, + { + label: 'UTC-6:30', + value: 'UTC-6.5', + }, + { + label: 'UTC-6', + value: 'UTC-6', + }, + { + label: 'UTC-5:30', + value: 'UTC-5.5', + }, + { + label: 'UTC-5', + value: 'UTC-5', + }, + { + label: 'UTC-4:30', + value: 'UTC-4.5', + }, + { + label: 'UTC-4', + value: 'UTC-4', + }, + { + label: 'UTC-3:30', + value: 'UTC-3.5', + }, + { + label: 'UTC-3', + value: 'UTC-3', + }, + { + label: 'UTC-2:30', + value: 'UTC-2.5', + }, + { + label: 'UTC-2', + value: 'UTC-2', + }, + { + label: 'UTC-1:30', + value: 'UTC-1.5', + }, + { + label: 'UTC-1', + value: 'UTC-1', + }, + { + label: 'UTC-0:30', + value: 'UTC-0.5', + }, + { + label: 'UTC+0', + value: 'UTC+0', + }, + { + label: 'UTC+0:30', + value: 'UTC+0.5', + }, + { + label: 'UTC+1', + value: 'UTC+1', + }, + { + label: 'UTC+1:30', + value: 'UTC+1.5', + }, + { + label: 'UTC+2', + value: 'UTC+2', + }, + { + label: 'UTC+2:30', + value: 'UTC+2.5', + }, + { + label: 'UTC+3', + value: 'UTC+3', + }, + { + label: 'UTC+3:30', + + value: 'UTC+3.5', + }, + { + label: 'UTC+4', + value: 'UTC+4', + }, + { + label: 'UTC+4:30', + value: 'UTC+4.5', + }, + { + label: 'UTC+5', + value: 'UTC+5', + }, + { + label: 'UTC+5:30', + value: 'UTC+5.5', + }, + { + label: 'UTC+5:45', + value: 'UTC+5.75', + }, + { + label: 'UTC+6', + value: 'UTC+6', + }, + { + label: 'UTC+6:30', + + value: 'UTC+6.5', + }, + { + label: 'UTC+7', + value: 'UTC+7', + }, + { + label: 'UTC+7:30', + value: 'UTC+7.5', + }, + { + label: 'UTC+8', + value: 'UTC+8', + }, + { + label: 'UTC+8:30', + value: 'UTC+8.5', + }, + { + label: 'UTC+8:45', + value: 'UTC+8.75', + }, + { + label: 'UTC+9', + value: 'UTC+9', + }, + { + label: 'UTC+9:30', + value: 'UTC+9.5', + }, + { + label: 'UTC+10', + value: 'UTC+10', + }, + { + label: 'UTC+10:30', + value: 'UTC+10.5', + }, + { + label: 'UTC+11', + value: 'UTC+11', + }, + { + label: 'UTC+11:30', + value: 'UTC+11.5', + }, + { + label: 'UTC+12', + value: 'UTC+12', + }, + { + label: 'UTC+12:45', + value: 'UTC+12.75', + }, + { + label: 'UTC+13', + value: 'UTC+13', + }, + { + label: 'UTC+13:45', + value: 'UTC+13.75', + }, + { + label: 'UTC+14', + value: 'UTC+14', + }, +]; +export const DEFAULT_TIMEZONE = 'UTC+0'; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 2319f1a6..98f7c046 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -109,7 +109,7 @@ export interface UserInfoBase { */ status?: string; /** roles */ - is_admin?: true; + is_admin?: boolean; } export interface UserInfoRes extends UserInfoBase { @@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; + query?: string; } /** @@ -263,6 +264,7 @@ export interface AdminSettingsInterface { logo: string; language: string; theme: string; + time_zone?: string; } export interface AdminSettingsSmtp { @@ -321,3 +323,21 @@ export interface SearchResItem { export interface SearchRes extends ListResult { extra: any; } + +export interface AdminDashboard { + info: { + question_count: number; + answer_count: number; + comment_count: number; + vote_count: number; + user_count: number; + report_count: number; + uploading_files: boolean; + smtp: boolean; + time_zone: string; + occupying_storage_space: string; + app_start_time: number; + app_version: string; + https: boolean; + }; +} diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index ccc14d44..b3403bc8 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom'; import { useAccordionButton } from 'react-bootstrap/AccordionButton'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; function MenuNode({ menu, callback, activeKey, isLeaf = false }) { const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 87c35cef..d9395723 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -4,11 +4,11 @@ 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 { useToast } from '@answer/hooks'; +import { Icon } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; +import { tryNormalLogged } from '@/utils/guard'; +import { bookmark, postVote } from '@/services'; interface Props { className?: string; @@ -32,7 +32,7 @@ const Index: FC = ({ 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 +48,7 @@ const Index: FC = ({ className, data }) => { }, []); const handleVote = (type: 'up' | 'down') => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -84,7 +84,7 @@ const Index: FC = ({ className, data }) => { }; const handleBookmark = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } bookmark({ diff --git a/ui/src/components/BaseUserCard/index.tsx b/ui/src/components/BaseUserCard/index.tsx index fa524bda..d170d3f8 100644 --- a/ui/src/components/BaseUserCard/index.tsx +++ b/ui/src/components/BaseUserCard/index.tsx @@ -1,8 +1,7 @@ import { memo, FC } from 'react'; import { Link } from 'react-router-dom'; -import { Avatar } from '@answer/components'; - +import { Avatar } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/components/Comment/components/ActionBar/index.tsx b/ui/src/components/Comment/components/ActionBar/index.tsx index a801a6bb..a13bff1a 100644 --- a/ui/src/components/Comment/components/ActionBar/index.tsx +++ b/ui/src/components/Comment/components/ActionBar/index.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; -import { Icon, FormatTime } from '@answer/components'; +import { Icon, FormatTime } from '@/components'; const ActionBar = ({ nickName, diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx index 4971c4ef..70c62fdd 100644 --- a/ui/src/components/Comment/components/Form/index.tsx +++ b/ui/src/components/Comment/components/Form/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ className = '', diff --git a/ui/src/components/Comment/components/Reply/index.tsx b/ui/src/components/Comment/components/Reply/index.tsx index 931a8909..01c9399c 100644 --- a/ui/src/components/Comment/components/Reply/index.tsx +++ b/ui/src/components/Comment/components/Reply/index.tsx @@ -2,8 +2,8 @@ import { useState, memo } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ userName, onSendReply, onCancel, mode }) => { const [value, setValue] = useState(''); diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index e7fd3bff..1f57e737 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -7,17 +7,18 @@ import classNames from 'classnames'; import { unionBy } from 'lodash'; import { marked } from 'marked'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; +import { Modal } from '@/components'; +import { usePageUsers, useReportModal } from '@/hooks'; +import { matchedUsers, parseUserInfo } from '@/utils'; +import { tryNormalLogged } from '@/utils/guard'; 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'; +} from '@/services'; import { Form, ActionBar, Reply } from './components'; @@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => { }; const handleVote = (id, is_cancel) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => { }; const handleAction = ({ action }, item) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'report') { diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx index 5f4475a2..bfa4c530 100644 --- a/ui/src/components/Editor/ToolBars/image.tsx +++ b/ui/src/components/Editor/ToolBars/image.tsx @@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react'; 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 { Modal as AnswerModal } from '@/components'; import ToolItem from '../toolItem'; import { IEditorContext } from '../types'; +import { uploadImage } from '@/services'; const Image: FC = ({ editor }) => { const { t } = useTranslation('translation', { keyPrefix: 'editor' }); diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 7f93a205..06fec6ec 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap'; 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 { TagSelector, Tag } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; +import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); @@ -32,7 +32,7 @@ const Index: FC = () => { }); }; - if (!isLogin()) { + if (!tryNormalLogged()) { return null; } diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 51711950..17e1bc64 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link, NavLink } from 'react-router-dom'; -import { Avatar, Icon } from '@answer/components'; +import { Avatar, Icon } from '@/components'; interface Props { redDot; diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 1d664db0..5775e32a 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -50,6 +50,10 @@ @media (max-width: 992.9px) { #header { + .logo { + max-width: 93px; + max-height: auto; + } .nav-grow { flex-grow: 1!important; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index abbd0c3c..8681c94c 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -17,9 +17,9 @@ 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 '@/stores'; +import { logout, useQueryNotificationStatus } from '@/services'; +import { RouteAlias } from '@/router/alias'; import NavItems from './components/NavItems'; @@ -27,7 +27,7 @@ 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 +42,8 @@ const Header: FC = () => { const handleLogout = async () => { await logout(); - Storage.remove('token'); clear(); - navigate('/'); + navigate(RouteAlias.home); }; useEffect(() => { diff --git a/ui/src/components/HotQuestions/index.tsx b/ui/src/components/HotQuestions/index.tsx index d398d172..da0305d0 100644 --- a/ui/src/components/HotQuestions/index.tsx +++ b/ui/src/components/HotQuestions/index.tsx @@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useHotQuestions } from '@answer/api'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; +import { useHotQuestions } from '@/services'; const HotQuestions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); diff --git a/ui/src/components/Mentions/index.tsx b/ui/src/components/Mentions/index.tsx index 9b29c86d..23cfa083 100644 --- a/ui/src/components/Mentions/index.tsx +++ b/ui/src/components/Mentions/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FC } from 'react'; import { Dropdown } from 'react-bootstrap'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; interface IProps { children: React.ReactNode; diff --git a/ui/src/components/Modal/PicAuthCodeModal.tsx b/ui/src/components/Modal/PicAuthCodeModal.tsx index f0fe2091..ec194a64 100644 --- a/ui/src/components/Modal/PicAuthCodeModal.tsx +++ b/ui/src/components/Modal/PicAuthCodeModal.tsx @@ -2,12 +2,10 @@ import React from 'react'; import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; -import type { - FormValue, - FormDataType, - ImgCodeRes, -} from '@answer/common/interface'; +import { Icon } from '@/components'; +import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface'; +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; interface IProps { /** control visible */ @@ -55,7 +53,7 @@ const Index: React.FC = ({ 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, diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 6d76af2a..cdacc751 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -3,11 +3,11 @@ import { Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; 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 { Modal } from '@/components'; +import { useReportModal, useToast } from '@/hooks'; import Share from '../Share'; +import { deleteQuestion, deleteAnswer } from '@/services'; +import { tryNormalLogged } from '@/utils/guard'; interface IProps { type: 'answer' | 'question'; @@ -98,7 +98,7 @@ const Index: FC = ({ }; const handleAction = (action) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'delete') { diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index c513f795..a353d9bd 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,8 +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 type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; import { Icon, Tag, @@ -13,7 +12,8 @@ import { Empty, BaseUserCard, QueryGroup, -} from '@answer/components'; +} from '@/components'; +import { useQuestionList } from '@/services'; const QuestionOrderKeys: Type.QuestionOrderBy[] = [ 'newest', diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index d5866d98..652dcef1 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -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 '@/stores'; interface IProps { type: 'answer' | 'question'; @@ -15,7 +15,7 @@ interface IProps { } const Index: FC = ({ 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); diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 790b8242..8747f65a 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { useTagModal } from '@answer/hooks'; -import { queryTags } from '@answer/api'; -import type * as Type from '@answer/common/interface'; +import { useTagModal } from '@/hooks'; +import type * as Type from '@/common/interface'; +import { queryTags } from '@/services'; import './index.scss'; diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 5ff85c2a..420891cc 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -3,14 +3,12 @@ import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { resendEmail, checkImgCode } from '@answer/api'; -import { PicAuthCodeModal } from '@answer/components/Modal'; -import type { - ImgCodeRes, - ImgCodeReq, - FormDataType, -} from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; +import { PicAuthCodeModal } from '@/components/Modal'; +import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; +import { resendEmail, checkImgCode } from '@/services'; +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; interface IProps { visible: boolean; @@ -20,7 +18,7 @@ const Index: React.FC = ({ 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({ captcha_code: { value: '', @@ -48,7 +46,7 @@ const Index: React.FC = ({ 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, diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 9688f818..16be5b0d 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -3,8 +3,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; -import { Avatar, FormatTime } from '@answer/components'; - +import { Avatar, FormatTime } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index b46aa80d..529f7a3a 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { changeUserStatus } from '@answer/api'; -import { Modal as AnswerModal } from '@answer/components'; +import { Modal as AnswerModal } from '@/components'; +import { changeUserStatus } from '@/services'; const div = document.createElement('div'); const root = ReactDOM.createRoot(div); diff --git a/ui/src/hooks/usePageUsers/index.tsx b/ui/src/hooks/usePageUsers/index.tsx index 72203619..2c828695 100644 --- a/ui/src/hooks/usePageUsers/index.tsx +++ b/ui/src/hooks/usePageUsers/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { uniqBy } from 'lodash'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; let globalUsers: Types.PageUser[] = []; const usePageUsers = () => { diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 2566ccd6..28b85eb5 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,9 +4,9 @@ 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 { useToast } from '@/hooks'; +import type * as Type from '@/common/interface'; +import { reportList, postReport, closeQuestion, putReport } from '@/services'; interface Params { isBackend?: boolean; diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index 495ecc1d..e8b41e31 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next'; import i18next from 'i18next'; import Backend from 'i18next-http-backend'; +import { DEFAULT_LANG } from '@/common/constants'; + import en from './locales/en.json'; import zh from './locales/zh_CN.json'; @@ -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, }, diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 5715e26c..19422b24 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -28,7 +28,10 @@ "confirm_email": "Confirm Email", "account_suspended": "Account Suspended", "admin": "Admin", - "change_email": "Modify Email" + "change_email": "Modify Email", + "install": "Answer Installation", + "upgrade": "Answer Upgrade", + "maintenance": "Webite Maintenance" }, "notifications": { "title": "Notifications", @@ -290,7 +293,9 @@ "now": "now", "x_seconds_ago": "{{count}}s ago", "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago" + "x_hours_ago": "{{count}}h ago", + "hour": "hour", + "day": "day" }, "comment": { "btn_add_comment": "Add comment", @@ -735,6 +740,84 @@ "x_answers": "answers", "x_questions": "questions" }, + "install": { + "title": "Answer", + "next": "Next", + "done": "Done", + "lang": { + "label": "Please choose a language" + }, + "db_type": { + "label": "Database Engine" + }, + "db_username": { + "label": "Username", + "placeholder": "root", + "msg": "Username cannot be empty." + }, + "db_password": { + "label": "Password", + "placeholder": "root", + "msg": "Password cannot be empty." + }, + "db_host": { + "label": "Database Host", + "placeholder": "db:3306", + "msg": "Database Host cannot be empty." + }, + "db_name": { + "label": "Database Name", + "placeholder": "answer", + "msg": "Database Name cannot be empty." + }, + "db_file": { + "label": "Database File", + "placeholder": "/data/answer.db", + "msg": "Database File cannot be empty." + }, + "config_yaml": { + "title": "Create config.yaml", + "label": "The config.yaml file created.", + "description": "You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it.", + "info": "After you’ve done that, click “Next” button." + }, + "site_information": "Site Information", + "admin_account": "Admin Account", + "site_name": { + "label": "Site Name" + }, + "contact_email": { + "label": "Contact Email", + "text": "Email address of key contact responsible for this site." + }, + "admin_name": { + "label": "Name" + }, + "admin_password": { + "label": "Password", + "text": "You will need this password to log in. Please store it in a secure location." + }, + "admin_email": { + "label": "Email", + "text": "You will need this email to log in." + }, + "ready_title": "Your Answer is Ready!", + "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", + "good_luck": "Have fun, and good luck!", + "warning": "Warning", + "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", + "installed": "Already installed", + "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." + }, + "upgrade": { + "title": "Answer", + "update_btn": "Update data", + "update_title": "Data update required", + "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", + "done_title": "No update required", + "done_btn": "Done", + "done_desscription": "Your Answer data is already up-to-date." + }, "page_404": { "description": "Unfortunately, this page doesn't exist.", "back_home": "Back to homepage" @@ -743,6 +826,9 @@ "description": "The server encountered an error and could not complete your request.", "back_home": "Back to homepage" }, + "page_maintenance": { + "description": "We are under maintenance, we’ll be back soon." + }, "admin": { "admin_header": { "title": "Admin" @@ -762,7 +848,36 @@ "dashboard": { "title": "Dashboard", "welcome": "Welcome to Answer Admin !", - "version": "Version" + "site_statistics": "Site Statistics", + "questions": "Questions:", + "answers": "Answers:", + "comments": "Comments:", + "votes": "Votes:", + "active_users": "Active users:", + "flags": "Flags:", + "site_health_status": "Site Health Status", + "version": "Version:", + "https": "HTTPS:", + "uploading_files": "Uploading files:", + "smtp": "SMTP:", + "timezone": "Timezone:", + "system_info": "System Info", + "storage_used": "Storage used:", + "uptime": "Uptime:", + "answer_links": "Answer Links", + "documents": "Documents", + "feedback": "Feedback", + "review": "Review", + "config": "Config", + "update_to": "Update to", + "latest": "Latest", + "check_failed": "Check failed", + "yes": "Yes", + "no": "No", + "not_allowed": "Not allowed", + "allowed": "Allowed", + "enabled": "Enabled", + "disabled": "Disabled" }, "flags": { "title": "Flags", @@ -819,7 +934,10 @@ "inactive": "Inactive", "suspended": "Suspended", "deleted": "Deleted", - "normal": "Normal" + "normal": "Normal", + "filter": { + "placeholder": "Filter by name, user:id" + } }, "questions": { "page_title": "Questions", @@ -832,7 +950,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, question:id" + } }, "answers": { "page_title": "Answers", @@ -843,7 +964,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, answer:id" + } }, "general": { "page_title": "General", @@ -879,6 +1003,11 @@ "label": "Interface Language", "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." + }, + "timezone": { + "label": "Timezone", + "msg": "Timezone cannot be empty.", + "text": "Choose a UTC (Coordinated Universal Time) time offset." } }, "smtp": { diff --git a/ui/src/index.scss b/ui/src/index.scss index e81a900c..b9114cd5 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -77,6 +77,10 @@ a { .page-wrap { min-height: calc(100vh - 148px); } +.page-wrap2 { + background-color: #f5f5f5; + min-height: 100vh; +} .btn-no-border, .btn-no-border:hover, diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8553972a..67e5ee50 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,15 +2,27 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { Guard } from '@/utils'; + import App from './App'; + import './i18n/init'; import './index.scss'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); -root.render( - - - , -); + +async function bootstrapApp() { + /** + * NOTICE: must pre init logged user info for router + */ + await Guard.pullLoggedUser(); + root.render( + + + , + ); +} + +bootstrapApp(); diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index f9a9de27..3a33fe49 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -11,21 +11,23 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal } from '@answer/hooks'; -import { useAnswerSearch, changeAnswerStatus } from '@answer/api'; -import * as Type from '@answer/common/interface'; +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal } from '@/hooks'; +import * as Type from '@/common/interface'; +import { useAnswerSearch, changeAnswerStatus } from '@/services'; import '../index.scss'; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; const Answers: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || answerFilterItems[0]; const PAGE_SIZE = 20; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; + const questionId = urlSearchParams.get('questionId') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { @@ -36,6 +38,8 @@ const Answers: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, + question_id: questionId, }); const count = listData?.count || 0; @@ -77,6 +81,11 @@ const Answers: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -89,19 +98,20 @@ const Answers: FC = () => { /> - +
- + - + {curFilter !== 'deleted' && } @@ -132,6 +142,7 @@ const Answers: FC = () => { __html: li.description, }} className="last-p text-truncate-2 fs-14" + style={{ maxWidth: '30rem' }} /> diff --git a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx new file mode 100644 index 00000000..8f5048f8 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx @@ -0,0 +1,31 @@ +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const AnswerLinks = () => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('answer_links')}
+ +
+ + {t('documents')} + + + + + {t('feedback')} + + + + + + ); +}; + +export default AnswerLinks; diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx new file mode 100644 index 00000000..1b1c7ced --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { Card, Row, Col, Badge } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import type * as Type from '@/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} + +const HealthStatus: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_health_status')}
+ +
+ {t('version')} + 90 + + {t('update_to')} {data.app_version} + + + + {t('https')} + {data.https ? t('yes') : t('yes')} + + + {t('uploading_files')} + + {data.uploading_files ? t('allowed') : t('not_allowed')} + + + + {t('smtp')} + {data.smtp ? t('enabled') : t('disabled')} + + {t('config')} + + + + {t('timezone')} + {data.time_zone} + + + + + ); +}; + +export default HealthStatus; diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx new file mode 100644 index 00000000..9e6c979e --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const Statistics: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_statistics')}
+ +
+ {t('questions')} + {data.question_count} + + + {t('answers')} + {data.answer_count} + + + {t('comments')} + {data.comment_count} + + + {t('votes')} + {data.vote_count} + + + {t('active_users')} + {data.user_count} + + + {t('flags')} + {data.report_count} + + {t('review')} + + + + + + ); +}; + +export default Statistics; diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx new file mode 100644 index 00000000..cbc065c7 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@/common/interface'; +import { formatUptime } from '@/utils'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const SystemInfo: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('system_info')}
+ +
+ {t('storage_used')} + {data.occupying_storage_space} + + + {t('uptime')} + {formatUptime(data.app_start_time)} + + + + + ); +}; + +export default SystemInfo; diff --git a/ui/src/pages/Admin/Dashboard/components/index.ts b/ui/src/pages/Admin/Dashboard/components/index.ts new file mode 100644 index 00000000..877f643f --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/index.ts @@ -0,0 +1,6 @@ +import SystemInfo from './SystemInfo'; +import Statistics from './Statistics'; +import AnswerLinks from './AnswerLinks'; +import HealthStatus from './HealthStatus'; + +export { SystemInfo, Statistics, AnswerLinks, HealthStatus }; diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 45c19721..2037016e 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -1,12 +1,41 @@ import { FC } from 'react'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { useDashBoard } from '@/services'; + +import { + AnswerLinks, + HealthStatus, + Statistics, + SystemInfo, +} from './components'; + const Dashboard: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + const { data } = useDashBoard(); + + if (!data) { + return null; + } return ( <>

{t('title')}

{t('welcome')}

+ + + + + + + + + + + + + + {process.env.REACT_APP_VERSION && (

{`${t('version')} `} diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index 5eb14ec4..353bd382 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -9,10 +9,10 @@ import { Empty, Pagination, QueryGroup, -} from '@answer/components'; -import { useReportModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; -import { useFlagSearch } from '@answer/api'; +} from '@/components'; +import { useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; +import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 87e473d3..14b95978 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -2,10 +2,10 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; -import { siteInfoStore } from '@answer/stores'; -import { useGeneralSetting, updateGeneralSetting } from '@answer/api'; +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; +import { siteInfoStore } from '@/stores'; +import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 5a332d20..395b9616 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -2,21 +2,22 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; import { Form, Button, Image, Stack } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { useToast } from '@answer/hooks'; +import { useToast } from '@/hooks'; import { LangsType, FormDataType, AdminSettingsInterface, -} from '@answer/common/interface'; +} from '@/common/interface'; +import { interfaceStore } from '@/stores'; +import { UploadImg } from '@/components'; +import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { languages, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, -} from '@answer/api'; -import { interfaceStore } from '@answer/stores'; -import { UploadImg } from '@answer/components'; +} from '@/services'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -27,6 +28,7 @@ const Interface: FC = () => { const Toast = useToast(); const [langs, setLangs] = useState(); const { data: setting } = useInterfaceSetting(); + const [formData, setFormData] = useState({ logo: { value: setting?.logo || '', @@ -43,6 +45,11 @@ const Interface: FC = () => { isInvalid: false, errorMsg: '', }, + time_zone: { + value: setting?.time_zone || DEFAULT_TIMEZONE, + isInvalid: false, + errorMsg: '', + }, }); const getLangs = async () => { const res: LangsType[] = await languages(); @@ -106,6 +113,7 @@ const Interface: FC = () => { logo: formData.logo.value, theme: formData.theme.value, language: formData.language.value, + time_zone: formData.time_zone.value, }; updateInterfaceSetting(reqParams) @@ -158,12 +166,14 @@ const Interface: FC = () => { Object.keys(setting).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - setFormData(formMeta); + setFormData({ ...formData, ...formMeta }); } }, [setting]); useEffect(() => { getLangs(); }, []); + + console.log('formData', formData); return ( <>

{t('page_title')}

@@ -249,7 +259,27 @@ const Interface: FC = () => { {formData.language.errorMsg} - + + {t('time_zone.label')} + { + onChange('time_zone', evt.target.value); + }}> + {TIMEZONES?.map((item) => { + return ( + + ); + })} + + {t('time_zone.text')} + + {formData.time_zone.errorMsg} + + diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 373faf94..c2f5e5c6 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { Button, Form, Table, Stack, Badge } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -11,15 +11,15 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal, useReportModal } from '@answer/hooks'; +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal, useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useQuestionSearch, changeQuestionStatus, deleteQuestion, -} from '@answer/api'; -import * as Type from '@answer/common/interface'; +} from '@/services'; import '../index.scss'; @@ -31,9 +31,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [ const PAGE_SIZE = 20; const Questions: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || questionFilterItems[0]; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' }); const { @@ -44,6 +45,7 @@ const Questions: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, }); const count = listData?.count || 0; @@ -96,6 +98,11 @@ const Questions: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -108,10 +115,11 @@ const Questions: FC = () => { /> @@ -147,12 +155,11 @@ const Questions: FC = () => {
+

{t('title')}

+ + + {showError && show error msg } + + + + + + + + + + + {step === 6 && ( +
+
{t('warning')}
+

+ + The file config.yaml already exists. If you + need to reset any of the configuration items in this + file, please delete it first. You may try{' '} + installing now. + +

+
+ )} + + {step === 7 && ( +
+
{t('installed')}
+

{t('installed_description')}

+
+ )} +
+
+ + + + + ); +}; + +export default Index; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 69c5195d..363c9b01 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,60 +1,42 @@ -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 { Header, AdminHeader, Footer, Toast } from '@answer/components'; -import { useSiteSettings, useCheckUserStatus } from '@answer/api'; - +import { siteInfoStore, interfaceStore, toastStore } from '@/stores'; +import { Header, AdminHeader, Footer, Toast } from '@/components'; +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 ( @@ -76,4 +58,4 @@ const Layout: FC = () => { ); }; -export default Layout; +export default memo(Layout); diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx new file mode 100644 index 00000000..560108bf --- /dev/null +++ b/ui/src/pages/Maintenance/index.tsx @@ -0,0 +1,27 @@ +import { Container } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'page_maintenance', + }); + return ( +
+ + +
+ (=‘_‘=) +
+
{t('description')}
+
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx index 16388963..99a8026a 100644 --- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx +++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { Accordion, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index abcc86f6..fe4d3319 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; +import { Editor, EditorRef, TagSelector, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { saveQuestion, questionDetail, @@ -14,8 +15,7 @@ import { useQueryRevisions, postAnswer, useQueryQuestionByTitle, -} from '@answer/api'; -import type * as Type from '@answer/common/interface'; +} from '@/services'; import SearchQuestion from './components/SearchQuestion'; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index b7bd25d8..2fe87d1c 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -10,10 +10,10 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { acceptanceAnswer } from '@answer/api'; -import { scrollTop } from '@answer/utils'; -import { AnswerItem } from '@answer/common/interface'; +} from '@/components'; +import { scrollTop } from '@/utils'; +import { AnswerItem } from '@/common/interface'; +import { acceptanceAnswer } from '@/services'; interface Props { data: AnswerItem; diff --git a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx index f2c7efe9..32abc17b 100644 --- a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx +++ b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx @@ -1,7 +1,7 @@ import { memo, FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; interface Props { count: number; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 14f85f24..5795ca42 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -11,9 +11,9 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { formatCount } from '@answer/utils'; -import { following } from '@answer/api'; +} from '@/components'; +import { formatCount } from '@/utils'; +import { following } from '@/services'; interface Props { data: any; diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index bada8207..3dd91c7f 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,16 +3,15 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useSimilarQuestion } from '@answer/api'; -import { Icon } from '@answer/components'; - -import { userInfoStore } from '@/stores'; +import { Icon } from '@/components'; +import { useSimilarQuestion } from '@/services'; +import { loggedUserInfoStore } from '@/stores'; interface Props { id: string; } const Index: FC = ({ id }) => { - const { user } = userInfoStore(); + const { user } = loggedUserInfoStore(); const { t } = useTranslation('translation', { keyPrefix: 'related_question', }); diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 6343f7ef..8d1ef1be 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { Editor, Modal } from '@answer/components'; -import { postAnswer } from '@answer/api'; -import { FormDataType } from '@answer/common/interface'; +import { Editor, Modal } from '@/components'; +import { FormDataType } from '@/common/interface'; +import { postAnswer } from '@/services'; interface Props { visible?: boolean; diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 2098b6a6..cc19a719 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,16 +2,16 @@ 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 { Pagination, PageTitle } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; -import { scrollTop } from '@answer/utils'; -import { usePageUsers } from '@answer/hooks'; +import { Pagination, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { scrollTop } from '@/utils'; +import { usePageUsers } from '@/hooks'; import type { ListResult, QuestionDetailRes, AnswerItem, -} from '@answer/common/interface'; +} from '@/common/interface'; +import { questionDetail, getAnswers } from '@/services'; import { Question, @@ -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({ diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 118fd33c..0fab103c 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; +import { Editor, EditorRef, Icon, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { useQueryAnswerInfo, modifyAnswer, useQueryRevisions, -} from '@answer/api'; -import type * as Type from '@answer/common/interface'; +} from '@/services'; import './index.scss'; diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx index 9e7ed318..ea10ec15 100644 --- a/ui/src/pages/Questions/index.tsx +++ b/ui/src/pages/Questions/index.tsx @@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useMatch } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { PageTitle, FollowingTags } from '@answer/components'; - +import { PageTitle, FollowingTags } from '@/components'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; import { siteInfoStore } from '@/stores'; diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index c44f67bd..095796d2 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -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/guard'; interface Props { data; @@ -20,7 +20,7 @@ const Index: FC = ({ data }) => { const [followed, setFollowed] = useState(data?.is_follower); const follow = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } following({ diff --git a/ui/src/pages/Search/components/SearchHead/index.tsx b/ui/src/pages/Search/components/SearchHead/index.tsx index 2d7549a4..fb185bc1 100644 --- a/ui/src/pages/Search/components/SearchHead/index.tsx +++ b/ui/src/pages/Search/components/SearchHead/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['relevance', 'newest', 'active', 'score']; diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index ec703277..09cecd32 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -2,8 +2,8 @@ import { memo, FC } from 'react'; import { ListGroupItem, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components'; -import type { SearchResItem } from '@answer/common/interface'; +import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; +import type { SearchResItem } from '@/common/interface'; interface Props { data: SearchResItem; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index d08968f4..f22dc3ce 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -3,8 +3,8 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; -import { Pagination, PageTitle } from '@answer/components'; -import { useSearch } from '@answer/api'; +import { Pagination, PageTitle } from '@/components'; +import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index cb736c92..452ef37d 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -3,10 +3,9 @@ import { Container, Row, Col, Button } from 'react-bootstrap'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import * as Type from '@answer/common/interface'; -import { PageTitle, FollowingTags } from '@answer/components'; -import { useTagInfo, useFollow } from '@answer/api'; - +import * as Type from '@/common/interface'; +import { PageTitle, FollowingTags } from '@/components'; +import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 2b0541e1..890bd32e 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next'; 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 type * as Type from '@answer/common/interface'; +import { Editor, EditorRef, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import type * as Type from '@/common/interface'; +import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; interface FormDataItem { displayName: Type.FormValue; @@ -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(); diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 96cd814a..3b792adb 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -5,19 +5,13 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { - Tag, - TagSelector, - FormatTime, - Modal, - PageTitle, -} from '@answer/components'; +import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components'; import { useTagInfo, useQuerySynonymsTags, saveSynonymsTags, deleteTag, -} from '@answer/api'; +} from '@/services'; const TagIntroduction = () => { const [isEdit, setEditState] = useState(false); diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 2f9719f6..a01e3e56 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,9 +3,9 @@ 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 { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; -import { formatCount } from '@answer/utils'; +import { Tag, Pagination, PageTitle, QueryGroup } from '@/components'; +import { formatCount } from '@/utils'; +import { useQueryTags, following } from '@/services'; const sortBtns = ['popular', 'name', 'newest']; diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx new file mode 100644 index 00000000..c65c9098 --- /dev/null +++ b/ui/src/pages/Upgrade/index.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { Container, Row, Col, Card, Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'upgrade', + }); + const [step, setStep] = useState(1); + + const handleUpdate = () => { + setStep(2); + }; + return ( +
+ + + +
+

{t('title')}

+ + + {step === 1 && ( + <> +
{t('update_title')}
+ }} + /> + + + )} + + {step === 2 && ( + <> +
{t('done_title')}
+

{t('done_desscription')}

+ + + )} +
+
+ + + + + ); +}; + +export default Index; diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index 9ae6d195..f919e560 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -2,13 +2,12 @@ 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 type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; - +} from '@/common/interface'; +import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; interface IProps { diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index b6a34610..e7a77ea5 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -2,12 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { isLogin } from '@answer/utils'; +import { tryNormalLogged } from '@/utils/guard'; +import { PageTitle } from '@/components'; import SendEmail from './components/sendEmail'; -import { PageTitle } from '@/components'; - const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' }); const [step, setStep] = useState(1); @@ -19,7 +18,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index dee3fcb7..ac2223a7 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,15 +1,14 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { activateAccount } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; - +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; +import { activateAccount } from '@/services'; 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'); diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 28de25c5..f87a6adf 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -3,14 +3,13 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { changeEmail, checkImgCode } from '@answer/api'; import type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; - +} from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; +import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; const Index: FC = () => { @@ -34,7 +33,7 @@ const Index: FC = () => { }); const [showModal, setModalState] = useState(false); const navigate = useNavigate(); - const { user: userInfo, update: updateUser } = userInfoStore(); + const { user: userInfo, update: updateUser } = loggedUserInfoStore(); const getImgCode = () => { checkImgCode({ diff --git a/ui/src/pages/Users/ChangeEmail/index.tsx b/ui/src/pages/Users/ChangeEmail/index.tsx index cbb743a5..cabc5e5f 100644 --- a/ui/src/pages/Users/ChangeEmail/index.tsx +++ b/ui/src/pages/Users/ChangeEmail/index.tsx @@ -2,10 +2,10 @@ import { FC, memo } from 'react'; import { Container, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import SendEmail from './components/sendEmail'; - import { PageTitle } from '@/components'; +import SendEmail from './components/sendEmail'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 83c48a66..79b55ee7 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify, getUserInfo } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/stores'; +import { changeEmailVerify, getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { @@ -13,7 +12,7 @@ const Index: FC = () => { const [searchParams] = useSearchParams(); const [step, setStep] = useState('loading'); - const updateUser = userInfoStore((state) => state.update); + const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { const code = searchParams.get('code'); @@ -22,7 +21,7 @@ const Index: FC = () => { changeEmailVerify({ code }) .then(() => { setStep('success'); - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { // update user info updateUser(res); }); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 8d18a500..423a6654 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -1,26 +1,28 @@ 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'; - +} from '@/common/interface'; +import { PageTitle, Unactivate } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString, Guard, floppyNavigation } from '@/utils'; +import { login, checkImgCode } from '@/services'; +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({ e_mail: { value: '', @@ -102,15 +104,18 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - if (res.mail_status === 2) { + const userStat = Guard.deriveLoginState(); + if (userStat.isNotActivated) { // 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); + floppyNavigation.navigate(path, () => { + 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(); + Guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx index 12cfe93b..54bd4cdb 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { Empty } from '@answer/components'; +import { Empty } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx index 27366b81..68739b0c 100644 --- a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { FormatTime, Empty } from '@answer/components'; +import { FormatTime, Empty } from '@/components'; const Inbox = ({ data, handleReadNotification }) => { if (!data) { diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index aef9be45..1ee7158c 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -3,13 +3,13 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; +import { PageTitle } from '@/components'; import { useQueryNotifications, clearUnreadNotification, clearNotificationStatus, readNotification, -} from '@answer/api'; -import { PageTitle } from '@answer/components'; +} from '@/services'; import Inbox from './components/Inbox'; import Achievements from './components/Achievements'; @@ -46,6 +46,9 @@ const Notifications = () => { const handleTypeChange = (evt, val) => { evt.preventDefault(); + if (type === val) { + return; + } setPage(1); setNotificationData([]); navigate(`/users/notifications/${val}`); diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index abffc6ba..b97bd707 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -3,19 +3,18 @@ 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 type { FormDataType } from '@answer/common/interface'; - -import Storage from '@/utils/storage'; +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; +import type { FormDataType } from '@/common/interface'; +import { replacementPassword } from '@/services'; +import { tryNormalLogged } from '@/utils/guard'; 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({ pass: { value: '', @@ -105,7 +104,6 @@ const Index: React.FC = () => { .then(() => { // clear login information then to login page clearUser(); - Storage.remove('token'); setStep(2); }) .catch((err) => { @@ -118,7 +116,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( <> diff --git a/ui/src/pages/Users/Personal/components/Answers/index.tsx b/ui/src/pages/Users/Personal/components/Answers/index.tsx index 12ab0059..a7d8c48a 100644 --- a/ui/src/pages/Users/Personal/components/Answers/index.tsx +++ b/ui/src/pages/Users/Personal/components/Answers/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag } from '@answer/components'; +import { Icon, FormatTime, Tag } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/Comments/index.tsx b/ui/src/pages/Users/Personal/components/Comments/index.tsx index 483ce361..f53e468e 100644 --- a/ui/src/pages/Users/Personal/components/Comments/index.tsx +++ b/ui/src/pages/Users/Personal/components/Comments/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx index 7eb67525..e249067f 100644 --- a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx +++ b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components'; +import { Icon, FormatTime, Tag, BaseUserCard } from '@/components'; interface Props { visible: boolean; @@ -34,7 +34,7 @@ const Index: FC = ({ visible, tabName, data }) => { : null} -
+
{tabName === 'bookmarks' && ( <> diff --git a/ui/src/pages/Users/Personal/components/ListHead/index.tsx b/ui/src/pages/Users/Personal/components/ListHead/index.tsx index e1ce494f..de550754 100644 --- a/ui/src/pages/Users/Personal/components/ListHead/index.tsx +++ b/ui/src/pages/Users/Personal/components/ListHead/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['newest', 'score']; diff --git a/ui/src/pages/Users/Personal/components/NavBar/index.tsx b/ui/src/pages/Users/Personal/components/NavBar/index.tsx index 75fe68ee..4c98ce50 100644 --- a/ui/src/pages/Users/Personal/components/NavBar/index.tsx +++ b/ui/src/pages/Users/Personal/components/NavBar/index.tsx @@ -44,7 +44,10 @@ const list = [ const Index: FC = ({ slug, tabName = 'overview', isSelf }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); return ( -
+ diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 2f067962..0d92c5fa 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,9 +3,8 @@ 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 type { FormDataType } from '@answer/common/interface'; - +import type { FormDataType } from '@/common/interface'; +import { register } from '@/services'; import userStore from '@/stores/userInfo'; interface Props { diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index c50c353c..e4d94b2d 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react'; import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { PageTitle, Unactivate } from '@answer/components'; -import { isLogin } from '@answer/utils'; +import { PageTitle, Unactivate } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; import SignUpForm from './components/SignUpForm'; @@ -16,7 +16,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 84182a53..8bfa6dfa 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -2,9 +2,9 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; 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 { useToast } from '@answer/hooks'; +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; +import { getLoggedUserInfo, changeEmail } from '@/services'; const reg = /(?<=.{2}).+(?=@)/gi; @@ -23,7 +23,7 @@ const Index: FC = () => { const [userInfo, setUserInfo] = useState(); const toast = useToast(); useEffect(() => { - getUserInfo().then((resp) => { + getLoggedUserInfo().then((resp) => { setUserInfo(resp); }); }, []); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 23265763..56d8f17b 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,9 +2,9 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { modifyPassword } from '@answer/api'; -import { useToast } from '@answer/hooks'; -import type { FormDataType } from '@answer/common/interface'; +import { useToast } from '@/hooks'; +import type { FormDataType } from '@/common/interface'; +import { modifyPassword } from '@/services'; const Index: FC = () => { const { t } = useTranslation('translation', { diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index ca7ce38d..3a3941b9 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,10 +6,10 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import { languages } from '@answer/api'; -import type { LangsType, FormDataType } from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type { LangsType, FormDataType } from '@/common/interface'; +import { useToast } from '@/hooks'; +import { languages } from '@/services'; +import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; const Index = () => { @@ -34,8 +34,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 +45,7 @@ const Index = () => { useEffect(() => { getLangs(); - const lang = Storage.get('LANG'); + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { setFormData({ lang: { @@ -60,7 +60,6 @@ const Index = () => {
{t('lang.label')} - { const toast = useToast(); @@ -20,7 +20,7 @@ const Index = () => { }); const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { setFormData({ notice_switch: { value: res.notice_status === 1, diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 84478fd1..500c2ef1 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -5,18 +5,18 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; import MD5 from 'md5'; -import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api'; -import type { FormDataType } from '@answer/common/interface'; -import { UploadImg, Avatar } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; +import type { FormDataType } from '@/common/interface'; +import { UploadImg, Avatar } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; +import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', }); const toast = useToast(); - const { user, update } = userInfoStore(); + const { user, update } = loggedUserInfoStore(); const [mailHash, setMailHash] = useState(''); const [count, setCount] = useState(0); @@ -188,7 +188,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; diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 45f3080c..dc591323 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,13 +3,12 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { getUserInfo } from '@answer/api'; -import type { FormDataType } from '@answer/common/interface'; +import type { FormDataType } from '@/common/interface'; +import { getLoggedUserInfo } from '@/services'; +import { PageTitle } from '@/components'; import Nav from './components/Nav'; -import { PageTitle } from '@/components'; - const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', @@ -43,7 +42,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; diff --git a/ui/src/pages/Users/Suspended/index.tsx b/ui/src/pages/Users/Suspended/index.tsx index 293603d0..403595a9 100644 --- a/ui/src/pages/Users/Suspended/index.tsx +++ b/ui/src/pages/Users/Suspended/index.tsx @@ -1,12 +1,11 @@ import { useTranslation } from 'react-i18next'; -import { userInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/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('/'); diff --git a/ui/src/router/alias.ts b/ui/src/router/alias.ts new file mode 100644 index 00000000..f6959ed3 --- /dev/null +++ b/ui/src/router/alias.ts @@ -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', +}; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 99d44723..e5aa2797 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -1,57 +1,56 @@ 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 ErrorBoundary from '@/pages/50X'; +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 = ; + rn.errorElement = ; } else { /** * cannot use a fully dynamic import statement * 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 = ( - + ); } 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 refLoader = rn.loader; + const refGuard = rn.guard; + rn.loader = async (args) => { + const gr = await refGuard(); + if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) { + return redirect(gr.redirect); } - }); - rn.loader = ({ params }) => { - ruleFunc.forEach((func) => { - func(params); - }); + + let lr; + if (typeof refLoader === 'function') { + lr = await refLoader(args); + } + return lr; }; } 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 }; diff --git a/ui/src/router/route-rules.ts b/ui/src/router/route-rules.ts deleted file mode 100644 index e7c2b83c..00000000 --- a/ui/src/router/route-rules.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isLogin } from '@answer/utils'; - -const RouteRules = { - isLoginAndNormal: () => { - return isLogin(true); - }, -}; - -export default RouteRules; diff --git a/ui/src/router/route-config.ts b/ui/src/router/routes.ts similarity index 67% rename from ui/src/router/route-config.ts rename to ui/src/router/routes.ts index 2e46ffbe..b5adce7c 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/routes.ts @@ -1,14 +1,28 @@ import { RouteObject } from 'react-router-dom'; +import { Guard } from '@/utils'; +import type { TGuardResult } from '@/utils/guard'; + export interface RouteNode extends RouteObject { page: string; children?: RouteNode[]; - rules?: string[]; + /** + * a method to auto guard route before route enter + * if the `ok` field in guard returned `TGuardResult` is true, + * it means the guard passed then enter the route. + * if guard returned the `TGuardResult` has `redirect` field, + * then auto redirect route to the `redirect` target. + */ + guard?: () => Promise; } -const routeConfig: RouteNode[] = [ + +const routes: RouteNode[] = [ { path: '/', page: 'pages/Layout', + guard: async () => { + return Guard.notForbidden(); + }, children: [ // question and answer { @@ -31,12 +45,16 @@ const routeConfig: RouteNode[] = [ { path: 'questions/ask', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/:aid/edit', @@ -62,6 +80,9 @@ const routeConfig: RouteNode[] = [ { path: 'tags/:tagId/edit', page: 'pages/Tags/Edit', + guard: async () => { + return Guard.activated(); + }, }, // users { @@ -75,6 +96,9 @@ const routeConfig: RouteNode[] = [ { path: 'users/settings', page: 'pages/Users/Settings', + guard: async () => { + return Guard.logged(); + }, children: [ { index: true, @@ -105,47 +129,85 @@ const routeConfig: RouteNode[] = [ { path: 'users/login', page: 'pages/Users/Login', + guard: async () => { + const notLogged = Guard.notLogged(); + if (notLogged.ok) { + return notLogged; + } + return Guard.notActivated(); + }, }, { path: 'users/register', page: 'pages/Users/Register', + guard: async () => { + return Guard.notLogged(); + }, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', + guard: async () => { + return Guard.activated(); + }, }, { path: 'users/change-email', page: 'pages/Users/ChangeEmail', + // TODO: guard this (change email when user not activated) ? }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', + guard: async () => { + return Guard.activated(); + }, }, { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', + guard: async () => { + const notActivated = Guard.notActivated(); + if (notActivated.ok) { + return notActivated; + } + return Guard.notLogged(); + }, }, { path: 'users/account-activation/success', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.activated(); + }, }, { path: '/users/account-activation/failed', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.notActivated(); + }, }, { path: '/users/confirm-new-email', page: 'pages/Users/ConfirmNewEmail', + // TODO: guard this }, { path: '/users/account-suspended', page: 'pages/Users/Suspended', + guard: async () => { + return Guard.forbidden(); + }, }, // for admin { path: 'admin', page: 'pages/Admin', + guard: async () => { + await Guard.pullLoggedUser(true); + return Guard.admin(); + }, children: [ { index: true, @@ -199,5 +261,17 @@ const routeConfig: RouteNode[] = [ }, ], }, + { + path: 'install', + page: 'pages/Install', + }, + { + path: '/maintenance', + page: 'pages/Maintenance', + }, + { + path: '/upgrade', + page: 'pages/Upgrade', + }, ]; -export default routeConfig; +export default routes; diff --git a/ui/src/services/admin/answer.ts b/ui/src/services/admin/answer.ts index 6fd0fbb6..08ad8e2f 100644 --- a/ui/src/services/admin/answer.ts +++ b/ui/src/services/admin/answer.ts @@ -1,10 +1,12 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; -export const useAnswerSearch = (params: Type.AdminContentsReq) => { +export const useAnswerSearch = ( + params: Type.AdminContentsReq & { question_id?: string }, +) => { const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`; const { data, error, mutate } = useSWR( [apiUrl], diff --git a/ui/src/services/admin/flag.ts b/ui/src/services/admin/flag.ts index 710cb447..64ea59f7 100644 --- a/ui/src/services/admin/flag.ts +++ b/ui/src/services/admin/flag.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const putReport = (params) => { return request.instance.put('/answer/admin/api/report', params); diff --git a/ui/src/services/admin/question.ts b/ui/src/services/admin/question.ts index a6308bf6..9e8d726a 100644 --- a/ui/src/services/admin/question.ts +++ b/ui/src/services/admin/question.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const changeUserStatus = (params) => { return request.put('/answer/admin/api/user/status', params); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index e1f486e7..5f370260 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useGeneralSetting = () => { const apiUrl = `/answer/admin/api/siteinfo/general`; @@ -70,3 +70,16 @@ export const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => { const apiUrl = `/answer/admin/api/setting/smtp`; return request.put(apiUrl, params); }; + +export const useDashBoard = () => { + const apiUrl = `/answer/admin/api/dashboard`; + const { data, error } = useSWR( + [apiUrl], + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + }; +}; diff --git a/ui/src/services/client/activity.ts b/ui/src/services/client/activity.ts index 7b7b539c..6f1a4fdb 100644 --- a/ui/src/services/client/activity.ts +++ b/ui/src/services/client/activity.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useFollow = (params?: Type.FollowParams) => { const apiUrl = '/answer/api/v1/follow'; diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index dbea39bf..1f1af66a 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -1,6 +1,5 @@ export * from './activity'; export * from './personal'; -export * from './user'; export * from './notification'; export * from './question'; export * from './search'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index 18b73343..a849db0a 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -1,9 +1,9 @@ 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 request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { @@ -33,7 +33,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, diff --git a/ui/src/services/client/personal.ts b/ui/src/services/client/personal.ts index b84e260f..6b61aaba 100644 --- a/ui/src/services/client/personal.ts +++ b/ui/src/services/client/personal.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const usePersonalInfoByName = (username: string) => { const apiUrl = '/answer/api/v1/personal/user/info'; diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index c35fe544..c418f26c 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useQuestionList = (params: Type.QueryQuestionsReq) => { const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`; diff --git a/ui/src/services/client/search.ts b/ui/src/services/client/search.ts index f5fe86fe..8d380294 100644 --- a/ui/src/services/client/search.ts +++ b/ui/src/services/client/search.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useSearch = (params?: Type.SearchParams) => { const apiUrl = '/answer/api/v1/search'; diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 87e46743..42b9f1ac 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import { isLogin } from '@answer/utils'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { @@ -24,7 +24,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(apiUrl, request.instance.get); diff --git a/ui/src/services/client/user.ts b/ui/src/services/client/user.ts deleted file mode 100644 index abe9ee3a..00000000 --- a/ui/src/services/client/user.ts +++ /dev/null @@ -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, - }; -}; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index c98c532c..f27eb0a9 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const uploadImage = (file) => { const form = new FormData(); @@ -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('/answer/api/v1/user/info'); }; diff --git a/ui/src/services/api.ts b/ui/src/services/index.ts similarity index 100% rename from ui/src/services/api.ts rename to ui/src/services/index.ts diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 0962911e..6bc6377e 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -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, diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index bbfb640e..017c3149 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -1,7 +1,11 @@ import create from 'zustand'; -import type { UserInfoRes } from '@answer/common/interface'; -import Storage from '@answer/utils/storage'; +import type { UserInfoRes } from '@/common/interface'; +import Storage from '@/utils/storage'; +import { + LOGGED_USER_STORAGE_KEY, + LOGGED_TOKEN_STORAGE_KEY, +} from '@/common/constants'; interface UserInfoStore { user: UserInfoRes; @@ -10,6 +14,7 @@ interface UserInfoStore { } const initUser: UserInfoRes = { + access_token: '', username: '', avatar: '', rank: 0, @@ -19,23 +24,23 @@ const initUser: UserInfoRes = { location: '', website: '', status: '', - mail_status: 0, + mail_status: 1, }; -const userInfoStore = create((set) => ({ +const loggedUserInfoStore = create((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; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts new file mode 100644 index 00000000..dcc2d084 --- /dev/null +++ b/ui/src/utils/common.ts @@ -0,0 +1,96 @@ +import i18next from 'i18next'; + +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)'); +} + +function formatUptime(value) { + const t = i18next.t.bind(i18next); + const second = parseInt(value, 10); + + if (second > 60 * 60 && second < 60 * 60 * 24) { + return `${Math.floor(second / 3600)} ${t('dates.hour')}`; + } + if (second > 60 * 60 * 24) { + return `${Math.floor(second / 3600 / 24)} ${t('dates.day')}`; + } + + return `< 1 ${t('dates.hour')}`; +} +export { + getQueryString, + thousandthDivision, + formatCount, + scrollTop, + matchedUsers, + parseUserInfo, + formatUptime, +}; diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts new file mode 100644 index 00000000..68e6f8a5 --- /dev/null +++ b/ui/src/utils/floppyNavigation.ts @@ -0,0 +1,41 @@ +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 loc = window.location; + const redirectUrl = loc.href.replace(loc.origin, ''); + Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl); + } + navigate(RouteAlias.login, () => { + window.location.replace(RouteAlias.login); + }); +}; + +export const floppyNavigation = { + differentCurrent, + navigate, + navigateToLogin, +}; diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts new file mode 100644 index 00000000..bc73ab9a --- /dev/null +++ b/ui/src/utils/guard.ts @@ -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 './floppyNavigation'; + +type TLoginState = { + isLogged: boolean; + isNotActivated: boolean; + isActivated: boolean; + isForbidden: boolean; + isNormal: boolean; + isAdmin: boolean; +}; + +export type TGuardResult = { + ok: boolean; + redirect?: string; +}; + +export const deriveLoginState = (): TLoginState => { + const ls: TLoginState = { + isLogged: false, + isNotActivated: false, + isActivated: false, + isForbidden: false, + isNormal: false, + isAdmin: false, + }; + const { user } = loggedUserInfoStore.getState(); + if (user.access_token) { + ls.isLogged = true; + } + if (ls.isLogged && user.mail_status === 1) { + ls.isActivated = true; + } + if (ls.isLogged && user.mail_status === 2) { + ls.isNotActivated = true; + } + if (ls.isLogged && user.status === 'forbidden') { + ls.isForbidden = true; + } + if (ls.isActivated && !ls.isForbidden) { + ls.isNormal = true; + } + if (ls.isNormal && user.is_admin === true) { + ls.isAdmin = true; + } + + return ls; +}; + +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 (!deriveLoginState().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 logged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (!us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.login; + } + return gr; +}; + +export const notLogged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notActivated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isActivated) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const activated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isNotActivated) { + gr.ok = false; + gr.redirect = RouteAlias.activation; + } + return gr; +}; + +export const forbidden = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notForbidden = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.suspended; + } + return gr; +}; + +export const admin = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isAdmin) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +/** + * try user was logged and all state ok + * @param autoLogin + */ +export const tryNormalLogged = (autoLogin: boolean = false) => { + const us = deriveLoginState(); + + if (us.isNormal) { + return true; + } + // must assert logged state first and return + if (!us.isLogged) { + if (autoLogin) { + floppyNavigation.navigateToLogin(); + } + return false; + } + if (us.isNotActivated) { + floppyNavigation.navigate(RouteAlias.activation, () => { + window.location.href = RouteAlias.activation; + }); + } else if (us.isForbidden) { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); + }); + } + + return false; +}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index 20cde293..69f70696 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -1,114 +1,6 @@ -import { LOGIN_NEED_BACK } from '@answer/common/constants'; +export { default as request } from './request'; +export { default as Storage } from './storage'; +export { floppyNavigation } from './floppyNavigation'; -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 * as Guard from './guard'; +export * from './common'; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index d532d707..4e878cb3 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -1,10 +1,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 { Modal } from '@/components'; +import { loggedUserInfoStore, toastStore } from '@/stores'; +import { + LOGGED_TOKEN_STORAGE_KEY, + CURRENT_LANG_STORAGE_KEY, + DEFAULT_LANG, +} from '@/common/constants'; +import { RouteAlias } from '@/router/alias'; import Storage from './storage'; +import { floppyNavigation } from './floppyNavigation'; const API = { development: '', @@ -25,12 +32,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 +60,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,65 +84,59 @@ 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; - 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'; - + // clear userinfo + loggedUserInfoStore.getState().clear(); + floppyNavigation.navigateToLogin(); 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') { + if (data?.type === 'url_expired') { // url expired - window.location.href = '/users/account-activation/failed'; + 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 (realData?.type === 'suspended') { - if (window.location.pathname !== '/users/account-suspended') { - window.location.href = '/users/account-suspended'; - } - + if (data?.type === 'suspended') { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); + }); return Promise.reject(false); } } - - toastStore.getState().show({ - msg: `statusCode: ${status}; ${msg || ''}`, - variant: 'danger', - }); + if (respMsg) { + toastStore.getState().show({ + msg: `statusCode: ${status}; ${respMsg || ''}`, + variant: 'danger', + }); + } return Promise.reject(false); }, ); @@ -178,6 +178,4 @@ class Request { } } -// export const Request; - export default new Request(baseConfig); diff --git a/ui/src/utils/storage.ts b/ui/src/utils/storage.ts index bf14d85e..0ab3b115 100644 --- a/ui/src/utils/storage.ts +++ b/ui/src/utils/storage.ts @@ -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') { diff --git a/ui/tsconfig.json b/ui/tsconfig.json index c3804747..f270b720 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -20,19 +20,7 @@ "jsx": "react-jsx", "baseUrl": "./", "paths": { - "@/*": ["src/*"], - "@answer/pages/*": ["src/pages/*"], - "@answer/components": ["src/components/index.ts"], - "@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"], - "@answer/common/*": ["src/common/*"], - "@answer/utils": ["src/utils"], - "@answer/utils/*": ["src/utils/*"] + "@/*": ["src/*"] } }, "include": ["src", "node_modules/@testing-library/jest-dom"]
{t('post')}{t('post')} {t('votes')}{t('created')}{t('created')} {t('status')}{t('action')}
{li.vote_count} - {li.answer_count} - + diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index f6714255..33de30ab 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; -import { useSmtpSetting, updateSmtpSetting } from '@answer/api'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; +import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; const Smtp: FC = () => { diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index f15db7b5..ae8942b4 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -1,18 +1,18 @@ -import { FC, useState } from 'react'; +import { FC } from 'react'; 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 { Pagination, FormatTime, BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import * as Type from '@answer/common/interface'; -import { useChangeModal } from '@answer/hooks'; +} from '@/components'; +import * as Type from '@/common/interface'; +import { useChangeModal } from '@/hooks'; +import { useQueryUsers } from '@/services'; import '../index.scss'; @@ -33,11 +33,11 @@ const bgMap = { const PAGE_SIZE = 10; const Users: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.users' }); - const [userName, setUserName] = useState(''); - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curPage = Number(urlSearchParams.get('page') || '1'); + const curQuery = urlSearchParams.get('query') || ''; const { data, isLoading, @@ -45,7 +45,7 @@ const Users: FC = () => { } = useQueryUsers({ page: curPage, page_size: PAGE_SIZE, - ...(userName ? { username: userName } : {}), + query: curQuery, ...(curFilter === 'all' ? {} : { status: curFilter }), }); const changeModal = useChangeModal({ @@ -59,6 +59,11 @@ const Users: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('title')}

@@ -71,11 +76,10 @@ const Users: FC = () => { /> setUserName(e.target.value)} - placeholder="Filter by name" + value={curQuery} + onChange={handleFilter} + placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} /> diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 8d7b452e..27ef8565 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { AccordionNav, PageTitle } from '@answer/components'; -import { ADMIN_NAV_MENUS } from '@answer/common/constants'; +import { AccordionNav, PageTitle } from '@/components'; +import { ADMIN_NAV_MENUS } from '@/common/constants'; import './index.scss'; diff --git a/ui/src/pages/Install/components/FifthStep/index.tsx b/ui/src/pages/Install/components/FifthStep/index.tsx new file mode 100644 index 00000000..63f008e0 --- /dev/null +++ b/ui/src/pages/Install/components/FifthStep/index.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('ready_title')}
+

+ + If you ever feel like changing more settings, visit + admin section; find it in the site menu. + +

+

{t('good_luck')}

+ +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx new file mode 100644 index 00000000..5690ca8e --- /dev/null +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -0,0 +1,68 @@ +import { FC, useEffect, useState } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type { LangsType, FormValue, FormDataType } from '@/common/interface'; +import Progress from '../Progress'; +import { languages } from '@/services'; + +interface Props { + data: FormValue; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; + visible: boolean; +} +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + const [langs, setLangs] = useState(); + + const getLangs = async () => { + const res: LangsType[] = await languages(); + setLangs(res); + }; + + const handleSubmit = () => { + nextCallback(); + }; + + useEffect(() => { + getLangs(); + }, []); + + if (!visible) return null; + return ( +
+ + {t('lang.label')} + { + changeCallback({ + lang: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {langs?.map((item) => { + return ( + + ); + })} + + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx new file mode 100644 index 00000000..ccd44be2 --- /dev/null +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -0,0 +1,207 @@ +import { FC, FormEvent } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type { FormDataType } from '@/common/interface'; +import Progress from '../Progress'; + +interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; + visible: boolean; +} +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + const checkValidated = (): boolean => { + let bol = true; + const { + site_name, + contact_email, + admin_name, + admin_password, + admin_email, + } = data; + + if (!site_name.value) { + bol = false; + data.site_name = { + value: '', + isInvalid: true, + errorMsg: t('site_name.msg'), + }; + } + + if (!contact_email.value) { + bol = false; + data.contact_email = { + value: '', + isInvalid: true, + errorMsg: t('contact_email.msg'), + }; + } + + if (!admin_name.value) { + bol = false; + data.admin_name = { + value: '', + isInvalid: true, + errorMsg: t('admin_name.msg'), + }; + } + + if (!admin_password.value) { + bol = false; + data.admin_password = { + value: '', + isInvalid: true, + errorMsg: t('admin_password.msg'), + }; + } + + if (!admin_email.value) { + bol = false; + data.admin_email = { + value: '', + isInvalid: true, + errorMsg: t('admin_email.msg'), + }; + } + + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + + if (!visible) return null; + return ( +
+
{t('site_information')}
+ + {t('site_name.label')} + { + changeCallback({ + site_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.site_name.errorMsg} + + + + {t('contact_email.label')} + { + changeCallback({ + contact_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + {t('contact_email.text')} + + {data.contact_email.errorMsg} + + + +
{t('admin_account')}
+ + {t('admin_name.label')} + { + changeCallback({ + admin_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.admin_name.errorMsg} + + + + + {t('admin_password.label')} + { + changeCallback({ + admin_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + {t('admin_password.text')} + + {data.admin_password.errorMsg} + + + + + {t('admin_email.label')} + { + changeCallback({ + admin_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + {t('admin_email.text')} + + {data.admin_email.errorMsg} + + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/Progress/index.tsx b/ui/src/pages/Install/components/Progress/index.tsx new file mode 100644 index 00000000..97f33ffe --- /dev/null +++ b/ui/src/pages/Install/components/Progress/index.tsx @@ -0,0 +1,22 @@ +import { FC, memo } from 'react'; +import { ProgressBar } from 'react-bootstrap'; + +interface IProps { + step: number; +} + +const Index: FC = ({ step }) => { + return ( +
+ + {step}/5 +
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx new file mode 100644 index 00000000..6c97ec61 --- /dev/null +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -0,0 +1,246 @@ +import { FC, FormEvent } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; +import type { FormDataType } from '@/common/interface'; + +interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; + visible: boolean; +} + +const sqlData = [ + { + value: 'mysql', + label: 'MariaDB/MySQL', + }, + { + value: 'sqlite3', + label: 'SQLite', + }, + { + value: 'postgres', + label: 'PostgreSQL', + }, +]; + +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + const checkValidated = (): boolean => { + let bol = true; + const { db_type, db_username, db_password, db_host, db_name, db_file } = + data; + + if (db_type.value !== 'sqllite3') { + if (!db_username.value) { + bol = false; + data.db_username = { + value: '', + isInvalid: true, + errorMsg: t('db_username.msg'), + }; + } + + if (!db_password.value) { + bol = false; + data.db_password = { + value: '', + isInvalid: true, + errorMsg: t('db_password.msg'), + }; + } + + if (!db_host.value) { + bol = false; + data.db_host = { + value: '', + isInvalid: true, + errorMsg: t('db_host.msg'), + }; + } + + if (!db_name.value) { + bol = false; + data.db_name = { + value: '', + isInvalid: true, + errorMsg: t('db_name.msg'), + }; + } + } else if (!db_file.value) { + bol = false; + data.db_file = { + value: '', + isInvalid: true, + errorMsg: t('db_file.msg'), + }; + } + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + + if (!visible) return null; + return ( +
+ + {t('db_type.label')} + { + changeCallback({ + db_type: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {sqlData.map((item) => { + return ( + + ); + })} + + + {data.db_type.value !== 'sqlite3' ? ( + <> + + {t('db_username.label')} + { + changeCallback({ + db_username: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_username.errorMsg} + + + + + {t('db_password.label')} + { + changeCallback({ + db_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + + {data.db_password.errorMsg} + + + + + {t('db_host.label')} + { + changeCallback({ + db_host: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_host.errorMsg} + + + + + {t('db_name.label')} + { + changeCallback({ + db_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_name.errorMsg} + + + + ) : ( + + {t('db_file.label')} + { + changeCallback({ + db_file: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_file.errorMsg} + + + )} + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx new file mode 100644 index 00000000..3b5ca08c --- /dev/null +++ b/ui/src/pages/Install/components/ThirdStep/index.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; +import { Form, Button, FormGroup } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; + nextCallback: () => void; +} + +const Index: FC = ({ visible, nextCallback }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('config_yaml.title')}
+
{t('config_yaml.label')}
+
+

+ }} + /> +

+
+ + + +
{t('config_yaml.info')}
+
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/index.ts b/ui/src/pages/Install/components/index.ts new file mode 100644 index 00000000..ecc22539 --- /dev/null +++ b/ui/src/pages/Install/components/index.ts @@ -0,0 +1,7 @@ +import FirstStep from './FirstStep'; +import SecondStep from './SecondStep'; +import ThirdStep from './ThirdStep'; +import FourthStep from './FourthStep'; +import Fifth from './FifthStep'; + +export { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx new file mode 100644 index 00000000..e8c40d5a --- /dev/null +++ b/ui/src/pages/Install/index.tsx @@ -0,0 +1,182 @@ +import { FC, useState, useEffect } from 'react'; +import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import type { FormDataType } from '@/common/interface'; +import { Storage } from '@/utils'; +import { PageTitle } from '@/components'; + +import { + FirstStep, + SecondStep, + ThirdStep, + FourthStep, + Fifth, +} from './components'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const [step, setStep] = useState(1); + const [showError] = useState(false); + + const [formData, setFormData] = useState({ + lang: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_type: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_username: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_host: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_file: { + value: '', + isInvalid: false, + errorMsg: '', + }, + + site_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + contact_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + + const handleChange = (params: FormDataType) => { + console.log(params); + setFormData({ ...formData, ...params }); + }; + + const handleStep = () => { + setStep((pre) => pre + 1); + }; + + // const handleSubmit = () => { + // const params = { + // lang: formData.lang.value, + // db_type: formData.db_type.value, + // db_username: formData.db_username.value, + // db_password: formData.db_password.value, + // db_host: formData.db_host.value, + // db_name: formData.db_name.value, + // db_file: formData.db_file.value, + // site_name: formData.site_name.value, + // contact_email: formData.contact_email.value, + // admin_name: formData.admin_name.value, + // admin_password: formData.admin_password.value, + // admin_email: formData.admin_email.value, + // }; + + // console.log(params); + // }; + + useEffect(() => { + console.log('step===', Storage.get('INSTALL_STEP')); + }, []); + + return ( +
+ + + +