mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-0.6.0' into feat/ui-0.7.0
This commit is contained in:
commit
1537f502f4
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
}
|
|
@ -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]);
|
||||
}
|
|
@ -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' }),
|
||||
|
|
|
@ -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')],
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
) : (
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -179,7 +179,6 @@ function diffText(newText: string, oldText: string): string {
|
|||
?.replace(/<input/gi, '<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) {
|
||||
|
|
Loading…
Reference in New Issue