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