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
interface: Interface
profile:
btn_name: Update profile
heading: Profile
btn_name: Save
display_name:
label: Display Name
msg: Display name cannot be empty.
@ -658,7 +659,7 @@ ui:
custom: Custom
btn_refresh: Refresh
custom_text: You can upload your image.
default: Default
default: System
msg: Please upload an avatar
bio:
label: About Me (optional)
@ -670,10 +671,12 @@ ui:
label: Location (optional)
placeholder: "City, Country"
notification:
heading: Notifications
email:
label: Email Notifications
radio: "Answers to your questions, comments, and more"
account:
heading: Account
change_email_btn: Change email
change_pass_btn: Change password
change_email_info: >-
@ -694,6 +697,7 @@ ui:
pass_confirm:
label: Confirm New Password
interface:
heading: Interface
lang:
label: Interface Language
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.
msg: Test email recipients is invalid
smtp_authentication:
label: SMTP Authentication
label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty.
"yes": "Yes"
"no": "No"

View File

@ -4,22 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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" />
<!--
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>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@ -51,15 +36,5 @@
<div class="spinner"></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>
</html>

View File

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

View File

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

View File

@ -9,7 +9,8 @@ import { DEFAULT_SITE_NAME } from '@/common/constants';
const Index = () => {
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}`;
return (
<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">
{listData?.list?.map((li) => {
return (
<ListGroup.Item
key={li.id}
className="border-bottom pt-3 pb-2 px-0">
<ListGroup.Item key={li.id} className="border-bottom py-3 px-0">
<h5 className="text-wrap text-break">
<NavLink
to={pathFactory.questionLanding(li.id, li.title)}
@ -131,7 +129,7 @@ const QuestionList: FC<Props> = ({ source }) => {
{li.status === 2 ? ` [${t('closed')}]` : ''}
</NavLink>
</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} />
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
<span>
@ -157,7 +155,7 @@ const QuestionList: FC<Props> = ({ source }) => {
</span>
</div>
</div>
<div className="question-tags mx-n1 mt-2">
<div className="question-tags m-n1">
{Array.isArray(li.tags)
? li.tags.map((tag) => {
return (

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import usePageUsers from './usePageUsers';
import useChangeModal from './useChangeModal';
import useEditStatusModal from './useEditStatusModal';
import useChangeUserRoleModal from './useChangeUserRoleModal';
import useHeadInfo from './useHeadInfo';
import useUserModal from './useUserModal';
import useChangePasswordModal from './useChangePasswordModal';
import usePageTags from './usePageTags';
export {
useTagModal,
@ -17,7 +17,7 @@ export {
useChangeModal,
useEditStatusModal,
useChangeUserRoleModal,
useHeadInfo,
useUserModal,
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,
};
brandSetting(params)
.then((res) => {
console.log(res);
.then(() => {
update(params);
Toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),

View File

@ -46,7 +46,8 @@ const Smtp: FC = () => {
},
smtp_authentication: {
type: 'boolean',
title: t('smtp_authentication.label'),
title: t('smtp_authentication.title'),
label: t('smtp_authentication.label'),
enum: [true, false],
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('role')}</th>
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
<th style={{ width: '12%' }}>{t('role')}</th>
)}
{curFilter !== 'deleted' ? (
<th style={{ width: '8%' }} className="text-end">
{t('action')}
@ -189,11 +191,13 @@ const Users: FC = () => {
{t(user.status)}
</span>
</td>
<td>
<span className="badge text-bg-light">
{t(user.role_name)}
</span>
</td>
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
<td>
<span className="badge text-bg-light">
{t(user.role_name)}
</span>
</td>
)}
{curFilter !== 'deleted' ? (
<td className="text-end">
<Dropdown>

View File

@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
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 './index.scss';
@ -20,10 +21,11 @@ const formPaths = [
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
const { pathname } = useLocation();
usePageTags({
title: t('admin'),
});
return (
<>
<PageTitle title={t('admin')} />
<div className="bg-light py-2">
<Container className="py-1">
<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 { useTranslation, Trans } from 'react-i18next';
import { usePageTags } from '@/hooks';
import type { FormDataType } from '@/common/interface';
import { PageTitle } from '@/components';
import {
dbCheck,
installInit,
@ -103,7 +103,6 @@ const Index: FC = () => {
});
const handleChange = (params: FormDataType) => {
// console.log(params);
setErrorData({
msg: '',
});
@ -240,13 +239,15 @@ const Index: FC = () => {
configYmlCheck();
}, []);
usePageTags({
title: t('install', { keyPrefix: 'page_title' })
});
if (loading) {
return <div />;
}
return (
<div className="bg-f5 py-5 flex-grow-1">
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
<Container className='py-3'>
<Row className="justify-content-center">
<Col lg={6}>

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import classNames from 'classnames';
import { Editor, EditorRef, TagSelector, PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import { Editor, EditorRef, TagSelector } from '@/components';
import type * as Type from '@/common/interface';
import {
saveQuestion,
@ -140,22 +141,22 @@ const Ask = () => {
});
const checkValidated = (): boolean => {
let bol = true;
const bol = true;
const { title, content, tags, answer } = formData;
if (!title.value) {
bol = false;
formData.title = {
value: '',
isInvalid: true,
errorMsg: t('form.fields.title.msg.empty'),
};
// bol = false;
// formData.title = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.title.msg.empty'),
// };
} else if (Array.from(title.value).length > 150) {
bol = false;
formData.title = {
value: title.value,
isInvalid: true,
errorMsg: t('form.fields.title.msg.range'),
};
// bol = false;
// formData.title = {
// value: title.value,
// isInvalid: true,
// errorMsg: t('form.fields.title.msg.range'),
// };
} else {
formData.title = {
value: title.value,
@ -165,12 +166,12 @@ const Ask = () => {
}
if (!content.value) {
bol = false;
formData.content = {
value: '',
isInvalid: true,
errorMsg: t('form.fields.body.msg.empty'),
};
// bol = false;
// formData.content = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.body.msg.empty'),
// };
} else {
formData.content = {
value: content.value,
@ -180,12 +181,12 @@ const Ask = () => {
}
if (tags.value.length === 0) {
bol = false;
formData.tags = {
value: [],
isInvalid: true,
errorMsg: t('form.fields.tags.msg.empty'),
};
// bol = false;
// formData.tags = {
// value: [],
// isInvalid: true,
// errorMsg: t('form.fields.tags.msg.empty'),
// };
} else {
formData.tags = {
value: tags.value,
@ -195,12 +196,12 @@ const Ask = () => {
}
if (checked) {
if (!answer.value) {
bol = false;
formData.answer = {
value: '',
isInvalid: true,
errorMsg: t('form.fields.answer.msg.empty'),
};
// bol = false;
// formData.answer = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.answer.msg.empty'),
// };
} else {
formData.answer = {
value: answer.value,
@ -292,189 +293,185 @@ const Ask = () => {
if (isEdit) {
pageTitle = t('edit_question', { keyPrefix: 'page_title' });
}
usePageTags({
title: pageTitle,
});
return (
<>
<PageTitle title={pageTitle} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}>
{isEdit && (
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(
({ reason, create_at, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(
t('long_date_with_time', { keyPrefix: 'dates' }),
);
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</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} />}
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}>
{isEdit && (
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ reason, create_at, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</option>
);
})}
</Form.Select>
</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 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' }),
}}
<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
/>
</Card>
</Col>
</Row>
</Container>
</>
<Form.Control.Feedback type="invalid">
{formData.title.errorMsg}
</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 Pattern from '@/common/pattern';
import { Pagination, PageTitle } from '@/components';
import { Pagination } from '@/components';
import { loggedUserInfoStore, toastStore } from '@/stores';
import { scrollTop } from '@/utils';
import { usePageUsers } from '@/hooks';
import { usePageTags, usePageUsers } from '@/hooks';
import type {
ListResult,
QuestionDetailRes,
@ -143,68 +143,69 @@ const Index = () => {
requestAnswers();
}
}, [page, order]);
usePageTags({
title: question?.title,
description: question?.description,
keywords: question?.tags.map((_) => _.slug_name).join(','),
});
return (
<>
<PageTitle title={question?.title} />
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
{question?.operation?.operation_type && (
<Alert data={question.operation} />
)}
<Question
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
{answers.count > 0 && (
<>
<AnswerHead count={answers.count} order={order} />
{answers?.list?.map((item) => {
return (
<Answer
aid={aid}
key={item?.id}
data={item}
questionTitle={question?.title || ''}
isAuthor={isAuthor}
callback={initPage}
isLogged={isLogged}
/>
);
})}
</>
)}
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
{question?.operation?.operation_type && (
<Alert data={question.operation} />
)}
<Question
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
{answers.count > 0 && (
<>
<AnswerHead count={answers.count} order={order} />
{answers?.list?.map((item) => {
return (
<Answer
aid={aid}
key={item?.id}
data={item}
questionTitle={question?.title || ''}
isAuthor={isAuthor}
callback={initPage}
isLogged={isLogged}
/>
);
})}
</>
)}
{Math.ceil(answers.count / 15) > 1 && (
<div className="d-flex justify-content-center answer-item pt-4">
<Pagination
currentPage={Number(page || 1)}
pageSize={15}
totalSize={answers?.count || 0}
/>
</div>
)}
{!question?.operation?.operation_type && (
<WriteAnswer
visible={answers.count === 0}
data={{
qid,
answered: question?.answered,
}}
callback={writeAnswerCallback}
{Math.ceil(answers.count / 15) > 1 && (
<div className="d-flex justify-content-center answer-item pt-4">
<Pagination
currentPage={Number(page || 1)}
pageSize={15}
totalSize={answers?.count || 0}
/>
)}
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<RelatedQuestions id={question?.id || ''} />
</Col>
</Row>
</Container>
</>
</div>
)}
{!question?.operation?.operation_type && (
<WriteAnswer
visible={answers.count === 0}
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 classNames from 'classnames';
import { usePageTags } from '@/hooks';
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 {
useQueryAnswerInfo,
@ -33,7 +34,7 @@ const initFormData = {
errorMsg: '',
},
};
const Ask = () => {
const Index = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData);
const { aid = '', qid = '' } = useParams();
const [focusType, setForceType] = useState('');
@ -133,128 +134,127 @@ const Ask = () => {
const backPage = () => {
navigate(-1);
};
usePageTags({
title: t('edit_answer', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('edit_answer', { keyPrefix: 'page_title' })} />
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<a
href={pathFactory.questionLanding(qid, data?.question.title)}
target="_blank"
rel="noreferrer">
<h5 className="mb-3">{data?.question.title}</h5>
</a>
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<a
href={pathFactory.questionLanding(qid, data?.question.title)}
target="_blank"
rel="noreferrer">
<h5 className="mb-3">{data?.question.title}</h5>
</a>
<div className="question-content-wrap">
<div
ref={questionContentRef}
className="content position-absolute top-0 w-100"
dangerouslySetInnerHTML={{ __html: data?.question.html }}
/>
<div
className="resize-bottom"
style={{ maxHeight: questionContentRef?.current?.scrollHeight }}
/>
<div className="line bg-light d-flex justify-content-center align-items-center">
<Icon name="three-dots" />
</div>
<div className="question-content-wrap">
<div
ref={questionContentRef}
className="content position-absolute top-0 w-100"
dangerouslySetInnerHTML={{ __html: data?.question.html }}
/>
<div
className="resize-bottom"
style={{ maxHeight: questionContentRef?.current?.scrollHeight }}
/>
<div className="line bg-light d-flex justify-content-center align-items-center">
<Icon name="three-dots" />
</div>
</div>
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</option>
);
})}
</Form.Select>
</Form.Group>
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</option>
);
})}
</Form.Select>
</Form.Group>
<Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
onChange={handleAnswerChange}
className={classNames(
'form-control p-0',
focusType === 'answer' && 'focus',
)}
onFocus={() => {
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' }),
<Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
onChange={handleAnswerChange}
className={classNames(
'form-control p-0',
focusType === 'answer' && 'focus',
)}
onFocus={() => {
setForceType('answer');
}}
onBlur={() => {
setForceType('');
}}
ref={editorRef}
/>
</Card>
</Col>
</Row>
</Container>
</>
<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('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 { useTranslation } from 'react-i18next';
import { PageTitle, FollowingTags } from '@/components';
import { usePageTags } from '@/hooks';
import { FollowingTags } from '@/components';
import QuestionList from '@/components/QuestionList';
import HotQuestions from '@/components/HotQuestions';
import { siteInfoStore } from '@/stores';
@ -20,21 +21,19 @@ const Questions: FC = () => {
slogan = `${siteInfo.short_description}`;
}
usePageTags({ title: pageTitle, subtitle: slogan });
return (
<>
<PageTitle title={pageTitle} suffix={slogan} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<QuestionList source="questions" />
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<FollowingTags />
<HotQuestions />
</Col>
</Row>
</Container>
</>
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<QuestionList source="questions" />
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<FollowingTags />
<HotQuestions />
</Col>
</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 { useTranslation } from 'react-i18next';
import {
BaseUserCard,
FormatTime,
Empty,
DiffContent,
PageTitle,
} from '@/components';
import { usePageTags } from '@/hooks';
import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
import { getReviewList, revisionAudit } from '@/services';
import { pathFactory } from '@/router/pathFactory';
import type * as Type from '@/common/interface';
@ -124,9 +119,11 @@ const Index: FC = () => {
useEffect(() => {
queryNextOne(page);
}, []);
usePageTags({
title: t('review'),
});
return (
<Container className="pt-2 mt-4 mb-5">
<PageTitle title={t('review')} />
<Row>
<Col lg={{ span: 7, offset: 1 }}>
<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 { useSearchParams } from 'react-router-dom';
import { Pagination, PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import { Pagination } from '@/components';
import { useSearch } from '@/services';
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
@ -27,38 +28,38 @@ const Index = () => {
if (q) {
pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
}
usePageTags({
title: pageTitle,
});
return (
<>
<PageTitle title={pageTitle} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-3">
<Head data={extra} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-3">
<Head data={extra} />
<ListGroup variant="flush" className="mb-5">
<SearchHead sort={order} count={count} />
<ListGroup variant="flush" className="mb-5">
<SearchHead sort={order} count={count} />
{list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />;
})}
</ListGroup>
{list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />;
})}
</ListGroup>
{!isLoading && !list?.length && <Empty />}
{!isLoading && !list?.length && <Empty />}
<div className="d-flex justify-content-center">
<Pagination
currentPage={Number(page)}
pageSize={20}
totalSize={count}
/>
</div>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Tips />
</Col>
</Row>
</Container>
</>
<div className="d-flex justify-content-center">
<Pagination
currentPage={Number(page)}
pageSize={20}
totalSize={count}
/>
</div>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Tips />
</Col>
</Row>
</Container>
);
};

View File

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

View File

@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import classNames from 'classnames';
import { Editor, EditorRef, PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import { Editor, EditorRef } from '@/components';
import { loggedUserInfoStore } from '@/stores';
import type * as Type from '@/common/interface';
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
@ -39,7 +40,7 @@ const initFormData = {
errorMsg: '',
},
};
const Ask = () => {
const Index = () => {
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
const { tagId } = useParams();
@ -143,132 +144,129 @@ const Ask = () => {
const backPage = () => {
navigate(-1);
};
usePageTags({
title: t('edit_tag', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('edit_tag', { keyPrefix: 'page_title' })} />
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</option>
);
})}
</Form.Select>
</Form.Group>
<Form.Group controlId="display_name" className="mb-3">
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
<Form.Control
value={formData.displayName.value}
isInvalid={formData.displayName.isInvalid}
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' }),
}}
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{t('title')}</h3>
</Col>
</Row>
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
<Form.Select onChange={handleSelectedRevision}>
{revisions.map(({ create_at, reason, user_info }, index) => {
const date = dayjs(create_at * 1000)
.tz()
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
return (
<option key={`${create_at}`} value={index}>
{`${date} - ${user_info.display_name} - ${
reason || t('default_reason')
}`}
</option>
);
})}
</Form.Select>
</Form.Group>
<Form.Group controlId="display_name" className="mb-3">
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
<Form.Control
value={formData.displayName.value}
isInvalid={formData.displayName.isInvalid}
disabled={!is_admin}
onChange={handleDisplayNameChange}
/>
</Card>
</Col>
</Row>
</Container>
</>
<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>
</Row>
</Container>
);
};
export default Ask;
export default Index;

View File

@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import { Tag, TagSelector, FormatTime, Modal } from '@/components';
import {
useTagInfo,
useQuerySynonymsTags,
@ -26,7 +27,15 @@ const TagIntroduction = () => {
const { t } = useTranslation('translation', { keyPrefix: 'tag_info' });
const navigate = useNavigate();
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(() => {
if (locationState?.isReview) {
toastStore.getState().show({
@ -35,7 +44,6 @@ const TagIntroduction = () => {
});
}
}, [locationState]);
if (!tagInfo) {
return null;
}
@ -100,145 +108,132 @@ const TagIntroduction = () => {
}
};
let pageTitle = '';
if (tagInfo) {
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
keyPrefix: 'page_title',
})}`;
}
return (
<>
<PageTitle title={pageTitle} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<h3 className="mb-3">
<Link
to={pathFactory.tagLanding(tagInfo.slug_name)}
replace
className="link-dark">
{tagInfo.display_name}
</Link>
</h3>
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<h3 className="mb-3">
<Link
to={pathFactory.tagLanding(tagInfo.slug_name)}
replace
className="link-dark">
{tagInfo.display_name}
</Link>
</h3>
<div className="text-secondary mb-4 fs-14">
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
<FormatTime
preFix={t('edited_at')}
className="ms-3"
time={tagInfo.updated_at}
/>
</div>
<div
className="content text-break"
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
<div className="text-secondary mb-4 fs-14">
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
<FormatTime
preFix={t('edited_at')}
className="ms-3"
time={tagInfo.updated_at}
/>
<div className="mt-4">
{tagInfo?.member_actions.map((action, index) => {
return (
<Button
key={action.name}
variant="link"
className={classNames(
'link-secondary btn-no-border p-0 fs-14',
index > 0 && 'ms-3',
)}
onClick={() => onAction(action)}>
{action.name}
</Button>
);
})}
{isLogged && (
<Link
to={`/tags/${tagInfo?.tag_id}/timeline`}
</div>
<div
className="content text-break"
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
/>
<div className="mt-4">
{tagInfo?.member_actions.map((action, index) => {
return (
<Button
key={action.name}
variant="link"
className={classNames(
'link-secondary btn-no-border p-0 fs-14',
tagInfo?.member_actions?.length > 0 && 'ms-3',
)}>
{t('history')}
</Link>
)}
</div>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header className="d-flex justify-content-between">
<span>{t('synonyms.title')}</span>
{isEdit ? (
<Button
variant="link"
className="p-0 btn-no-border"
onClick={handleSave}>
{t('synonyms.btn_save')}
</Button>
) : synonymsData?.member_actions?.find(
(v) => v.action === 'edit',
) ? (
<Button
variant="link"
className="p-0 btn-no-border"
onClick={handleEdit}>
{t('synonyms.btn_edit')}
</Button>
) : null}
</Card.Header>
<Card.Body>
{isEdit && (
<>
<div className="mb-3">
{t('synonyms.text')}{' '}
<Tag
data={{
slug_name: tagName || '',
main_tag_slug_name: '',
display_name: '',
recommend: false,
reserved: false,
}}
/>
</div>
<TagSelector
value={synonymsData?.synonyms}
onChange={handleTagsChange}
hiddenDescription
index > 0 && 'ms-3',
)}
onClick={() => onAction(action)}>
{action.name}
</Button>
);
})}
{isLogged && (
<Link
to={`/tags/${tagInfo?.tag_id}/timeline`}
className={classNames(
'link-secondary btn-no-border p-0 fs-14',
tagInfo?.member_actions?.length > 0 && 'ms-3',
)}>
{t('history')}
</Link>
)}
</div>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
<Card>
<Card.Header className="d-flex justify-content-between">
<span>{t('synonyms.title')}</span>
{isEdit ? (
<Button
variant="link"
className="p-0 btn-no-border"
onClick={handleSave}>
{t('synonyms.btn_save')}
</Button>
) : synonymsData?.member_actions?.find(
(v) => v.action === 'edit',
) ? (
<Button
variant="link"
className="p-0 btn-no-border"
onClick={handleEdit}>
{t('synonyms.btn_edit')}
</Button>
) : null}
</Card.Header>
<Card.Body>
{isEdit && (
<>
<div className="mb-3">
{t('synonyms.text')}{' '}
<Tag
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 &&
(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>
)}
</>
))}
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
))}
</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 { 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 { useQueryTags, following } from '@/services';
@ -37,81 +38,81 @@ const Tags = () => {
mutate();
});
};
usePageTags({
title: t('tags', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('tags', { keyPrefix: 'page_title' })} />
<Container className="py-3 my-3">
<Row className="mb-4 d-flex justify-content-center">
<Col xxl={10} sm={12}>
<h3 className="mb-4">{t('title')}</h3>
<div className="d-flex justify-content-between align-items-center flex-wrap">
<Form>
<Form.Group controlId="formBasicEmail">
<Form.Control
value={searchTag}
placeholder={t('search_placeholder')}
type="text"
onChange={handleChange}
size="sm"
/>
</Form.Group>
</Form>
<QueryGroup
data={sortBtns}
currentSort={sort || 'popular'}
sortKey="sort"
i18nKeyPrefix="tags.sort_buttons"
/>
</div>
</Col>
<Container className="py-3 my-3">
<Row className="mb-4 d-flex justify-content-center">
<Col xxl={10} sm={12}>
<h3 className="mb-4">{t('title')}</h3>
<div className="d-flex justify-content-between align-items-center flex-wrap">
<Form>
<Form.Group controlId="formBasicEmail">
<Form.Control
value={searchTag}
placeholder={t('search_placeholder')}
type="text"
onChange={handleChange}
size="sm"
/>
</Form.Group>
</Form>
<QueryGroup
data={sortBtns}
currentSort={sort || 'popular'}
sortKey="sort"
i18nKeyPrefix="tags.sort_buttons"
/>
</div>
</Col>
<Col className="mt-4" xxl={10} sm={12}>
<Row>
{tags?.list?.map((tag) => (
<Col
key={tag.slug_name}
xs={12}
lg={3}
md={4}
sm={6}
className="mb-4">
<Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} />
<Col className="mt-4" xxl={10} sm={12}>
<Row>
{tags?.list?.map((tag) => (
<Col
key={tag.slug_name}
xs={12}
lg={3}
md={4}
sm={6}
className="mb-4">
<Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} />
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
{tag.original_text}
</p>
<div className="d-flex align-items-center">
<Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary"
size="sm"
onClick={() => handleFollow(tag)}>
{tag.is_follower
? t('button_following')
: t('button_follow')}
</Button>
<span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')}
</span>
</div>
</Card.Body>
</Card>
</Col>
))}
</Row>
<div className="d-flex justify-content-center">
<Pagination
currentPage={page}
totalSize={tags?.count || 0}
pageSize={pageSize}
/>
</div>
</Col>
</Row>
</Container>
</>
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
{tag.original_text}
</p>
<div className="d-flex align-items-center">
<Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary"
size="sm"
onClick={() => handleFollow(tag)}>
{tag.is_follower
? t('button_following')
: t('button_follow')}
</Button>
<span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')}
</span>
</div>
</Card.Body>
</Card>
</Col>
))}
</Row>
<div className="d-flex justify-content-center">
<Pagination
currentPage={page}
totalSize={tags?.count || 0}
pageSize={pageSize}
/>
</div>
</Col>
</Row>
</Container>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -2,23 +2,22 @@ import { FC, memo } from 'react';
import { Container, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import SendEmail from './components/sendEmail';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
usePageTags({
title: t('change_email', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('change_email', { keyPrefix: 'page_title' })} />
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<Col className="mx-auto" md={3}>
<SendEmail />
</Col>
</Container>
</>
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<Col className="mx-auto" md={3}>
<SendEmail />
</Col>
</Container>
);
};

View File

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

View File

@ -3,12 +3,13 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import type {
LoginReqParams,
ImgCodeRes,
FormDataType,
} from '@/common/interface';
import { PageTitle, Unactivate } from '@/components';
import { Unactivate } from '@/components';
import { loggedUserInfoStore } from '@/stores';
import { guard, floppyNavigation, handleFormError } from '@/utils';
import { login, checkImgCode } from '@/services';
@ -167,11 +168,12 @@ const Index: React.FC = () => {
setStep(2);
}
}, []);
usePageTags({
title: t('login', { keyPrefix: 'page_title' }),
});
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<PageTitle title={t('login', { keyPrefix: 'page_title' })} />
{step === 1 && (
<Col className="mx-auto" md={3}>
<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 { useParams, useNavigate } from 'react-router-dom';
import { PageTitle } from '@/components';
import { usePageTags } from '@/hooks';
import {
useQueryNotifications,
clearUnreadNotification,
@ -66,67 +66,66 @@ const Notifications = () => {
const handleReadNotification = (id) => {
readNotification(id);
};
usePageTags({
title: t('notifications', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('notifications', { keyPrefix: 'page_title' })} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<h3 className="mb-4">{t('title')}</h3>
<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>
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<h3 className="mb-4">{t('title')}</h3>
<div className="d-flex justify-content-between mb-3">
<ButtonGroup size="sm">
<Button
size="sm"
as="a"
href="/users/notifications/inbox"
variant="outline-secondary"
onClick={handleUnreadNotification}>
{t('all_read')}
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
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>
</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>
</div>
)}
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
</Row>
</Container>
</>
)}
</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 { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores';
import type { FormDataType } from '@/common/interface';
import { replacementPassword } from '@/services';
import { PageTitle } from '@/components';
import { handleFormError } from '@/utils';
const Index: React.FC = () => {
@ -112,89 +112,88 @@ const Index: React.FC = () => {
}
});
};
usePageTags({
title: t('account_recovery', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('account_recovery', { keyPrefix: 'page_title' })} />
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
{step === 1 && (
<Col className="mx-auto" md={3}>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('password.label')}</Form.Label>
<Form.Control
autoComplete="off"
required
type="password"
maxLength={32}
isInvalid={formData.pass.isInvalid}
onChange={(e) => {
handleChange({
pass: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.pass.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
{step === 1 && (
<Col className="mx-auto" md={3}>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('password.label')}</Form.Label>
<Form.Control
autoComplete="off"
required
type="password"
maxLength={32}
isInvalid={formData.pass.isInvalid}
onChange={(e) => {
handleChange({
pass: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.pass.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="password" className="mb-3">
<Form.Label>{t('password_confirm.label')}</Form.Label>
<Form.Control
autoComplete="off"
required
type="password"
maxLength={32}
isInvalid={formData.passSecond.isInvalid}
onChange={(e) => {
handleChange({
passSecond: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.passSecond.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="password" className="mb-3">
<Form.Label>{t('password_confirm.label')}</Form.Label>
<Form.Control
autoComplete="off"
required
type="password"
maxLength={32}
isInvalid={formData.passSecond.isInvalid}
onChange={(e) => {
handleChange({
passSecond: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.passSecond.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_name')}
</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 className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</div>
</Col>
)}
</Form>
</Col>
)}
{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>
</>
{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>
</Col>
)}
{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 { 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 {
usePersonalInfoByName,
@ -50,9 +51,11 @@ const Personal: FC = () => {
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`;
}
const { count = 0, list = [] } = listData?.[tabName] || {};
usePageTags({
title: pageTitle,
});
return (
<Container className="pt-4 mt-2 mb-5">
<PageTitle title={pageTitle} />
<Row className="justify-content-center">
{userInfo?.info?.status !== 'normal' && 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 { useTranslation } from 'react-i18next';
import { PageTitle, Unactivate } from '@/components';
import { usePageTags } from '@/hooks';
import { Unactivate } from '@/components';
import SignUpForm from './components/SignUpForm';
@ -13,11 +14,12 @@ const Index: React.FC = () => {
const onStep = () => {
setShowForm((bol) => !bol);
};
usePageTags({
title: t('sign_up', { keyPrefix: 'page_title' }),
});
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<h3 className="text-center mb-5">{t('page_title')}</h3>
<PageTitle title={t('sign_up', { keyPrefix: 'page_title' })} />
{showForm ? (
<SignUpForm callback={onStep} />
) : (

View File

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

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState, FormEvent } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type { LangsType, FormDataType } from '@/common/interface';
@ -7,6 +6,7 @@ import { useToast } from '@/hooks';
import { updateUserInterface } from '@/services';
import { localize } from '@/utils';
import { loggedUserInfoStore } from '@/stores';
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
const Index = () => {
const { t } = useTranslation('translation', {
@ -15,19 +15,34 @@ const Index = () => {
const loggedUserInfo = loggedUserInfoStore.getState().user;
const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>();
const [formData, setFormData] = useState<FormDataType>({
lang: {
value: loggedUserInfo.language,
isInvalid: false,
errorMsg: '',
const schema: JSONSchema = {
title: t('heading'),
properties: {
lang: {
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 res: LangsType[] = await localize.loadLanguageOptions();
setLangs(res);
};
const handleOnChange = (d) => {
setFormData(d);
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
const lang = formData.lang.value;
@ -48,39 +63,16 @@ const Index = () => {
getLangs();
}, []);
return (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="emailSend" className="mb-3">
<Form.Label>{t('lang.label')}</Form.Label>
<Form.Select
value={formData.lang.value}
isInvalid={formData.lang.isInvalid}
onChange={(e) => {
setFormData({
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>
<>
<h3 className="mb-4">{t('heading')}</h3>
<SchemaForm
schema={schema}
uiSchema={uiSchema}
formData={formData}
onChange={handleOnChange}
onSubmit={handleSubmit}
/>
</>
);
};

View File

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

View File

@ -1,12 +1,12 @@
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 { marked } from 'marked';
import MD5 from 'md5';
import type { FormDataType } from '@/common/interface';
import { UploadImg, Avatar } from '@/components';
import { UploadImg, Avatar, Icon } from '@/components';
import { loggedUserInfoStore } from '@/stores';
import { useToast } from '@/hooks';
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
@ -19,7 +19,7 @@ const Index: React.FC = () => {
const toast = useToast();
const { user, update } = loggedUserInfoStore();
const [mailHash, setMailHash] = useState('');
const [count, setCount] = useState(0);
const [count] = useState(0);
const [formData, setFormData] = useState<FormDataType>({
display_name: {
@ -60,6 +60,40 @@ const Index: React.FC = () => {
const handleChange = (params: FormDataType) => {
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) => {
setFormData({
@ -73,6 +107,17 @@ const Index: React.FC = () => {
},
});
};
const removeCustomAvatar = () => {
setFormData({
...formData,
avatar: {
...formData.avatar,
custom: '',
isInvalid: false,
errorMsg: '',
},
});
};
const checkValidated = (): boolean => {
let bol = true;
@ -201,265 +246,220 @@ const Index: React.FC = () => {
});
};
const refreshGravatar = () => {
setCount((pre) => pre + 1);
};
// const refreshGravatar = () => {
// setCount((pre) => pre + 1);
// };
useEffect(() => {
getProfile();
}, []);
return (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="displayName" className="mb-3">
<Form.Label>{t('display_name.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.display_name.value}
isInvalid={formData.display_name.isInvalid}
onChange={(e) =>
handleChange({
display_name: {
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={() =>
<>
<h3 className="mb-4">{t('heading')}</h3>
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="displayName" className="mb-3">
<Form.Label>{t('display_name.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.display_name.value}
isInvalid={formData.display_name.isInvalid}
onChange={(e) =>
handleChange({
avatar: {
...formData.avatar,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
display_name: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Check
inline
type="radio"
label={t('avatar.custom')}
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>
</>
)}
<Form.Control.Feedback type="invalid">
{formData.display_name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
{formData.avatar.type === 'custom' && (
<>
<Avatar
size="128px"
searchStr="s=256"
avatar={formData.avatar.custom}
className="me-3 rounded"
/>
<div>
<UploadImg
type="avatar"
uploadCallback={avatarUpload}
className="mb-2"
<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-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-0">
<Trans i18nKey="settings.profile.avatar.text">
You can upload your image.
</Trans>
</Form.Text>
</div>
</div>
</>
)}
{formData.avatar.type === 'default' && (
<Avatar size="128px" avatar="" className="me-3 rounded" />
)}
</div>
<Form.Control
isInvalid={formData.avatar.isInvalid}
className="d-none"
/>
<Form.Control.Feedback type="invalid">
{formData.avatar.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Text className="text-muted mt-1">
<Trans i18nKey="settings.profile.avatar.gravatar_text">
You can change image on
<a
href="https://gravatar.com"
target="_blank"
rel="noreferrer">
gravatar.com
</a>
</Trans>
</Form.Text>
</Stack>
)}
<Form.Group controlId="bio" className="mb-3">
<Form.Label>{t('bio.label')}</Form.Label>
<Form.Control
className="font-monospace"
required
as="textarea"
rows={5}
value={formData.bio.value}
isInvalid={formData.bio.isInvalid}
onChange={(e) =>
handleChange({
bio: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.bio.errorMsg}
</Form.Control.Feedback>
</Form.Group>
{formData.avatar.type === 'custom' && (
<Stack>
<Stack direction="horizontal" className="align-items-start">
<Avatar
size="160px"
searchStr="s=256"
avatar={formData.avatar.custom}
className="me-2 bg-gray-300 "
/>
<ButtonGroup vertical className="fit-content">
<UploadImg type="avatar" uploadCallback={avatarUpload}>
<Icon name="cloud-upload" />
</UploadImg>
<Button
variant="outline-secondary"
onClick={removeCustomAvatar}>
<Icon name="trash" />
</Button>
</ButtonGroup>
</Stack>
<Form.Text className="text-muted mt-1">
<Trans i18nKey="settings.profile.avatar.text">
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.Label>{t('website.label')}</Form.Label>
<Form.Control
required
type="text"
placeholder={t('website.placeholder')}
value={formData.website.value}
isInvalid={formData.website.isInvalid}
onChange={(e) =>
handleChange({
website: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.website.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="bio" className="mb-3">
<Form.Label>{t('bio.label')}</Form.Label>
<Form.Control
className="font-monospace"
required
as="textarea"
rows={5}
value={formData.bio.value}
isInvalid={formData.bio.isInvalid}
onChange={(e) =>
handleChange({
bio: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.bio.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('location.label')}</Form.Label>
<Form.Control
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>
<Form.Group controlId="website" className="mb-3">
<Form.Label>{t('website.label')}</Form.Label>
<Form.Control
required
type="text"
placeholder={t('website.placeholder')}
value={formData.website.value}
isInvalid={formData.website.isInvalid}
onChange={(e) =>
handleChange({
website: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.website.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</Form>
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('location.label')}</Form.Label>
<Form.Control
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 { Outlet } from 'react-router-dom';
import { usePageTags } from '@/hooks';
import type { FormDataType } from '@/common/interface';
import { getLoggedUserInfo } from '@/services';
import { PageTitle } from '@/components';
import Nav from './components/Nav';
@ -55,29 +55,27 @@ const Index: React.FC = () => {
useEffect(() => {
getProfile();
}, []);
usePageTags({
title: t('settings', { keyPrefix: 'page_title' }),
});
return (
<>
<PageTitle title={t('settings', { keyPrefix: 'page_title' })} />
<Container className="mt-4 mb-5 pb-5">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">
{t('page_title', { keyPrefix: 'settings' })}
</h3>
</Col>
</Row>
<Container className="mt-4 mb-5 pb-5">
<Row className="justify-content-center">
<Col xxl={10} md={12}>
<h3 className="mb-4">{t('page_title', { keyPrefix: 'settings' })}</h3>
</Col>
</Row>
<Row>
<Col xxl={1} />
<Col md={3} lg={2} className="mb-3">
<Nav />
</Col>
<Col md={9} lg={6}>
<Outlet />
</Col>
</Row>
</Container>
</>
<Row>
<Col xxl={1} />
<Col md={3} lg={2} className="mb-3">
<Nav />
</Col>
<Col md={9} lg={6}>
<Outlet />
</Col>
</Row>
</Container>
);
};

View File

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

View File

@ -80,7 +80,7 @@ export const getBrandSetting = () => {
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);
};

View File

@ -42,10 +42,10 @@ export const useFollowingTags = () => {
export const useTagInfo = ({ id = '', name = '' }) => {
let apiUrl;
if (id) {
apiUrl = `/answer/api/v1/tag/?id=${id}`;
apiUrl = `/answer/api/v1/tag?id=${id}`;
} else if (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);
return {

View File

@ -1,11 +1,11 @@
import create from 'zustand';
import { AdmingSettingBranding } from '@/common/interface';
import { AdminSettingBranding } from '@/common/interface';
import { DEFAULT_LANG } from '@/common/constants';
interface InterfaceType {
branding: AdmingSettingBranding;
update: (params: AdmingSettingBranding) => void;
branding: AdminSettingBranding;
update: (params: AdminSettingBranding) => void;
}
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 interfaceStore from './interface';
import brandingStore from './branding';
import headInfoStore from './headInfo';
import pageTagStore from './pageTags';
export {
toastStore,
@ -13,5 +13,5 @@ export {
siteInfoStore,
interfaceStore,
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');
}
const diff = Diff.diffChars(oldText, newText);
// console.log(diff);
const result = diff.map((part) => {
if (part.added) {
if (part.value.replace(/\n/g, '').length <= 0) {