Merge branch 'ui-v0.3' into 'test'

Ui v0.3

See merge request opensource/answer!167
This commit is contained in:
Ren Yubin 2022-11-02 10:36:41 +00:00
commit d48cf86512
134 changed files with 2416 additions and 647 deletions

View File

@ -64,7 +64,7 @@ module.exports = {
position: 'before', position: 'before',
}, },
{ {
pattern: '@answer/**', pattern: '@/**',
group: 'internal', group: 'internal',
}, },
{ {

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/routes-conventional'],
}; };

View File

@ -8,13 +8,6 @@ module.exports = {
config.resolve.alias = { config.resolve.alias = {
...config.resolve.alias, ...config.resolve.alias,
'@': path.resolve(__dirname, 'src'), '@': path.resolve(__dirname, 'src'),
'@answer/pages': path.resolve(__dirname, 'src/pages'),
'@answer/components': path.resolve(__dirname, 'src/components'),
'@answer/stores': path.resolve(__dirname, 'src/stores'),
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
'@answer/utils': path.resolve(__dirname, 'src/utils'),
'@answer/common': path.resolve(__dirname, 'src/common'),
'@answer/api': path.resolve(__dirname, 'src/services/api'),
}; };
return config; return config;

View File

@ -1,8 +1,9 @@
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import router from '@/router'; import { routes, createBrowserRouter } from '@/router';
function App() { function App() {
const router = createBrowserRouter(routes);
return <RouterProvider router={router} />; return <RouterProvider router={router} />;
} }

View File

@ -1,9 +1,9 @@
export const LOGIN_NEED_BACK = [ export const DEFAULT_LANG = 'en_US';
'/users/login', export const CURRENT_LANG_STORAGE_KEY = '_a_lang__';
'/users/register', export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
'/users/account-recovery', export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
'/users/password-reset', export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
]; export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
export const ADMIN_LIST_STATUS = { export const ADMIN_LIST_STATUS = {
// normal; // normal;
@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
}, },
]; ];
// timezones
export const TIMEZONES = [
{
label: 'UTC-12',
value: 'UTC-12',
},
{
label: 'UTC-11:30',
value: 'UTC-11.5',
},
{
label: 'UTC-11',
value: 'UTC-11',
},
{
label: 'UTC-10:30',
value: 'UTC-10.5',
},
{
label: 'UTC-10',
value: 'UTC-10',
},
{
label: 'UTC-9:30',
value: 'UTC-9.5',
},
{
label: 'UTC-9',
value: 'UTC-9',
},
{
label: 'UTC-8:30',
value: 'UTC-8.5',
},
{
label: 'UTC-8',
value: 'UTC-8',
},
{
label: 'UTC-7:30',
value: 'UTC-7.5',
},
{
label: 'UTC-7',
value: 'UTC-7',
},
{
label: 'UTC-6:30',
value: 'UTC-6.5',
},
{
label: 'UTC-6',
value: 'UTC-6',
},
{
label: 'UTC-5:30',
value: 'UTC-5.5',
},
{
label: 'UTC-5',
value: 'UTC-5',
},
{
label: 'UTC-4:30',
value: 'UTC-4.5',
},
{
label: 'UTC-4',
value: 'UTC-4',
},
{
label: 'UTC-3:30',
value: 'UTC-3.5',
},
{
label: 'UTC-3',
value: 'UTC-3',
},
{
label: 'UTC-2:30',
value: 'UTC-2.5',
},
{
label: 'UTC-2',
value: 'UTC-2',
},
{
label: 'UTC-1:30',
value: 'UTC-1.5',
},
{
label: 'UTC-1',
value: 'UTC-1',
},
{
label: 'UTC-0:30',
value: 'UTC-0.5',
},
{
label: 'UTC+0',
value: 'UTC+0',
},
{
label: 'UTC+0:30',
value: 'UTC+0.5',
},
{
label: 'UTC+1',
value: 'UTC+1',
},
{
label: 'UTC+1:30',
value: 'UTC+1.5',
},
{
label: 'UTC+2',
value: 'UTC+2',
},
{
label: 'UTC+2:30',
value: 'UTC+2.5',
},
{
label: 'UTC+3',
value: 'UTC+3',
},
{
label: 'UTC+3:30',
value: 'UTC+3.5',
},
{
label: 'UTC+4',
value: 'UTC+4',
},
{
label: 'UTC+4:30',
value: 'UTC+4.5',
},
{
label: 'UTC+5',
value: 'UTC+5',
},
{
label: 'UTC+5:30',
value: 'UTC+5.5',
},
{
label: 'UTC+5:45',
value: 'UTC+5.75',
},
{
label: 'UTC+6',
value: 'UTC+6',
},
{
label: 'UTC+6:30',
value: 'UTC+6.5',
},
{
label: 'UTC+7',
value: 'UTC+7',
},
{
label: 'UTC+7:30',
value: 'UTC+7.5',
},
{
label: 'UTC+8',
value: 'UTC+8',
},
{
label: 'UTC+8:30',
value: 'UTC+8.5',
},
{
label: 'UTC+8:45',
value: 'UTC+8.75',
},
{
label: 'UTC+9',
value: 'UTC+9',
},
{
label: 'UTC+9:30',
value: 'UTC+9.5',
},
{
label: 'UTC+10',
value: 'UTC+10',
},
{
label: 'UTC+10:30',
value: 'UTC+10.5',
},
{
label: 'UTC+11',
value: 'UTC+11',
},
{
label: 'UTC+11:30',
value: 'UTC+11.5',
},
{
label: 'UTC+12',
value: 'UTC+12',
},
{
label: 'UTC+12:45',
value: 'UTC+12.75',
},
{
label: 'UTC+13',
value: 'UTC+13',
},
{
label: 'UTC+13:45',
value: 'UTC+13.75',
},
{
label: 'UTC+14',
value: 'UTC+14',
},
];
export const DEFAULT_TIMEZONE = 'UTC+0';

View File

@ -109,7 +109,7 @@ export interface UserInfoBase {
*/ */
status?: string; status?: string;
/** roles */ /** roles */
is_admin?: true; is_admin?: boolean;
} }
export interface UserInfoRes extends UserInfoBase { export interface UserInfoRes extends UserInfoBase {
@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
export interface AdminContentsReq extends Paging { export interface AdminContentsReq extends Paging {
status: AdminContentsFilterBy; status: AdminContentsFilterBy;
query?: string;
} }
/** /**
@ -263,6 +264,7 @@ export interface AdminSettingsInterface {
logo: string; logo: string;
language: string; language: string;
theme: string; theme: string;
time_zone?: string;
} }
export interface AdminSettingsSmtp { export interface AdminSettingsSmtp {
@ -321,3 +323,21 @@ export interface SearchResItem {
export interface SearchRes extends ListResult<SearchResItem> { export interface SearchRes extends ListResult<SearchResItem> {
extra: any; extra: any;
} }
export interface AdminDashboard {
info: {
question_count: number;
answer_count: number;
comment_count: number;
vote_count: number;
user_count: number;
report_count: number;
uploading_files: boolean;
smtp: boolean;
time_zone: string;
occupying_storage_space: string;
app_start_time: number;
app_version: string;
https: boolean;
};
}

View File

@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
import { useAccordionButton } from 'react-bootstrap/AccordionButton'; import { useAccordionButton } from 'react-bootstrap/AccordionButton';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
function MenuNode({ menu, callback, activeKey, isLeaf = false }) { function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });

View File

@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
import { bookmark, postVote } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { isLogin } from '@answer/utils'; import { useToast } from '@/hooks';
import { userInfoStore } from '@answer/stores'; import { tryNormalLogged } from '@/utils/guard';
import { useToast } from '@answer/hooks'; import { bookmark, postVote } from '@/services';
interface Props { interface Props {
className?: string; className?: string;
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
state: data?.collected, state: data?.collected,
count: data?.collectCount, count: data?.collectCount,
}); });
const { username = '' } = userInfoStore((state) => state.user); const { username = '' } = loggedUserInfoStore((state) => state.user);
const toast = useToast(); const toast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
}, []); }, []);
const handleVote = (type: 'up' | 'down') => { const handleVote = (type: 'up' | 'down') => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
}; };
const handleBookmark = () => { const handleBookmark = () => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
bookmark({ bookmark({

View File

@ -1,8 +1,7 @@
import { memo, FC } from 'react'; import { memo, FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar } from '@answer/components'; import { Avatar } from '@/components';
import { formatCount } from '@/utils'; import { formatCount } from '@/utils';
interface Props { interface Props {

View File

@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon, FormatTime } from '@answer/components'; import { Icon, FormatTime } from '@/components';
const ActionBar = ({ const ActionBar = ({
nickName, nickName,

View File

@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { TextArea, Mentions } from '@answer/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@answer/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ const Form = ({
className = '', className = '',

View File

@ -2,8 +2,8 @@ import { useState, memo } from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TextArea, Mentions } from '@answer/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@answer/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ userName, onSendReply, onCancel, mode }) => { const Form = ({ userName, onSendReply, onCancel, mode }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');

View File

@ -7,17 +7,18 @@ import classNames from 'classnames';
import { unionBy } from 'lodash'; import { unionBy } from 'lodash';
import { marked } from 'marked'; import { marked } from 'marked';
import * as Types from '@answer/common/interface'; import * as Types from '@/common/interface';
import { Modal } from '@/components';
import { usePageUsers, useReportModal } from '@/hooks';
import { matchedUsers, parseUserInfo } from '@/utils';
import { tryNormalLogged } from '@/utils/guard';
import { import {
useQueryComments, useQueryComments,
addComment, addComment,
deleteComment, deleteComment,
updateComment, updateComment,
postVote, postVote,
} from '@answer/api'; } from '@/services';
import { Modal } from '@answer/components';
import { usePageUsers, useReportModal } from '@answer/hooks';
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
import { Form, ActionBar, Reply } from './components'; import { Form, ActionBar, Reply } from './components';
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
}; };
const handleVote = (id, is_cancel) => { const handleVote = (id, is_cancel) => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
}; };
const handleAction = ({ action }, item) => { const handleAction = ({ action }, item) => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
if (action === 'report') { if (action === 'report') {

View File

@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal as AnswerModal } from '@answer/components'; import { Modal as AnswerModal } from '@/components';
import { uploadImage } from '@answer/api';
import ToolItem from '../toolItem'; import ToolItem from '../toolItem';
import { IEditorContext } from '../types'; import { IEditorContext } from '../types';
import { uploadImage } from '@/services';
const Image: FC<IEditorContext> = ({ editor }) => { const Image: FC<IEditorContext> = ({ editor }) => {
const { t } = useTranslation('translation', { keyPrefix: 'editor' }); const { t } = useTranslation('translation', { keyPrefix: 'editor' });

View File

@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { TagSelector, Tag } from '@answer/components'; import { TagSelector, Tag } from '@/components';
import { isLogin } from '@answer/utils'; import { tryNormalLogged } from '@/utils/guard';
import { useFollowingTags, followTags } from '@answer/api'; import { useFollowingTags, followTags } from '@/services';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' }); const { t } = useTranslation('translation', { keyPrefix: 'question' });
@ -32,7 +32,7 @@ const Index: FC = () => {
}); });
}; };
if (!isLogin()) { if (!tryNormalLogged()) {
return null; return null;
} }

View File

@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { Avatar, Icon } from '@answer/components'; import { Avatar, Icon } from '@/components';
interface Props { interface Props {
redDot; redDot;

View File

@ -50,6 +50,10 @@
@media (max-width: 992.9px) { @media (max-width: 992.9px) {
#header { #header {
.logo {
max-width: 93px;
max-height: auto;
}
.nav-grow { .nav-grow {
flex-grow: 1!important; flex-grow: 1!important;
} }

View File

@ -17,9 +17,9 @@ import {
useLocation, useLocation,
} from 'react-router-dom'; } from 'react-router-dom';
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores'; import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
import { logout, useQueryNotificationStatus } from '@answer/api'; import { logout, useQueryNotificationStatus } from '@/services';
import Storage from '@answer/utils/storage'; import { RouteAlias } from '@/router/alias';
import NavItems from './components/NavItems'; import NavItems from './components/NavItems';
@ -27,7 +27,7 @@ import './index.scss';
const Header: FC = () => { const Header: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, clear } = userInfoStore(); const { user, clear } = loggedUserInfoStore();
const { t } = useTranslation(); const { t } = useTranslation();
const [urlSearch] = useSearchParams(); const [urlSearch] = useSearchParams();
const q = urlSearch.get('q'); const q = urlSearch.get('q');
@ -42,9 +42,8 @@ const Header: FC = () => {
const handleLogout = async () => { const handleLogout = async () => {
await logout(); await logout();
Storage.remove('token');
clear(); clear();
navigate('/'); navigate(RouteAlias.home);
}; };
useEffect(() => { useEffect(() => {

View File

@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHotQuestions } from '@answer/api'; import { Icon } from '@/components';
import { Icon } from '@answer/components'; import { useHotQuestions } from '@/services';
const HotQuestions: FC = () => { const HotQuestions: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' }); const { t } = useTranslation('translation', { keyPrefix: 'question' });

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, FC } from 'react'; import React, { useEffect, useRef, useState, FC } from 'react';
import { Dropdown } from 'react-bootstrap'; import { Dropdown } from 'react-bootstrap';
import * as Types from '@answer/common/interface'; import * as Types from '@/common/interface';
interface IProps { interface IProps {
children: React.ReactNode; children: React.ReactNode;

View File

@ -2,12 +2,10 @@ import React from 'react';
import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
import type { import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface';
FormValue, import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
FormDataType, import Storage from '@/utils/storage';
ImgCodeRes,
} from '@answer/common/interface';
interface IProps { interface IProps {
/** control visible */ /** control visible */
@ -55,7 +53,7 @@ const Index: React.FC<IProps> = ({
placeholder={t('placeholder')} placeholder={t('placeholder')}
isInvalid={captcha.isInvalid} isInvalid={captcha.isInvalid}
onChange={(e) => { onChange={(e) => {
localStorage.setItem('captchaCode', e.target.value); Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
handleCaptcha({ handleCaptcha({
captcha_code: { captcha_code: {
value: e.target.value, value: e.target.value,

View File

@ -3,11 +3,11 @@ import { Button } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal } from '@answer/components'; import { Modal } from '@/components';
import { useReportModal, useToast } from '@answer/hooks'; import { useReportModal, useToast } from '@/hooks';
import { deleteQuestion, deleteAnswer } from '@answer/api';
import { isLogin } from '@answer/utils';
import Share from '../Share'; import Share from '../Share';
import { deleteQuestion, deleteAnswer } from '@/services';
import { tryNormalLogged } from '@/utils/guard';
interface IProps { interface IProps {
type: 'answer' | 'question'; type: 'answer' | 'question';
@ -98,7 +98,7 @@ const Index: FC<IProps> = ({
}; };
const handleAction = (action) => { const handleAction = (action) => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
if (action === 'delete') { if (action === 'delete') {

View File

@ -3,8 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap';
import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { NavLink, useParams, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuestionList } from '@answer/api'; import type * as Type from '@/common/interface';
import type * as Type from '@answer/common/interface';
import { import {
Icon, Icon,
Tag, Tag,
@ -13,7 +12,8 @@ import {
Empty, Empty,
BaseUserCard, BaseUserCard,
QueryGroup, QueryGroup,
} from '@answer/components'; } from '@/components';
import { useQuestionList } from '@/services';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [ const QuestionOrderKeys: Type.QuestionOrderBy[] = [
'newest', 'newest',

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { FacebookShareButton, TwitterShareButton } from 'next-share'; import { FacebookShareButton, TwitterShareButton } from 'next-share';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { userInfoStore } from '@answer/stores'; import { loggedUserInfoStore } from '@/stores';
interface IProps { interface IProps {
type: 'answer' | 'question'; type: 'answer' | 'question';
@ -15,7 +15,7 @@ interface IProps {
} }
const Index: FC<IProps> = ({ type, qid, aid, title }) => { const Index: FC<IProps> = ({ type, qid, aid, title }) => {
const user = userInfoStore((state) => state.user); const user = loggedUserInfoStore((state) => state.user);
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [showTip, setShowTip] = useState(false); const [showTip, setShowTip] = useState(false);
const [canSystemShare, setSystemShareState] = useState(false); const [canSystemShare, setSystemShareState] = useState(false);

View File

@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
import { marked } from 'marked'; import { marked } from 'marked';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTagModal } from '@answer/hooks'; import { useTagModal } from '@/hooks';
import { queryTags } from '@answer/api'; import type * as Type from '@/common/interface';
import type * as Type from '@answer/common/interface'; import { queryTags } from '@/services';
import './index.scss'; import './index.scss';

View File

@ -3,14 +3,12 @@ import { Button, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { resendEmail, checkImgCode } from '@answer/api'; import { PicAuthCodeModal } from '@/components/Modal';
import { PicAuthCodeModal } from '@answer/components/Modal'; import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface';
import type { import { loggedUserInfoStore } from '@/stores';
ImgCodeRes, import { resendEmail, checkImgCode } from '@/services';
ImgCodeReq, import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
FormDataType, import Storage from '@/utils/storage';
} from '@answer/common/interface';
import { userInfoStore } from '@answer/stores';
interface IProps { interface IProps {
visible: boolean; visible: boolean;
@ -20,7 +18,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
const { t } = useTranslation('translation', { keyPrefix: 'inactive' }); const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
const [isSuccess, setSuccess] = useState(false); const [isSuccess, setSuccess] = useState(false);
const [showModal, setModalState] = useState(false); const [showModal, setModalState] = useState(false);
const { e_mail } = userInfoStore((state) => state.user); const { e_mail } = loggedUserInfoStore((state) => state.user);
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
captcha_code: { captcha_code: {
value: '', value: '',
@ -48,7 +46,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
} }
let obj: ImgCodeReq = {}; let obj: ImgCodeReq = {};
if (imgCode.verify) { if (imgCode.verify) {
const code = localStorage.getItem('captchaCode') || ''; const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
obj = { obj = {
captcha_code: code, captcha_code: code,
captcha_id: imgCode.captcha_id, captcha_id: imgCode.captcha_id,

View File

@ -3,8 +3,7 @@ import { Link } from 'react-router-dom';
import classnames from 'classnames'; import classnames from 'classnames';
import { Avatar, FormatTime } from '@answer/components'; import { Avatar, FormatTime } from '@/components';
import { formatCount } from '@/utils'; import { formatCount } from '@/utils';
interface Props { interface Props {

View File

@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { changeUserStatus } from '@answer/api'; import { Modal as AnswerModal } from '@/components';
import { Modal as AnswerModal } from '@answer/components'; import { changeUserStatus } from '@/services';
const div = document.createElement('div'); const div = document.createElement('div');
const root = ReactDOM.createRoot(div); const root = ReactDOM.createRoot(div);

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import * as Types from '@answer/common/interface'; import * as Types from '@/common/interface';
let globalUsers: Types.PageUser[] = []; let globalUsers: Types.PageUser[] = [];
const usePageUsers = () => { const usePageUsers = () => {

View File

@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { reportList, postReport, closeQuestion, putReport } from '@answer/api'; import { useToast } from '@/hooks';
import { useToast } from '@answer/hooks'; import type * as Type from '@/common/interface';
import type * as Type from '@answer/common/interface'; import { reportList, postReport, closeQuestion, putReport } from '@/services';
interface Params { interface Params {
isBackend?: boolean; isBackend?: boolean;

View File

@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next';
import i18next from 'i18next'; import i18next from 'i18next';
import Backend from 'i18next-http-backend'; import Backend from 'i18next-http-backend';
import { DEFAULT_LANG } from '@/common/constants';
import en from './locales/en.json'; import en from './locales/en.json';
import zh from './locales/zh_CN.json'; import zh from './locales/zh_CN.json';
@ -21,7 +23,7 @@ i18next
}, },
}, },
// debug: process.env.NODE_ENV === 'development', // debug: process.env.NODE_ENV === 'development',
fallbackLng: process.env.REACT_APP_LANG || 'en_US', fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },

View File

@ -28,7 +28,10 @@
"confirm_email": "Confirm Email", "confirm_email": "Confirm Email",
"account_suspended": "Account Suspended", "account_suspended": "Account Suspended",
"admin": "Admin", "admin": "Admin",
"change_email": "Modify Email" "change_email": "Modify Email",
"install": "Answer Installation",
"upgrade": "Answer Upgrade",
"maintenance": "Webite Maintenance"
}, },
"notifications": { "notifications": {
"title": "Notifications", "title": "Notifications",
@ -290,7 +293,9 @@
"now": "now", "now": "now",
"x_seconds_ago": "{{count}}s ago", "x_seconds_ago": "{{count}}s ago",
"x_minutes_ago": "{{count}}m ago", "x_minutes_ago": "{{count}}m ago",
"x_hours_ago": "{{count}}h ago" "x_hours_ago": "{{count}}h ago",
"hour": "hour",
"day": "day"
}, },
"comment": { "comment": {
"btn_add_comment": "Add comment", "btn_add_comment": "Add comment",
@ -735,6 +740,84 @@
"x_answers": "answers", "x_answers": "answers",
"x_questions": "questions" "x_questions": "questions"
}, },
"install": {
"title": "Answer",
"next": "Next",
"done": "Done",
"lang": {
"label": "Please choose a language"
},
"db_type": {
"label": "Database Engine"
},
"db_username": {
"label": "Username",
"placeholder": "root",
"msg": "Username cannot be empty."
},
"db_password": {
"label": "Password",
"placeholder": "root",
"msg": "Password cannot be empty."
},
"db_host": {
"label": "Database Host",
"placeholder": "db:3306",
"msg": "Database Host cannot be empty."
},
"db_name": {
"label": "Database Name",
"placeholder": "answer",
"msg": "Database Name cannot be empty."
},
"db_file": {
"label": "Database File",
"placeholder": "/data/answer.db",
"msg": "Database File cannot be empty."
},
"config_yaml": {
"title": "Create config.yaml",
"label": "The config.yaml file created.",
"description": "You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.",
"info": "After youve done that, click “Next” button."
},
"site_information": "Site Information",
"admin_account": "Admin Account",
"site_name": {
"label": "Site Name"
},
"contact_email": {
"label": "Contact Email",
"text": "Email address of key contact responsible for this site."
},
"admin_name": {
"label": "Name"
},
"admin_password": {
"label": "Password",
"text": "You will need this password to log in. Please store it in a secure location."
},
"admin_email": {
"label": "Email",
"text": "You will need this email to log in."
},
"ready_title": "Your Answer is Ready!",
"ready_description": "If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.",
"good_luck": "Have fun, and good luck!",
"warning": "Warning",
"warning_description": "The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now</2>.",
"installed": "Already installed",
"installed_description": "You appear to have already installed. To reinstall please clear your old database tables first."
},
"upgrade": {
"title": "Answer",
"update_btn": "Update data",
"update_title": "Data update required",
"update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.</1><1>The update process may take a little while, so please be patient.</1>",
"done_title": "No update required",
"done_btn": "Done",
"done_desscription": "Your Answer data is already up-to-date."
},
"page_404": { "page_404": {
"description": "Unfortunately, this page doesn't exist.", "description": "Unfortunately, this page doesn't exist.",
"back_home": "Back to homepage" "back_home": "Back to homepage"
@ -743,6 +826,9 @@
"description": "The server encountered an error and could not complete your request.", "description": "The server encountered an error and could not complete your request.",
"back_home": "Back to homepage" "back_home": "Back to homepage"
}, },
"page_maintenance": {
"description": "We are under maintenance, well be back soon."
},
"admin": { "admin": {
"admin_header": { "admin_header": {
"title": "Admin" "title": "Admin"
@ -762,7 +848,36 @@
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
"welcome": "Welcome to Answer Admin !", "welcome": "Welcome to Answer Admin !",
"version": "Version" "site_statistics": "Site Statistics",
"questions": "Questions:",
"answers": "Answers:",
"comments": "Comments:",
"votes": "Votes:",
"active_users": "Active users:",
"flags": "Flags:",
"site_health_status": "Site Health Status",
"version": "Version:",
"https": "HTTPS:",
"uploading_files": "Uploading files:",
"smtp": "SMTP:",
"timezone": "Timezone:",
"system_info": "System Info",
"storage_used": "Storage used:",
"uptime": "Uptime:",
"answer_links": "Answer Links",
"documents": "Documents",
"feedback": "Feedback",
"review": "Review",
"config": "Config",
"update_to": "Update to",
"latest": "Latest",
"check_failed": "Check failed",
"yes": "Yes",
"no": "No",
"not_allowed": "Not allowed",
"allowed": "Allowed",
"enabled": "Enabled",
"disabled": "Disabled"
}, },
"flags": { "flags": {
"title": "Flags", "title": "Flags",
@ -819,7 +934,10 @@
"inactive": "Inactive", "inactive": "Inactive",
"suspended": "Suspended", "suspended": "Suspended",
"deleted": "Deleted", "deleted": "Deleted",
"normal": "Normal" "normal": "Normal",
"filter": {
"placeholder": "Filter by name, user:id"
}
}, },
"questions": { "questions": {
"page_title": "Questions", "page_title": "Questions",
@ -832,7 +950,10 @@
"created": "Created", "created": "Created",
"status": "Status", "status": "Status",
"action": "Action", "action": "Action",
"change": "Change" "change": "Change",
"filter": {
"placeholder": "Filter by title, question:id"
}
}, },
"answers": { "answers": {
"page_title": "Answers", "page_title": "Answers",
@ -843,7 +964,10 @@
"created": "Created", "created": "Created",
"status": "Status", "status": "Status",
"action": "Action", "action": "Action",
"change": "Change" "change": "Change",
"filter": {
"placeholder": "Filter by title, answer:id"
}
}, },
"general": { "general": {
"page_title": "General", "page_title": "General",
@ -879,6 +1003,11 @@
"label": "Interface Language", "label": "Interface Language",
"msg": "Interface language cannot be empty.", "msg": "Interface language cannot be empty.",
"text": "User interface language. It will change when you refresh the page." "text": "User interface language. It will change when you refresh the page."
},
"timezone": {
"label": "Timezone",
"msg": "Timezone cannot be empty.",
"text": "Choose a UTC (Coordinated Universal Time) time offset."
} }
}, },
"smtp": { "smtp": {

View File

@ -77,6 +77,10 @@ a {
.page-wrap { .page-wrap {
min-height: calc(100vh - 148px); min-height: calc(100vh - 148px);
} }
.page-wrap2 {
background-color: #f5f5f5;
min-height: 100vh;
}
.btn-no-border, .btn-no-border,
.btn-no-border:hover, .btn-no-border:hover,

View File

@ -2,15 +2,27 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { Guard } from '@/utils';
import App from './App'; import App from './App';
import './i18n/init'; import './i18n/init';
import './index.scss'; import './index.scss';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
root.render(
<React.StrictMode> async function bootstrapApp() {
<App /> /**
</React.StrictMode>, * NOTICE: must pre init logged user info for router
); */
await Guard.pullLoggedUser();
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}
bootstrapApp();

View File

@ -11,21 +11,23 @@ import {
BaseUserCard, BaseUserCard,
Empty, Empty,
QueryGroup, QueryGroup,
} from '@answer/components'; } from '@/components';
import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { ADMIN_LIST_STATUS } from '@/common/constants';
import { useEditStatusModal } from '@answer/hooks'; import { useEditStatusModal } from '@/hooks';
import { useAnswerSearch, changeAnswerStatus } from '@answer/api'; import * as Type from '@/common/interface';
import * as Type from '@answer/common/interface'; import { useAnswerSearch, changeAnswerStatus } from '@/services';
import '../index.scss'; import '../index.scss';
const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
const Answers: FC = () => { const Answers: FC = () => {
const [urlSearchParams] = useSearchParams(); const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('status') || answerFilterItems[0]; const curFilter = urlSearchParams.get('status') || answerFilterItems[0];
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const curPage = Number(urlSearchParams.get('page')) || 1; const curPage = Number(urlSearchParams.get('page')) || 1;
const curQuery = urlSearchParams.get('query') || '';
const questionId = urlSearchParams.get('questionId') || '';
const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });
const { const {
@ -36,6 +38,8 @@ const Answers: FC = () => {
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
page: curPage, page: curPage,
status: curFilter as Type.AdminContentsFilterBy, status: curFilter as Type.AdminContentsFilterBy,
query: curQuery,
question_id: questionId,
}); });
const count = listData?.count || 0; const count = listData?.count || 0;
@ -77,6 +81,11 @@ const Answers: FC = () => {
}); });
}; };
const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value);
urlSearchParams.delete('page');
setUrlSearchParams(urlSearchParams);
};
return ( return (
<> <>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
@ -89,19 +98,20 @@ const Answers: FC = () => {
/> />
<Form.Control <Form.Control
value={curQuery}
onChange={handleFilter}
size="sm" size="sm"
type="input" type="input"
placeholder="Filter by title" placeholder={t('filter.placeholder')}
className="d-none"
style={{ width: '12.25rem' }} style={{ width: '12.25rem' }}
/> />
</div> </div>
<Table> <Table responsive>
<thead> <thead>
<tr> <tr>
<th style={{ width: '45%' }}>{t('post')}</th> <th>{t('post')}</th>
<th>{t('votes')}</th> <th>{t('votes')}</th>
<th style={{ width: '20%' }}>{t('created')}</th> <th>{t('created')}</th>
<th>{t('status')}</th> <th>{t('status')}</th>
{curFilter !== 'deleted' && <th>{t('action')}</th>} {curFilter !== 'deleted' && <th>{t('action')}</th>}
</tr> </tr>
@ -132,6 +142,7 @@ const Answers: FC = () => {
__html: li.description, __html: li.description,
}} }}
className="last-p text-truncate-2 fs-14" className="last-p text-truncate-2 fs-14"
style={{ maxWidth: '30rem' }}
/> />
</Stack> </Stack>
</td> </td>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -1,12 +1,41 @@
import { FC } from 'react'; import { FC } from 'react';
import { Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDashBoard } from '@/services';
import {
AnswerLinks,
HealthStatus,
Statistics,
SystemInfo,
} from './components';
const Dashboard: FC = () => { const Dashboard: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
const { data } = useDashBoard();
if (!data) {
return null;
}
return ( return (
<> <>
<h3 className="text-capitalize">{t('title')}</h3> <h3 className="text-capitalize">{t('title')}</h3>
<p className="mt-4">{t('welcome')}</p> <p className="mt-4">{t('welcome')}</p>
<Row>
<Col lg={6}>
<Statistics data={data.info} />
</Col>
<Col lg={6}>
<HealthStatus data={data.info} />
</Col>
<Col lg={6}>
<SystemInfo data={data.info} />
</Col>
<Col lg={6}>
<AnswerLinks />
</Col>
</Row>
{process.env.REACT_APP_VERSION && ( {process.env.REACT_APP_VERSION && (
<p className="mt-4"> <p className="mt-4">
{`${t('version')} `} {`${t('version')} `}

View File

@ -9,10 +9,10 @@ import {
Empty, Empty,
Pagination, Pagination,
QueryGroup, QueryGroup,
} from '@answer/components'; } from '@/components';
import { useReportModal } from '@answer/hooks'; import { useReportModal } from '@/hooks';
import * as Type from '@answer/common/interface'; import * as Type from '@/common/interface';
import { useFlagSearch } from '@answer/api'; import { useFlagSearch } from '@/services';
import '../index.scss'; import '../index.scss';

View File

@ -2,10 +2,10 @@ import React, { FC, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type * as Type from '@answer/common/interface'; import type * as Type from '@/common/interface';
import { useToast } from '@answer/hooks'; import { useToast } from '@/hooks';
import { siteInfoStore } from '@answer/stores'; import { siteInfoStore } from '@/stores';
import { useGeneralSetting, updateGeneralSetting } from '@answer/api'; import { useGeneralSetting, updateGeneralSetting } from '@/services';
import '../index.scss'; import '../index.scss';

View File

@ -2,21 +2,22 @@ import React, { FC, FormEvent, useEffect, useState } from 'react';
import { Form, Button, Image, Stack } from 'react-bootstrap'; import { Form, Button, Image, Stack } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useToast } from '@answer/hooks'; import { useToast } from '@/hooks';
import { import {
LangsType, LangsType,
FormDataType, FormDataType,
AdminSettingsInterface, AdminSettingsInterface,
} from '@answer/common/interface'; } from '@/common/interface';
import { interfaceStore } from '@/stores';
import { UploadImg } from '@/components';
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
import { import {
languages, languages,
uploadAvatar, uploadAvatar,
updateInterfaceSetting, updateInterfaceSetting,
useInterfaceSetting, useInterfaceSetting,
useThemeOptions, useThemeOptions,
} from '@answer/api'; } from '@/services';
import { interfaceStore } from '@answer/stores';
import { UploadImg } from '@answer/components';
const Interface: FC = () => { const Interface: FC = () => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
@ -27,6 +28,7 @@ const Interface: FC = () => {
const Toast = useToast(); const Toast = useToast();
const [langs, setLangs] = useState<LangsType[]>(); const [langs, setLangs] = useState<LangsType[]>();
const { data: setting } = useInterfaceSetting(); const { data: setting } = useInterfaceSetting();
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
logo: { logo: {
value: setting?.logo || '', value: setting?.logo || '',
@ -43,6 +45,11 @@ const Interface: FC = () => {
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
time_zone: {
value: setting?.time_zone || DEFAULT_TIMEZONE,
isInvalid: false,
errorMsg: '',
},
}); });
const getLangs = async () => { const getLangs = async () => {
const res: LangsType[] = await languages(); const res: LangsType[] = await languages();
@ -106,6 +113,7 @@ const Interface: FC = () => {
logo: formData.logo.value, logo: formData.logo.value,
theme: formData.theme.value, theme: formData.theme.value,
language: formData.language.value, language: formData.language.value,
time_zone: formData.time_zone.value,
}; };
updateInterfaceSetting(reqParams) updateInterfaceSetting(reqParams)
@ -158,12 +166,14 @@ const Interface: FC = () => {
Object.keys(setting).forEach((k) => { Object.keys(setting).forEach((k) => {
formMeta[k] = { ...formData[k], value: setting[k] }; formMeta[k] = { ...formData[k], value: setting[k] };
}); });
setFormData(formMeta); setFormData({ ...formData, ...formMeta });
} }
}, [setting]); }, [setting]);
useEffect(() => { useEffect(() => {
getLangs(); getLangs();
}, []); }, []);
console.log('formData', formData);
return ( return (
<> <>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
@ -249,7 +259,27 @@ const Interface: FC = () => {
{formData.language.errorMsg} {formData.language.errorMsg}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Form.Group controlId="time-zone" className="mb-3">
<Form.Label>{t('time_zone.label')}</Form.Label>
<Form.Select
value={formData.time_zone.value}
isInvalid={formData.time_zone.isInvalid}
onChange={(evt) => {
onChange('time_zone', evt.target.value);
}}>
{TIMEZONES?.map((item) => {
return (
<option value={item.value} key={item.value}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('time_zone.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.time_zone.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })} {t('save', { keyPrefix: 'btns' })}
</Button> </Button>

View File

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import { Button, Form, Table, Stack, Badge } from 'react-bootstrap'; import { Button, Form, Table, Stack, Badge } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -11,15 +11,15 @@ import {
BaseUserCard, BaseUserCard,
Empty, Empty,
QueryGroup, QueryGroup,
} from '@answer/components'; } from '@/components';
import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { ADMIN_LIST_STATUS } from '@/common/constants';
import { useEditStatusModal, useReportModal } from '@answer/hooks'; import { useEditStatusModal, useReportModal } from '@/hooks';
import * as Type from '@/common/interface';
import { import {
useQuestionSearch, useQuestionSearch,
changeQuestionStatus, changeQuestionStatus,
deleteQuestion, deleteQuestion,
} from '@answer/api'; } from '@/services';
import * as Type from '@answer/common/interface';
import '../index.scss'; import '../index.scss';
@ -31,9 +31,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const Questions: FC = () => { const Questions: FC = () => {
const [urlSearchParams] = useSearchParams(); const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('status') || questionFilterItems[0]; const curFilter = urlSearchParams.get('status') || questionFilterItems[0];
const curPage = Number(urlSearchParams.get('page')) || 1; const curPage = Number(urlSearchParams.get('page')) || 1;
const curQuery = urlSearchParams.get('query') || '';
const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' });
const { const {
@ -44,6 +45,7 @@ const Questions: FC = () => {
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
page: curPage, page: curPage,
status: curFilter as Type.AdminContentsFilterBy, status: curFilter as Type.AdminContentsFilterBy,
query: curQuery,
}); });
const count = listData?.count || 0; const count = listData?.count || 0;
@ -96,6 +98,11 @@ const Questions: FC = () => {
}); });
}; };
const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value);
urlSearchParams.delete('page');
setUrlSearchParams(urlSearchParams);
};
return ( return (
<> <>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
@ -108,10 +115,11 @@ const Questions: FC = () => {
/> />
<Form.Control <Form.Control
value={curQuery}
size="sm" size="sm"
type="input" type="input"
placeholder="Filter by title" placeholder={t('filter.placeholder')}
className="d-none" onChange={handleFilter}
style={{ width: '12.25rem' }} style={{ width: '12.25rem' }}
/> />
</div> </div>
@ -147,12 +155,11 @@ const Questions: FC = () => {
</td> </td>
<td>{li.vote_count}</td> <td>{li.vote_count}</td>
<td> <td>
<a <Link
href={`/questions/${li.id}`} to={`/admin/answers?questionId=${li.id}`}
target="_blank"
rel="noreferrer"> rel="noreferrer">
{li.answer_count} {li.answer_count}
</a> </Link>
</td> </td>
<td> <td>
<Stack> <Stack>

View File

@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react';
import { Form, Button, Stack } from 'react-bootstrap'; import { Form, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type * as Type from '@answer/common/interface'; import type * as Type from '@/common/interface';
import { useToast } from '@answer/hooks'; import { useToast } from '@/hooks';
import { useSmtpSetting, updateSmtpSetting } from '@answer/api'; import { useSmtpSetting, updateSmtpSetting } from '@/services';
import pattern from '@/common/pattern'; import pattern from '@/common/pattern';
const Smtp: FC = () => { const Smtp: FC = () => {

View File

@ -1,18 +1,18 @@
import { FC, useState } from 'react'; import { FC } from 'react';
import { Button, Form, Table, Badge } from 'react-bootstrap'; import { Button, Form, Table, Badge } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueryUsers } from '@answer/api';
import { import {
Pagination, Pagination,
FormatTime, FormatTime,
BaseUserCard, BaseUserCard,
Empty, Empty,
QueryGroup, QueryGroup,
} from '@answer/components'; } from '@/components';
import * as Type from '@answer/common/interface'; import * as Type from '@/common/interface';
import { useChangeModal } from '@answer/hooks'; import { useChangeModal } from '@/hooks';
import { useQueryUsers } from '@/services';
import '../index.scss'; import '../index.scss';
@ -33,11 +33,11 @@ const bgMap = {
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const Users: FC = () => { const Users: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const [userName, setUserName] = useState('');
const [urlSearchParams] = useSearchParams(); const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
const curPage = Number(urlSearchParams.get('page') || '1'); const curPage = Number(urlSearchParams.get('page') || '1');
const curQuery = urlSearchParams.get('query') || '';
const { const {
data, data,
isLoading, isLoading,
@ -45,7 +45,7 @@ const Users: FC = () => {
} = useQueryUsers({ } = useQueryUsers({
page: curPage, page: curPage,
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
...(userName ? { username: userName } : {}), query: curQuery,
...(curFilter === 'all' ? {} : { status: curFilter }), ...(curFilter === 'all' ? {} : { status: curFilter }),
}); });
const changeModal = useChangeModal({ const changeModal = useChangeModal({
@ -59,6 +59,11 @@ const Users: FC = () => {
}); });
}; };
const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value);
urlSearchParams.delete('page');
setUrlSearchParams(urlSearchParams);
};
return ( return (
<> <>
<h3 className="mb-4">{t('title')}</h3> <h3 className="mb-4">{t('title')}</h3>
@ -71,11 +76,10 @@ const Users: FC = () => {
/> />
<Form.Control <Form.Control
className="d-none"
size="sm" size="sm"
value={userName} value={curQuery}
onChange={(e) => setUserName(e.target.value)} onChange={handleFilter}
placeholder="Filter by name" placeholder={t('filter.placeholder')}
style={{ width: '12.25rem' }} style={{ width: '12.25rem' }}
/> />
</div> </div>

View File

@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { AccordionNav, PageTitle } from '@answer/components'; import { AccordionNav, PageTitle } from '@/components';
import { ADMIN_NAV_MENUS } from '@answer/common/constants'; import { ADMIN_NAV_MENUS } from '@/common/constants';
import './index.scss'; import './index.scss';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -1,60 +1,42 @@
import { FC, useEffect } from 'react'; import { FC, useEffect, memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { Helmet, HelmetProvider } from 'react-helmet-async'; import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import { import { siteInfoStore, interfaceStore, toastStore } from '@/stores';
userInfoStore, import { Header, AdminHeader, Footer, Toast } from '@/components';
siteInfoStore, import { useSiteSettings } from '@/services';
interfaceStore,
toastStore,
} from '@answer/stores';
import { Header, AdminHeader, Footer, Toast } from '@answer/components';
import { useSiteSettings, useCheckUserStatus } from '@answer/api';
import Storage from '@/utils/storage'; import Storage from '@/utils/storage';
import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
let isMounted = false; let isMounted = false;
const Layout: FC = () => { const Layout: FC = () => {
const { siteInfo, update: siteStoreUpdate } = siteInfoStore(); const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
const { update: interfaceStoreUpdate } = interfaceStore(); const { update: interfaceStoreUpdate } = interfaceStore();
const { data: siteSettings } = useSiteSettings(); const { data: siteSettings } = useSiteSettings();
const { data: userStatus } = useCheckUserStatus();
useEffect(() => {
if (siteSettings) {
siteStoreUpdate(siteSettings.general);
interfaceStoreUpdate(siteSettings.interface);
}
}, [siteSettings]);
const updateUser = userInfoStore((state) => state.update);
const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { msg: toastMsg, variant, clear: toastClear } = toastStore();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const closeToast = () => { const closeToast = () => {
toastClear(); toastClear();
}; };
useEffect(() => {
if (siteSettings) {
siteStoreUpdate(siteSettings.general);
interfaceStoreUpdate(siteSettings.interface);
}
}, [siteSettings]);
if (!isMounted) { if (!isMounted) {
isMounted = true; isMounted = true;
const lang = Storage.get('LANG'); const lang = Storage.get(CURRENT_LANG_STORAGE_KEY);
const user = Storage.get('userInfo');
if (user) {
updateUser(user);
}
if (lang) { if (lang) {
i18n.changeLanguage(lang); i18n.changeLanguage(lang);
} }
} }
if (userStatus?.status) {
const user = Storage.get('userInfo');
if (userStatus.status !== user.status) {
user.status = userStatus?.status;
updateUser(user);
}
}
return ( return (
<HelmetProvider> <HelmetProvider>
<Helmet> <Helmet>
@ -76,4 +58,4 @@ const Layout: FC = () => {
); );
}; };
export default Layout; export default memo(Layout);

View File

@ -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;

View File

@ -2,7 +2,7 @@ import { memo } from 'react';
import { Accordion, ListGroup } from 'react-bootstrap'; import { Accordion, ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
import './index.scss'; import './index.scss';

View File

@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import classNames from 'classnames'; import classNames from 'classnames';
import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; import { Editor, EditorRef, TagSelector, PageTitle } from '@/components';
import type * as Type from '@/common/interface';
import { import {
saveQuestion, saveQuestion,
questionDetail, questionDetail,
@ -14,8 +15,7 @@ import {
useQueryRevisions, useQueryRevisions,
postAnswer, postAnswer,
useQueryQuestionByTitle, useQueryQuestionByTitle,
} from '@answer/api'; } from '@/services';
import type * as Type from '@answer/common/interface';
import SearchQuestion from './components/SearchQuestion'; import SearchQuestion from './components/SearchQuestion';

View File

@ -10,10 +10,10 @@ import {
Comment, Comment,
FormatTime, FormatTime,
htmlRender, htmlRender,
} from '@answer/components'; } from '@/components';
import { acceptanceAnswer } from '@answer/api'; import { scrollTop } from '@/utils';
import { scrollTop } from '@answer/utils'; import { AnswerItem } from '@/common/interface';
import { AnswerItem } from '@answer/common/interface'; import { acceptanceAnswer } from '@/services';
interface Props { interface Props {
data: AnswerItem; data: AnswerItem;

View File

@ -1,7 +1,7 @@
import { memo, FC } from 'react'; import { memo, FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryGroup } from '@answer/components'; import { QueryGroup } from '@/components';
interface Props { interface Props {
count: number; count: number;

View File

@ -11,9 +11,9 @@ import {
Comment, Comment,
FormatTime, FormatTime,
htmlRender, htmlRender,
} from '@answer/components'; } from '@/components';
import { formatCount } from '@answer/utils'; import { formatCount } from '@/utils';
import { following } from '@answer/api'; import { following } from '@/services';
interface Props { interface Props {
data: any; data: any;

View File

@ -3,16 +3,15 @@ import { Card, ListGroup } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSimilarQuestion } from '@answer/api'; import { Icon } from '@/components';
import { Icon } from '@answer/components'; import { useSimilarQuestion } from '@/services';
import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@/stores';
interface Props { interface Props {
id: string; id: string;
} }
const Index: FC<Props> = ({ id }) => { const Index: FC<Props> = ({ id }) => {
const { user } = userInfoStore(); const { user } = loggedUserInfoStore();
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'related_question', keyPrefix: 'related_question',
}); });

View File

@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
import { marked } from 'marked'; import { marked } from 'marked';
import classNames from 'classnames'; import classNames from 'classnames';
import { Editor, Modal } from '@answer/components'; import { Editor, Modal } from '@/components';
import { postAnswer } from '@answer/api'; import { FormDataType } from '@/common/interface';
import { FormDataType } from '@answer/common/interface'; import { postAnswer } from '@/services';
interface Props { interface Props {
visible?: boolean; visible?: boolean;

View File

@ -2,16 +2,16 @@ import { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap'; import { Container, Row, Col } from 'react-bootstrap';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import { questionDetail, getAnswers } from '@answer/api'; import { Pagination, PageTitle } from '@/components';
import { Pagination, PageTitle } from '@answer/components'; import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@answer/stores'; import { scrollTop } from '@/utils';
import { scrollTop } from '@answer/utils'; import { usePageUsers } from '@/hooks';
import { usePageUsers } from '@answer/hooks';
import type { import type {
ListResult, ListResult,
QuestionDetailRes, QuestionDetailRes,
AnswerItem, AnswerItem,
} from '@answer/common/interface'; } from '@/common/interface';
import { questionDetail, getAnswers } from '@/services';
import { import {
Question, Question,
@ -37,7 +37,7 @@ const Index = () => {
list: [], list: [],
}); });
const { setUsers } = usePageUsers(); const { setUsers } = usePageUsers();
const userInfo = userInfoStore((state) => state.user); const userInfo = loggedUserInfoStore((state) => state.user);
const isAuthor = userInfo?.username === question?.user_info?.username; const isAuthor = userInfo?.username === question?.user_info?.username;
const requestAnswers = async () => { const requestAnswers = async () => {
const res = await getAnswers({ const res = await getAnswers({

View File

@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import classNames from 'classnames'; import classNames from 'classnames';
import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; import { Editor, EditorRef, Icon, PageTitle } from '@/components';
import type * as Type from '@/common/interface';
import { import {
useQueryAnswerInfo, useQueryAnswerInfo,
modifyAnswer, modifyAnswer,
useQueryRevisions, useQueryRevisions,
} from '@answer/api'; } from '@/services';
import type * as Type from '@answer/common/interface';
import './index.scss'; import './index.scss';

View File

@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap';
import { useMatch } from 'react-router-dom'; import { useMatch } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageTitle, FollowingTags } from '@answer/components'; import { PageTitle, FollowingTags } from '@/components';
import QuestionList from '@/components/QuestionList'; import QuestionList from '@/components/QuestionList';
import HotQuestions from '@/components/HotQuestions'; import HotQuestions from '@/components/HotQuestions';
import { siteInfoStore } from '@/stores'; import { siteInfoStore } from '@/stores';

View File

@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { following } from '@answer/api'; import { following } from '@/services';
import { isLogin } from '@answer/utils'; import { tryNormalLogged } from '@/utils/guard';
interface Props { interface Props {
data; data;
@ -20,7 +20,7 @@ const Index: FC<Props> = ({ data }) => {
const [followed, setFollowed] = useState(data?.is_follower); const [followed, setFollowed] = useState(data?.is_follower);
const follow = () => { const follow = () => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
following({ following({

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroupItem } from 'react-bootstrap'; import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryGroup } from '@answer/components'; import { QueryGroup } from '@/components';
const sortBtns = ['relevance', 'newest', 'active', 'score']; const sortBtns = ['relevance', 'newest', 'active', 'score'];

View File

@ -2,8 +2,8 @@ import { memo, FC } from 'react';
import { ListGroupItem, Badge } from 'react-bootstrap'; import { ListGroupItem, Badge } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components'; import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
import type { SearchResItem } from '@answer/common/interface'; import type { SearchResItem } from '@/common/interface';
interface Props { interface Props {
data: SearchResItem; data: SearchResItem;

View File

@ -3,8 +3,8 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { Pagination, PageTitle } from '@answer/components'; import { Pagination, PageTitle } from '@/components';
import { useSearch } from '@answer/api'; import { useSearch } from '@/services';
import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components';

View File

@ -3,10 +3,9 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
import { useParams, Link, useNavigate } from 'react-router-dom'; import { useParams, Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import * as Type from '@answer/common/interface'; import * as Type from '@/common/interface';
import { PageTitle, FollowingTags } from '@answer/components'; import { PageTitle, FollowingTags } from '@/components';
import { useTagInfo, useFollow } from '@answer/api'; import { useTagInfo, useFollow } from '@/services';
import QuestionList from '@/components/QuestionList'; import QuestionList from '@/components/QuestionList';
import HotQuestions from '@/components/HotQuestions'; import HotQuestions from '@/components/HotQuestions';

View File

@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import classNames from 'classnames'; import classNames from 'classnames';
import { Editor, EditorRef, PageTitle } from '@answer/components'; import { Editor, EditorRef, PageTitle } from '@/components';
import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@answer/stores'; import type * as Type from '@/common/interface';
import type * as Type from '@answer/common/interface'; import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
interface FormDataItem { interface FormDataItem {
displayName: Type.FormValue<string>; displayName: Type.FormValue<string>;
@ -40,7 +40,7 @@ const initFormData = {
}, },
}; };
const Ask = () => { const Ask = () => {
const { is_admin = false } = userInfoStore((state) => state.user); const { is_admin = false } = loggedUserInfoStore((state) => state.user);
const { tagId } = useParams(); const { tagId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -5,19 +5,13 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components';
Tag,
TagSelector,
FormatTime,
Modal,
PageTitle,
} from '@answer/components';
import { import {
useTagInfo, useTagInfo,
useQuerySynonymsTags, useQuerySynonymsTags,
saveSynonymsTags, saveSynonymsTags,
deleteTag, deleteTag,
} from '@answer/api'; } from '@/services';
const TagIntroduction = () => { const TagIntroduction = () => {
const [isEdit, setEditState] = useState(false); const [isEdit, setEditState] = useState(false);

View File

@ -3,9 +3,9 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueryTags, following } from '@answer/api'; import { Tag, Pagination, PageTitle, QueryGroup } from '@/components';
import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; import { formatCount } from '@/utils';
import { formatCount } from '@answer/utils'; import { useQueryTags, following } from '@/services';
const sortBtns = ['popular', 'name', 'newest']; const sortBtns = ['popular', 'name', 'newest'];

View File

@ -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;

View File

@ -2,13 +2,12 @@ import { FC, memo, useEffect, useState } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { resetPassword, checkImgCode } from '@answer/api';
import type { import type {
ImgCodeRes, ImgCodeRes,
PasswordResetReq, PasswordResetReq,
FormDataType, FormDataType,
} from '@answer/common/interface'; } from '@/common/interface';
import { resetPassword, checkImgCode } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal'; import { PicAuthCodeModal } from '@/components/Modal';
interface IProps { interface IProps {

View File

@ -2,12 +2,11 @@ import React, { useState, useEffect } from 'react';
import { Container, Col } from 'react-bootstrap'; import { Container, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { isLogin } from '@answer/utils'; import { tryNormalLogged } from '@/utils/guard';
import { PageTitle } from '@/components';
import SendEmail from './components/sendEmail'; import SendEmail from './components/sendEmail';
import { PageTitle } from '@/components';
const Index: React.FC = () => { const Index: React.FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' }); const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
@ -19,7 +18,7 @@ const Index: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
isLogin(); tryNormalLogged();
}, []); }, []);
return ( return (

View File

@ -1,15 +1,14 @@
import { FC, memo, useEffect } from 'react'; import { FC, memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { activateAccount } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@answer/stores'; import { getQueryString } from '@/utils';
import { getQueryString } from '@answer/utils'; import { activateAccount } from '@/services';
import { PageTitle } from '@/components'; import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
const updateUser = userInfoStore((state) => state.update); const updateUser = loggedUserInfoStore((state) => state.update);
useEffect(() => { useEffect(() => {
const code = getQueryString('code'); const code = getQueryString('code');

View File

@ -3,14 +3,13 @@ import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { changeEmail, checkImgCode } from '@answer/api';
import type { import type {
ImgCodeRes, ImgCodeRes,
PasswordResetReq, PasswordResetReq,
FormDataType, FormDataType,
} from '@answer/common/interface'; } from '@/common/interface';
import { userInfoStore } from '@answer/stores'; import { loggedUserInfoStore } from '@/stores';
import { changeEmail, checkImgCode } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal'; import { PicAuthCodeModal } from '@/components/Modal';
const Index: FC = () => { const Index: FC = () => {
@ -34,7 +33,7 @@ const Index: FC = () => {
}); });
const [showModal, setModalState] = useState(false); const [showModal, setModalState] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { user: userInfo, update: updateUser } = userInfoStore(); const { user: userInfo, update: updateUser } = loggedUserInfoStore();
const getImgCode = () => { const getImgCode = () => {
checkImgCode({ checkImgCode({

View File

@ -2,10 +2,10 @@ import { FC, memo } from 'react';
import { Container, Col } from 'react-bootstrap'; import { Container, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SendEmail from './components/sendEmail';
import { PageTitle } from '@/components'; import { PageTitle } from '@/components';
import SendEmail from './components/sendEmail';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); const { t } = useTranslation('translation', { keyPrefix: 'change_email' });

View File

@ -3,9 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { changeEmailVerify, getUserInfo } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@answer/stores'; import { changeEmailVerify, getLoggedUserInfo } from '@/services';
import { PageTitle } from '@/components'; import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
@ -13,7 +12,7 @@ const Index: FC = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [step, setStep] = useState('loading'); const [step, setStep] = useState('loading');
const updateUser = userInfoStore((state) => state.update); const updateUser = loggedUserInfoStore((state) => state.update);
useEffect(() => { useEffect(() => {
const code = searchParams.get('code'); const code = searchParams.get('code');
@ -22,7 +21,7 @@ const Index: FC = () => {
changeEmailVerify({ code }) changeEmailVerify({ code })
.then(() => { .then(() => {
setStep('success'); setStep('success');
getUserInfo().then((res) => { getLoggedUserInfo().then((res) => {
// update user info // update user info
updateUser(res); updateUser(res);
}); });

View File

@ -1,26 +1,28 @@
import React, { FormEvent, useState, useEffect } from 'react'; import React, { FormEvent, useState, useEffect } from 'react';
import { Container, Form, Button, Col } from 'react-bootstrap'; import { Container, Form, Button, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { login, checkImgCode } from '@answer/api';
import type { import type {
LoginReqParams, LoginReqParams,
ImgCodeRes, ImgCodeRes,
FormDataType, FormDataType,
} from '@answer/common/interface'; } from '@/common/interface';
import { PageTitle, Unactivate } from '@answer/components'; import { PageTitle, Unactivate } from '@/components';
import { userInfoStore } from '@answer/stores'; import { loggedUserInfoStore } from '@/stores';
import { isLogin, getQueryString } from '@answer/utils'; import { getQueryString, Guard, floppyNavigation } from '@/utils';
import { login, checkImgCode } from '@/services';
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
import { RouteAlias } from '@/router/alias';
import { PicAuthCodeModal } from '@/components/Modal'; import { PicAuthCodeModal } from '@/components/Modal';
import Storage from '@/utils/storage'; import Storage from '@/utils/storage';
const Index: React.FC = () => { const Index: React.FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'login' }); const { t } = useTranslation('translation', { keyPrefix: 'login' });
const navigate = useNavigate();
const [refresh, setRefresh] = useState(0); const [refresh, setRefresh] = useState(0);
const updateUser = userInfoStore((state) => state.update); const updateUser = loggedUserInfoStore((state) => state.update);
const storeUser = userInfoStore((state) => state.user); const storeUser = loggedUserInfoStore((state) => state.user);
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
e_mail: { e_mail: {
value: '', value: '',
@ -102,15 +104,18 @@ const Index: React.FC = () => {
login(params) login(params)
.then((res) => { .then((res) => {
updateUser(res); updateUser(res);
if (res.mail_status === 2) { const userStat = Guard.deriveLoginState();
if (userStat.isNotActivated) {
// inactive // inactive
setStep(2); setStep(2);
setRefresh((pre) => pre + 1); setRefresh((pre) => pre + 1);
} } else {
if (res.mail_status === 1) { const path =
const path = Storage.get('ANSWER_PATH') || '/'; Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
Storage.remove('ANSWER_PATH'); Storage.remove(REDIRECT_PATH_STORAGE_KEY);
window.location.replace(path); floppyNavigation.navigate(path, () => {
navigate(path, { replace: true });
});
} }
setModalState(false); setModalState(false);
@ -154,7 +159,7 @@ const Index: React.FC = () => {
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
setStep(2); setStep(2);
} else { } else {
isLogin(); Guard.tryNormalLogged();
} }
}, []); }, []);

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Empty } from '@answer/components'; import { Empty } from '@/components';
import './index.scss'; import './index.scss';

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { FormatTime, Empty } from '@answer/components'; import { FormatTime, Empty } from '@/components';
const Inbox = ({ data, handleReadNotification }) => { const Inbox = ({ data, handleReadNotification }) => {
if (!data) { if (!data) {

View File

@ -3,13 +3,13 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { PageTitle } from '@/components';
import { import {
useQueryNotifications, useQueryNotifications,
clearUnreadNotification, clearUnreadNotification,
clearNotificationStatus, clearNotificationStatus,
readNotification, readNotification,
} from '@answer/api'; } from '@/services';
import { PageTitle } from '@answer/components';
import Inbox from './components/Inbox'; import Inbox from './components/Inbox';
import Achievements from './components/Achievements'; import Achievements from './components/Achievements';
@ -46,6 +46,9 @@ const Notifications = () => {
const handleTypeChange = (evt, val) => { const handleTypeChange = (evt, val) => {
evt.preventDefault(); evt.preventDefault();
if (type === val) {
return;
}
setPage(1); setPage(1);
setNotificationData([]); setNotificationData([]);
navigate(`/users/notifications/${val}`); navigate(`/users/notifications/${val}`);

View File

@ -3,19 +3,18 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { replacementPassword } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { userInfoStore } from '@answer/stores'; import { getQueryString } from '@/utils';
import { getQueryString, isLogin } from '@answer/utils'; import type { FormDataType } from '@/common/interface';
import type { FormDataType } from '@answer/common/interface'; import { replacementPassword } from '@/services';
import { tryNormalLogged } from '@/utils/guard';
import Storage from '@/utils/storage';
import { PageTitle } from '@/components'; import { PageTitle } from '@/components';
const Index: React.FC = () => { const Index: React.FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' }); const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const clearUser = userInfoStore((state) => state.clear); const clearUser = loggedUserInfoStore((state) => state.clear);
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
pass: { pass: {
value: '', value: '',
@ -105,7 +104,6 @@ const Index: React.FC = () => {
.then(() => { .then(() => {
// clear login information then to login page // clear login information then to login page
clearUser(); clearUser();
Storage.remove('token');
setStep(2); setStep(2);
}) })
.catch((err) => { .catch((err) => {
@ -118,7 +116,7 @@ const Index: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
isLogin(); tryNormalLogged();
}, []); }, []);
return ( return (
<> <>

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag } from '@answer/components'; import { Icon, FormatTime, Tag } from '@/components';
interface Props { interface Props {
visible: boolean; visible: boolean;

View File

@ -1,7 +1,7 @@
import { FC, memo } from 'react'; import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { FormatTime } from '@answer/components'; import { FormatTime } from '@/components';
interface Props { interface Props {
visible: boolean; visible: boolean;

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components'; import { Icon, FormatTime, Tag, BaseUserCard } from '@/components';
interface Props { interface Props {
visible: boolean; visible: boolean;
@ -34,7 +34,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
: null} : null}
</a> </a>
</h6> </h6>
<div className="d-flex align-items-center fs-14 text-secondary mb-2"> <div className="d-flex flex-wrap align-items-center fs-14 text-secondary mb-2">
{tabName === 'bookmarks' && ( {tabName === 'bookmarks' && (
<> <>
<BaseUserCard data={item.user_info} showAvatar={false} /> <BaseUserCard data={item.user_info} showAvatar={false} />

View File

@ -1,7 +1,7 @@
import { FC, memo } from 'react'; import { FC, memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QueryGroup } from '@answer/components'; import { QueryGroup } from '@/components';
const sortBtns = ['newest', 'score']; const sortBtns = ['newest', 'score'];

View File

@ -44,7 +44,10 @@ const list = [
const Index: FC<Props> = ({ slug, tabName = 'overview', isSelf }) => { const Index: FC<Props> = ({ slug, tabName = 'overview', isSelf }) => {
const { t } = useTranslation('translation', { keyPrefix: 'personal' }); const { t } = useTranslation('translation', { keyPrefix: 'personal' });
return ( return (
<Nav className="pt-2 mb-4" variant="pills"> <Nav
className="pt-2 mb-4 flex-nowrap"
variant="pills"
style={{ overflow: 'auto' }}>
{list.map((item) => { {list.map((item) => {
if (item.role && !isSelf) { if (item.role && !isSelf) {
return null; return null;

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FormatTime } from '@answer/components'; import { FormatTime } from '@/components';
interface Props { interface Props {
visible: boolean; visible: boolean;

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
interface Props { interface Props {
data: any[]; data: any[];

View File

@ -3,8 +3,8 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar, Icon } from '@answer/components'; import { Avatar, Icon } from '@/components';
import type { UserInfoRes } from '@answer/common/interface'; import type { UserInfoRes } from '@/common/interface';
interface Props { interface Props {
data: UserInfoRes; data: UserInfoRes;
@ -16,7 +16,7 @@ const Index: FC<Props> = ({ data }) => {
return null; return null;
} }
return ( return (
<div className="d-flex mb-4"> <div className="d-flex flex-column flex-md-row mb-4">
{data?.status !== 'deleted' ? ( {data?.status !== 'deleted' ? (
<Link to={`/users/${data.username}`} reloadDocument> <Link to={`/users/${data.username}`} reloadDocument>
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" /> <Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
@ -25,7 +25,7 @@ const Index: FC<Props> = ({ data }) => {
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" /> <Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
)} )}
<div className="ms-4"> <div className="ms-0 ms-md-4 mt-4 mt-md-0">
<div className="d-flex align-items-center mb-2"> <div className="d-flex align-items-center mb-2">
{data?.status !== 'deleted' ? ( {data?.status !== 'deleted' ? (
<Link <Link
@ -51,7 +51,7 @@ const Index: FC<Props> = ({ data }) => {
</div> </div>
<div className="text-secondary mb-4">@{data.username}</div> <div className="text-secondary mb-4">@{data.username}</div>
<div className="d-flex mb-3"> <div className="d-flex flex-wrap mb-3">
<div className="me-3"> <div className="me-3">
<strong className="fs-5">{data.rank || 0}</strong> <strong className="fs-5">{data.rank || 0}</strong>
<span className="text-secondary"> {t('x_reputation')}</span> <span className="text-secondary"> {t('x_reputation')}</span>

View File

@ -1,7 +1,7 @@
import { FC, memo } from 'react'; import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { FormatTime } from '@answer/components'; import { FormatTime } from '@/components';
interface Props { interface Props {
visible: boolean; visible: boolean;
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, data }) => {
return ( return (
<ListGroupItem className="d-flex py-3 px-0" key={item.object_id}> <ListGroupItem className="d-flex py-3 px-0" key={item.object_id}>
<div <div
className="me-3 text-end text-secondary" className="me-3 text-end text-secondary flex-shrink-0"
style={{ width: '80px' }}> style={{ width: '80px' }}>
{item.vote_type} {item.vote_type}
</div> </div>

View File

@ -3,13 +3,13 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; import { Pagination, FormatTime, PageTitle, Empty } from '@/components';
import { userInfoStore } from '@answer/stores'; import { loggedUserInfoStore } from '@/stores';
import { import {
usePersonalInfoByName, usePersonalInfoByName,
usePersonalTop, usePersonalTop,
usePersonalListByTabName, usePersonalListByTabName,
} from '@answer/api'; } from '@/services';
import { import {
UserInfo, UserInfo,
@ -30,7 +30,7 @@ const Personal: FC = () => {
const page = searchParams.get('page') || 1; const page = searchParams.get('page') || 1;
const order = searchParams.get('order') || 'newest'; const order = searchParams.get('order') || 'newest';
const { t } = useTranslation('translation', { keyPrefix: 'personal' }); const { t } = useTranslation('translation', { keyPrefix: 'personal' });
const sessionUser = userInfoStore((state) => state.user); const sessionUser = loggedUserInfoStore((state) => state.user);
const isSelf = sessionUser?.username === username; const isSelf = sessionUser?.username === username;
const { data: userInfo } = usePersonalInfoByName(username); const { data: userInfo } = usePersonalInfoByName(username);
@ -64,9 +64,9 @@ const Personal: FC = () => {
xxl={3} xxl={3}
lg={4} lg={4}
sm={12} sm={12}
className="d-flex justify-content-end mt-5 mt-lg-0"> className="d-flex justify-content-start justify-content-md-end">
{isSelf && ( {isSelf && (
<div> <div className="mb-3">
<Button <Button
variant="outline-secondary" variant="outline-secondary"
href="/users/settings/profile" href="/users/settings/profile"
@ -79,7 +79,7 @@ const Personal: FC = () => {
</Row> </Row>
<Row className="justify-content-center"> <Row className="justify-content-center">
<Col lg={10}> <Col lg={12}>
<NavBar tabName={tabName} slug={username} isSelf={isSelf} /> <NavBar tabName={tabName} slug={username} isSelf={isSelf} />
</Col> </Col>
<Col xxl={7} lg={8} sm={12}> <Col xxl={7} lg={8} sm={12}>

View File

@ -3,9 +3,8 @@ import { Form, Button, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { register } from '@answer/api'; import type { FormDataType } from '@/common/interface';
import type { FormDataType } from '@answer/common/interface'; import { register } from '@/services';
import userStore from '@/stores/userInfo'; import userStore from '@/stores/userInfo';
interface Props { interface Props {

View File

@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react';
import { Container } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageTitle, Unactivate } from '@answer/components'; import { PageTitle, Unactivate } from '@/components';
import { isLogin } from '@answer/utils'; import { tryNormalLogged } from '@/utils/guard';
import SignUpForm from './components/SignUpForm'; import SignUpForm from './components/SignUpForm';
@ -16,7 +16,7 @@ const Index: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
isLogin(); tryNormalLogged();
}, []); }, []);
return ( return (

Some files were not shown because too many files have changed in this diff Show More