= ({ 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 (
+
+ {item.label}
+
+ );
+ })}
+
+ {t('time_zone.text')}
+
+ {formData.time_zone.errorMsg}
+
+
{t('save', { keyPrefix: 'btns' })}
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 = () => {
{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 (
+
+ {item.label}
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+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_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 (
+
+ );
+};
+
+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 (
+
+ {item.label}
+
+ );
+ })}
+
+
+ {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 (
+
+
+
+
+
+ {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')}
+ }}
+ />
+
+ {t('update_btn')}
+
+ >
+ )}
+
+ {step === 2 && (
+ <>
+ {t('done_title')}
+ {t('done_desscription')}
+ {t('done_btn')}
+ >
+ )}
+
+
+
+
+
+
+ );
+};
+
+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 (
-
+
{list.map((item) => {
if (item.role && !isSelf) {
return null;
diff --git a/ui/src/pages/Users/Personal/components/Reputation/index.tsx b/ui/src/pages/Users/Personal/components/Reputation/index.tsx
index fa1cb3db..6458e921 100644
--- a/ui/src/pages/Users/Personal/components/Reputation/index.tsx
+++ b/ui/src/pages/Users/Personal/components/Reputation/index.tsx
@@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-import { FormatTime } from '@answer/components';
+import { FormatTime } from '@/components';
interface Props {
visible: boolean;
diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx
index cdd7ba2e..86179305 100644
--- a/ui/src/pages/Users/Personal/components/TopList/index.tsx
+++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx
@@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-import { Icon } from '@answer/components';
+import { Icon } from '@/components';
interface Props {
data: any[];
diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx
index 9cc40004..d2bee725 100644
--- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx
+++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx
@@ -3,8 +3,8 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
-import { Avatar, Icon } from '@answer/components';
-import type { UserInfoRes } from '@answer/common/interface';
+import { Avatar, Icon } from '@/components';
+import type { UserInfoRes } from '@/common/interface';
interface Props {
data: UserInfoRes;
@@ -16,7 +16,7 @@ const Index: FC = ({ data }) => {
return null;
}
return (
-
+
{data?.status !== 'deleted' ? (
@@ -25,7 +25,7 @@ const Index: FC
= ({ data }) => {
)}
-
+
{data?.status !== 'deleted' ? (
= ({ data }) => {
@{data.username}
-
+
{data.rank || 0}
{t('x_reputation')}
diff --git a/ui/src/pages/Users/Personal/components/Votes/index.tsx b/ui/src/pages/Users/Personal/components/Votes/index.tsx
index 308af877..86a37aba 100644
--- a/ui/src/pages/Users/Personal/components/Votes/index.tsx
+++ b/ui/src/pages/Users/Personal/components/Votes/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;
@@ -18,7 +18,7 @@ const Index: FC
= ({ visible, data }) => {
return (
{item.vote_type}
diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx
index c514d5d3..bc8f9bce 100644
--- a/ui/src/pages/Users/Personal/index.tsx
+++ b/ui/src/pages/Users/Personal/index.tsx
@@ -3,13 +3,13 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom';
-import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components';
-import { userInfoStore } from '@answer/stores';
+import { Pagination, FormatTime, PageTitle, Empty } from '@/components';
+import { loggedUserInfoStore } from '@/stores';
import {
usePersonalInfoByName,
usePersonalTop,
usePersonalListByTabName,
-} from '@answer/api';
+} from '@/services';
import {
UserInfo,
@@ -30,7 +30,7 @@ const Personal: FC = () => {
const page = searchParams.get('page') || 1;
const order = searchParams.get('order') || 'newest';
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
- const sessionUser = userInfoStore((state) => state.user);
+ const sessionUser = loggedUserInfoStore((state) => state.user);
const isSelf = sessionUser?.username === username;
const { data: userInfo } = usePersonalInfoByName(username);
@@ -64,9 +64,9 @@ const Personal: FC = () => {
xxl={3}
lg={4}
sm={12}
- className="d-flex justify-content-end mt-5 mt-lg-0">
+ className="d-flex justify-content-start justify-content-md-end">
{isSelf && (
-
+
{
-
+
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"]