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',
},
{
pattern: '@answer/**',
pattern: '@/**',
group: 'internal',
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,10 @@
"confirm_email": "Confirm Email",
"account_suspended": "Account Suspended",
"admin": "Admin",
"change_email": "Modify Email"
"change_email": "Modify Email",
"install": "Answer Installation",
"upgrade": "Answer Upgrade",
"maintenance": "Webite Maintenance"
},
"notifications": {
"title": "Notifications",
@ -290,7 +293,9 @@
"now": "now",
"x_seconds_ago": "{{count}}s ago",
"x_minutes_ago": "{{count}}m ago",
"x_hours_ago": "{{count}}h ago"
"x_hours_ago": "{{count}}h ago",
"hour": "hour",
"day": "day"
},
"comment": {
"btn_add_comment": "Add comment",
@ -735,6 +740,84 @@
"x_answers": "answers",
"x_questions": "questions"
},
"install": {
"title": "Answer",
"next": "Next",
"done": "Done",
"lang": {
"label": "Please choose a language"
},
"db_type": {
"label": "Database Engine"
},
"db_username": {
"label": "Username",
"placeholder": "root",
"msg": "Username cannot be empty."
},
"db_password": {
"label": "Password",
"placeholder": "root",
"msg": "Password cannot be empty."
},
"db_host": {
"label": "Database Host",
"placeholder": "db:3306",
"msg": "Database Host cannot be empty."
},
"db_name": {
"label": "Database Name",
"placeholder": "answer",
"msg": "Database Name cannot be empty."
},
"db_file": {
"label": "Database File",
"placeholder": "/data/answer.db",
"msg": "Database File cannot be empty."
},
"config_yaml": {
"title": "Create config.yaml",
"label": "The config.yaml file created.",
"description": "You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.",
"info": "After 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": {
"description": "Unfortunately, this page doesn't exist.",
"back_home": "Back to homepage"
@ -743,6 +826,9 @@
"description": "The server encountered an error and could not complete your request.",
"back_home": "Back to homepage"
},
"page_maintenance": {
"description": "We are under maintenance, well be back soon."
},
"admin": {
"admin_header": {
"title": "Admin"
@ -762,7 +848,36 @@
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome to Answer Admin !",
"version": "Version"
"site_statistics": "Site Statistics",
"questions": "Questions:",
"answers": "Answers:",
"comments": "Comments:",
"votes": "Votes:",
"active_users": "Active users:",
"flags": "Flags:",
"site_health_status": "Site Health Status",
"version": "Version:",
"https": "HTTPS:",
"uploading_files": "Uploading files:",
"smtp": "SMTP:",
"timezone": "Timezone:",
"system_info": "System Info",
"storage_used": "Storage used:",
"uptime": "Uptime:",
"answer_links": "Answer Links",
"documents": "Documents",
"feedback": "Feedback",
"review": "Review",
"config": "Config",
"update_to": "Update to",
"latest": "Latest",
"check_failed": "Check failed",
"yes": "Yes",
"no": "No",
"not_allowed": "Not allowed",
"allowed": "Allowed",
"enabled": "Enabled",
"disabled": "Disabled"
},
"flags": {
"title": "Flags",
@ -819,7 +934,10 @@
"inactive": "Inactive",
"suspended": "Suspended",
"deleted": "Deleted",
"normal": "Normal"
"normal": "Normal",
"filter": {
"placeholder": "Filter by name, user:id"
}
},
"questions": {
"page_title": "Questions",
@ -832,7 +950,10 @@
"created": "Created",
"status": "Status",
"action": "Action",
"change": "Change"
"change": "Change",
"filter": {
"placeholder": "Filter by title, question:id"
}
},
"answers": {
"page_title": "Answers",
@ -843,7 +964,10 @@
"created": "Created",
"status": "Status",
"action": "Action",
"change": "Change"
"change": "Change",
"filter": {
"placeholder": "Filter by title, answer:id"
}
},
"general": {
"page_title": "General",
@ -879,6 +1003,11 @@
"label": "Interface Language",
"msg": "Interface language cannot be empty.",
"text": "User interface language. It will change when you refresh the page."
},
"timezone": {
"label": "Timezone",
"msg": "Timezone cannot be empty.",
"text": "Choose a UTC (Coordinated Universal Time) time offset."
}
},
"smtp": {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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 { useTranslation } from 'react-i18next';
import { Icon } from '@answer/components';
import { Icon } from '@/components';
import './index.scss';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { useTranslation } from 'react-i18next';
import { resetPassword, checkImgCode } from '@answer/api';
import type {
ImgCodeRes,
PasswordResetReq,
FormDataType,
} from '@answer/common/interface';
} from '@/common/interface';
import { resetPassword, checkImgCode } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal';
interface IProps {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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