mirror of https://gitee.com/answerdev/answer.git
Merge branch 'ui-v0.3' into 'test'
Ui v0.3 See merge request opensource/answer!167
This commit is contained in:
commit
d48cf86512
|
@ -64,7 +64,7 @@ module.exports = {
|
||||||
position: 'before',
|
position: 'before',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: '@answer/**',
|
pattern: '@/**',
|
||||||
group: 'internal',
|
group: 'internal',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ['@commitlint/routes-conventional'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,13 +8,6 @@ module.exports = {
|
||||||
config.resolve.alias = {
|
config.resolve.alias = {
|
||||||
...config.resolve.alias,
|
...config.resolve.alias,
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
'@answer/pages': path.resolve(__dirname, 'src/pages'),
|
|
||||||
'@answer/components': path.resolve(__dirname, 'src/components'),
|
|
||||||
'@answer/stores': path.resolve(__dirname, 'src/stores'),
|
|
||||||
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
|
|
||||||
'@answer/utils': path.resolve(__dirname, 'src/utils'),
|
|
||||||
'@answer/common': path.resolve(__dirname, 'src/common'),
|
|
||||||
'@answer/api': path.resolve(__dirname, 'src/services/api'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import router from '@/router';
|
import { routes, createBrowserRouter } from '@/router';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const router = createBrowserRouter(routes);
|
||||||
return <RouterProvider router={router} />;
|
return <RouterProvider router={router} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
export const LOGIN_NEED_BACK = [
|
export const DEFAULT_LANG = 'en_US';
|
||||||
'/users/login',
|
export const CURRENT_LANG_STORAGE_KEY = '_a_lang__';
|
||||||
'/users/register',
|
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||||
'/users/account-recovery',
|
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||||
'/users/password-reset',
|
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||||
];
|
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||||
|
|
||||||
export const ADMIN_LIST_STATUS = {
|
export const ADMIN_LIST_STATUS = {
|
||||||
// normal;
|
// normal;
|
||||||
|
@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [
|
||||||
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// timezones
|
||||||
|
export const TIMEZONES = [
|
||||||
|
{
|
||||||
|
label: 'UTC-12',
|
||||||
|
value: 'UTC-12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-11:30',
|
||||||
|
value: 'UTC-11.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-11',
|
||||||
|
value: 'UTC-11',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-10:30',
|
||||||
|
value: 'UTC-10.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-10',
|
||||||
|
value: 'UTC-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-9:30',
|
||||||
|
value: 'UTC-9.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-9',
|
||||||
|
value: 'UTC-9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-8:30',
|
||||||
|
value: 'UTC-8.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-8',
|
||||||
|
value: 'UTC-8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-7:30',
|
||||||
|
value: 'UTC-7.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-7',
|
||||||
|
value: 'UTC-7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-6:30',
|
||||||
|
value: 'UTC-6.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-6',
|
||||||
|
value: 'UTC-6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-5:30',
|
||||||
|
value: 'UTC-5.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-5',
|
||||||
|
value: 'UTC-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-4:30',
|
||||||
|
value: 'UTC-4.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-4',
|
||||||
|
value: 'UTC-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-3:30',
|
||||||
|
value: 'UTC-3.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-3',
|
||||||
|
value: 'UTC-3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-2:30',
|
||||||
|
value: 'UTC-2.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-2',
|
||||||
|
value: 'UTC-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-1:30',
|
||||||
|
value: 'UTC-1.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-1',
|
||||||
|
value: 'UTC-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC-0:30',
|
||||||
|
value: 'UTC-0.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+0',
|
||||||
|
value: 'UTC+0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+0:30',
|
||||||
|
value: 'UTC+0.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+1',
|
||||||
|
value: 'UTC+1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+1:30',
|
||||||
|
value: 'UTC+1.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+2',
|
||||||
|
value: 'UTC+2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+2:30',
|
||||||
|
value: 'UTC+2.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+3',
|
||||||
|
value: 'UTC+3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+3:30',
|
||||||
|
|
||||||
|
value: 'UTC+3.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+4',
|
||||||
|
value: 'UTC+4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+4:30',
|
||||||
|
value: 'UTC+4.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+5',
|
||||||
|
value: 'UTC+5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+5:30',
|
||||||
|
value: 'UTC+5.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+5:45',
|
||||||
|
value: 'UTC+5.75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+6',
|
||||||
|
value: 'UTC+6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+6:30',
|
||||||
|
|
||||||
|
value: 'UTC+6.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+7',
|
||||||
|
value: 'UTC+7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+7:30',
|
||||||
|
value: 'UTC+7.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+8',
|
||||||
|
value: 'UTC+8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+8:30',
|
||||||
|
value: 'UTC+8.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+8:45',
|
||||||
|
value: 'UTC+8.75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+9',
|
||||||
|
value: 'UTC+9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+9:30',
|
||||||
|
value: 'UTC+9.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+10',
|
||||||
|
value: 'UTC+10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+10:30',
|
||||||
|
value: 'UTC+10.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+11',
|
||||||
|
value: 'UTC+11',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+11:30',
|
||||||
|
value: 'UTC+11.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+12',
|
||||||
|
value: 'UTC+12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+12:45',
|
||||||
|
value: 'UTC+12.75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+13',
|
||||||
|
value: 'UTC+13',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+13:45',
|
||||||
|
value: 'UTC+13.75',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTC+14',
|
||||||
|
value: 'UTC+14',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const DEFAULT_TIMEZONE = 'UTC+0';
|
||||||
|
|
|
@ -109,7 +109,7 @@ export interface UserInfoBase {
|
||||||
*/
|
*/
|
||||||
status?: string;
|
status?: string;
|
||||||
/** roles */
|
/** roles */
|
||||||
is_admin?: true;
|
is_admin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfoRes extends UserInfoBase {
|
export interface UserInfoRes extends UserInfoBase {
|
||||||
|
@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
|
||||||
|
|
||||||
export interface AdminContentsReq extends Paging {
|
export interface AdminContentsReq extends Paging {
|
||||||
status: AdminContentsFilterBy;
|
status: AdminContentsFilterBy;
|
||||||
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -263,6 +264,7 @@ export interface AdminSettingsInterface {
|
||||||
logo: string;
|
logo: string;
|
||||||
language: string;
|
language: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
time_zone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsSmtp {
|
export interface AdminSettingsSmtp {
|
||||||
|
@ -321,3 +323,21 @@ export interface SearchResItem {
|
||||||
export interface SearchRes extends ListResult<SearchResItem> {
|
export interface SearchRes extends ListResult<SearchResItem> {
|
||||||
extra: any;
|
extra: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminDashboard {
|
||||||
|
info: {
|
||||||
|
question_count: number;
|
||||||
|
answer_count: number;
|
||||||
|
comment_count: number;
|
||||||
|
vote_count: number;
|
||||||
|
user_count: number;
|
||||||
|
report_count: number;
|
||||||
|
uploading_files: boolean;
|
||||||
|
smtp: boolean;
|
||||||
|
time_zone: string;
|
||||||
|
occupying_storage_space: string;
|
||||||
|
app_start_time: number;
|
||||||
|
app_version: string;
|
||||||
|
https: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
import { bookmark, postVote } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { isLogin } from '@answer/utils';
|
import { useToast } from '@/hooks';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import { useToast } from '@answer/hooks';
|
import { bookmark, postVote } from '@/services';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
state: data?.collected,
|
state: data?.collected,
|
||||||
count: data?.collectCount,
|
count: data?.collectCount,
|
||||||
});
|
});
|
||||||
const { username = '' } = userInfoStore((state) => state.user);
|
const { username = '' } = loggedUserInfoStore((state) => state.user);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleVote = (type: 'up' | 'down') => {
|
const handleVote = (type: 'up' | 'down') => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBookmark = () => {
|
const handleBookmark = () => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bookmark({
|
bookmark({
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { memo, FC } from 'react';
|
import { memo, FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar } from '@answer/components';
|
import { Avatar } from '@/components';
|
||||||
|
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon, FormatTime } from '@answer/components';
|
import { Icon, FormatTime } from '@/components';
|
||||||
|
|
||||||
const ActionBar = ({
|
const ActionBar = ({
|
||||||
nickName,
|
nickName,
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { TextArea, Mentions } from '@answer/components';
|
import { TextArea, Mentions } from '@/components';
|
||||||
import { usePageUsers } from '@answer/hooks';
|
import { usePageUsers } from '@/hooks';
|
||||||
|
|
||||||
const Form = ({
|
const Form = ({
|
||||||
className = '',
|
className = '',
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { useState, memo } from 'react';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { TextArea, Mentions } from '@answer/components';
|
import { TextArea, Mentions } from '@/components';
|
||||||
import { usePageUsers } from '@answer/hooks';
|
import { usePageUsers } from '@/hooks';
|
||||||
|
|
||||||
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
|
@ -7,17 +7,18 @@ import classNames from 'classnames';
|
||||||
import { unionBy } from 'lodash';
|
import { unionBy } from 'lodash';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
import * as Types from '@answer/common/interface';
|
import * as Types from '@/common/interface';
|
||||||
|
import { Modal } from '@/components';
|
||||||
|
import { usePageUsers, useReportModal } from '@/hooks';
|
||||||
|
import { matchedUsers, parseUserInfo } from '@/utils';
|
||||||
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import {
|
import {
|
||||||
useQueryComments,
|
useQueryComments,
|
||||||
addComment,
|
addComment,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
updateComment,
|
updateComment,
|
||||||
postVote,
|
postVote,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import { Modal } from '@answer/components';
|
|
||||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
|
||||||
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
|
|
||||||
|
|
||||||
import { Form, ActionBar, Reply } from './components';
|
import { Form, ActionBar, Reply } from './components';
|
||||||
|
|
||||||
|
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVote = (id, is_cancel) => {
|
const handleVote = (id, is_cancel) => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = ({ action }, item) => {
|
const handleAction = ({ action }, item) => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'report') {
|
if (action === 'report') {
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
|
||||||
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Modal as AnswerModal } from '@answer/components';
|
import { Modal as AnswerModal } from '@/components';
|
||||||
import { uploadImage } from '@answer/api';
|
|
||||||
import ToolItem from '../toolItem';
|
import ToolItem from '../toolItem';
|
||||||
import { IEditorContext } from '../types';
|
import { IEditorContext } from '../types';
|
||||||
|
import { uploadImage } from '@/services';
|
||||||
|
|
||||||
const Image: FC<IEditorContext> = ({ editor }) => {
|
const Image: FC<IEditorContext> = ({ editor }) => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { TagSelector, Tag } from '@answer/components';
|
import { TagSelector, Tag } from '@/components';
|
||||||
import { isLogin } from '@answer/utils';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import { useFollowingTags, followTags } from '@answer/api';
|
import { useFollowingTags, followTags } from '@/services';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLogin()) {
|
if (!tryNormalLogged()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar, Icon } from '@answer/components';
|
import { Avatar, Icon } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redDot;
|
redDot;
|
||||||
|
|
|
@ -50,6 +50,10 @@
|
||||||
|
|
||||||
@media (max-width: 992.9px) {
|
@media (max-width: 992.9px) {
|
||||||
#header {
|
#header {
|
||||||
|
.logo {
|
||||||
|
max-width: 93px;
|
||||||
|
max-height: auto;
|
||||||
|
}
|
||||||
.nav-grow {
|
.nav-grow {
|
||||||
flex-grow: 1!important;
|
flex-grow: 1!important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@ import {
|
||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
|
import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
|
||||||
import { logout, useQueryNotificationStatus } from '@answer/api';
|
import { logout, useQueryNotificationStatus } from '@/services';
|
||||||
import Storage from '@answer/utils/storage';
|
import { RouteAlias } from '@/router/alias';
|
||||||
|
|
||||||
import NavItems from './components/NavItems';
|
import NavItems from './components/NavItems';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import './index.scss';
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, clear } = userInfoStore();
|
const { user, clear } = loggedUserInfoStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [urlSearch] = useSearchParams();
|
const [urlSearch] = useSearchParams();
|
||||||
const q = urlSearch.get('q');
|
const q = urlSearch.get('q');
|
||||||
|
@ -42,9 +42,8 @@ const Header: FC = () => {
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
await logout();
|
||||||
Storage.remove('token');
|
|
||||||
clear();
|
clear();
|
||||||
navigate('/');
|
navigate(RouteAlias.home);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useHotQuestions } from '@answer/api';
|
import { Icon } from '@/components';
|
||||||
import { Icon } from '@answer/components';
|
import { useHotQuestions } from '@/services';
|
||||||
|
|
||||||
const HotQuestions: FC = () => {
|
const HotQuestions: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState, FC } from 'react';
|
import React, { useEffect, useRef, useState, FC } from 'react';
|
||||||
import { Dropdown } from 'react-bootstrap';
|
import { Dropdown } from 'react-bootstrap';
|
||||||
|
|
||||||
import * as Types from '@answer/common/interface';
|
import * as Types from '@/common/interface';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
@ -2,12 +2,10 @@ import React from 'react';
|
||||||
import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
|
import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
import type {
|
import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface';
|
||||||
FormValue,
|
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||||
FormDataType,
|
import Storage from '@/utils/storage';
|
||||||
ImgCodeRes,
|
|
||||||
} from '@answer/common/interface';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/** control visible */
|
/** control visible */
|
||||||
|
@ -55,7 +53,7 @@ const Index: React.FC<IProps> = ({
|
||||||
placeholder={t('placeholder')}
|
placeholder={t('placeholder')}
|
||||||
isInvalid={captcha.isInvalid}
|
isInvalid={captcha.isInvalid}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
localStorage.setItem('captchaCode', e.target.value);
|
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||||
handleCaptcha({
|
handleCaptcha({
|
||||||
captcha_code: {
|
captcha_code: {
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { Button } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Modal } from '@answer/components';
|
import { Modal } from '@/components';
|
||||||
import { useReportModal, useToast } from '@answer/hooks';
|
import { useReportModal, useToast } from '@/hooks';
|
||||||
import { deleteQuestion, deleteAnswer } from '@answer/api';
|
|
||||||
import { isLogin } from '@answer/utils';
|
|
||||||
import Share from '../Share';
|
import Share from '../Share';
|
||||||
|
import { deleteQuestion, deleteAnswer } from '@/services';
|
||||||
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: 'answer' | 'question';
|
type: 'answer' | 'question';
|
||||||
|
@ -98,7 +98,7 @@ const Index: FC<IProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (action) => {
|
const handleAction = (action) => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap';
|
||||||
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useQuestionList } from '@answer/api';
|
import type * as Type from '@/common/interface';
|
||||||
import type * as Type from '@answer/common/interface';
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Tag,
|
Tag,
|
||||||
|
@ -13,7 +12,8 @@ import {
|
||||||
Empty,
|
Empty,
|
||||||
BaseUserCard,
|
BaseUserCard,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
|
import { useQuestionList } from '@/services';
|
||||||
|
|
||||||
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
||||||
'newest',
|
'newest',
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { FacebookShareButton, TwitterShareButton } from 'next-share';
|
import { FacebookShareButton, TwitterShareButton } from 'next-share';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: 'answer' | 'question';
|
type: 'answer' | 'question';
|
||||||
|
@ -15,7 +15,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<IProps> = ({ type, qid, aid, title }) => {
|
const Index: FC<IProps> = ({ type, qid, aid, title }) => {
|
||||||
const user = userInfoStore((state) => state.user);
|
const user = loggedUserInfoStore((state) => state.user);
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const [showTip, setShowTip] = useState(false);
|
const [showTip, setShowTip] = useState(false);
|
||||||
const [canSystemShare, setSystemShareState] = useState(false);
|
const [canSystemShare, setSystemShareState] = useState(false);
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useTagModal } from '@answer/hooks';
|
import { useTagModal } from '@/hooks';
|
||||||
import { queryTags } from '@answer/api';
|
import type * as Type from '@/common/interface';
|
||||||
import type * as Type from '@answer/common/interface';
|
import { queryTags } from '@/services';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,12 @@ import { Button, Col } from 'react-bootstrap';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { resendEmail, checkImgCode } from '@answer/api';
|
import { PicAuthCodeModal } from '@/components/Modal';
|
||||||
import { PicAuthCodeModal } from '@answer/components/Modal';
|
import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface';
|
||||||
import type {
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
ImgCodeRes,
|
import { resendEmail, checkImgCode } from '@/services';
|
||||||
ImgCodeReq,
|
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
|
||||||
FormDataType,
|
import Storage from '@/utils/storage';
|
||||||
} from '@answer/common/interface';
|
|
||||||
import { userInfoStore } from '@answer/stores';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -20,7 +18,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
|
const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
|
||||||
const [isSuccess, setSuccess] = useState(false);
|
const [isSuccess, setSuccess] = useState(false);
|
||||||
const [showModal, setModalState] = useState(false);
|
const [showModal, setModalState] = useState(false);
|
||||||
const { e_mail } = userInfoStore((state) => state.user);
|
const { e_mail } = loggedUserInfoStore((state) => state.user);
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
captcha_code: {
|
captcha_code: {
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -48,7 +46,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||||
}
|
}
|
||||||
let obj: ImgCodeReq = {};
|
let obj: ImgCodeReq = {};
|
||||||
if (imgCode.verify) {
|
if (imgCode.verify) {
|
||||||
const code = localStorage.getItem('captchaCode') || '';
|
const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
|
||||||
obj = {
|
obj = {
|
||||||
captcha_code: code,
|
captcha_code: code,
|
||||||
captcha_id: imgCode.captcha_id,
|
captcha_id: imgCode.captcha_id,
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { Avatar, FormatTime } from '@answer/components';
|
import { Avatar, FormatTime } from '@/components';
|
||||||
|
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import { changeUserStatus } from '@answer/api';
|
import { Modal as AnswerModal } from '@/components';
|
||||||
import { Modal as AnswerModal } from '@answer/components';
|
import { changeUserStatus } from '@/services';
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
const root = ReactDOM.createRoot(div);
|
const root = ReactDOM.createRoot(div);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||||
|
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
import * as Types from '@answer/common/interface';
|
import * as Types from '@/common/interface';
|
||||||
|
|
||||||
let globalUsers: Types.PageUser[] = [];
|
let globalUsers: Types.PageUser[] = [];
|
||||||
const usePageUsers = () => {
|
const usePageUsers = () => {
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import { reportList, postReport, closeQuestion, putReport } from '@answer/api';
|
import { useToast } from '@/hooks';
|
||||||
import { useToast } from '@answer/hooks';
|
import type * as Type from '@/common/interface';
|
||||||
import type * as Type from '@answer/common/interface';
|
import { reportList, postReport, closeQuestion, putReport } from '@/services';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
isBackend?: boolean;
|
isBackend?: boolean;
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import Backend from 'i18next-http-backend';
|
import Backend from 'i18next-http-backend';
|
||||||
|
|
||||||
|
import { DEFAULT_LANG } from '@/common/constants';
|
||||||
|
|
||||||
import en from './locales/en.json';
|
import en from './locales/en.json';
|
||||||
import zh from './locales/zh_CN.json';
|
import zh from './locales/zh_CN.json';
|
||||||
|
|
||||||
|
@ -21,7 +23,7 @@ i18next
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// debug: process.env.NODE_ENV === 'development',
|
// debug: process.env.NODE_ENV === 'development',
|
||||||
fallbackLng: process.env.REACT_APP_LANG || 'en_US',
|
fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,7 +28,10 @@
|
||||||
"confirm_email": "Confirm Email",
|
"confirm_email": "Confirm Email",
|
||||||
"account_suspended": "Account Suspended",
|
"account_suspended": "Account Suspended",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"change_email": "Modify Email"
|
"change_email": "Modify Email",
|
||||||
|
"install": "Answer Installation",
|
||||||
|
"upgrade": "Answer Upgrade",
|
||||||
|
"maintenance": "Webite Maintenance"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
|
@ -290,7 +293,9 @@
|
||||||
"now": "now",
|
"now": "now",
|
||||||
"x_seconds_ago": "{{count}}s ago",
|
"x_seconds_ago": "{{count}}s ago",
|
||||||
"x_minutes_ago": "{{count}}m ago",
|
"x_minutes_ago": "{{count}}m ago",
|
||||||
"x_hours_ago": "{{count}}h ago"
|
"x_hours_ago": "{{count}}h ago",
|
||||||
|
"hour": "hour",
|
||||||
|
"day": "day"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"btn_add_comment": "Add comment",
|
"btn_add_comment": "Add comment",
|
||||||
|
@ -735,6 +740,84 @@
|
||||||
"x_answers": "answers",
|
"x_answers": "answers",
|
||||||
"x_questions": "questions"
|
"x_questions": "questions"
|
||||||
},
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "Answer",
|
||||||
|
"next": "Next",
|
||||||
|
"done": "Done",
|
||||||
|
"lang": {
|
||||||
|
"label": "Please choose a language"
|
||||||
|
},
|
||||||
|
"db_type": {
|
||||||
|
"label": "Database Engine"
|
||||||
|
},
|
||||||
|
"db_username": {
|
||||||
|
"label": "Username",
|
||||||
|
"placeholder": "root",
|
||||||
|
"msg": "Username cannot be empty."
|
||||||
|
},
|
||||||
|
"db_password": {
|
||||||
|
"label": "Password",
|
||||||
|
"placeholder": "root",
|
||||||
|
"msg": "Password cannot be empty."
|
||||||
|
},
|
||||||
|
"db_host": {
|
||||||
|
"label": "Database Host",
|
||||||
|
"placeholder": "db:3306",
|
||||||
|
"msg": "Database Host cannot be empty."
|
||||||
|
},
|
||||||
|
"db_name": {
|
||||||
|
"label": "Database Name",
|
||||||
|
"placeholder": "answer",
|
||||||
|
"msg": "Database Name cannot be empty."
|
||||||
|
},
|
||||||
|
"db_file": {
|
||||||
|
"label": "Database File",
|
||||||
|
"placeholder": "/data/answer.db",
|
||||||
|
"msg": "Database File cannot be empty."
|
||||||
|
},
|
||||||
|
"config_yaml": {
|
||||||
|
"title": "Create config.yaml",
|
||||||
|
"label": "The config.yaml file created.",
|
||||||
|
"description": "You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.",
|
||||||
|
"info": "After you’ve done that, click “Next” button."
|
||||||
|
},
|
||||||
|
"site_information": "Site Information",
|
||||||
|
"admin_account": "Admin Account",
|
||||||
|
"site_name": {
|
||||||
|
"label": "Site Name"
|
||||||
|
},
|
||||||
|
"contact_email": {
|
||||||
|
"label": "Contact Email",
|
||||||
|
"text": "Email address of key contact responsible for this site."
|
||||||
|
},
|
||||||
|
"admin_name": {
|
||||||
|
"label": "Name"
|
||||||
|
},
|
||||||
|
"admin_password": {
|
||||||
|
"label": "Password",
|
||||||
|
"text": "You will need this password to log in. Please store it in a secure location."
|
||||||
|
},
|
||||||
|
"admin_email": {
|
||||||
|
"label": "Email",
|
||||||
|
"text": "You will need this email to log in."
|
||||||
|
},
|
||||||
|
"ready_title": "Your Answer is Ready!",
|
||||||
|
"ready_description": "If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.",
|
||||||
|
"good_luck": "Have fun, and good luck!",
|
||||||
|
"warning": "Warning",
|
||||||
|
"warning_description": "The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now</2>.",
|
||||||
|
"installed": "Already installed",
|
||||||
|
"installed_description": "You appear to have already installed. To reinstall please clear your old database tables first."
|
||||||
|
},
|
||||||
|
"upgrade": {
|
||||||
|
"title": "Answer",
|
||||||
|
"update_btn": "Update data",
|
||||||
|
"update_title": "Data update required",
|
||||||
|
"update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.</1><1>The update process may take a little while, so please be patient.</1>",
|
||||||
|
"done_title": "No update required",
|
||||||
|
"done_btn": "Done",
|
||||||
|
"done_desscription": "Your Answer data is already up-to-date."
|
||||||
|
},
|
||||||
"page_404": {
|
"page_404": {
|
||||||
"description": "Unfortunately, this page doesn't exist.",
|
"description": "Unfortunately, this page doesn't exist.",
|
||||||
"back_home": "Back to homepage"
|
"back_home": "Back to homepage"
|
||||||
|
@ -743,6 +826,9 @@
|
||||||
"description": "The server encountered an error and could not complete your request.",
|
"description": "The server encountered an error and could not complete your request.",
|
||||||
"back_home": "Back to homepage"
|
"back_home": "Back to homepage"
|
||||||
},
|
},
|
||||||
|
"page_maintenance": {
|
||||||
|
"description": "We are under maintenance, we’ll be back soon."
|
||||||
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"admin_header": {
|
"admin_header": {
|
||||||
"title": "Admin"
|
"title": "Admin"
|
||||||
|
@ -762,7 +848,36 @@
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"welcome": "Welcome to Answer Admin !",
|
"welcome": "Welcome to Answer Admin !",
|
||||||
"version": "Version"
|
"site_statistics": "Site Statistics",
|
||||||
|
"questions": "Questions:",
|
||||||
|
"answers": "Answers:",
|
||||||
|
"comments": "Comments:",
|
||||||
|
"votes": "Votes:",
|
||||||
|
"active_users": "Active users:",
|
||||||
|
"flags": "Flags:",
|
||||||
|
"site_health_status": "Site Health Status",
|
||||||
|
"version": "Version:",
|
||||||
|
"https": "HTTPS:",
|
||||||
|
"uploading_files": "Uploading files:",
|
||||||
|
"smtp": "SMTP:",
|
||||||
|
"timezone": "Timezone:",
|
||||||
|
"system_info": "System Info",
|
||||||
|
"storage_used": "Storage used:",
|
||||||
|
"uptime": "Uptime:",
|
||||||
|
"answer_links": "Answer Links",
|
||||||
|
"documents": "Documents",
|
||||||
|
"feedback": "Feedback",
|
||||||
|
"review": "Review",
|
||||||
|
"config": "Config",
|
||||||
|
"update_to": "Update to",
|
||||||
|
"latest": "Latest",
|
||||||
|
"check_failed": "Check failed",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"not_allowed": "Not allowed",
|
||||||
|
"allowed": "Allowed",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
"title": "Flags",
|
"title": "Flags",
|
||||||
|
@ -819,7 +934,10 @@
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"suspended": "Suspended",
|
"suspended": "Suspended",
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
"normal": "Normal"
|
"normal": "Normal",
|
||||||
|
"filter": {
|
||||||
|
"placeholder": "Filter by name, user:id"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"questions": {
|
"questions": {
|
||||||
"page_title": "Questions",
|
"page_title": "Questions",
|
||||||
|
@ -832,7 +950,10 @@
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"change": "Change"
|
"change": "Change",
|
||||||
|
"filter": {
|
||||||
|
"placeholder": "Filter by title, question:id"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"answers": {
|
"answers": {
|
||||||
"page_title": "Answers",
|
"page_title": "Answers",
|
||||||
|
@ -843,7 +964,10 @@
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"change": "Change"
|
"change": "Change",
|
||||||
|
"filter": {
|
||||||
|
"placeholder": "Filter by title, answer:id"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"page_title": "General",
|
"page_title": "General",
|
||||||
|
@ -879,6 +1003,11 @@
|
||||||
"label": "Interface Language",
|
"label": "Interface Language",
|
||||||
"msg": "Interface language cannot be empty.",
|
"msg": "Interface language cannot be empty.",
|
||||||
"text": "User interface language. It will change when you refresh the page."
|
"text": "User interface language. It will change when you refresh the page."
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"label": "Timezone",
|
||||||
|
"msg": "Timezone cannot be empty.",
|
||||||
|
"text": "Choose a UTC (Coordinated Universal Time) time offset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
|
|
@ -77,6 +77,10 @@ a {
|
||||||
.page-wrap {
|
.page-wrap {
|
||||||
min-height: calc(100vh - 148px);
|
min-height: calc(100vh - 148px);
|
||||||
}
|
}
|
||||||
|
.page-wrap2 {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-no-border,
|
.btn-no-border,
|
||||||
.btn-no-border:hover,
|
.btn-no-border:hover,
|
||||||
|
|
|
@ -2,15 +2,27 @@ import React from 'react';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import { Guard } from '@/utils';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
import './i18n/init';
|
import './i18n/init';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function bootstrapApp() {
|
||||||
|
/**
|
||||||
|
* NOTICE: must pre init logged user info for router
|
||||||
|
*/
|
||||||
|
await Guard.pullLoggedUser();
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapApp();
|
||||||
|
|
|
@ -11,21 +11,23 @@ import {
|
||||||
BaseUserCard,
|
BaseUserCard,
|
||||||
Empty,
|
Empty,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
|
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
||||||
import { useEditStatusModal } from '@answer/hooks';
|
import { useEditStatusModal } from '@/hooks';
|
||||||
import { useAnswerSearch, changeAnswerStatus } from '@answer/api';
|
import * as Type from '@/common/interface';
|
||||||
import * as Type from '@answer/common/interface';
|
import { useAnswerSearch, changeAnswerStatus } from '@/services';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
|
const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
|
||||||
|
|
||||||
const Answers: FC = () => {
|
const Answers: FC = () => {
|
||||||
const [urlSearchParams] = useSearchParams();
|
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
|
||||||
const curFilter = urlSearchParams.get('status') || answerFilterItems[0];
|
const curFilter = urlSearchParams.get('status') || answerFilterItems[0];
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||||
|
const curQuery = urlSearchParams.get('query') || '';
|
||||||
|
const questionId = urlSearchParams.get('questionId') || '';
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -36,6 +38,8 @@ const Answers: FC = () => {
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
page: curPage,
|
page: curPage,
|
||||||
status: curFilter as Type.AdminContentsFilterBy,
|
status: curFilter as Type.AdminContentsFilterBy,
|
||||||
|
query: curQuery,
|
||||||
|
question_id: questionId,
|
||||||
});
|
});
|
||||||
const count = listData?.count || 0;
|
const count = listData?.count || 0;
|
||||||
|
|
||||||
|
@ -77,6 +81,11 @@ const Answers: FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilter = (e) => {
|
||||||
|
urlSearchParams.set('query', e.target.value);
|
||||||
|
urlSearchParams.delete('page');
|
||||||
|
setUrlSearchParams(urlSearchParams);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
|
@ -89,19 +98,20 @@ const Answers: FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
value={curQuery}
|
||||||
|
onChange={handleFilter}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="input"
|
type="input"
|
||||||
placeholder="Filter by title"
|
placeholder={t('filter.placeholder')}
|
||||||
className="d-none"
|
|
||||||
style={{ width: '12.25rem' }}
|
style={{ width: '12.25rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table responsive>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: '45%' }}>{t('post')}</th>
|
<th>{t('post')}</th>
|
||||||
<th>{t('votes')}</th>
|
<th>{t('votes')}</th>
|
||||||
<th style={{ width: '20%' }}>{t('created')}</th>
|
<th>{t('created')}</th>
|
||||||
<th>{t('status')}</th>
|
<th>{t('status')}</th>
|
||||||
{curFilter !== 'deleted' && <th>{t('action')}</th>}
|
{curFilter !== 'deleted' && <th>{t('action')}</th>}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -132,6 +142,7 @@ const Answers: FC = () => {
|
||||||
__html: li.description,
|
__html: li.description,
|
||||||
}}
|
}}
|
||||||
className="last-p text-truncate-2 fs-14"
|
className="last-p text-truncate-2 fs-14"
|
||||||
|
style={{ maxWidth: '30rem' }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Card, Row, Col } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const AnswerLinks = () => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<Card.Body>
|
||||||
|
<h6 className="mb-3">{t('answer_links')}</h6>
|
||||||
|
<Row>
|
||||||
|
<Col xs={6}>
|
||||||
|
<a href="https://answer.dev" target="_blank" rel="noreferrer">
|
||||||
|
{t('documents')}
|
||||||
|
</a>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<a
|
||||||
|
href="https://github.com/answerdev/answer/issues"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
{t('feedback')}
|
||||||
|
</a>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnswerLinks;
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Card, Row, Col, Badge } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
data: Type.AdminDashboard['info'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const HealthStatus: FC<IProps> = ({ data }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<Card.Body>
|
||||||
|
<h6 className="mb-3">{t('site_health_status')}</h6>
|
||||||
|
<Row>
|
||||||
|
<Col xs={6} className="mb-1 d-flex align-items-center">
|
||||||
|
<span className="text-secondary me-1">{t('version')}</span>
|
||||||
|
<strong>90</strong>
|
||||||
|
<Badge pill bg="warning" text="dark" className="ms-1">
|
||||||
|
{t('update_to')} {data.app_version}
|
||||||
|
</Badge>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('https')}</span>
|
||||||
|
<strong>{data.https ? t('yes') : t('yes')}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('uploading_files')}</span>
|
||||||
|
<strong>
|
||||||
|
{data.uploading_files ? t('allowed') : t('not_allowed')}
|
||||||
|
</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('smtp')}</span>
|
||||||
|
<strong>{data.smtp ? t('enabled') : t('disabled')}</strong>
|
||||||
|
<Link to="/admin/smtp" className="ms-2">
|
||||||
|
{t('config')}
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('timezone')}</span>
|
||||||
|
<strong>{data.time_zone}</strong>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HealthStatus;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Card, Row, Col } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
data: Type.AdminDashboard['info'];
|
||||||
|
}
|
||||||
|
const Statistics: FC<IProps> = ({ data }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<Card.Body>
|
||||||
|
<h6 className="mb-3">{t('site_statistics')}</h6>
|
||||||
|
<Row>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('questions')}</span>
|
||||||
|
<strong>{data.question_count}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('answers')}</span>
|
||||||
|
<strong>{data.answer_count}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('comments')}</span>
|
||||||
|
<strong>{data.comment_count}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} className="mb-1">
|
||||||
|
<span className="text-secondary me-1">{t('votes')}</span>
|
||||||
|
<strong>{data.vote_count}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('active_users')}</span>
|
||||||
|
<strong>{data.user_count}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('flags')}</span>
|
||||||
|
<strong>{data.report_count}</strong>
|
||||||
|
<a href="###" className="ms-2">
|
||||||
|
{t('review')}
|
||||||
|
</a>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statistics;
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Card, Row, Col } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { formatUptime } from '@/utils';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
data: Type.AdminDashboard['info'];
|
||||||
|
}
|
||||||
|
const SystemInfo: FC<IProps> = ({ data }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<Card.Body>
|
||||||
|
<h6 className="mb-3">{t('system_info')}</h6>
|
||||||
|
<Row>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('storage_used')}</span>
|
||||||
|
<strong>{data.occupying_storage_space}</strong>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<span className="text-secondary me-1">{t('uptime')}</span>
|
||||||
|
<strong>{formatUptime(data.app_start_time)}</strong>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemInfo;
|
|
@ -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 };
|
|
@ -1,12 +1,41 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
import { Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useDashBoard } from '@/services';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnswerLinks,
|
||||||
|
HealthStatus,
|
||||||
|
Statistics,
|
||||||
|
SystemInfo,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
const Dashboard: FC = () => {
|
const Dashboard: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
const { data } = useDashBoard();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-capitalize">{t('title')}</h3>
|
<h3 className="text-capitalize">{t('title')}</h3>
|
||||||
<p className="mt-4">{t('welcome')}</p>
|
<p className="mt-4">{t('welcome')}</p>
|
||||||
|
<Row>
|
||||||
|
<Col lg={6}>
|
||||||
|
<Statistics data={data.info} />
|
||||||
|
</Col>
|
||||||
|
<Col lg={6}>
|
||||||
|
<HealthStatus data={data.info} />
|
||||||
|
</Col>
|
||||||
|
<Col lg={6}>
|
||||||
|
<SystemInfo data={data.info} />
|
||||||
|
</Col>
|
||||||
|
<Col lg={6}>
|
||||||
|
<AnswerLinks />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
{process.env.REACT_APP_VERSION && (
|
{process.env.REACT_APP_VERSION && (
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
{`${t('version')} `}
|
{`${t('version')} `}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import {
|
||||||
Empty,
|
Empty,
|
||||||
Pagination,
|
Pagination,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import { useReportModal } from '@answer/hooks';
|
import { useReportModal } from '@/hooks';
|
||||||
import * as Type from '@answer/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import { useFlagSearch } from '@answer/api';
|
import { useFlagSearch } from '@/services';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
import { Form, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type * as Type from '@answer/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useToast } from '@answer/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { siteInfoStore } from '@answer/stores';
|
import { siteInfoStore } from '@/stores';
|
||||||
import { useGeneralSetting, updateGeneralSetting } from '@answer/api';
|
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,22 @@ import React, { FC, FormEvent, useEffect, useState } from 'react';
|
||||||
import { Form, Button, Image, Stack } from 'react-bootstrap';
|
import { Form, Button, Image, Stack } from 'react-bootstrap';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useToast } from '@answer/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
LangsType,
|
LangsType,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
AdminSettingsInterface,
|
AdminSettingsInterface,
|
||||||
} from '@answer/common/interface';
|
} from '@/common/interface';
|
||||||
|
import { interfaceStore } from '@/stores';
|
||||||
|
import { UploadImg } from '@/components';
|
||||||
|
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
|
||||||
import {
|
import {
|
||||||
languages,
|
languages,
|
||||||
uploadAvatar,
|
uploadAvatar,
|
||||||
updateInterfaceSetting,
|
updateInterfaceSetting,
|
||||||
useInterfaceSetting,
|
useInterfaceSetting,
|
||||||
useThemeOptions,
|
useThemeOptions,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import { interfaceStore } from '@answer/stores';
|
|
||||||
import { UploadImg } from '@answer/components';
|
|
||||||
|
|
||||||
const Interface: FC = () => {
|
const Interface: FC = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
|
@ -27,6 +28,7 @@ const Interface: FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [langs, setLangs] = useState<LangsType[]>();
|
const [langs, setLangs] = useState<LangsType[]>();
|
||||||
const { data: setting } = useInterfaceSetting();
|
const { data: setting } = useInterfaceSetting();
|
||||||
|
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
logo: {
|
logo: {
|
||||||
value: setting?.logo || '',
|
value: setting?.logo || '',
|
||||||
|
@ -43,6 +45,11 @@ const Interface: FC = () => {
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
|
time_zone: {
|
||||||
|
value: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const getLangs = async () => {
|
const getLangs = async () => {
|
||||||
const res: LangsType[] = await languages();
|
const res: LangsType[] = await languages();
|
||||||
|
@ -106,6 +113,7 @@ const Interface: FC = () => {
|
||||||
logo: formData.logo.value,
|
logo: formData.logo.value,
|
||||||
theme: formData.theme.value,
|
theme: formData.theme.value,
|
||||||
language: formData.language.value,
|
language: formData.language.value,
|
||||||
|
time_zone: formData.time_zone.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateInterfaceSetting(reqParams)
|
updateInterfaceSetting(reqParams)
|
||||||
|
@ -158,12 +166,14 @@ const Interface: FC = () => {
|
||||||
Object.keys(setting).forEach((k) => {
|
Object.keys(setting).forEach((k) => {
|
||||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||||
});
|
});
|
||||||
setFormData(formMeta);
|
setFormData({ ...formData, ...formMeta });
|
||||||
}
|
}
|
||||||
}, [setting]);
|
}, [setting]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLangs();
|
getLangs();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
console.log('formData', formData);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
|
@ -249,7 +259,27 @@ const Interface: FC = () => {
|
||||||
{formData.language.errorMsg}
|
{formData.language.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
<Form.Group controlId="time-zone" className="mb-3">
|
||||||
|
<Form.Label>{t('time_zone.label')}</Form.Label>
|
||||||
|
<Form.Select
|
||||||
|
value={formData.time_zone.value}
|
||||||
|
isInvalid={formData.time_zone.isInvalid}
|
||||||
|
onChange={(evt) => {
|
||||||
|
onChange('time_zone', evt.target.value);
|
||||||
|
}}>
|
||||||
|
{TIMEZONES?.map((item) => {
|
||||||
|
return (
|
||||||
|
<option value={item.value} key={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
<Form.Text as="div">{t('time_zone.text')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.time_zone.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
{t('save', { keyPrefix: 'btns' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Button, Form, Table, Stack, Badge } from 'react-bootstrap';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -11,15 +11,15 @@ import {
|
||||||
BaseUserCard,
|
BaseUserCard,
|
||||||
Empty,
|
Empty,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import { ADMIN_LIST_STATUS } from '@answer/common/constants';
|
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
||||||
import { useEditStatusModal, useReportModal } from '@answer/hooks';
|
import { useEditStatusModal, useReportModal } from '@/hooks';
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
import {
|
import {
|
||||||
useQuestionSearch,
|
useQuestionSearch,
|
||||||
changeQuestionStatus,
|
changeQuestionStatus,
|
||||||
deleteQuestion,
|
deleteQuestion,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import * as Type from '@answer/common/interface';
|
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
@ -31,9 +31,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
const Questions: FC = () => {
|
const Questions: FC = () => {
|
||||||
const [urlSearchParams] = useSearchParams();
|
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
|
||||||
const curFilter = urlSearchParams.get('status') || questionFilterItems[0];
|
const curFilter = urlSearchParams.get('status') || questionFilterItems[0];
|
||||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||||
|
const curQuery = urlSearchParams.get('query') || '';
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -44,6 +45,7 @@ const Questions: FC = () => {
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
page: curPage,
|
page: curPage,
|
||||||
status: curFilter as Type.AdminContentsFilterBy,
|
status: curFilter as Type.AdminContentsFilterBy,
|
||||||
|
query: curQuery,
|
||||||
});
|
});
|
||||||
const count = listData?.count || 0;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
|
@ -108,10 +115,11 @@ const Questions: FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
value={curQuery}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="input"
|
type="input"
|
||||||
placeholder="Filter by title"
|
placeholder={t('filter.placeholder')}
|
||||||
className="d-none"
|
onChange={handleFilter}
|
||||||
style={{ width: '12.25rem' }}
|
style={{ width: '12.25rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -147,12 +155,11 @@ const Questions: FC = () => {
|
||||||
</td>
|
</td>
|
||||||
<td>{li.vote_count}</td>
|
<td>{li.vote_count}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<Link
|
||||||
href={`/questions/${li.id}`}
|
to={`/admin/answers?questionId=${li.id}`}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{li.answer_count}
|
{li.answer_count}
|
||||||
</a>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|
|
@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
import { Form, Button, Stack } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type * as Type from '@answer/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useToast } from '@answer/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { useSmtpSetting, updateSmtpSetting } from '@answer/api';
|
import { useSmtpSetting, updateSmtpSetting } from '@/services';
|
||||||
|
|
||||||
import pattern from '@/common/pattern';
|
import pattern from '@/common/pattern';
|
||||||
|
|
||||||
const Smtp: FC = () => {
|
const Smtp: FC = () => {
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { FC, useState } from 'react';
|
import { FC } from 'react';
|
||||||
import { Button, Form, Table, Badge } from 'react-bootstrap';
|
import { Button, Form, Table, Badge } from 'react-bootstrap';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useQueryUsers } from '@answer/api';
|
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
BaseUserCard,
|
BaseUserCard,
|
||||||
Empty,
|
Empty,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import * as Type from '@answer/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import { useChangeModal } from '@answer/hooks';
|
import { useChangeModal } from '@/hooks';
|
||||||
|
import { useQueryUsers } from '@/services';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
@ -33,11 +33,11 @@ const bgMap = {
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const Users: FC = () => {
|
const Users: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
|
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 curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
|
||||||
const curPage = Number(urlSearchParams.get('page') || '1');
|
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||||
|
const curQuery = urlSearchParams.get('query') || '';
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -45,7 +45,7 @@ const Users: FC = () => {
|
||||||
} = useQueryUsers({
|
} = useQueryUsers({
|
||||||
page: curPage,
|
page: curPage,
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
...(userName ? { username: userName } : {}),
|
query: curQuery,
|
||||||
...(curFilter === 'all' ? {} : { status: curFilter }),
|
...(curFilter === 'all' ? {} : { status: curFilter }),
|
||||||
});
|
});
|
||||||
const changeModal = useChangeModal({
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
|
@ -71,11 +76,10 @@ const Users: FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="d-none"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
value={userName}
|
value={curQuery}
|
||||||
onChange={(e) => setUserName(e.target.value)}
|
onChange={handleFilter}
|
||||||
placeholder="Filter by name"
|
placeholder={t('filter.placeholder')}
|
||||||
style={{ width: '12.25rem' }}
|
style={{ width: '12.25rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { AccordionNav, PageTitle } from '@answer/components';
|
import { AccordionNav, PageTitle } from '@/components';
|
||||||
import { ADMIN_NAV_MENUS } from '@answer/common/constants';
|
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|
|
@ -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<Props> = ({ visible }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h5>{t('ready_title')}</h5>
|
||||||
|
<p>
|
||||||
|
<Trans i18nKey="install.ready_description">
|
||||||
|
If you ever feel like changing more settings, visit
|
||||||
|
<a href="/">admin section</a>; find it in the site menu.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
<p>{t('good_luck')}</p>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Progress step={5} />
|
||||||
|
<Button>{t('done')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -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<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||||
|
|
||||||
|
const [langs, setLangs] = useState<LangsType[]>();
|
||||||
|
|
||||||
|
const getLangs = async () => {
|
||||||
|
const res: LangsType[] = await languages();
|
||||||
|
setLangs(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
nextCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLangs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
return (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Form.Group controlId="lang" className="mb-3">
|
||||||
|
<Form.Label>{t('lang.label')}</Form.Label>
|
||||||
|
<Form.Select
|
||||||
|
value={data.value}
|
||||||
|
isInvalid={data.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
lang: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{langs?.map((item) => {
|
||||||
|
return (
|
||||||
|
<option value={item.value} key={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Progress step={1} />
|
||||||
|
<Button type="submit">{t('next')}</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -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<Props> = ({ 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 (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<h5>{t('site_information')}</h5>
|
||||||
|
<Form.Group controlId="site_name" className="mb-3">
|
||||||
|
<Form.Label>{t('site_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
value={data.site_name.value}
|
||||||
|
isInvalid={data.site_name.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
site_name: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.site_name.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="contact_email" className="mb-3">
|
||||||
|
<Form.Label>{t('contact_email.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
value={data.contact_email.value}
|
||||||
|
isInvalid={data.contact_email.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
contact_email: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Text>{t('contact_email.text')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.contact_email.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<h5>{t('admin_account')}</h5>
|
||||||
|
<Form.Group controlId="admin_name" className="mb-3">
|
||||||
|
<Form.Label>{t('admin_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
value={data.admin_name.value}
|
||||||
|
isInvalid={data.admin_name.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
admin_name: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.admin_name.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="admin_password" className="mb-3">
|
||||||
|
<Form.Label>{t('admin_password.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
value={data.admin_password.value}
|
||||||
|
isInvalid={data.admin_password.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
admin_password: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Text>{t('admin_password.text')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.admin_password.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="admin_email" className="mb-3">
|
||||||
|
<Form.Label>{t('admin_email.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
value={data.admin_email.value}
|
||||||
|
isInvalid={data.admin_email.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
admin_email: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Text>{t('admin_email.text')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.admin_email.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Progress step={4} />
|
||||||
|
<Button type="submit">{t('next')}</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { FC, memo } from 'react';
|
||||||
|
import { ProgressBar } from 'react-bootstrap';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Index: FC<IProps> = ({ step }) => {
|
||||||
|
return (
|
||||||
|
<div className="d-flex align-items-center fs-14 text-secondary">
|
||||||
|
<ProgressBar
|
||||||
|
now={(step / 5) * 100}
|
||||||
|
variant="success"
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
className="me-2"
|
||||||
|
/>
|
||||||
|
<span>{step}/5</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -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<Props> = ({ 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 (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Form.Group controlId="database_engine" className="mb-3">
|
||||||
|
<Form.Label>{t('db_type.label')}</Form.Label>
|
||||||
|
<Form.Select
|
||||||
|
value={data.db_type.value}
|
||||||
|
isInvalid={data.db_type.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_type: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{sqlData.map((item) => {
|
||||||
|
return (
|
||||||
|
<option key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
</Form.Group>
|
||||||
|
{data.db_type.value !== 'sqlite3' ? (
|
||||||
|
<>
|
||||||
|
<Form.Group controlId="username" className="mb-3">
|
||||||
|
<Form.Label>{t('db_username.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
placeholder={t('db_username.placeholder')}
|
||||||
|
value={data.db_username.value}
|
||||||
|
isInvalid={data.db_username.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_username: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.db_username.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="db_password" className="mb-3">
|
||||||
|
<Form.Label>{t('db_password.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
placeholder={t('db_password.placeholder')}
|
||||||
|
value={data.db_password.value}
|
||||||
|
isInvalid={data.db_password.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_password: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.db_password.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="db_host" className="mb-3">
|
||||||
|
<Form.Label>{t('db_host.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
placeholder={t('db_host.placeholder')}
|
||||||
|
value={data.db_host.value}
|
||||||
|
isInvalid={data.db_host.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_host: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.db_host.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="name" className="mb-3">
|
||||||
|
<Form.Label>{t('db_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
placeholder={t('db_name.placeholder')}
|
||||||
|
value={data.db_name.value}
|
||||||
|
isInvalid={data.db_name.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_name: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.db_name.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Form.Group controlId="file" className="mb-3">
|
||||||
|
<Form.Label>{t('db_file.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
placeholder={t('db_file.placeholder')}
|
||||||
|
value={data.db_file.value}
|
||||||
|
isInvalid={data.db_file.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
changeCallback({
|
||||||
|
db_file: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{data.db_file.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Progress step={2} />
|
||||||
|
<Button type="submit">{t('next')}</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -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<Props> = ({ visible, nextCallback }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'install' });
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h5>{t('config_yaml.title')}</h5>
|
||||||
|
<div className="mb-3">{t('config_yaml.label')}</div>
|
||||||
|
<div className="fmt">
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="install.config_yaml.description"
|
||||||
|
components={{ 1: <code /> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FormGroup className="mb-3">
|
||||||
|
<Form.Control type="text" as="textarea" rows={5} className="fs-14" />
|
||||||
|
</FormGroup>
|
||||||
|
<div className="mb-3">{t('config_yaml.info')}</div>
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<Progress step={3} />
|
||||||
|
<Button onClick={nextCallback}>{t('next')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -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 };
|
|
@ -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<FormDataType>({
|
||||||
|
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 (
|
||||||
|
<div className="page-wrap2">
|
||||||
|
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
|
||||||
|
<Container style={{ paddingTop: '74px' }}>
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col lg={6}>
|
||||||
|
<h2 className="mb-4 text-center">{t('title')}</h2>
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
{showError && <Alert variant="danger"> show error msg </Alert>}
|
||||||
|
|
||||||
|
<FirstStep
|
||||||
|
visible={step === 1}
|
||||||
|
data={formData.lang}
|
||||||
|
changeCallback={handleChange}
|
||||||
|
nextCallback={handleStep}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SecondStep
|
||||||
|
visible={step === 2}
|
||||||
|
data={formData}
|
||||||
|
changeCallback={handleChange}
|
||||||
|
nextCallback={handleStep}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ThirdStep visible={step === 3} nextCallback={handleStep} />
|
||||||
|
|
||||||
|
<FourthStep
|
||||||
|
visible={step === 4}
|
||||||
|
data={formData}
|
||||||
|
changeCallback={handleChange}
|
||||||
|
nextCallback={handleStep}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Fifth visible={step === 5} />
|
||||||
|
{step === 6 && (
|
||||||
|
<div>
|
||||||
|
<h5>{t('warning')}</h5>
|
||||||
|
<p>
|
||||||
|
<Trans i18nKey="install.warning_description">
|
||||||
|
The file <code>config.yaml</code> already exists. If you
|
||||||
|
need to reset any of the configuration items in this
|
||||||
|
file, please delete it first. You may try{' '}
|
||||||
|
<a href="/">installing now</a>.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 7 && (
|
||||||
|
<div>
|
||||||
|
<h5>{t('installed')}</h5>
|
||||||
|
<p>{t('installed_description')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -1,60 +1,42 @@
|
||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||||
|
|
||||||
import { SWRConfig } from 'swr';
|
import { SWRConfig } from 'swr';
|
||||||
|
|
||||||
import {
|
import { siteInfoStore, interfaceStore, toastStore } from '@/stores';
|
||||||
userInfoStore,
|
import { Header, AdminHeader, Footer, Toast } from '@/components';
|
||||||
siteInfoStore,
|
import { useSiteSettings } from '@/services';
|
||||||
interfaceStore,
|
|
||||||
toastStore,
|
|
||||||
} from '@answer/stores';
|
|
||||||
import { Header, AdminHeader, Footer, Toast } from '@answer/components';
|
|
||||||
import { useSiteSettings, useCheckUserStatus } from '@answer/api';
|
|
||||||
|
|
||||||
import Storage from '@/utils/storage';
|
import Storage from '@/utils/storage';
|
||||||
|
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
|
||||||
|
|
||||||
let isMounted = false;
|
let isMounted = false;
|
||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
|
const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
|
||||||
const { update: interfaceStoreUpdate } = interfaceStore();
|
const { update: interfaceStoreUpdate } = interfaceStore();
|
||||||
const { data: siteSettings } = useSiteSettings();
|
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 { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
const closeToast = () => {
|
const closeToast = () => {
|
||||||
toastClear();
|
toastClear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (siteSettings) {
|
||||||
|
siteStoreUpdate(siteSettings.general);
|
||||||
|
interfaceStoreUpdate(siteSettings.interface);
|
||||||
|
}
|
||||||
|
}, [siteSettings]);
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
isMounted = true;
|
isMounted = true;
|
||||||
const lang = Storage.get('LANG');
|
const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
|
||||||
const user = Storage.get('userInfo');
|
|
||||||
if (user) {
|
|
||||||
updateUser(user);
|
|
||||||
}
|
|
||||||
if (lang) {
|
if (lang) {
|
||||||
i18n.changeLanguage(lang);
|
i18n.changeLanguage(lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userStatus?.status) {
|
|
||||||
const user = Storage.get('userInfo');
|
|
||||||
if (userStatus.status !== user.status) {
|
|
||||||
user.status = userStatus?.status;
|
|
||||||
updateUser(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -76,4 +58,4 @@ const Layout: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default memo(Layout);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className="page-wrap2">
|
||||||
|
<Container
|
||||||
|
className="d-flex flex-column justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: '100vh' }}>
|
||||||
|
<PageTitle title={t('maintenance', { keyPrefix: 'page_title' })} />
|
||||||
|
<div
|
||||||
|
className="mb-4 text-secondary"
|
||||||
|
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||||
|
(=‘_‘=)
|
||||||
|
</div>
|
||||||
|
<div className="text-center mb-4">{t('description')}</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -2,7 +2,7 @@ import { memo } from 'react';
|
||||||
import { Accordion, ListGroup } from 'react-bootstrap';
|
import { Accordion, ListGroup } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
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 {
|
import {
|
||||||
saveQuestion,
|
saveQuestion,
|
||||||
questionDetail,
|
questionDetail,
|
||||||
|
@ -14,8 +15,7 @@ import {
|
||||||
useQueryRevisions,
|
useQueryRevisions,
|
||||||
postAnswer,
|
postAnswer,
|
||||||
useQueryQuestionByTitle,
|
useQueryQuestionByTitle,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import type * as Type from '@answer/common/interface';
|
|
||||||
|
|
||||||
import SearchQuestion from './components/SearchQuestion';
|
import SearchQuestion from './components/SearchQuestion';
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,10 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
htmlRender,
|
htmlRender,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import { acceptanceAnswer } from '@answer/api';
|
import { scrollTop } from '@/utils';
|
||||||
import { scrollTop } from '@answer/utils';
|
import { AnswerItem } from '@/common/interface';
|
||||||
import { AnswerItem } from '@answer/common/interface';
|
import { acceptanceAnswer } from '@/services';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: AnswerItem;
|
data: AnswerItem;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { memo, FC } from 'react';
|
import { memo, FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { QueryGroup } from '@answer/components';
|
import { QueryGroup } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
count: number;
|
count: number;
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
htmlRender,
|
htmlRender,
|
||||||
} from '@answer/components';
|
} from '@/components';
|
||||||
import { formatCount } from '@answer/utils';
|
import { formatCount } from '@/utils';
|
||||||
import { following } from '@answer/api';
|
import { following } from '@/services';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any;
|
data: any;
|
||||||
|
|
|
@ -3,16 +3,15 @@ import { Card, ListGroup } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useSimilarQuestion } from '@answer/api';
|
import { Icon } from '@/components';
|
||||||
import { Icon } from '@answer/components';
|
import { useSimilarQuestion } from '@/services';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@/stores';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
const Index: FC<Props> = ({ id }) => {
|
const Index: FC<Props> = ({ id }) => {
|
||||||
const { user } = userInfoStore();
|
const { user } = loggedUserInfoStore();
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'related_question',
|
keyPrefix: 'related_question',
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Editor, Modal } from '@answer/components';
|
import { Editor, Modal } from '@/components';
|
||||||
import { postAnswer } from '@answer/api';
|
import { FormDataType } from '@/common/interface';
|
||||||
import { FormDataType } from '@answer/common/interface';
|
import { postAnswer } from '@/services';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { useEffect, useState } from 'react';
|
||||||
import { Container, Row, Col } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { questionDetail, getAnswers } from '@answer/api';
|
import { Pagination, PageTitle } from '@/components';
|
||||||
import { Pagination, PageTitle } from '@answer/components';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { scrollTop } from '@/utils';
|
||||||
import { scrollTop } from '@answer/utils';
|
import { usePageUsers } from '@/hooks';
|
||||||
import { usePageUsers } from '@answer/hooks';
|
|
||||||
import type {
|
import type {
|
||||||
ListResult,
|
ListResult,
|
||||||
QuestionDetailRes,
|
QuestionDetailRes,
|
||||||
AnswerItem,
|
AnswerItem,
|
||||||
} from '@answer/common/interface';
|
} from '@/common/interface';
|
||||||
|
import { questionDetail, getAnswers } from '@/services';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Question,
|
Question,
|
||||||
|
@ -37,7 +37,7 @@ const Index = () => {
|
||||||
list: [],
|
list: [],
|
||||||
});
|
});
|
||||||
const { setUsers } = usePageUsers();
|
const { setUsers } = usePageUsers();
|
||||||
const userInfo = userInfoStore((state) => state.user);
|
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||||
const requestAnswers = async () => {
|
const requestAnswers = async () => {
|
||||||
const res = await getAnswers({
|
const res = await getAnswers({
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
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 {
|
import {
|
||||||
useQueryAnswerInfo,
|
useQueryAnswerInfo,
|
||||||
modifyAnswer,
|
modifyAnswer,
|
||||||
useQueryRevisions,
|
useQueryRevisions,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import type * as Type from '@answer/common/interface';
|
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useMatch } from 'react-router-dom';
|
import { useMatch } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle, FollowingTags } from '@answer/components';
|
import { PageTitle, FollowingTags } from '@/components';
|
||||||
|
|
||||||
import QuestionList from '@/components/QuestionList';
|
import QuestionList from '@/components/QuestionList';
|
||||||
import HotQuestions from '@/components/HotQuestions';
|
import HotQuestions from '@/components/HotQuestions';
|
||||||
import { siteInfoStore } from '@/stores';
|
import { siteInfoStore } from '@/stores';
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { following } from '@answer/api';
|
import { following } from '@/services';
|
||||||
import { isLogin } from '@answer/utils';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data;
|
data;
|
||||||
|
@ -20,7 +20,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
const [followed, setFollowed] = useState(data?.is_follower);
|
const [followed, setFollowed] = useState(data?.is_follower);
|
||||||
|
|
||||||
const follow = () => {
|
const follow = () => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
following({
|
following({
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
||||||
import { ListGroupItem } from 'react-bootstrap';
|
import { ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { QueryGroup } from '@answer/components';
|
import { QueryGroup } from '@/components';
|
||||||
|
|
||||||
const sortBtns = ['relevance', 'newest', 'active', 'score'];
|
const sortBtns = ['relevance', 'newest', 'active', 'score'];
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { memo, FC } from 'react';
|
||||||
import { ListGroupItem, Badge } from 'react-bootstrap';
|
import { ListGroupItem, Badge } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components';
|
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
||||||
import type { SearchResItem } from '@answer/common/interface';
|
import type { SearchResItem } from '@/common/interface';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: SearchResItem;
|
data: SearchResItem;
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Pagination, PageTitle } from '@answer/components';
|
import { Pagination, PageTitle } from '@/components';
|
||||||
import { useSearch } from '@answer/api';
|
import { useSearch } from '@/services';
|
||||||
|
|
||||||
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import * as Type from '@answer/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import { PageTitle, FollowingTags } from '@answer/components';
|
import { PageTitle, FollowingTags } from '@/components';
|
||||||
import { useTagInfo, useFollow } from '@answer/api';
|
import { useTagInfo, useFollow } from '@/services';
|
||||||
|
|
||||||
import QuestionList from '@/components/QuestionList';
|
import QuestionList from '@/components/QuestionList';
|
||||||
import HotQuestions from '@/components/HotQuestions';
|
import HotQuestions from '@/components/HotQuestions';
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Editor, EditorRef, PageTitle } from '@answer/components';
|
import { Editor, EditorRef, PageTitle } from '@/components';
|
||||||
import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import type * as Type from '@/common/interface';
|
||||||
import type * as Type from '@answer/common/interface';
|
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
|
||||||
|
|
||||||
interface FormDataItem {
|
interface FormDataItem {
|
||||||
displayName: Type.FormValue<string>;
|
displayName: Type.FormValue<string>;
|
||||||
|
@ -40,7 +40,7 @@ const initFormData = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const Ask = () => {
|
const Ask = () => {
|
||||||
const { is_admin = false } = userInfoStore((state) => state.user);
|
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||||
|
|
||||||
const { tagId } = useParams();
|
const { tagId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
@ -5,19 +5,13 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components';
|
||||||
Tag,
|
|
||||||
TagSelector,
|
|
||||||
FormatTime,
|
|
||||||
Modal,
|
|
||||||
PageTitle,
|
|
||||||
} from '@answer/components';
|
|
||||||
import {
|
import {
|
||||||
useTagInfo,
|
useTagInfo,
|
||||||
useQuerySynonymsTags,
|
useQuerySynonymsTags,
|
||||||
saveSynonymsTags,
|
saveSynonymsTags,
|
||||||
deleteTag,
|
deleteTag,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
|
|
||||||
const TagIntroduction = () => {
|
const TagIntroduction = () => {
|
||||||
const [isEdit, setEditState] = useState(false);
|
const [isEdit, setEditState] = useState(false);
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useQueryTags, following } from '@answer/api';
|
import { Tag, Pagination, PageTitle, QueryGroup } from '@/components';
|
||||||
import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components';
|
import { formatCount } from '@/utils';
|
||||||
import { formatCount } from '@answer/utils';
|
import { useQueryTags, following } from '@/services';
|
||||||
|
|
||||||
const sortBtns = ['popular', 'name', 'newest'];
|
const sortBtns = ['popular', 'name', 'newest'];
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className="page-wrap2">
|
||||||
|
<Container style={{ paddingTop: '74px' }}>
|
||||||
|
<PageTitle title={t('upgrade', { keyPrefix: 'page_title' })} />
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col lg={6}>
|
||||||
|
<h2 className="text-center mb-4">{t('title')}</h2>
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
{step === 1 && (
|
||||||
|
<>
|
||||||
|
<h5>{t('update_title')}</h5>
|
||||||
|
<Trans
|
||||||
|
i18nKey="upgrade.update_description"
|
||||||
|
components={{ 1: <p /> }}
|
||||||
|
/>
|
||||||
|
<Button className="float-end" onClick={handleUpdate}>
|
||||||
|
{t('update_btn')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
<h5>{t('done_title')}</h5>
|
||||||
|
<p>{t('done_desscription')}</p>
|
||||||
|
<Button className="float-end">{t('done_btn')}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -2,13 +2,12 @@ import { FC, memo, useEffect, useState } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
import { Form, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { resetPassword, checkImgCode } from '@answer/api';
|
|
||||||
import type {
|
import type {
|
||||||
ImgCodeRes,
|
ImgCodeRes,
|
||||||
PasswordResetReq,
|
PasswordResetReq,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
} from '@answer/common/interface';
|
} from '@/common/interface';
|
||||||
|
import { resetPassword, checkImgCode } from '@/services';
|
||||||
import { PicAuthCodeModal } from '@/components/Modal';
|
import { PicAuthCodeModal } from '@/components/Modal';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -2,12 +2,11 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { Container, Col } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
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 SendEmail from './components/sendEmail';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });
|
const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
|
@ -19,7 +18,7 @@ const Index: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isLogin();
|
tryNormalLogged();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { FC, memo, useEffect } from 'react';
|
import { FC, memo, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { activateAccount } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { getQueryString } from '@/utils';
|
||||||
import { getQueryString } from '@answer/utils';
|
import { activateAccount } from '@/services';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
const updateUser = userInfoStore((state) => state.update);
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const code = getQueryString('code');
|
const code = getQueryString('code');
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,13 @@ import { Form, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { changeEmail, checkImgCode } from '@answer/api';
|
|
||||||
import type {
|
import type {
|
||||||
ImgCodeRes,
|
ImgCodeRes,
|
||||||
PasswordResetReq,
|
PasswordResetReq,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
} from '@answer/common/interface';
|
} from '@/common/interface';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
import { changeEmail, checkImgCode } from '@/services';
|
||||||
import { PicAuthCodeModal } from '@/components/Modal';
|
import { PicAuthCodeModal } from '@/components/Modal';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
|
@ -34,7 +33,7 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
const [showModal, setModalState] = useState(false);
|
const [showModal, setModalState] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user: userInfo, update: updateUser } = userInfoStore();
|
const { user: userInfo, update: updateUser } = loggedUserInfoStore();
|
||||||
|
|
||||||
const getImgCode = () => {
|
const getImgCode = () => {
|
||||||
checkImgCode({
|
checkImgCode({
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { FC, memo } from 'react';
|
||||||
import { Container, Col } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import SendEmail from './components/sendEmail';
|
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
|
import SendEmail from './components/sendEmail';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { changeEmailVerify, getUserInfo } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
|
@ -13,7 +12,7 @@ const Index: FC = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [step, setStep] = useState('loading');
|
const [step, setStep] = useState('loading');
|
||||||
|
|
||||||
const updateUser = userInfoStore((state) => state.update);
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const code = searchParams.get('code');
|
const code = searchParams.get('code');
|
||||||
|
@ -22,7 +21,7 @@ const Index: FC = () => {
|
||||||
changeEmailVerify({ code })
|
changeEmailVerify({ code })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setStep('success');
|
setStep('success');
|
||||||
getUserInfo().then((res) => {
|
getLoggedUserInfo().then((res) => {
|
||||||
// update user info
|
// update user info
|
||||||
updateUser(res);
|
updateUser(res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
import React, { FormEvent, useState, useEffect } from 'react';
|
import React, { FormEvent, useState, useEffect } from 'react';
|
||||||
import { Container, Form, Button, Col } from 'react-bootstrap';
|
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 { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { login, checkImgCode } from '@answer/api';
|
|
||||||
import type {
|
import type {
|
||||||
LoginReqParams,
|
LoginReqParams,
|
||||||
ImgCodeRes,
|
ImgCodeRes,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
} from '@answer/common/interface';
|
} from '@/common/interface';
|
||||||
import { PageTitle, Unactivate } from '@answer/components';
|
import { PageTitle, Unactivate } from '@/components';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { isLogin, getQueryString } from '@answer/utils';
|
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 { PicAuthCodeModal } from '@/components/Modal';
|
||||||
import Storage from '@/utils/storage';
|
import Storage from '@/utils/storage';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||||
|
const navigate = useNavigate();
|
||||||
const [refresh, setRefresh] = useState(0);
|
const [refresh, setRefresh] = useState(0);
|
||||||
const updateUser = userInfoStore((state) => state.update);
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
const storeUser = userInfoStore((state) => state.user);
|
const storeUser = loggedUserInfoStore((state) => state.user);
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
e_mail: {
|
e_mail: {
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -102,15 +104,18 @@ const Index: React.FC = () => {
|
||||||
login(params)
|
login(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateUser(res);
|
updateUser(res);
|
||||||
if (res.mail_status === 2) {
|
const userStat = Guard.deriveLoginState();
|
||||||
|
if (userStat.isNotActivated) {
|
||||||
// inactive
|
// inactive
|
||||||
setStep(2);
|
setStep(2);
|
||||||
setRefresh((pre) => pre + 1);
|
setRefresh((pre) => pre + 1);
|
||||||
}
|
} else {
|
||||||
if (res.mail_status === 1) {
|
const path =
|
||||||
const path = Storage.get('ANSWER_PATH') || '/';
|
Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||||
Storage.remove('ANSWER_PATH');
|
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||||
window.location.replace(path);
|
floppyNavigation.navigate(path, () => {
|
||||||
|
navigate(path, { replace: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalState(false);
|
setModalState(false);
|
||||||
|
@ -154,7 +159,7 @@ const Index: React.FC = () => {
|
||||||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
} else {
|
} else {
|
||||||
isLogin();
|
Guard.tryNormalLogged();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { Empty } from '@answer/components';
|
import { Empty } from '@/components';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { FormatTime, Empty } from '@answer/components';
|
import { FormatTime, Empty } from '@/components';
|
||||||
|
|
||||||
const Inbox = ({ data, handleReadNotification }) => {
|
const Inbox = ({ data, handleReadNotification }) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PageTitle } from '@/components';
|
||||||
import {
|
import {
|
||||||
useQueryNotifications,
|
useQueryNotifications,
|
||||||
clearUnreadNotification,
|
clearUnreadNotification,
|
||||||
clearNotificationStatus,
|
clearNotificationStatus,
|
||||||
readNotification,
|
readNotification,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import { PageTitle } from '@answer/components';
|
|
||||||
|
|
||||||
import Inbox from './components/Inbox';
|
import Inbox from './components/Inbox';
|
||||||
import Achievements from './components/Achievements';
|
import Achievements from './components/Achievements';
|
||||||
|
@ -46,6 +46,9 @@ const Notifications = () => {
|
||||||
|
|
||||||
const handleTypeChange = (evt, val) => {
|
const handleTypeChange = (evt, val) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
if (type === val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setNotificationData([]);
|
setNotificationData([]);
|
||||||
navigate(`/users/notifications/${val}`);
|
navigate(`/users/notifications/${val}`);
|
||||||
|
|
|
@ -3,19 +3,18 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { replacementPassword } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { getQueryString } from '@/utils';
|
||||||
import { getQueryString, isLogin } from '@answer/utils';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import type { FormDataType } from '@answer/common/interface';
|
import { replacementPassword } from '@/services';
|
||||||
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import Storage from '@/utils/storage';
|
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
|
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
|
||||||
|
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const clearUser = userInfoStore((state) => state.clear);
|
const clearUser = loggedUserInfoStore((state) => state.clear);
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
pass: {
|
pass: {
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -105,7 +104,6 @@ const Index: React.FC = () => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// clear login information then to login page
|
// clear login information then to login page
|
||||||
clearUser();
|
clearUser();
|
||||||
Storage.remove('token');
|
|
||||||
setStep(2);
|
setStep(2);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -118,7 +116,7 @@ const Index: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isLogin();
|
tryNormalLogged();
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon, FormatTime, Tag } from '@answer/components';
|
import { Icon, FormatTime, Tag } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC, memo } from 'react';
|
import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
|
|
||||||
import { FormatTime } from '@answer/components';
|
import { FormatTime } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components';
|
import { Icon, FormatTime, Tag, BaseUserCard } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -34,7 +34,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
||||||
: null}
|
: null}
|
||||||
</a>
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
<div className="d-flex align-items-center fs-14 text-secondary mb-2">
|
<div className="d-flex flex-wrap align-items-center fs-14 text-secondary mb-2">
|
||||||
{tabName === 'bookmarks' && (
|
{tabName === 'bookmarks' && (
|
||||||
<>
|
<>
|
||||||
<BaseUserCard data={item.user_info} showAvatar={false} />
|
<BaseUserCard data={item.user_info} showAvatar={false} />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC, memo } from 'react';
|
import { FC, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { QueryGroup } from '@answer/components';
|
import { QueryGroup } from '@/components';
|
||||||
|
|
||||||
const sortBtns = ['newest', 'score'];
|
const sortBtns = ['newest', 'score'];
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,10 @@ const list = [
|
||||||
const Index: FC<Props> = ({ slug, tabName = 'overview', isSelf }) => {
|
const Index: FC<Props> = ({ slug, tabName = 'overview', isSelf }) => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||||
return (
|
return (
|
||||||
<Nav className="pt-2 mb-4" variant="pills">
|
<Nav
|
||||||
|
className="pt-2 mb-4 flex-nowrap"
|
||||||
|
variant="pills"
|
||||||
|
style={{ overflow: 'auto' }}>
|
||||||
{list.map((item) => {
|
{list.map((item) => {
|
||||||
if (item.role && !isSelf) {
|
if (item.role && !isSelf) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FormatTime } from '@answer/components';
|
import { FormatTime } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any[];
|
data: any[];
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar, Icon } from '@answer/components';
|
import { Avatar, Icon } from '@/components';
|
||||||
import type { UserInfoRes } from '@answer/common/interface';
|
import type { UserInfoRes } from '@/common/interface';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: UserInfoRes;
|
data: UserInfoRes;
|
||||||
|
@ -16,7 +16,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="d-flex mb-4">
|
<div className="d-flex flex-column flex-md-row mb-4">
|
||||||
{data?.status !== 'deleted' ? (
|
{data?.status !== 'deleted' ? (
|
||||||
<Link to={`/users/${data.username}`} reloadDocument>
|
<Link to={`/users/${data.username}`} reloadDocument>
|
||||||
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
||||||
|
@ -25,7 +25,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="ms-4">
|
<div className="ms-0 ms-md-4 mt-4 mt-md-0">
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
{data?.status !== 'deleted' ? (
|
{data?.status !== 'deleted' ? (
|
||||||
<Link
|
<Link
|
||||||
|
@ -51,7 +51,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="text-secondary mb-4">@{data.username}</div>
|
<div className="text-secondary mb-4">@{data.username}</div>
|
||||||
|
|
||||||
<div className="d-flex mb-3">
|
<div className="d-flex flex-wrap mb-3">
|
||||||
<div className="me-3">
|
<div className="me-3">
|
||||||
<strong className="fs-5">{data.rank || 0}</strong>
|
<strong className="fs-5">{data.rank || 0}</strong>
|
||||||
<span className="text-secondary"> {t('x_reputation')}</span>
|
<span className="text-secondary"> {t('x_reputation')}</span>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC, memo } from 'react';
|
import { FC, memo } from 'react';
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
|
|
||||||
import { FormatTime } from '@answer/components';
|
import { FormatTime } from '@/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, data }) => {
|
||||||
return (
|
return (
|
||||||
<ListGroupItem className="d-flex py-3 px-0" key={item.object_id}>
|
<ListGroupItem className="d-flex py-3 px-0" key={item.object_id}>
|
||||||
<div
|
<div
|
||||||
className="me-3 text-end text-secondary"
|
className="me-3 text-end text-secondary flex-shrink-0"
|
||||||
style={{ width: '80px' }}>
|
style={{ width: '80px' }}>
|
||||||
{item.vote_type}
|
{item.vote_type}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components';
|
import { Pagination, FormatTime, PageTitle, Empty } from '@/components';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import {
|
import {
|
||||||
usePersonalInfoByName,
|
usePersonalInfoByName,
|
||||||
usePersonalTop,
|
usePersonalTop,
|
||||||
usePersonalListByTabName,
|
usePersonalListByTabName,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
@ -30,7 +30,7 @@ const Personal: FC = () => {
|
||||||
const page = searchParams.get('page') || 1;
|
const page = searchParams.get('page') || 1;
|
||||||
const order = searchParams.get('order') || 'newest';
|
const order = searchParams.get('order') || 'newest';
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||||
const sessionUser = userInfoStore((state) => state.user);
|
const sessionUser = loggedUserInfoStore((state) => state.user);
|
||||||
const isSelf = sessionUser?.username === username;
|
const isSelf = sessionUser?.username === username;
|
||||||
|
|
||||||
const { data: userInfo } = usePersonalInfoByName(username);
|
const { data: userInfo } = usePersonalInfoByName(username);
|
||||||
|
@ -64,9 +64,9 @@ const Personal: FC = () => {
|
||||||
xxl={3}
|
xxl={3}
|
||||||
lg={4}
|
lg={4}
|
||||||
sm={12}
|
sm={12}
|
||||||
className="d-flex justify-content-end mt-5 mt-lg-0">
|
className="d-flex justify-content-start justify-content-md-end">
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline-secondary"
|
variant="outline-secondary"
|
||||||
href="/users/settings/profile"
|
href="/users/settings/profile"
|
||||||
|
@ -79,7 +79,7 @@ const Personal: FC = () => {
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row className="justify-content-center">
|
<Row className="justify-content-center">
|
||||||
<Col lg={10}>
|
<Col lg={12}>
|
||||||
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
|
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xxl={7} lg={8} sm={12}>
|
<Col xxl={7} lg={8} sm={12}>
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { Form, Button, Col } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { register } from '@answer/api';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import type { FormDataType } from '@answer/common/interface';
|
import { register } from '@/services';
|
||||||
|
|
||||||
import userStore from '@/stores/userInfo';
|
import userStore from '@/stores/userInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle, Unactivate } from '@answer/components';
|
import { PageTitle, Unactivate } from '@/components';
|
||||||
import { isLogin } from '@answer/utils';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
|
|
||||||
import SignUpForm from './components/SignUpForm';
|
import SignUpForm from './components/SignUpForm';
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ const Index: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isLogin();
|
tryNormalLogged();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue