Merge branch 'feat/ui-0.6.0' into feat/ui-0.7.0

This commit is contained in:
haitao(lj) 2022-12-09 10:12:12 +08:00
commit 1537f502f4
55 changed files with 1530 additions and 1565 deletions

View File

@ -640,7 +640,8 @@ ui:
account: Account account: Account
interface: Interface interface: Interface
profile: profile:
btn_name: Update profile heading: Profile
btn_name: Save
display_name: display_name:
label: Display Name label: Display Name
msg: Display name cannot be empty. msg: Display name cannot be empty.
@ -658,7 +659,7 @@ ui:
custom: Custom custom: Custom
btn_refresh: Refresh btn_refresh: Refresh
custom_text: You can upload your image. custom_text: You can upload your image.
default: Default default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me (optional)
@ -670,10 +671,12 @@ ui:
label: Location (optional) label: Location (optional)
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications
email: email:
label: Email Notifications label: Email Notifications
radio: "Answers to your questions, comments, and more" radio: "Answers to your questions, comments, and more"
account: account:
heading: Account
change_email_btn: Change email change_email_btn: Change email
change_pass_btn: Change password change_pass_btn: Change password
change_email_info: >- change_email_info: >-
@ -694,6 +697,7 @@ ui:
pass_confirm: pass_confirm:
label: Confirm New Password label: Confirm New Password
interface: interface:
heading: Interface
lang: lang:
label: Interface Language label: Interface Language
text: User interface language. It will change when you refresh the page. text: User interface language. It will change when you refresh the page.
@ -1212,7 +1216,8 @@ ui:
text: Provide email address that will receive test sends. text: Provide email address that will receive test sends.
msg: Test email recipients is invalid msg: Test email recipients is invalid
smtp_authentication: smtp_authentication:
label: SMTP Authentication label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty. msg: SMTP authentication cannot be empty.
"yes": "Yes" "yes": "Yes"
"no": "No" "no": "No"

View File

@ -4,22 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Answer</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
@ -51,15 +36,5 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
</div> </div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View File

@ -287,11 +287,15 @@ export interface AdminSettingsGeneral {
permalink: number; permalink: number;
} }
export interface HeadInfo { export interface HelmetBase {
title?: string; pageTitle?: string;
description?: string; description?: string;
keywords?: string; keywords?: string;
ldJSON?; }
export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
title?: string;
subtitle?: string;
} }
export interface AdminSettingsInterface { export interface AdminSettingsInterface {
@ -313,12 +317,12 @@ export interface AdminSettingsSmtp {
} }
export interface SiteSettings { export interface SiteSettings {
branding: AdmingSettingBranding; branding: AdminSettingBranding;
general: AdminSettingsGeneral; general: AdminSettingsGeneral;
interface: AdminSettingsInterface; interface: AdminSettingsInterface;
} }
export interface AdmingSettingBranding { export interface AdminSettingBranding {
logo: string; logo: string;
square_icon: string; square_icon: string;
mobile_logo?: string; mobile_logo?: string;

View File

@ -47,7 +47,7 @@ const Index: FC = () => {
{t('save')} {t('save')}
</Button> </Button>
</Card.Header> </Card.Header>
<Card.Body className="pb-2"> <Card.Body className="my-n1">
<TagSelector <TagSelector
value={followingTags} value={followingTags}
onChange={handleTagsChange} onChange={handleTagsChange}

View File

@ -9,7 +9,8 @@ import { DEFAULT_SITE_NAME } from '@/common/constants';
const Index = () => { const Index = () => {
const fullYear = dayjs().format('YYYY'); const fullYear = dayjs().format('YYYY');
const siteName = siteInfoStore.getState().siteInfo.name || DEFAULT_SITE_NAME; const siteName =
siteInfoStore((state) => state.siteInfo.name) || DEFAULT_SITE_NAME;
const cc = `${fullYear} ${siteName}`; const cc = `${fullYear} ${siteName}`;
return ( return (
<footer className="bg-light py-3"> <footer className="bg-light py-3">

View File

@ -1,25 +0,0 @@
import { FC } from 'react';
import { siteInfoStore } from '@/stores';
interface IProp {
title?: string;
suffix?: string;
}
const setPageTitle = (title) => {
if (document) {
document.title = title;
}
return null;
};
// TODO: use Helmet for static response
const PageTitle: FC<IProp> = ({ title = '', suffix = '' }) => {
const { siteInfo } = siteInfoStore();
if (!suffix) {
suffix = `${siteInfo.name}`;
}
title = title ? `${title}${suffix ? ` - ${suffix}` : ''}` : suffix;
return <>{setPageTitle(title)}</>;
};
export default PageTitle;

View File

@ -120,9 +120,7 @@ const QuestionList: FC<Props> = ({ source }) => {
<ListGroup variant="flush" className="border-top border-bottom-0"> <ListGroup variant="flush" className="border-top border-bottom-0">
{listData?.list?.map((li) => { {listData?.list?.map((li) => {
return ( return (
<ListGroup.Item <ListGroup.Item key={li.id} className="border-bottom py-3 px-0">
key={li.id}
className="border-bottom pt-3 pb-2 px-0">
<h5 className="text-wrap text-break"> <h5 className="text-wrap text-break">
<NavLink <NavLink
to={pathFactory.questionLanding(li.id, li.title)} to={pathFactory.questionLanding(li.id, li.title)}
@ -131,7 +129,7 @@ const QuestionList: FC<Props> = ({ source }) => {
{li.status === 2 ? ` [${t('closed')}]` : ''} {li.status === 2 ? ` [${t('closed')}]` : ''}
</NavLink> </NavLink>
</h5> </h5>
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary"> <div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-3 text-secondary">
<QuestionLastUpdate q={li} /> <QuestionLastUpdate q={li} />
<div className="ms-0 ms-md-3 mt-2 mt-md-0"> <div className="ms-0 ms-md-3 mt-2 mt-md-0">
<span> <span>
@ -157,7 +155,7 @@ const QuestionList: FC<Props> = ({ source }) => {
</span> </span>
</div> </div>
</div> </div>
<div className="question-tags mx-n1 mt-2"> <div className="question-tags m-n1">
{Array.isArray(li.tags) {Array.isArray(li.tags)
? li.tags.map((tag) => { ? li.tags.map((tag) => {
return ( return (

View File

@ -23,7 +23,6 @@ const Index: React.FC<IProps> = ({
const [status, setStatus] = useState(false); const [status, setStatus] = useState(false);
const onChange = (e: any) => { const onChange = (e: any) => {
console.log('uploading', e);
if (status) { if (status) {
return; return;
} }
@ -37,7 +36,6 @@ const Index: React.FC<IProps> = ({
// return; // return;
// } // }
setStatus(true); setStatus(true);
console.log('uploading', e.target.files);
uploadImage({ file: e.target.files[0], type }) uploadImage({ file: e.target.files[0], type })
.then((res) => { .then((res) => {
uploadCallback(res); uploadCallback(res);

View File

@ -19,7 +19,6 @@ import Mentions from './Mentions';
import FormatTime from './FormatTime'; import FormatTime from './FormatTime';
import Toast from './Toast'; import Toast from './Toast';
import AccordionNav from './AccordionNav'; import AccordionNav from './AccordionNav';
import PageTitle from './PageTitle';
import Empty from './Empty'; import Empty from './Empty';
import BaseUserCard from './BaseUserCard'; import BaseUserCard from './BaseUserCard';
import FollowingTags from './FollowingTags'; import FollowingTags from './FollowingTags';
@ -51,7 +50,6 @@ export {
FormatTime, FormatTime,
Toast, Toast,
AccordionNav, AccordionNav,
PageTitle,
Empty, Empty,
BaseUserCard, BaseUserCard,
FollowingTags, FollowingTags,

View File

@ -5,9 +5,9 @@ import usePageUsers from './usePageUsers';
import useChangeModal from './useChangeModal'; import useChangeModal from './useChangeModal';
import useEditStatusModal from './useEditStatusModal'; import useEditStatusModal from './useEditStatusModal';
import useChangeUserRoleModal from './useChangeUserRoleModal'; import useChangeUserRoleModal from './useChangeUserRoleModal';
import useHeadInfo from './useHeadInfo';
import useUserModal from './useUserModal'; import useUserModal from './useUserModal';
import useChangePasswordModal from './useChangePasswordModal'; import useChangePasswordModal from './useChangePasswordModal';
import usePageTags from './usePageTags';
export { export {
useTagModal, useTagModal,
@ -17,7 +17,7 @@ export {
useChangeModal, useChangeModal,
useEditStatusModal, useEditStatusModal,
useChangeUserRoleModal, useChangeUserRoleModal,
useHeadInfo,
useUserModal, useUserModal,
useChangePasswordModal, useChangePasswordModal,
usePageTags,
}; };

View File

@ -1,12 +0,0 @@
import { useEffect } from 'react';
import { HeadInfo } from '@/common/interface';
import { headInfoStore } from '@/stores';
export default function useHeadInfo(info: HeadInfo) {
const { update } = headInfoStore.getState();
useEffect(() => {
update(info);
}, [info]);
}

View File

@ -0,0 +1,11 @@
import { useEffect } from 'react';
import { HelmetUpdate } from '@/common/interface';
import { pageTagStore } from '@/stores';
export default function usePageTags(info: HelmetUpdate) {
const { update } = pageTagStore.getState();
useEffect(() => {
update(info);
}, [info.title, info.subtitle, info.description, info.keywords]);
}

View File

@ -105,8 +105,7 @@ const Index: FC = () => {
favicon: formData.favicon.value, favicon: formData.favicon.value,
}; };
brandSetting(params) brandSetting(params)
.then((res) => { .then(() => {
console.log(res);
update(params); update(params);
Toast.onShow({ Toast.onShow({
msg: t('update', { keyPrefix: 'toast' }), msg: t('update', { keyPrefix: 'toast' }),

View File

@ -46,7 +46,8 @@ const Smtp: FC = () => {
}, },
smtp_authentication: { smtp_authentication: {
type: 'boolean', type: 'boolean',
title: t('smtp_authentication.label'), title: t('smtp_authentication.title'),
label: t('smtp_authentication.label'),
enum: [true, false], enum: [true, false],
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')], enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
}, },

View File

@ -147,7 +147,9 @@ const Users: FC = () => {
)} )}
<th style={{ width: '12%' }}>{t('status')}</th> <th style={{ width: '12%' }}>{t('status')}</th>
<th style={{ width: '12%' }}>{t('role')}</th> {curFilter !== 'suspended' && curFilter !== 'deleted' && (
<th style={{ width: '12%' }}>{t('role')}</th>
)}
{curFilter !== 'deleted' ? ( {curFilter !== 'deleted' ? (
<th style={{ width: '8%' }} className="text-end"> <th style={{ width: '8%' }} className="text-end">
{t('action')} {t('action')}
@ -189,11 +191,13 @@ const Users: FC = () => {
{t(user.status)} {t(user.status)}
</span> </span>
</td> </td>
<td> {curFilter !== 'suspended' && curFilter !== 'deleted' && (
<span className="badge text-bg-light"> <td>
{t(user.role_name)} <span className="badge text-bg-light">
</span> {t(user.role_name)}
</td> </span>
</td>
)}
{curFilter !== 'deleted' ? ( {curFilter !== 'deleted' ? (
<td className="text-end"> <td className="text-end">
<Dropdown> <Dropdown>

View File

@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { AccordionNav, PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
import { AccordionNav } from '@/components';
import { ADMIN_NAV_MENUS } from '@/common/constants'; import { ADMIN_NAV_MENUS } from '@/common/constants';
import './index.scss'; import './index.scss';
@ -20,10 +21,11 @@ const formPaths = [
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
const { pathname } = useLocation(); const { pathname } = useLocation();
usePageTags({
title: t('admin'),
});
return ( return (
<> <>
<PageTitle title={t('admin')} />
<div className="bg-light py-2"> <div className="bg-light py-2">
<Container className="py-1"> <Container className="py-1">
<h6 className="mb-0 fw-bold lh-base"> <h6 className="mb-0 fw-bold lh-base">

View File

@ -3,8 +3,8 @@ import { FC, useState, useEffect } from 'react';
import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { usePageTags } from '@/hooks';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { PageTitle } from '@/components';
import { import {
dbCheck, dbCheck,
installInit, installInit,
@ -103,7 +103,6 @@ const Index: FC = () => {
}); });
const handleChange = (params: FormDataType) => { const handleChange = (params: FormDataType) => {
// console.log(params);
setErrorData({ setErrorData({
msg: '', msg: '',
}); });
@ -240,13 +239,15 @@ const Index: FC = () => {
configYmlCheck(); configYmlCheck();
}, []); }, []);
usePageTags({
title: t('install', { keyPrefix: 'page_title' })
});
if (loading) { if (loading) {
return <div />; return <div />;
} }
return ( return (
<div className="bg-f5 py-5 flex-grow-1"> <div className="bg-f5 py-5 flex-grow-1">
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
<Container className='py-3'> <Container className='py-3'>
<Row className="justify-content-center"> <Row className="justify-content-center">
<Col lg={6}> <Col lg={6}>

View File

@ -4,29 +4,18 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import { import { toastStore, brandingStore, pageTagStore } from '@/stores';
siteInfoStore,
toastStore,
brandingStore,
headInfoStore,
} from '@/stores';
import { Header, Footer, Toast } from '@/components'; import { Header, Footer, Toast } from '@/components';
const Layout: FC = () => { const Layout: FC = () => {
const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { msg: toastMsg, variant, clear: toastClear } = toastStore();
const siteInfo = siteInfoStore((state) => state.siteInfo);
const headInfo = headInfoStore((state) => state.headInfo);
const { favicon, square_icon } = brandingStore((state) => state.branding);
const closeToast = () => { const closeToast = () => {
toastClear(); toastClear();
}; };
const { const { favicon, square_icon } = brandingStore((state) => state.branding);
title, const { pageTitle, keywords, description } = pageTagStore(
keywords, (state) => state.items,
description = siteInfo.description, );
ldJSON,
} = headInfo;
return ( return (
<HelmetProvider> <HelmetProvider>
@ -38,10 +27,9 @@ const Layout: FC = () => {
/> />
<link rel="icon" type="image/png" sizes="192x192" href={square_icon} /> <link rel="icon" type="image/png" sizes="192x192" href={square_icon} />
<link rel="apple-touch-icon" type="image/png" href={square_icon} /> <link rel="apple-touch-icon" type="image/png" href={square_icon} />
{title && <title>{title}</title>} <title>{pageTitle}</title>
{keywords && <meta name="keywords" content={keywords} />} {keywords && <meta name="keywords" content={keywords} />}
{description && <meta name="description" content={description} />} {description && <meta name="description" content={description} />}
{ldJSON && <script type="application/ld+json">{ldJSON}</script>}
</Helmet> </Helmet>
<SWRConfig <SWRConfig
value={{ value={{

View File

@ -1,11 +1,14 @@
import { FC } from 'react'; import { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { useLegalPrivacy } from '@/services'; import { useLegalPrivacy } from '@/services';
import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
usePageTags({
title: t('privacy'),
});
const { data: privacy } = useLegalPrivacy(); const { data: privacy } = useLegalPrivacy();
const contentText = privacy?.privacy_policy_original_text; const contentText = privacy?.privacy_policy_original_text;
let matchUrl: URL | undefined; let matchUrl: URL | undefined;
@ -19,10 +22,8 @@ const Index: FC = () => {
window.location.replace(matchUrl.toString()); window.location.replace(matchUrl.toString());
return null; return null;
} }
return ( return (
<> <>
<PageTitle title={t('privacy')} />
<h3 className="mb-4">{t('privacy')}</h3> <h3 className="mb-4">{t('privacy')}</h3>
<div <div
className="fmt" className="fmt"

View File

@ -1,11 +1,14 @@
import { FC } from 'react'; import { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { useLegalTos } from '@/services'; import { useLegalTos } from '@/services';
import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
usePageTags({
title: t('tos'),
});
const { data: tos } = useLegalTos(); const { data: tos } = useLegalTos();
const contentText = tos?.terms_of_service_original_text; const contentText = tos?.terms_of_service_original_text;
let matchUrl: URL | undefined; let matchUrl: URL | undefined;
@ -21,7 +24,6 @@ const Index: FC = () => {
} }
return ( return (
<> <>
<PageTitle title={t('tos')} />
<h3 className="mb-4">{t('tos')}</h3> <h3 className="mb-4">{t('tos')}</h3>
<div <div
className="fmt" className="fmt"

View File

@ -1,24 +1,26 @@
import { Container } from 'react-bootstrap'; import { Container } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'page_maintenance', keyPrefix: 'page_maintenance',
}); });
usePageTags({
title: t('maintenance', { keyPrefix: 'page_title' }),
});
return ( return (
<div className="bg-f5"> <div className="bg-f5">
<Container <Container
className="d-flex flex-column justify-content-center align-items-center" className="d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: '100vh' }}> style={{ minHeight: '100vh' }}>
<PageTitle title={t('maintenance', { keyPrefix: 'page_title' })} />
<div <div
className="mb-4 text-secondary" className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}> style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=_=) (=_=)
</div> </div>
<div className="text-center mb-4">{t('description')}</div> <div className="text-center mb-4">{t('desc')}</div>
</Container> </Container>
</div> </div>
); );

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 '@/components'; import { usePageTags } from '@/hooks';
import { Editor, EditorRef, TagSelector } from '@/components';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
import { import {
saveQuestion, saveQuestion,
@ -140,22 +141,22 @@ const Ask = () => {
}); });
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
let bol = true; const bol = true;
const { title, content, tags, answer } = formData; const { title, content, tags, answer } = formData;
if (!title.value) { if (!title.value) {
bol = false; // bol = false;
formData.title = { // formData.title = {
value: '', // value: '',
isInvalid: true, // isInvalid: true,
errorMsg: t('form.fields.title.msg.empty'), // errorMsg: t('form.fields.title.msg.empty'),
}; // };
} else if (Array.from(title.value).length > 150) { } else if (Array.from(title.value).length > 150) {
bol = false; // bol = false;
formData.title = { // formData.title = {
value: title.value, // value: title.value,
isInvalid: true, // isInvalid: true,
errorMsg: t('form.fields.title.msg.range'), // errorMsg: t('form.fields.title.msg.range'),
}; // };
} else { } else {
formData.title = { formData.title = {
value: title.value, value: title.value,
@ -165,12 +166,12 @@ const Ask = () => {
} }
if (!content.value) { if (!content.value) {
bol = false; // bol = false;
formData.content = { // formData.content = {
value: '', // value: '',
isInvalid: true, // isInvalid: true,
errorMsg: t('form.fields.body.msg.empty'), // errorMsg: t('form.fields.body.msg.empty'),
}; // };
} else { } else {
formData.content = { formData.content = {
value: content.value, value: content.value,
@ -180,12 +181,12 @@ const Ask = () => {
} }
if (tags.value.length === 0) { if (tags.value.length === 0) {
bol = false; // bol = false;
formData.tags = { // formData.tags = {
value: [], // value: [],
isInvalid: true, // isInvalid: true,
errorMsg: t('form.fields.tags.msg.empty'), // errorMsg: t('form.fields.tags.msg.empty'),
}; // };
} else { } else {
formData.tags = { formData.tags = {
value: tags.value, value: tags.value,
@ -195,12 +196,12 @@ const Ask = () => {
} }
if (checked) { if (checked) {
if (!answer.value) { if (!answer.value) {
bol = false; // bol = false;
formData.answer = { // formData.answer = {
value: '', // value: '',
isInvalid: true, // isInvalid: true,
errorMsg: t('form.fields.answer.msg.empty'), // errorMsg: t('form.fields.answer.msg.empty'),
}; // };
} else { } else {
formData.answer = { formData.answer = {
value: answer.value, value: answer.value,
@ -292,189 +293,185 @@ const Ask = () => {
if (isEdit) { if (isEdit) {
pageTitle = t('edit_question', { keyPrefix: 'page_title' }); pageTitle = t('edit_question', { keyPrefix: 'page_title' });
} }
usePageTags({
title: pageTitle,
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={10} md={12}>
<Row className="justify-content-center"> <h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
<Col xxl={10} md={12}> </Col>
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3> </Row>
</Col> <Row className="justify-content-center">
</Row> <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Row className="justify-content-center"> <Form noValidate onSubmit={handleSubmit}>
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> {isEdit && (
<Form noValidate onSubmit={handleSubmit}> <Form.Group controlId="revision" className="mb-3">
{isEdit && ( <Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Group controlId="revision" className="mb-3"> <Form.Select onChange={handleSelectedRevision}>
<Form.Label>{t('form.fields.revision.label')}</Form.Label> {revisions.map(({ reason, create_at, user_info }, index) => {
<Form.Select onChange={handleSelectedRevision}> const date = dayjs(create_at * 1000)
{revisions.map( .tz()
({ reason, create_at, user_info }, index) => { .format(t('long_date_with_time', { keyPrefix: 'dates' }));
const date = dayjs(create_at * 1000) return (
.tz() <option key={`${create_at}`} value={index}>
.format( {`${date} - ${user_info.display_name} - ${
t('long_date_with_time', { keyPrefix: 'dates' }), reason || t('default_reason')
); }`}
return ( </option>
<option key={`${create_at}`} value={index}> );
{`${date} - ${user_info.display_name} - ${ })}
reason || t('default_reason') </Form.Select>
}`}
</option>
);
},
)}
</Form.Select>
</Form.Group>
)}
<Form.Group controlId="title" className="mb-3">
<Form.Label>{t('form.fields.title.label')}</Form.Label>
<Form.Control
value={formData.title.value}
isInvalid={formData.title.isInvalid}
onChange={handleTitleChange}
placeholder={t('form.fields.title.placeholder')}
autoFocus
/>
<Form.Control.Feedback type="invalid">
{formData.title.errorMsg}
</Form.Control.Feedback>
{bool && <SearchQuestion similarQuestions={similarQuestions} />}
</Form.Group> </Form.Group>
<Form.Group controlId="body"> )}
<Form.Label>{t('form.fields.body.label')}</Form.Label>
<Form.Control
defaultValue={formData.content.value}
isInvalid={formData.content.isInvalid}
hidden
/>
<Editor
value={formData.content.value}
onChange={handleContentChange}
className={classNames(
'form-control p-0',
focusType === 'content' && 'focus',
)}
onFocus={() => {
setForceType('content');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
<Form.Control.Feedback type="invalid">
{formData.content.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="tags" className="my-3">
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
<Form.Control
defaultValue={JSON.stringify(formData.tags.value)}
isInvalid={formData.tags.isInvalid}
hidden
/>
<TagSelector
value={formData.tags.value}
onChange={handleTagsChange}
showRequiredTagText
/>
<Form.Control.Feedback type="invalid">
{formData.tags.errorMsg}
</Form.Control.Feedback>
</Form.Group>
{isEdit && (
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
defaultValue={formData.edit_summary.value}
isInvalid={formData.edit_summary.isInvalid}
placeholder={t('form.fields.edit_summary.placeholder')}
onChange={handleSummaryChange}
/>
<Form.Control.Feedback type="invalid">
{formData.edit_summary.errorMsg}
</Form.Control.Feedback>
</Form.Group>
)}
{!checked && (
<div className="mt-3">
<Button type="submit" className="me-2">
{isEdit ? t('btn_save_edits') : t('btn_post_question')}
</Button>
<Button variant="link" onClick={backPage}> <Form.Group controlId="title" className="mb-3">
{t('cancel', { keyPrefix: 'btns' })} <Form.Label>{t('form.fields.title.label')}</Form.Label>
</Button> <Form.Control
</div> value={formData.title.value}
)} isInvalid={formData.title.isInvalid}
{!isEdit && ( onChange={handleTitleChange}
<> placeholder={t('form.fields.title.placeholder')}
<Form.Check autoFocus
className="mt-5"
checked={checked}
type="checkbox"
label={t('answer_question')}
onChange={(e) => setCheckState(e.target.checked)}
id="radio-answer"
/>
{checked && (
<Form.Group controlId="answer" className="mt-4">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
onChange={handleAnswerChange}
ref={editorRef2}
className={classNames(
'form-control p-0',
focusType === 'answer' && 'focus',
)}
onFocus={() => {
setForceType('answer');
}}
onBlur={() => {
setForceType('');
}}
/>
<Form.Control
value={formData.answer.value}
type="text"
isInvalid={formData.answer.isInvalid}
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.answer.errorMsg}
</Form.Control.Feedback>
</Form.Group>
)}
</>
)}
{checked && (
<Button type="submit" className="mt-3">
{t('post_question&answer')}
</Button>
)}
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card className="mb-4">
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('desc', { keyPrefix: 'how_to_format' }),
}}
/> />
</Card>
</Col> <Form.Control.Feedback type="invalid">
</Row> {formData.title.errorMsg}
</Container> </Form.Control.Feedback>
</> {bool && <SearchQuestion similarQuestions={similarQuestions} />}
</Form.Group>
<Form.Group controlId="body">
<Form.Label>{t('form.fields.body.label')}</Form.Label>
<Form.Control
defaultValue={formData.content.value}
isInvalid={formData.content.isInvalid}
hidden
/>
<Editor
value={formData.content.value}
onChange={handleContentChange}
className={classNames(
'form-control p-0',
focusType === 'content' && 'focus',
)}
onFocus={() => {
setForceType('content');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
<Form.Control.Feedback type="invalid">
{formData.content.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="tags" className="my-3">
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
<Form.Control
defaultValue={JSON.stringify(formData.tags.value)}
isInvalid={formData.tags.isInvalid}
hidden
/>
<TagSelector
value={formData.tags.value}
onChange={handleTagsChange}
showRequiredTagText
/>
<Form.Control.Feedback type="invalid">
{formData.tags.errorMsg}
</Form.Control.Feedback>
</Form.Group>
{isEdit && (
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
defaultValue={formData.edit_summary.value}
isInvalid={formData.edit_summary.isInvalid}
placeholder={t('form.fields.edit_summary.placeholder')}
onChange={handleSummaryChange}
/>
<Form.Control.Feedback type="invalid">
{formData.edit_summary.errorMsg}
</Form.Control.Feedback>
</Form.Group>
)}
{!checked && (
<div className="mt-3">
<Button type="submit" className="me-2">
{isEdit ? t('btn_save_edits') : t('btn_post_question')}
</Button>
<Button variant="link" onClick={backPage}>
{t('cancel', { keyPrefix: 'btns' })}
</Button>
</div>
)}
{!isEdit && (
<>
<Form.Check
className="mt-5"
checked={checked}
type="checkbox"
label={t('answer_question')}
onChange={(e) => setCheckState(e.target.checked)}
id="radio-answer"
/>
{checked && (
<Form.Group controlId="answer" className="mt-4">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
onChange={handleAnswerChange}
ref={editorRef2}
className={classNames(
'form-control p-0',
focusType === 'answer' && 'focus',
)}
onFocus={() => {
setForceType('answer');
}}
onBlur={() => {
setForceType('');
}}
/>
<Form.Control
value={formData.answer.value}
type="text"
isInvalid={formData.answer.isInvalid}
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.answer.errorMsg}
</Form.Control.Feedback>
</Form.Group>
)}
</>
)}
{checked && (
<Button type="submit" className="mt-3">
{t('post_question&answer')}
</Button>
)}
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('desc', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>
</Container>
); );
}; };

View File

@ -9,10 +9,10 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Pattern from '@/common/pattern'; import Pattern from '@/common/pattern';
import { Pagination, PageTitle } from '@/components'; import { Pagination } from '@/components';
import { loggedUserInfoStore, toastStore } from '@/stores'; import { loggedUserInfoStore, toastStore } from '@/stores';
import { scrollTop } from '@/utils'; import { scrollTop } from '@/utils';
import { usePageUsers } from '@/hooks'; import { usePageTags, usePageUsers } from '@/hooks';
import type { import type {
ListResult, ListResult,
QuestionDetailRes, QuestionDetailRes,
@ -143,68 +143,69 @@ const Index = () => {
requestAnswers(); requestAnswers();
} }
}, [page, order]); }, [page, order]);
usePageTags({
title: question?.title,
description: question?.description,
keywords: question?.tags.map((_) => _.slug_name).join(','),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5 questionDetailPage">
<PageTitle title={question?.title} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5 questionDetailPage"> <Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
<Row className="justify-content-center"> {question?.operation?.operation_type && (
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0"> <Alert data={question.operation} />
{question?.operation?.operation_type && ( )}
<Alert data={question.operation} /> <Question
)} data={question}
<Question initPage={initPage}
data={question} hasAnswer={answers.count > 0}
initPage={initPage} isLogged={isLogged}
hasAnswer={answers.count > 0} />
isLogged={isLogged} {answers.count > 0 && (
/> <>
{answers.count > 0 && ( <AnswerHead count={answers.count} order={order} />
<> {answers?.list?.map((item) => {
<AnswerHead count={answers.count} order={order} /> return (
{answers?.list?.map((item) => { <Answer
return ( aid={aid}
<Answer key={item?.id}
aid={aid} data={item}
key={item?.id} questionTitle={question?.title || ''}
data={item} isAuthor={isAuthor}
questionTitle={question?.title || ''} callback={initPage}
isAuthor={isAuthor} isLogged={isLogged}
callback={initPage} />
isLogged={isLogged} );
/> })}
); </>
})} )}
</>
)}
{Math.ceil(answers.count / 15) > 1 && ( {Math.ceil(answers.count / 15) > 1 && (
<div className="d-flex justify-content-center answer-item pt-4"> <div className="d-flex justify-content-center answer-item pt-4">
<Pagination <Pagination
currentPage={Number(page || 1)} currentPage={Number(page || 1)}
pageSize={15} pageSize={15}
totalSize={answers?.count || 0} totalSize={answers?.count || 0}
/>
</div>
)}
{!question?.operation?.operation_type && (
<WriteAnswer
visible={answers.count === 0}
data={{
qid,
answered: question?.answered,
}}
callback={writeAnswerCallback}
/> />
)} </div>
</Col> )}
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<RelatedQuestions id={question?.id || ''} /> {!question?.operation?.operation_type && (
</Col> <WriteAnswer
</Row> visible={answers.count === 0}
</Container> data={{
</> qid,
answered: question?.answered,
}}
callback={writeAnswerCallback}
/>
)}
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<RelatedQuestions id={question?.id || ''} />
</Col>
</Row>
</Container>
); );
}; };

View File

@ -6,8 +6,9 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import classNames from 'classnames'; import classNames from 'classnames';
import { usePageTags } from '@/hooks';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import { Editor, EditorRef, Icon, PageTitle } from '@/components'; import { Editor, EditorRef, Icon } from '@/components';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
import { import {
useQueryAnswerInfo, useQueryAnswerInfo,
@ -33,7 +34,7 @@ const initFormData = {
errorMsg: '', errorMsg: '',
}, },
}; };
const Ask = () => { const Index = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData); const [formData, setFormData] = useState<FormDataItem>(initFormData);
const { aid = '', qid = '' } = useParams(); const { aid = '', qid = '' } = useParams();
const [focusType, setForceType] = useState(''); const [focusType, setForceType] = useState('');
@ -133,128 +134,127 @@ const Ask = () => {
const backPage = () => { const backPage = () => {
navigate(-1); navigate(-1);
}; };
usePageTags({
title: t('edit_answer', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<PageTitle title={t('edit_answer', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> <Col xxl={10} md={12}>
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} md={12}> </Col>
<h3 className="mb-4">{t('title')}</h3> </Row>
</Col> <Row className="justify-content-center">
</Row> <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Row className="justify-content-center"> <a
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> href={pathFactory.questionLanding(qid, data?.question.title)}
<a target="_blank"
href={pathFactory.questionLanding(qid, data?.question.title)} rel="noreferrer">
target="_blank" <h5 className="mb-3">{data?.question.title}</h5>
rel="noreferrer"> </a>
<h5 className="mb-3">{data?.question.title}</h5>
</a>
<div className="question-content-wrap"> <div className="question-content-wrap">
<div <div
ref={questionContentRef} ref={questionContentRef}
className="content position-absolute top-0 w-100" className="content position-absolute top-0 w-100"
dangerouslySetInnerHTML={{ __html: data?.question.html }} dangerouslySetInnerHTML={{ __html: data?.question.html }}
/> />
<div <div
className="resize-bottom" className="resize-bottom"
style={{ maxHeight: questionContentRef?.current?.scrollHeight }} style={{ maxHeight: questionContentRef?.current?.scrollHeight }}
/> />
<div className="line bg-light d-flex justify-content-center align-items-center"> <div className="line bg-light d-flex justify-content-center align-items-center">
<Icon name="three-dots" /> <Icon name="three-dots" />
</div>
</div> </div>
</div>
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3"> <Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label> <Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}> <Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason, user_info }, index) => { {revisions.map(({ create_at, reason, user_info }, index) => {
const date = dayjs(create_at * 1000) const date = dayjs(create_at * 1000)
.tz() .tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' })); .format(t('long_date_with_time', { keyPrefix: 'dates' }));
return ( return (
<option key={`${create_at}`} value={index}> <option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${ {`${date} - ${user_info.display_name} - ${
reason || t('default_reason') reason || t('default_reason')
}`} }`}
</option> </option>
); );
})} })}
</Form.Select> </Form.Select>
</Form.Group> </Form.Group>
<Form.Group controlId="answer" className="mt-3"> <Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label> <Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor <Editor
value={formData.answer.value} value={formData.answer.value}
onChange={handleAnswerChange} onChange={handleAnswerChange}
className={classNames( className={classNames(
'form-control p-0', 'form-control p-0',
focusType === 'answer' && 'focus', focusType === 'answer' && 'focus',
)} )}
onFocus={() => { onFocus={() => {
setForceType('answer'); setForceType('answer');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
<Form.Control
value={formData.answer.value}
type="text"
isInvalid={formData.answer.isInvalid}
readOnly
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.answer.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
onChange={handleSummaryChange}
defaultValue={formData.description.value}
isInvalid={formData.description.isInvalid}
placeholder={t('form.fields.edit_summary.placeholder')}
/>
<Form.Control.Feedback type="invalid">
{formData.description.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="mt-3">
<Button type="submit" className="me-2">
{t('btn_save_edits')}
</Button>
<Button variant="link" onClick={backPage}>
{t('btn_cancel')}
</Button>
</div>
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('description', { keyPrefix: 'how_to_format' }),
}} }}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/> />
</Card> <Form.Control
</Col> value={formData.answer.value}
</Row> type="text"
</Container> isInvalid={formData.answer.isInvalid}
</> readOnly
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.answer.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
onChange={handleSummaryChange}
defaultValue={formData.description.value}
isInvalid={formData.description.isInvalid}
placeholder={t('form.fields.edit_summary.placeholder')}
/>
<Form.Control.Feedback type="invalid">
{formData.description.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="mt-3">
<Button type="submit" className="me-2">
{t('btn_save_edits')}
</Button>
<Button variant="link" onClick={backPage}>
{t('btn_cancel')}
</Button>
</div>
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('desc', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>
</Container>
); );
}; };
export default Ask; export default Index;

View File

@ -3,7 +3,8 @@ 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 '@/components'; import { usePageTags } from '@/hooks';
import { 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';
@ -20,21 +21,19 @@ const Questions: FC = () => {
slogan = `${siteInfo.short_description}`; slogan = `${siteInfo.short_description}`;
} }
usePageTags({ title: pageTitle, subtitle: slogan });
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} suffix={slogan} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={7} lg={8} sm={12}>
<Row className="justify-content-center"> <QuestionList source="questions" />
<Col xxl={7} lg={8} sm={12}> </Col>
<QuestionList source="questions" /> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
</Col> <FollowingTags />
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <HotQuestions />
<FollowingTags /> </Col>
<HotQuestions /> </Row>
</Col> </Container>
</Row>
</Container>
</>
); );
}; };

View File

@ -3,13 +3,8 @@ import { Container, Row, Col, Alert, Stack, 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 { import { usePageTags } from '@/hooks';
BaseUserCard, import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
FormatTime,
Empty,
DiffContent,
PageTitle,
} from '@/components';
import { getReviewList, revisionAudit } from '@/services'; import { getReviewList, revisionAudit } from '@/services';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
@ -124,9 +119,11 @@ const Index: FC = () => {
useEffect(() => { useEffect(() => {
queryNextOne(page); queryNextOne(page);
}, []); }, []);
usePageTags({
title: t('review'),
});
return ( return (
<Container className="pt-2 mt-4 mb-5"> <Container className="pt-2 mt-4 mb-5">
<PageTitle title={t('review')} />
<Row> <Row>
<Col lg={{ span: 7, offset: 1 }}> <Col lg={{ span: 7, offset: 1 }}>
<h3 className="mb-4">{t('review')}</h3> <h3 className="mb-4">{t('review')}</h3>

View File

@ -3,7 +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 '@/components'; import { usePageTags } from '@/hooks';
import { Pagination } from '@/components';
import { useSearch } from '@/services'; import { useSearch } from '@/services';
import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
@ -27,38 +28,38 @@ const Index = () => {
if (q) { if (q) {
pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`; pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
} }
usePageTags({
title: pageTitle,
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={7} lg={8} sm={12} className="mb-3">
<Row className="justify-content-center"> <Head data={extra} />
<Col xxl={7} lg={8} sm={12} className="mb-3">
<Head data={extra} />
<ListGroup variant="flush" className="mb-5"> <ListGroup variant="flush" className="mb-5">
<SearchHead sort={order} count={count} /> <SearchHead sort={order} count={count} />
{list?.map((item) => { {list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />; return <SearchItem key={item.object.id} data={item} />;
})} })}
</ListGroup> </ListGroup>
{!isLoading && !list?.length && <Empty />} {!isLoading && !list?.length && <Empty />}
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
<Pagination <Pagination
currentPage={Number(page)} currentPage={Number(page)}
pageSize={20} pageSize={20}
totalSize={count} totalSize={count}
/> />
</div> </div>
</Col> </Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Tips /> <Tips />
</Col> </Col>
</Row> </Row>
</Container> </Container>
</>
); );
}; };

View File

@ -3,9 +3,10 @@ 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 { usePageTags } from '@/hooks';
import * as Type from '@/common/interface'; import * as Type from '@/common/interface';
import { PageTitle, FollowingTags } from '@/components'; import { FollowingTags } from '@/components';
import { useTagInfo, useFollow } from '@/services'; import { useTagInfo, useFollow, useQuerySynonymsTags } from '@/services';
import QuestionList from '@/components/QuestionList'; import QuestionList from '@/components/QuestionList';
import HotQuestions from '@/components/HotQuestions'; import HotQuestions from '@/components/HotQuestions';
import { escapeRemove } from '@/utils'; import { escapeRemove } from '@/utils';
@ -20,7 +21,7 @@ const Questions: FC = () => {
const [tagFollow, setTagFollow] = useState<Type.FollowParams>(); const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
const { data: tagResp } = useTagInfo({ name: curTagName }); const { data: tagResp } = useTagInfo({ name: curTagName });
const { data: followResp } = useFollow(tagFollow); const { data: followResp } = useFollow(tagFollow);
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
const toggleFollow = () => { const toggleFollow = () => {
setTagFollow({ setTagFollow({
is_cancel: tagInfo.is_follower, is_cancel: tagInfo.is_follower,
@ -51,57 +52,66 @@ const Questions: FC = () => {
} }
}, [tagResp, followResp]); }, [tagResp, followResp]);
let pageTitle = ''; let pageTitle = '';
if (tagInfo) { if (tagInfo?.display_name) {
pageTitle = `'${tagInfo.display_name}' ${t('questions', { pageTitle = `'${tagInfo.display_name}' ${t('questions', {
keyPrefix: 'page_title', keyPrefix: 'page_title',
})}`; })}`;
} }
const keywords: string[] = [];
if (tagInfo?.slug_name) {
keywords.push(tagInfo.slug_name);
}
synonymsRes?.synonyms.forEach((o) => {
keywords.push(o.slug_name);
});
usePageTags({
title: pageTitle,
description: tagInfo?.description,
keywords: keywords.join(','),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={7} lg={8} sm={12}>
<Row className="justify-content-center"> <div className="tag-box mb-5">
<Col xxl={7} lg={8} sm={12}> <h3 className="mb-3">
<div className="tag-box mb-5"> <Link
<h3 className="mb-3"> to={pathFactory.tagLanding(tagInfo.slug_name)}
<Link replace
to={pathFactory.tagLanding(tagInfo.slug_name)} className="link-dark">
replace {tagInfo.display_name}
className="link-dark"> </Link>
{tagInfo.display_name} </h3>
</Link>
</h3>
<p className="text-break"> <p className="text-break">
{escapeRemove(tagInfo.excerpt) || t('no_desc')} {escapeRemove(tagInfo.excerpt) || t('no_desc')}
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1"> <Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
[{t('more')}] [{t('more')}]
</Link> </Link>
</p> </p>
<div className="box-ft"> <div className="box-ft">
{tagInfo.is_follower ? ( {tagInfo.is_follower ? (
<Button variant="primary" onClick={() => toggleFollow()}> <Button variant="primary" onClick={() => toggleFollow()}>
{t('button_following')} {t('button_following')}
</Button> </Button>
) : ( ) : (
<Button <Button
variant="outline-primary" variant="outline-primary"
onClick={() => toggleFollow()}> onClick={() => toggleFollow()}>
{t('button_follow')} {t('button_follow')}
</Button> </Button>
)} )}
</div>
</div> </div>
<QuestionList source="tag" /> </div>
</Col> <QuestionList source="tag" />
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> </Col>
<FollowingTags /> <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<HotQuestions /> <FollowingTags />
</Col> <HotQuestions />
</Row> </Col>
</Container> </Row>
</> </Container>
); );
}; };

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, PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
import { Editor, EditorRef } from '@/components';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
@ -39,7 +40,7 @@ const initFormData = {
errorMsg: '', errorMsg: '',
}, },
}; };
const Ask = () => { const Index = () => {
const { is_admin = false } = loggedUserInfoStore((state) => state.user); const { is_admin = false } = loggedUserInfoStore((state) => state.user);
const { tagId } = useParams(); const { tagId } = useParams();
@ -143,132 +144,129 @@ const Ask = () => {
const backPage = () => { const backPage = () => {
navigate(-1); navigate(-1);
}; };
usePageTags({
title: t('edit_tag', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<PageTitle title={t('edit_tag', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap"> <Col xxl={10} md={12}>
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} md={12}> </Col>
<h3 className="mb-4">{t('title')}</h3> </Row>
</Col> <Row className="justify-content-center">
</Row> <Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Row className="justify-content-center"> <Form noValidate onSubmit={handleSubmit}>
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0"> <Form.Group controlId="revision" className="mb-3">
<Form noValidate onSubmit={handleSubmit}> <Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Group controlId="revision" className="mb-3"> <Form.Select onChange={handleSelectedRevision}>
<Form.Label>{t('form.fields.revision.label')}</Form.Label> {revisions.map(({ create_at, reason, user_info }, index) => {
<Form.Select onChange={handleSelectedRevision}> const date = dayjs(create_at * 1000)
{revisions.map(({ create_at, reason, user_info }, index) => { .tz()
const date = dayjs(create_at * 1000) .format(t('long_date_with_time', { keyPrefix: 'dates' }));
.tz() return (
.format(t('long_date_with_time', { keyPrefix: 'dates' })); <option key={`${create_at}`} value={index}>
return ( {`${date} - ${user_info.display_name} - ${
<option key={`${create_at}`} value={index}> reason || t('default_reason')
{`${date} - ${user_info.display_name} - ${ }`}
reason || t('default_reason') </option>
}`} );
</option> })}
); </Form.Select>
})} </Form.Group>
</Form.Select> <Form.Group controlId="display_name" className="mb-3">
</Form.Group> <Form.Label>{t('form.fields.display_name.label')}</Form.Label>
<Form.Group controlId="display_name" className="mb-3"> <Form.Control
<Form.Label>{t('form.fields.display_name.label')}</Form.Label> value={formData.displayName.value}
<Form.Control isInvalid={formData.displayName.isInvalid}
value={formData.displayName.value} disabled={!is_admin}
isInvalid={formData.displayName.isInvalid} onChange={handleDisplayNameChange}
disabled={!is_admin}
onChange={handleDisplayNameChange}
/>
<Form.Control.Feedback type="invalid">
{formData.displayName.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="slug_name" className="mb-3">
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
<Form.Control
value={formData.slugName.value}
isInvalid={formData.slugName.isInvalid}
disabled={!is_admin}
onChange={handleSlugNameChange}
/>
<Form.Text as="div">
{t('form.fields.slug_name.info')}
</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.slugName.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="description" className="mt-4">
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
<Editor
value={formData.description.value}
onChange={handleDescriptionChange}
className={classNames(
'form-control p-0',
focusType === 'description' && 'focus',
)}
onFocus={() => {
setForceType('description');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
<Form.Control
value={formData.description.value}
type="text"
isInvalid={formData.description.isInvalid}
readOnly
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.description.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
defaultValue={formData.editSummary.value}
isInvalid={formData.editSummary.isInvalid}
onChange={handleEditSummaryChange}
placeholder={t('form.fields.edit_summary.placeholder')}
/>
<Form.Control.Feedback type="invalid">
{formData.editSummary.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="mt-3">
<Button type="submit">{t('btn_save_edits')}</Button>
<Button variant="link" className="ms-2" onClick={backPage}>
{t('btn_cancel')}
</Button>
</div>
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('desc', { keyPrefix: 'how_to_format' }),
}}
/> />
</Card>
</Col> <Form.Control.Feedback type="invalid">
</Row> {formData.displayName.errorMsg}
</Container> </Form.Control.Feedback>
</> </Form.Group>
<Form.Group controlId="slug_name" className="mb-3">
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
<Form.Control
value={formData.slugName.value}
isInvalid={formData.slugName.isInvalid}
disabled={!is_admin}
onChange={handleSlugNameChange}
/>
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.slugName.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="description" className="mt-4">
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
<Editor
value={formData.description.value}
onChange={handleDescriptionChange}
className={classNames(
'form-control p-0',
focusType === 'description' && 'focus',
)}
onFocus={() => {
setForceType('description');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
<Form.Control
value={formData.description.value}
type="text"
isInvalid={formData.description.isInvalid}
readOnly
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.description.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="edit_summary" className="my-3">
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
defaultValue={formData.editSummary.value}
isInvalid={formData.editSummary.isInvalid}
onChange={handleEditSummaryChange}
placeholder={t('form.fields.edit_summary.placeholder')}
/>
<Form.Control.Feedback type="invalid">
{formData.editSummary.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="mt-3">
<Button type="submit">{t('btn_save_edits')}</Button>
<Button variant="link" className="ms-2" onClick={backPage}>
{t('btn_cancel')}
</Button>
</div>
</Form>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header>
{t('title', { keyPrefix: 'how_to_format' })}
</Card.Header>
<Card.Body
className="fmt small"
dangerouslySetInnerHTML={{
__html: t('desc', { keyPrefix: 'how_to_format' }),
}}
/>
</Card>
</Col>
</Row>
</Container>
); );
}; };
export default Ask; export default Index;

View File

@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
import { Tag, TagSelector, FormatTime, Modal } from '@/components';
import { import {
useTagInfo, useTagInfo,
useQuerySynonymsTags, useQuerySynonymsTags,
@ -26,7 +27,15 @@ const TagIntroduction = () => {
const { t } = useTranslation('translation', { keyPrefix: 'tag_info' }); const { t } = useTranslation('translation', { keyPrefix: 'tag_info' });
const navigate = useNavigate(); const navigate = useNavigate();
const { data: synonymsData, mutate } = useQuerySynonymsTags(tagInfo?.tag_id); const { data: synonymsData, mutate } = useQuerySynonymsTags(tagInfo?.tag_id);
let pageTitle = '';
if (tagInfo) {
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
keyPrefix: 'page_title',
})}`;
}
usePageTags({
title: pageTitle,
});
useEffect(() => { useEffect(() => {
if (locationState?.isReview) { if (locationState?.isReview) {
toastStore.getState().show({ toastStore.getState().show({
@ -35,7 +44,6 @@ const TagIntroduction = () => {
}); });
} }
}, [locationState]); }, [locationState]);
if (!tagInfo) { if (!tagInfo) {
return null; return null;
} }
@ -100,145 +108,132 @@ const TagIntroduction = () => {
} }
}; };
let pageTitle = '';
if (tagInfo) {
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
keyPrefix: 'page_title',
})}`;
}
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={7} lg={8} sm={12}>
<Row className="justify-content-center"> <h3 className="mb-3">
<Col xxl={7} lg={8} sm={12}> <Link
<h3 className="mb-3"> to={pathFactory.tagLanding(tagInfo.slug_name)}
<Link replace
to={pathFactory.tagLanding(tagInfo.slug_name)} className="link-dark">
replace {tagInfo.display_name}
className="link-dark"> </Link>
{tagInfo.display_name} </h3>
</Link>
</h3>
<div className="text-secondary mb-4 fs-14"> <div className="text-secondary mb-4 fs-14">
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} /> <FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
<FormatTime <FormatTime
preFix={t('edited_at')} preFix={t('edited_at')}
className="ms-3" className="ms-3"
time={tagInfo.updated_at} time={tagInfo.updated_at}
/>
</div>
<div
className="content text-break"
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
/> />
<div className="mt-4"> </div>
{tagInfo?.member_actions.map((action, index) => {
return ( <div
<Button className="content text-break"
key={action.name} dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
variant="link" />
className={classNames( <div className="mt-4">
'link-secondary btn-no-border p-0 fs-14', {tagInfo?.member_actions.map((action, index) => {
index > 0 && 'ms-3', return (
)} <Button
onClick={() => onAction(action)}> key={action.name}
{action.name} variant="link"
</Button>
);
})}
{isLogged && (
<Link
to={`/tags/${tagInfo?.tag_id}/timeline`}
className={classNames( className={classNames(
'link-secondary btn-no-border p-0 fs-14', 'link-secondary btn-no-border p-0 fs-14',
tagInfo?.member_actions?.length > 0 && 'ms-3', index > 0 && 'ms-3',
)}> )}
{t('history')} onClick={() => onAction(action)}>
</Link> {action.name}
)} </Button>
</div> );
</Col> })}
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> {isLogged && (
<Card> <Link
<Card.Header className="d-flex justify-content-between"> to={`/tags/${tagInfo?.tag_id}/timeline`}
<span>{t('synonyms.title')}</span> className={classNames(
{isEdit ? ( 'link-secondary btn-no-border p-0 fs-14',
<Button tagInfo?.member_actions?.length > 0 && 'ms-3',
variant="link" )}>
className="p-0 btn-no-border" {t('history')}
onClick={handleSave}> </Link>
{t('synonyms.btn_save')} )}
</Button> </div>
) : synonymsData?.member_actions?.find( </Col>
(v) => v.action === 'edit', <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
) ? ( <Card>
<Button <Card.Header className="d-flex justify-content-between">
variant="link" <span>{t('synonyms.title')}</span>
className="p-0 btn-no-border" {isEdit ? (
onClick={handleEdit}> <Button
{t('synonyms.btn_edit')} variant="link"
</Button> className="p-0 btn-no-border"
) : null} onClick={handleSave}>
</Card.Header> {t('synonyms.btn_save')}
<Card.Body> </Button>
{isEdit && ( ) : synonymsData?.member_actions?.find(
<> (v) => v.action === 'edit',
<div className="mb-3"> ) ? (
{t('synonyms.text')}{' '} <Button
<Tag variant="link"
data={{ className="p-0 btn-no-border"
slug_name: tagName || '', onClick={handleEdit}>
main_tag_slug_name: '', {t('synonyms.btn_edit')}
display_name: '', </Button>
recommend: false, ) : null}
reserved: false, </Card.Header>
}} <Card.Body>
/> {isEdit && (
</div> <>
<TagSelector <div className="mb-3">
value={synonymsData?.synonyms} {t('synonyms.text')}{' '}
onChange={handleTagsChange} <Tag
hiddenDescription data={{
slug_name: tagName || '',
main_tag_slug_name: '',
display_name: '',
recommend: false,
reserved: false,
}}
/> />
</div>
<TagSelector
value={synonymsData?.synonyms}
onChange={handleTagsChange}
hiddenDescription
/>
</>
)}
{!isEdit &&
(synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? (
<div className="m-n1">
{synonymsData.synonyms.map((item) => {
return (
<Tag key={item.tag_id} className="m-1" data={item} />
);
})}
</div>
) : (
<>
<div className="text-muted mb-3">{t('synonyms.empty')}</div>
{synonymsData?.member_actions?.find(
(v) => v.action === 'edit',
) && (
<Button
variant="outline-primary"
size="sm"
onClick={handleEdit}>
{t('synonyms.btn_add')}
</Button>
)}
</> </>
)} ))}
{!isEdit && </Card.Body>
(synonymsData?.synonyms && </Card>
synonymsData.synonyms.length > 0 ? ( </Col>
<div className="m-n1"> </Row>
{synonymsData.synonyms.map((item) => { </Container>
return (
<Tag key={item.tag_id} className="m-1" data={item} />
);
})}
</div>
) : (
<>
<div className="text-muted mb-3">
{t('synonyms.empty')}
</div>
{synonymsData?.member_actions?.find(
(v) => v.action === 'edit',
) && (
<Button
variant="outline-primary"
size="sm"
onClick={handleEdit}>
{t('synonyms.btn_add')}
</Button>
)}
</>
))}
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
); );
}; };

View File

@ -3,7 +3,8 @@ 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 { Tag, Pagination, PageTitle, QueryGroup } from '@/components'; import { usePageTags } from '@/hooks';
import { Tag, Pagination, QueryGroup } from '@/components';
import { formatCount } from '@/utils'; import { formatCount } from '@/utils';
import { useQueryTags, following } from '@/services'; import { useQueryTags, following } from '@/services';
@ -37,81 +38,81 @@ const Tags = () => {
mutate(); mutate();
}); });
}; };
usePageTags({
title: t('tags', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="py-3 my-3">
<PageTitle title={t('tags', { keyPrefix: 'page_title' })} /> <Row className="mb-4 d-flex justify-content-center">
<Container className="py-3 my-3"> <Col xxl={10} sm={12}>
<Row className="mb-4 d-flex justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={10} sm={12}> <div className="d-flex justify-content-between align-items-center flex-wrap">
<h3 className="mb-4">{t('title')}</h3> <Form>
<div className="d-flex justify-content-between align-items-center flex-wrap"> <Form.Group controlId="formBasicEmail">
<Form> <Form.Control
<Form.Group controlId="formBasicEmail"> value={searchTag}
<Form.Control placeholder={t('search_placeholder')}
value={searchTag} type="text"
placeholder={t('search_placeholder')} onChange={handleChange}
type="text" size="sm"
onChange={handleChange} />
size="sm" </Form.Group>
/> </Form>
</Form.Group> <QueryGroup
</Form> data={sortBtns}
<QueryGroup currentSort={sort || 'popular'}
data={sortBtns} sortKey="sort"
currentSort={sort || 'popular'} i18nKeyPrefix="tags.sort_buttons"
sortKey="sort" />
i18nKeyPrefix="tags.sort_buttons" </div>
/> </Col>
</div>
</Col>
<Col className="mt-4" xxl={10} sm={12}> <Col className="mt-4" xxl={10} sm={12}>
<Row> <Row>
{tags?.list?.map((tag) => ( {tags?.list?.map((tag) => (
<Col <Col
key={tag.slug_name} key={tag.slug_name}
xs={12} xs={12}
lg={3} lg={3}
md={4} md={4}
sm={6} sm={6}
className="mb-4"> className="mb-4">
<Card className="h-100"> <Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start"> <Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} /> <Tag className="mb-3" data={tag} />
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4"> <p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
{tag.original_text} {tag.original_text}
</p> </p>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Button <Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`} className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary" variant="outline-primary"
size="sm" size="sm"
onClick={() => handleFollow(tag)}> onClick={() => handleFollow(tag)}>
{tag.is_follower {tag.is_follower
? t('button_following') ? t('button_following')
: t('button_follow')} : t('button_follow')}
</Button> </Button>
<span className="text-secondary fs-14 text-nowrap"> <span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')} {formatCount(tag.question_count)} {t('tag_label')}
</span> </span>
</div> </div>
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
<div className="d-flex justify-content-center"> <div className="d-flex justify-content-center">
<Pagination <Pagination
currentPage={page} currentPage={page}
totalSize={tags?.count || 0} totalSize={tags?.count || 0}
pageSize={pageSize} pageSize={pageSize}
/> />
</div> </div>
</Col> </Col>
</Row> </Row>
</Container> </Container>
</>
); );
}; };

View File

@ -3,10 +3,11 @@ import { Container, Row, Col, Form, Table } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { getTimelineData } from '@/services'; import { getTimelineData } from '@/services';
import { PageTitle, Empty } from '@/components'; import { Empty } from '@/components';
import * as Type from '@/common/interface'; import * as Type from '@/common/interface';
import HistoryItem from './components/Item'; import HistoryItem from './components/Item';
@ -74,10 +75,11 @@ const Index: FC = () => {
const revisionList = const revisionList =
timelineData?.timeline?.filter((item) => item.revision_id > 0) || []; timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
usePageTags({
title: pageTitle,
});
return ( return (
<Container className="py-3"> <Container className="py-3">
<PageTitle title={pageTitle} />
<Row className="py-3 justify-content-center"> <Row className="py-3 justify-content-center">
<Col xxl={10}> <Col xxl={10}>
<h5 className="mb-4"> <h5 className="mb-4">

View File

@ -2,7 +2,7 @@ import React, { useState } 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 { PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
import SendEmail from './components/sendEmail'; import SendEmail from './components/sendEmail';
@ -15,32 +15,31 @@ const Index: React.FC = () => {
setStep(param); setStep(param);
setEmail(mail); setEmail(mail);
}; };
usePageTags({
title: t('account_recovery', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<PageTitle title={t('account_recovery', { keyPrefix: 'page_title' })} /> <h3 className="text-center mb-5">{t('page_title')}</h3>
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}> {step === 1 && (
<h3 className="text-center mb-5">{t('page_title')}</h3> <Col className="mx-auto" md={3}>
{step === 1 && ( <SendEmail visible={step === 1} callback={callback} />
<Col className="mx-auto" md={3}> </Col>
<SendEmail visible={step === 1} callback={callback} /> )}
</Col> {step === 2 && (
)} <Col className="mx-auto px-4" md={6}>
{step === 2 && ( <div className="text-center">
<Col className="mx-auto px-4" md={6}> <p>
<div className="text-center"> <Trans
<p> i18nKey="account_forgot.send_success"
<Trans values={{ mail: email }}
i18nKey="account_forgot.send_success" components={{ bold: <strong /> }}
values={{ mail: email }} />
components={{ bold: <strong /> }} </p>
/> </div>
</p> </Col>
</div> )}
</Col> </Container>
)}
</Container>
</>
); );
}; };

View File

@ -3,34 +3,34 @@ import { Container, Row, Col } from 'react-bootstrap';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'account_result' }); const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
const location = useLocation(); const location = useLocation();
usePageTags({
title: t('account_activation', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={t('account_activation', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col lg={6}>
<Row className="justify-content-center"> <h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
<Col lg={6}> {location.pathname?.includes('success') && (
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3> <>
{location.pathname?.includes('success') && ( <p className="text-center">{t('success')}</p>
<> <div className="text-center">
<p className="text-center">{t('success')}</p> <Link to="/">{t('link')}</Link>
<div className="text-center"> </div>
<Link to="/">{t('link')}</Link> </>
</div> )}
</>
)}
{location.pathname?.includes('failed') && ( {location.pathname?.includes('failed') && (
<p className="text-center">{t('invalid')}</p> <p className="text-center">{t('invalid')}</p>
)} )}
</Col> </Col>
</Row> </Row>
</Container> </Container>
</>
); );
}; };

View File

@ -2,9 +2,9 @@ import { FC, memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate } from 'react-router-dom'; import { useSearchParams, useNavigate } from 'react-router-dom';
import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { activateAccount } from '@/services'; import { activateAccount } from '@/services';
import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
@ -25,7 +25,10 @@ const Index: FC = () => {
navigate('/', { replace: true }); navigate('/', { replace: true });
} }
}, []); }, []);
return <PageTitle title={t('account_activation')} />; usePageTags({
title: t('account_activation'),
});
return null;
}; };
export default memo(Index); export default memo(Index);

View File

@ -2,23 +2,22 @@ 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 { PageTitle } from '@/components'; import { usePageTags } from '@/hooks';
import SendEmail from './components/sendEmail'; 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' });
usePageTags({
title: t('change_email', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<PageTitle title={t('change_email', { keyPrefix: 'page_title' })} /> <h3 className="text-center mb-5">{t('page_title')}</h3>
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}> <Col className="mx-auto" md={3}>
<h3 className="text-center mb-5">{t('page_title')}</h3> <SendEmail />
<Col className="mx-auto" md={3}> </Col>
<SendEmail /> </Container>
</Col>
</Container>
</>
); );
}; };

View File

@ -3,9 +3,9 @@ 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 { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { changeEmailVerify, getLoggedUserInfo } from '@/services'; import { changeEmailVerify, getLoggedUserInfo } from '@/services';
import { PageTitle } from '@/components';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'account_result' }); const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
@ -31,30 +31,29 @@ const Index: FC = () => {
}); });
} }
}, []); }, []);
usePageTags({
title: t('confirm_email', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={t('confirm_email', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col lg={6}>
<Row className="justify-content-center"> <h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
<Col lg={6}> {step === 'success' && (
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3> <>
{step === 'success' && ( <p className="text-center">{t('confirm_new_email')}</p>
<> <div className="text-center">
<p className="text-center">{t('confirm_new_email')}</p> <Link to="/">{t('link')}</Link>
<div className="text-center"> </div>
<Link to="/">{t('link')}</Link> </>
</div> )}
</>
)}
{step === 'invalid' && ( {step === 'invalid' && (
<p className="text-center">{t('confirm_new_email_invalid')}</p> <p className="text-center">{t('confirm_new_email_invalid')}</p>
)} )}
</Col> </Col>
</Row> </Row>
</Container> </Container>
</>
); );
}; };

View File

@ -3,12 +3,13 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import type { import type {
LoginReqParams, LoginReqParams,
ImgCodeRes, ImgCodeRes,
FormDataType, FormDataType,
} from '@/common/interface'; } from '@/common/interface';
import { PageTitle, Unactivate } from '@/components'; import { Unactivate } from '@/components';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { guard, floppyNavigation, handleFormError } from '@/utils'; import { guard, floppyNavigation, handleFormError } from '@/utils';
import { login, checkImgCode } from '@/services'; import { login, checkImgCode } from '@/services';
@ -167,11 +168,12 @@ const Index: React.FC = () => {
setStep(2); setStep(2);
} }
}, []); }, []);
usePageTags({
title: t('login', { keyPrefix: 'page_title' }),
});
return ( return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}> <Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3> <h3 className="text-center mb-5">{t('page_title')}</h3>
<PageTitle title={t('login', { keyPrefix: 'page_title' })} />
{step === 1 && ( {step === 1 && (
<Col className="mx-auto" md={3}> <Col className="mx-auto" md={3}>
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>

View File

@ -3,7 +3,7 @@ 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 { usePageTags } from '@/hooks';
import { import {
useQueryNotifications, useQueryNotifications,
clearUnreadNotification, clearUnreadNotification,
@ -66,67 +66,66 @@ const Notifications = () => {
const handleReadNotification = (id) => { const handleReadNotification = (id) => {
readNotification(id); readNotification(id);
}; };
usePageTags({
title: t('notifications', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={t('notifications', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="pt-4 mt-2 mb-5"> <Col xxl={7} lg={8} sm={12}>
<Row className="justify-content-center"> <h3 className="mb-4">{t('title')}</h3>
<Col xxl={7} lg={8} sm={12}> <div className="d-flex justify-content-between mb-3">
<h3 className="mb-4">{t('title')}</h3> <ButtonGroup size="sm">
<div className="d-flex justify-content-between mb-3">
<ButtonGroup size="sm">
<Button
as="a"
href="/users/notifications/inbox"
variant="outline-secondary"
active={type === 'inbox'}
onClick={(evt) => handleTypeChange(evt, 'inbox')}>
{t('inbox')}
</Button>
<Button
as="a"
href="/users/notifications/achievement"
variant="outline-secondary"
active={type === 'achievement'}
onClick={(evt) => handleTypeChange(evt, 'achievement')}>
{t('achievement')}
</Button>
</ButtonGroup>
<Button <Button
size="sm" as="a"
href="/users/notifications/inbox"
variant="outline-secondary" variant="outline-secondary"
onClick={handleUnreadNotification}> active={type === 'inbox'}
{t('all_read')} onClick={(evt) => handleTypeChange(evt, 'inbox')}>
{t('inbox')}
</Button>
<Button
as="a"
href="/users/notifications/achievement"
variant="outline-secondary"
active={type === 'achievement'}
onClick={(evt) => handleTypeChange(evt, 'achievement')}>
{t('achievement')}
</Button>
</ButtonGroup>
<Button
size="sm"
variant="outline-secondary"
onClick={handleUnreadNotification}>
{t('all_read')}
</Button>
</div>
{type === 'inbox' && (
<Inbox
data={notificationData}
handleReadNotification={handleReadNotification}
/>
)}
{type === 'achievement' && (
<Achievements
data={notificationData}
handleReadNotification={handleReadNotification}
/>
)}
{(data?.count || 0) > PAGE_SIZE * page && (
<div className="d-flex justify-content-center align-items-center py-3">
<Button
variant="link"
className="btn-no-border"
onClick={handleLoadMore}>
{t('show_more')}
</Button> </Button>
</div> </div>
{type === 'inbox' && ( )}
<Inbox </Col>
data={notificationData} <Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
handleReadNotification={handleReadNotification} </Row>
/> </Container>
)}
{type === 'achievement' && (
<Achievements
data={notificationData}
handleReadNotification={handleReadNotification}
/>
)}
{(data?.count || 0) > PAGE_SIZE * page && (
<div className="d-flex justify-content-center align-items-center py-3">
<Button
variant="link"
className="btn-no-border"
onClick={handleLoadMore}>
{t('show_more')}
</Button>
</div>
)}
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
</Row>
</Container>
</>
); );
}; };

View File

@ -3,10 +3,10 @@ import { Container, Col, Form, Button } 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 { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { replacementPassword } from '@/services'; import { replacementPassword } from '@/services';
import { PageTitle } from '@/components';
import { handleFormError } from '@/utils'; import { handleFormError } from '@/utils';
const Index: React.FC = () => { const Index: React.FC = () => {
@ -112,89 +112,88 @@ const Index: React.FC = () => {
} }
}); });
}; };
usePageTags({
title: t('account_recovery', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<PageTitle title={t('account_recovery', { keyPrefix: 'page_title' })} /> <h3 className="text-center mb-5">{t('page_title')}</h3>
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}> {step === 1 && (
<h3 className="text-center mb-5">{t('page_title')}</h3> <Col className="mx-auto" md={3}>
{step === 1 && ( <Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Col className="mx-auto" md={3}> <Form.Group controlId="email" className="mb-3">
<Form noValidate onSubmit={handleSubmit} autoComplete="off"> <Form.Label>{t('password.label')}</Form.Label>
<Form.Group controlId="email" className="mb-3"> <Form.Control
<Form.Label>{t('password.label')}</Form.Label> autoComplete="off"
<Form.Control required
autoComplete="off" type="password"
required maxLength={32}
type="password" isInvalid={formData.pass.isInvalid}
maxLength={32} onChange={(e) => {
isInvalid={formData.pass.isInvalid} handleChange({
onChange={(e) => { pass: {
handleChange({ value: e.target.value,
pass: { isInvalid: false,
value: e.target.value, errorMsg: '',
isInvalid: false, },
errorMsg: '', });
}, }}
}); />
}} <Form.Control.Feedback type="invalid">
/> {formData.pass.errorMsg}
<Form.Control.Feedback type="invalid"> </Form.Control.Feedback>
{formData.pass.errorMsg} </Form.Group>
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="password" className="mb-3"> <Form.Group controlId="password" className="mb-3">
<Form.Label>{t('password_confirm.label')}</Form.Label> <Form.Label>{t('password_confirm.label')}</Form.Label>
<Form.Control <Form.Control
autoComplete="off" autoComplete="off"
required required
type="password" type="password"
maxLength={32} maxLength={32}
isInvalid={formData.passSecond.isInvalid} isInvalid={formData.passSecond.isInvalid}
onChange={(e) => { onChange={(e) => {
handleChange({ handleChange({
passSecond: { passSecond: {
value: e.target.value, value: e.target.value,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
}); });
}} }}
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formData.passSecond.errorMsg} {formData.passSecond.errorMsg}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<div className="d-grid mb-3"> <div className="d-grid mb-3">
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
{t('btn_name')} {t('btn_name')}
</Button> </Button>
</div>
</Form>
</Col>
)}
{step === 2 && (
<Col className="mx-auto px-4" md={6}>
<div className="text-center">
<p>{t('reset_success')}</p>
<Link to="/users/login">{t('to_login')}</Link>
</div> </div>
</Col> </Form>
)} </Col>
)}
{step === 3 && ( {step === 2 && (
<Col className="mx-auto px-4" md={6}> <Col className="mx-auto px-4" md={6}>
<div className="text-center"> <div className="text-center">
<p>{t('link_invalid')}</p> <p>{t('reset_success')}</p>
<Link to="/users/login">{t('to_login')}</Link> <Link to="/users/login">{t('to_login')}</Link>
</div> </div>
</Col> </Col>
)} )}
</Container>
</> {step === 3 && (
<Col className="mx-auto px-4" md={6}>
<div className="text-center">
<p>{t('link_invalid')}</p>
<Link to="/users/login">{t('to_login')}</Link>
</div>
</Col>
)}
</Container>
); );
}; };

View File

@ -3,7 +3,8 @@ 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 '@/components'; import { usePageTags } from '@/hooks';
import { Pagination, FormatTime, Empty } from '@/components';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { import {
usePersonalInfoByName, usePersonalInfoByName,
@ -50,9 +51,11 @@ const Personal: FC = () => {
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`; pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`;
} }
const { count = 0, list = [] } = listData?.[tabName] || {}; const { count = 0, list = [] } = listData?.[tabName] || {};
usePageTags({
title: pageTitle,
});
return ( return (
<Container className="pt-4 mt-2 mb-5"> <Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} />
<Row className="justify-content-center"> <Row className="justify-content-center">
{userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && ( {userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && (
<Alert data={userInfo?.info.status_msg} /> <Alert data={userInfo?.info.status_msg} />

View File

@ -2,7 +2,8 @@ import React, { useState } 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 '@/components'; import { usePageTags } from '@/hooks';
import { Unactivate } from '@/components';
import SignUpForm from './components/SignUpForm'; import SignUpForm from './components/SignUpForm';
@ -13,11 +14,12 @@ const Index: React.FC = () => {
const onStep = () => { const onStep = () => {
setShowForm((bol) => !bol); setShowForm((bol) => !bol);
}; };
usePageTags({
title: t('sign_up', { keyPrefix: 'page_title' }),
});
return ( return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}> <Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3> <h3 className="text-center mb-5">{t('page_title')}</h3>
<PageTitle title={t('sign_up', { keyPrefix: 'page_title' })} />
{showForm ? ( {showForm ? (
<SignUpForm callback={onStep} /> <SignUpForm callback={onStep} />
) : ( ) : (

View File

@ -1,11 +1,16 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import ModifyEmail from './components/ModifyEmail'; import ModifyEmail from './components/ModifyEmail';
import ModifyPassword from './components/ModifyPass'; import ModifyPassword from './components/ModifyPass';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'settings.account',
});
return ( return (
<> <>
<h3 className="mb-4">{t('heading')}</h3>
<ModifyEmail /> <ModifyEmail />
<ModifyPassword /> <ModifyPassword />
</> </>

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState, FormEvent } from 'react'; import React, { useEffect, useState, FormEvent } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { LangsType, FormDataType } from '@/common/interface'; import type { LangsType, FormDataType } from '@/common/interface';
@ -7,6 +6,7 @@ import { useToast } from '@/hooks';
import { updateUserInterface } from '@/services'; import { updateUserInterface } from '@/services';
import { localize } from '@/utils'; import { localize } from '@/utils';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
@ -15,19 +15,34 @@ const Index = () => {
const loggedUserInfo = loggedUserInfoStore.getState().user; const loggedUserInfo = loggedUserInfoStore.getState().user;
const toast = useToast(); const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>(); const [langs, setLangs] = useState<LangsType[]>();
const [formData, setFormData] = useState<FormDataType>({ const schema: JSONSchema = {
lang: { title: t('heading'),
value: loggedUserInfo.language, properties: {
isInvalid: false, lang: {
errorMsg: '', type: 'string',
title: t('lang.label'),
description: t('lang.text'),
enum: langs?.map((_) => _.value),
enumNames: langs?.map((_) => _.label),
default: loggedUserInfo.language,
},
}, },
}); };
const uiSchema: UISchema = {
lang: {
'ui:widget': 'select',
},
};
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
const getLangs = async () => { const getLangs = async () => {
const res: LangsType[] = await localize.loadLanguageOptions(); const res: LangsType[] = await localize.loadLanguageOptions();
setLangs(res); setLangs(res);
}; };
const handleOnChange = (d) => {
setFormData(d);
};
const handleSubmit = (event: FormEvent) => { const handleSubmit = (event: FormEvent) => {
event.preventDefault(); event.preventDefault();
const lang = formData.lang.value; const lang = formData.lang.value;
@ -48,39 +63,16 @@ const Index = () => {
getLangs(); getLangs();
}, []); }, []);
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="emailSend" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('lang.label')}</Form.Label> <SchemaForm
<Form.Select schema={schema}
value={formData.lang.value} uiSchema={uiSchema}
isInvalid={formData.lang.isInvalid} formData={formData}
onChange={(e) => { onChange={handleOnChange}
setFormData({ onSubmit={handleSubmit}
lang: { />
value: e.target.value, </>
isInvalid: false,
errorMsg: '',
},
});
}}>
{langs?.map((item) => {
return (
<option value={item.value} key={item.label}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('lang.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.lang.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
); );
}; };

View File

@ -1,23 +1,33 @@
import React, { useState, FormEvent, useEffect } from 'react'; import React, { useState, FormEvent, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
import { setNotice, getLoggedUserInfo } from '@/services'; import { setNotice, getLoggedUserInfo } from '@/services';
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
const Index = () => { const Index = () => {
const toast = useToast(); const toast = useToast();
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'settings.notification', keyPrefix: 'settings.notification',
}); });
const [formData, setFormData] = useState<FormDataType>({ const schema: JSONSchema = {
notice_switch: { title: t('heading'),
value: false, properties: {
isInvalid: false, notice_switch: {
errorMsg: '', type: 'boolean',
title: t('email.label'),
label: t('email.radio'),
default: false,
},
}, },
}); };
const uiSchema: UISchema = {
notice_switch: {
'ui:widget': 'switch',
},
};
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
const getProfile = () => { const getProfile = () => {
getLoggedUserInfo().then((res) => { getLoggedUserInfo().then((res) => {
@ -47,34 +57,20 @@ const Index = () => {
useEffect(() => { useEffect(() => {
getProfile(); getProfile();
}, []); }, []);
const handleChange = (ud) => {
setFormData(ud);
};
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="emailSend" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('email.label')}</Form.Label> <SchemaForm
<Form.Check schema={schema}
required uiSchema={uiSchema}
type="checkbox" formData={formData}
label={t('email.radio')} onChange={handleChange}
checked={formData.notice_switch.value} onSubmit={handleSubmit}
onChange={(e) => { />
setFormData({ </>
notice_switch: {
value: e.target.checked,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.notice_switch.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
); );
}; };

View File

@ -1,12 +1,12 @@
import React, { FormEvent, useState, useEffect } from 'react'; import React, { FormEvent, useState, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { marked } from 'marked'; import { marked } from 'marked';
import MD5 from 'md5'; import MD5 from 'md5';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { UploadImg, Avatar } from '@/components'; import { UploadImg, Avatar, Icon } from '@/components';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
import { modifyUserInfo, getLoggedUserInfo } from '@/services'; import { modifyUserInfo, getLoggedUserInfo } from '@/services';
@ -19,7 +19,7 @@ const Index: React.FC = () => {
const toast = useToast(); const toast = useToast();
const { user, update } = loggedUserInfoStore(); const { user, update } = loggedUserInfoStore();
const [mailHash, setMailHash] = useState(''); const [mailHash, setMailHash] = useState('');
const [count, setCount] = useState(0); const [count] = useState(0);
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
display_name: { display_name: {
@ -60,6 +60,40 @@ const Index: React.FC = () => {
const handleChange = (params: FormDataType) => { const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params }); setFormData({ ...formData, ...params });
}; };
const handleAvatarChange = (evt) => {
const { value: v } = evt.currentTarget;
if (v === 'gravatar') {
handleChange({
avatar: {
...formData.avatar,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
isInvalid: false,
errorMsg: '',
},
});
}
if (v === 'custom') {
handleChange({
avatar: {
...formData.avatar,
type: 'custom',
isInvalid: false,
errorMsg: '',
},
});
}
if (v === 'default') {
handleChange({
avatar: {
...formData.avatar,
type: 'default',
isInvalid: false,
errorMsg: '',
},
});
}
};
const avatarUpload = (path: string) => { const avatarUpload = (path: string) => {
setFormData({ setFormData({
@ -73,6 +107,17 @@ const Index: React.FC = () => {
}, },
}); });
}; };
const removeCustomAvatar = () => {
setFormData({
...formData,
avatar: {
...formData.avatar,
custom: '',
isInvalid: false,
errorMsg: '',
},
});
};
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
let bol = true; let bol = true;
@ -201,265 +246,220 @@ const Index: React.FC = () => {
}); });
}; };
const refreshGravatar = () => { // const refreshGravatar = () => {
setCount((pre) => pre + 1); // setCount((pre) => pre + 1);
}; // };
useEffect(() => { useEffect(() => {
getProfile(); getProfile();
}, []); }, []);
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="displayName" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('display_name.label')}</Form.Label> <Form noValidate onSubmit={handleSubmit}>
<Form.Control <Form.Group controlId="displayName" className="mb-3">
required <Form.Label>{t('display_name.label')}</Form.Label>
type="text" <Form.Control
value={formData.display_name.value} required
isInvalid={formData.display_name.isInvalid} type="text"
onChange={(e) => value={formData.display_name.value}
handleChange({ isInvalid={formData.display_name.isInvalid}
display_name: { onChange={(e) =>
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.display_name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="userName" className="mb-3">
<Form.Label>{t('username.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.username.value}
isInvalid={formData.username.isInvalid}
onChange={(e) =>
handleChange({
username: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Text as="div">{t('username.caption')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.username.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-2">
<Form.Check
inline
type="radio"
id="gravatar"
label={t('avatar.gravatar')}
className="mb-0"
checked={formData.avatar.type === 'gravatar'}
onChange={() =>
handleChange({ handleChange({
avatar: { display_name: {
...formData.avatar, value: e.target.value,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
}) })
} }
/> />
<Form.Check <Form.Control.Feedback type="invalid">
inline {formData.display_name.errorMsg}
type="radio" </Form.Control.Feedback>
label={t('avatar.custom')} </Form.Group>
id="custom"
className="mb-0"
checked={formData.avatar.type === 'custom'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'custom',
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Check
inline
type="radio"
id="default"
label={t('avatar.default')}
className="mb-0"
checked={formData.avatar.type === 'default'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'default',
isInvalid: false,
errorMsg: '',
},
})
}
/>
</div>
<div className="d-flex align-items-center">
{formData.avatar.type === 'gravatar' && (
<>
<Avatar
size="128px"
avatar={formData.avatar.gravatar}
searchStr={`s=256&d=identicon${
count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/>
<div>
<Button
variant="outline-secondary"
className="mb-2"
onClick={refreshGravatar}>
{t('avatar.btn_refresh')}
</Button>
<div>
<Form.Text className="text-muted mt-0">
<Trans i18nKey="settings.profile.gravatar_text">
You can change your image on{' '}
<a
href="https://gravatar.com"
target="_blank"
rel="noreferrer">
gravatar.com
</a>
</Trans>
</Form.Text>
</div>
</div>
</>
)}
{formData.avatar.type === 'custom' && ( <Form.Group controlId="userName" className="mb-3">
<> <Form.Label>{t('username.label')}</Form.Label>
<Avatar <Form.Control
size="128px" required
searchStr="s=256" type="text"
avatar={formData.avatar.custom} value={formData.username.value}
className="me-3 rounded" isInvalid={formData.username.isInvalid}
/> onChange={(e) =>
<div> handleChange({
<UploadImg username: {
type="avatar" value: e.target.value,
uploadCallback={avatarUpload} isInvalid: false,
className="mb-2" errorMsg: '',
},
})
}
/>
<Form.Text as="div">{t('username.caption')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.username.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-3">
<Form.Select
name="avatar.type"
value={formData.avatar.type}
onChange={handleAvatarChange}>
<option value="gravatar" key="gravatar">
{t('avatar.gravatar')}
</option>
<option value="default" key="default">
{t('avatar.default')}
</option>
<option value="custom" key="custom">
{t('avatar.custom')}
</option>
</Form.Select>
</div>
<div className="d-flex">
{formData.avatar.type === 'gravatar' && (
<Stack>
<Avatar
size="160px"
avatar={formData.avatar.gravatar}
searchStr={`s=256&d=identicon${
count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/> />
<div> <Form.Text className="text-muted mt-1">
<Form.Text className="text-muted mt-0"> <Trans i18nKey="settings.profile.avatar.gravatar_text">
<Trans i18nKey="settings.profile.avatar.text"> You can change image on
You can upload your image. <a
</Trans> href="https://gravatar.com"
</Form.Text> target="_blank"
</div> rel="noreferrer">
</div> gravatar.com
</> </a>
)} </Trans>
{formData.avatar.type === 'default' && ( </Form.Text>
<Avatar size="128px" avatar="" className="me-3 rounded" /> </Stack>
)} )}
</div>
<Form.Control
isInvalid={formData.avatar.isInvalid}
className="d-none"
/>
<Form.Control.Feedback type="invalid">
{formData.avatar.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="bio" className="mb-3"> {formData.avatar.type === 'custom' && (
<Form.Label>{t('bio.label')}</Form.Label> <Stack>
<Form.Control <Stack direction="horizontal" className="align-items-start">
className="font-monospace" <Avatar
required size="160px"
as="textarea" searchStr="s=256"
rows={5} avatar={formData.avatar.custom}
value={formData.bio.value} className="me-2 bg-gray-300 "
isInvalid={formData.bio.isInvalid} />
onChange={(e) => <ButtonGroup vertical className="fit-content">
handleChange({ <UploadImg type="avatar" uploadCallback={avatarUpload}>
bio: { <Icon name="cloud-upload" />
value: e.target.value, </UploadImg>
isInvalid: false, <Button
errorMsg: '', variant="outline-secondary"
}, onClick={removeCustomAvatar}>
}) <Icon name="trash" />
} </Button>
/> </ButtonGroup>
<Form.Control.Feedback type="invalid"> </Stack>
{formData.bio.errorMsg} <Form.Text className="text-muted mt-1">
</Form.Control.Feedback> <Trans i18nKey="settings.profile.avatar.text">
</Form.Group> You can upload your image.
</Trans>
</Form.Text>
</Stack>
)}
{formData.avatar.type === 'default' && (
<Avatar size="160px" avatar="" />
)}
</div>
<Form.Control
isInvalid={formData.avatar.isInvalid}
className="d-none"
/>
<Form.Control.Feedback type="invalid">
{formData.avatar.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="website" className="mb-3"> <Form.Group controlId="bio" className="mb-3">
<Form.Label>{t('website.label')}</Form.Label> <Form.Label>{t('bio.label')}</Form.Label>
<Form.Control <Form.Control
required className="font-monospace"
type="text" required
placeholder={t('website.placeholder')} as="textarea"
value={formData.website.value} rows={5}
isInvalid={formData.website.isInvalid} value={formData.bio.value}
onChange={(e) => isInvalid={formData.bio.isInvalid}
handleChange({ onChange={(e) =>
website: { handleChange({
value: e.target.value, bio: {
isInvalid: false, value: e.target.value,
errorMsg: '', isInvalid: false,
}, errorMsg: '',
}) },
} })
/> }
<Form.Control.Feedback type="invalid"> />
{formData.website.errorMsg} <Form.Control.Feedback type="invalid">
</Form.Control.Feedback> {formData.bio.errorMsg}
</Form.Group> </Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="email" className="mb-3"> <Form.Group controlId="website" className="mb-3">
<Form.Label>{t('location.label')}</Form.Label> <Form.Label>{t('website.label')}</Form.Label>
<Form.Control <Form.Control
required required
type="text" type="text"
placeholder={t('location.placeholder')} placeholder={t('website.placeholder')}
value={formData.location.value} value={formData.website.value}
isInvalid={formData.location.isInvalid} isInvalid={formData.website.isInvalid}
onChange={(e) => onChange={(e) =>
handleChange({ handleChange({
location: { website: {
value: e.target.value, value: e.target.value,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
}) })
} }
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formData.location.errorMsg} {formData.website.errorMsg}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Button variant="primary" type="submit"> <Form.Group controlId="email" className="mb-3">
{t('btn_name')} <Form.Label>{t('location.label')}</Form.Label>
</Button> <Form.Control
</Form> required
type="text"
placeholder={t('location.placeholder')}
value={formData.location.value}
isInvalid={formData.location.isInvalid}
onChange={(e) =>
handleChange({
location: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.location.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</Form>
</>
); );
}; };

View File

@ -3,9 +3,9 @@ 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 { usePageTags } from '@/hooks';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { getLoggedUserInfo } from '@/services'; import { getLoggedUserInfo } from '@/services';
import { PageTitle } from '@/components';
import Nav from './components/Nav'; import Nav from './components/Nav';
@ -55,29 +55,27 @@ const Index: React.FC = () => {
useEffect(() => { useEffect(() => {
getProfile(); getProfile();
}, []); }, []);
usePageTags({
title: t('settings', { keyPrefix: 'page_title' }),
});
return ( return (
<> <Container className="mt-4 mb-5 pb-5">
<PageTitle title={t('settings', { keyPrefix: 'page_title' })} /> <Row className="justify-content-center">
<Container className="mt-4 mb-5 pb-5"> <Col xxl={10} md={12}>
<Row className="justify-content-center"> <h3 className="mb-4">{t('page_title', { keyPrefix: 'settings' })}</h3>
<Col xxl={10} md={12}> </Col>
<h3 className="mb-4"> </Row>
{t('page_title', { keyPrefix: 'settings' })}
</h3>
</Col>
</Row>
<Row> <Row>
<Col xxl={1} /> <Col xxl={1} />
<Col md={3} lg={2} className="mb-3"> <Col md={3} lg={2} className="mb-3">
<Nav /> <Nav />
</Col> </Col>
<Col md={9} lg={6}> <Col md={9} lg={6}>
<Outlet /> <Outlet />
</Col> </Col>
</Row> </Row>
</Container> </Container>
</>
); );
}; };

View File

@ -1,29 +1,27 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { PageTitle } from '@/components';
const Suspended = () => { const Suspended = () => {
const { t } = useTranslation('translation', { keyPrefix: 'suspended' }); const { t } = useTranslation('translation', { keyPrefix: 'suspended' });
const userInfo = loggedUserInfoStore((state) => state.user); const userInfo = loggedUserInfoStore((state) => state.user);
usePageTags({
title: t('account_suspended', { keyPrefix: 'page_title' }),
});
if (userInfo.status !== 'forbidden') { if (userInfo.status !== 'forbidden') {
window.location.replace('/'); window.location.replace('/');
return null; return null;
} }
return ( return (
<> <div className="d-flex flex-column align-items-center mt-5 pt-3">
<PageTitle title={t('account_suspended', { keyPrefix: 'page_title' })} /> <h3 className="mb-5">{t('title')}</h3>
<div className="d-flex flex-column align-items-center mt-5 pt-3"> <p className="text-center">
<h3 className="mb-5">{t('title')}</h3> {t('forever')}
<p className="text-center"> <br />
{t('forever')} {t('end')}
<br /> </p>
{t('end')} </div>
</p>
</div>
</>
); );
}; };

View File

@ -80,7 +80,7 @@ export const getBrandSetting = () => {
return request.get('/answer/admin/api/siteinfo/branding'); return request.get('/answer/admin/api/siteinfo/branding');
}; };
export const brandSetting = (params: Type.AdmingSettingBranding) => { export const brandSetting = (params: Type.AdminSettingBranding) => {
return request.put('/answer/admin/api/siteinfo/branding', params); return request.put('/answer/admin/api/siteinfo/branding', params);
}; };

View File

@ -42,10 +42,10 @@ export const useFollowingTags = () => {
export const useTagInfo = ({ id = '', name = '' }) => { export const useTagInfo = ({ id = '', name = '' }) => {
let apiUrl; let apiUrl;
if (id) { if (id) {
apiUrl = `/answer/api/v1/tag/?id=${id}`; apiUrl = `/answer/api/v1/tag?id=${id}`;
} else if (name) { } else if (name) {
name = encodeURIComponent(name); name = encodeURIComponent(name);
apiUrl = `/answer/api/v1/tag/?name=${name}`; apiUrl = `/answer/api/v1/tag?name=${name}`;
} }
const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get); const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get);
return { return {

View File

@ -1,11 +1,11 @@
import create from 'zustand'; import create from 'zustand';
import { AdmingSettingBranding } from '@/common/interface'; import { AdminSettingBranding } from '@/common/interface';
import { DEFAULT_LANG } from '@/common/constants'; import { DEFAULT_LANG } from '@/common/constants';
interface InterfaceType { interface InterfaceType {
branding: AdmingSettingBranding; branding: AdminSettingBranding;
update: (params: AdmingSettingBranding) => void; update: (params: AdminSettingBranding) => void;
} }
const interfaceSetting = create<InterfaceType>((set) => ({ const interfaceSetting = create<InterfaceType>((set) => ({

View File

@ -1,27 +0,0 @@
import create from 'zustand';
import { HeadInfo } from '@/common/interface';
interface HeadInfoType {
headInfo: HeadInfo;
update: (params: HeadInfo) => void;
}
const headInfo = create<HeadInfoType>((set) => ({
headInfo: {
title: '',
description: '',
keywords: '',
},
update: (params) =>
set((state) => {
return {
headInfo: {
...state.headInfo,
...params,
},
};
}),
}));
export default headInfo;

View File

@ -4,7 +4,7 @@ import globalStore from './global';
import siteInfoStore from './siteInfo'; import siteInfoStore from './siteInfo';
import interfaceStore from './interface'; import interfaceStore from './interface';
import brandingStore from './branding'; import brandingStore from './branding';
import headInfoStore from './headInfo'; import pageTagStore from './pageTags';
export { export {
toastStore, toastStore,
@ -13,5 +13,5 @@ export {
siteInfoStore, siteInfoStore,
interfaceStore, interfaceStore,
brandingStore, brandingStore,
headInfoStore, pageTagStore,
}; };

45
ui/src/stores/pageTags.ts Normal file
View File

@ -0,0 +1,45 @@
import create from 'zustand';
import { HelmetBase, HelmetUpdate } from '@/common/interface';
import siteInfoStore from './siteInfo';
interface HelmetStore {
items: HelmetBase;
update: (params: HelmetUpdate) => void;
}
const makePageTitle = (title = '', subtitle = '') => {
const { siteInfo } = siteInfoStore.getState();
if (!subtitle) {
subtitle = `${siteInfo.name}`;
}
let pageTitle = subtitle;
if (title && title !== subtitle) {
pageTitle = `${title}${subtitle ? ` - ${subtitle}` : ''}`;
}
return pageTitle;
};
const pageTags = create<HelmetStore>((set) => ({
items: {
pageTitle: '',
description: '',
keywords: '',
},
update: (params) => {
const o: HelmetBase = {};
if (params.title || params.subtitle) {
o.pageTitle = makePageTitle(params.title, params.subtitle);
}
o.description =
params.description || siteInfoStore.getState().siteInfo.description;
o.keywords = params.keywords || '';
set({
items: o,
});
},
}));
export default pageTags;

View File

@ -179,7 +179,6 @@ function diffText(newText: string, oldText: string): string {
?.replace(/<input/gi, '&lt;input'); ?.replace(/<input/gi, '&lt;input');
} }
const diff = Diff.diffChars(oldText, newText); const diff = Diff.diffChars(oldText, newText);
// console.log(diff);
const result = diff.map((part) => { const result = diff.map((part) => {
if (part.added) { if (part.added) {
if (part.value.replace(/\n/g, '').length <= 0) { if (part.value.replace(/\n/g, '').length <= 0) {