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
|
account: Account
|
||||||
interface: Interface
|
interface: Interface
|
||||||
profile:
|
profile:
|
||||||
btn_name: Update profile
|
heading: Profile
|
||||||
|
btn_name: Save
|
||||||
display_name:
|
display_name:
|
||||||
label: Display Name
|
label: Display Name
|
||||||
msg: Display name cannot be empty.
|
msg: Display name cannot be empty.
|
||||||
|
@ -658,7 +659,7 @@ ui:
|
||||||
custom: Custom
|
custom: Custom
|
||||||
btn_refresh: Refresh
|
btn_refresh: Refresh
|
||||||
custom_text: You can upload your image.
|
custom_text: You can upload your image.
|
||||||
default: Default
|
default: System
|
||||||
msg: Please upload an avatar
|
msg: Please upload an avatar
|
||||||
bio:
|
bio:
|
||||||
label: About Me (optional)
|
label: About Me (optional)
|
||||||
|
@ -670,10 +671,12 @@ ui:
|
||||||
label: Location (optional)
|
label: Location (optional)
|
||||||
placeholder: "City, Country"
|
placeholder: "City, Country"
|
||||||
notification:
|
notification:
|
||||||
|
heading: Notifications
|
||||||
email:
|
email:
|
||||||
label: Email Notifications
|
label: Email Notifications
|
||||||
radio: "Answers to your questions, comments, and more"
|
radio: "Answers to your questions, comments, and more"
|
||||||
account:
|
account:
|
||||||
|
heading: Account
|
||||||
change_email_btn: Change email
|
change_email_btn: Change email
|
||||||
change_pass_btn: Change password
|
change_pass_btn: Change password
|
||||||
change_email_info: >-
|
change_email_info: >-
|
||||||
|
@ -694,6 +697,7 @@ ui:
|
||||||
pass_confirm:
|
pass_confirm:
|
||||||
label: Confirm New Password
|
label: Confirm New Password
|
||||||
interface:
|
interface:
|
||||||
|
heading: Interface
|
||||||
lang:
|
lang:
|
||||||
label: Interface Language
|
label: Interface Language
|
||||||
text: User interface language. It will change when you refresh the page.
|
text: User interface language. It will change when you refresh the page.
|
||||||
|
@ -1212,7 +1216,8 @@ ui:
|
||||||
text: Provide email address that will receive test sends.
|
text: Provide email address that will receive test sends.
|
||||||
msg: Test email recipients is invalid
|
msg: Test email recipients is invalid
|
||||||
smtp_authentication:
|
smtp_authentication:
|
||||||
label: SMTP Authentication
|
label: Enable authentication
|
||||||
|
title: SMTP Authentication
|
||||||
msg: SMTP authentication cannot be empty.
|
msg: SMTP authentication cannot be empty.
|
||||||
"yes": "Yes"
|
"yes": "Yes"
|
||||||
"no": "No"
|
"no": "No"
|
||||||
|
|
|
@ -4,22 +4,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Answer</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
@ -51,15 +36,5 @@
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -287,11 +287,15 @@ export interface AdminSettingsGeneral {
|
||||||
permalink: number;
|
permalink: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadInfo {
|
export interface HelmetBase {
|
||||||
title?: string;
|
pageTitle?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
ldJSON?;
|
}
|
||||||
|
|
||||||
|
export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsInterface {
|
export interface AdminSettingsInterface {
|
||||||
|
@ -313,12 +317,12 @@ export interface AdminSettingsSmtp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SiteSettings {
|
export interface SiteSettings {
|
||||||
branding: AdmingSettingBranding;
|
branding: AdminSettingBranding;
|
||||||
general: AdminSettingsGeneral;
|
general: AdminSettingsGeneral;
|
||||||
interface: AdminSettingsInterface;
|
interface: AdminSettingsInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdmingSettingBranding {
|
export interface AdminSettingBranding {
|
||||||
logo: string;
|
logo: string;
|
||||||
square_icon: string;
|
square_icon: string;
|
||||||
mobile_logo?: string;
|
mobile_logo?: string;
|
||||||
|
|
|
@ -47,7 +47,7 @@ const Index: FC = () => {
|
||||||
{t('save')}
|
{t('save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Body className="pb-2">
|
<Card.Body className="my-n1">
|
||||||
<TagSelector
|
<TagSelector
|
||||||
value={followingTags}
|
value={followingTags}
|
||||||
onChange={handleTagsChange}
|
onChange={handleTagsChange}
|
||||||
|
|
|
@ -9,7 +9,8 @@ import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const fullYear = dayjs().format('YYYY');
|
const fullYear = dayjs().format('YYYY');
|
||||||
const siteName = siteInfoStore.getState().siteInfo.name || DEFAULT_SITE_NAME;
|
const siteName =
|
||||||
|
siteInfoStore((state) => state.siteInfo.name) || DEFAULT_SITE_NAME;
|
||||||
const cc = `${fullYear} ${siteName}`;
|
const cc = `${fullYear} ${siteName}`;
|
||||||
return (
|
return (
|
||||||
<footer className="bg-light py-3">
|
<footer className="bg-light py-3">
|
||||||
|
|
|
@ -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">
|
<ListGroup variant="flush" className="border-top border-bottom-0">
|
||||||
{listData?.list?.map((li) => {
|
{listData?.list?.map((li) => {
|
||||||
return (
|
return (
|
||||||
<ListGroup.Item
|
<ListGroup.Item key={li.id} className="border-bottom py-3 px-0">
|
||||||
key={li.id}
|
|
||||||
className="border-bottom pt-3 pb-2 px-0">
|
|
||||||
<h5 className="text-wrap text-break">
|
<h5 className="text-wrap text-break">
|
||||||
<NavLink
|
<NavLink
|
||||||
to={pathFactory.questionLanding(li.id, li.title)}
|
to={pathFactory.questionLanding(li.id, li.title)}
|
||||||
|
@ -131,7 +129,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
||||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</h5>
|
</h5>
|
||||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary">
|
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-3 text-secondary">
|
||||||
<QuestionLastUpdate q={li} />
|
<QuestionLastUpdate q={li} />
|
||||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||||
<span>
|
<span>
|
||||||
|
@ -157,7 +155,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="question-tags mx-n1 mt-2">
|
<div className="question-tags m-n1">
|
||||||
{Array.isArray(li.tags)
|
{Array.isArray(li.tags)
|
||||||
? li.tags.map((tag) => {
|
? li.tags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -23,7 +23,6 @@ const Index: React.FC<IProps> = ({
|
||||||
const [status, setStatus] = useState(false);
|
const [status, setStatus] = useState(false);
|
||||||
|
|
||||||
const onChange = (e: any) => {
|
const onChange = (e: any) => {
|
||||||
console.log('uploading', e);
|
|
||||||
if (status) {
|
if (status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +36,6 @@ const Index: React.FC<IProps> = ({
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
setStatus(true);
|
setStatus(true);
|
||||||
console.log('uploading', e.target.files);
|
|
||||||
uploadImage({ file: e.target.files[0], type })
|
uploadImage({ file: e.target.files[0], type })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
uploadCallback(res);
|
uploadCallback(res);
|
||||||
|
|
|
@ -19,7 +19,6 @@ import Mentions from './Mentions';
|
||||||
import FormatTime from './FormatTime';
|
import FormatTime from './FormatTime';
|
||||||
import Toast from './Toast';
|
import Toast from './Toast';
|
||||||
import AccordionNav from './AccordionNav';
|
import AccordionNav from './AccordionNav';
|
||||||
import PageTitle from './PageTitle';
|
|
||||||
import Empty from './Empty';
|
import Empty from './Empty';
|
||||||
import BaseUserCard from './BaseUserCard';
|
import BaseUserCard from './BaseUserCard';
|
||||||
import FollowingTags from './FollowingTags';
|
import FollowingTags from './FollowingTags';
|
||||||
|
@ -51,7 +50,6 @@ export {
|
||||||
FormatTime,
|
FormatTime,
|
||||||
Toast,
|
Toast,
|
||||||
AccordionNav,
|
AccordionNav,
|
||||||
PageTitle,
|
|
||||||
Empty,
|
Empty,
|
||||||
BaseUserCard,
|
BaseUserCard,
|
||||||
FollowingTags,
|
FollowingTags,
|
||||||
|
|
|
@ -5,9 +5,9 @@ import usePageUsers from './usePageUsers';
|
||||||
import useChangeModal from './useChangeModal';
|
import useChangeModal from './useChangeModal';
|
||||||
import useEditStatusModal from './useEditStatusModal';
|
import useEditStatusModal from './useEditStatusModal';
|
||||||
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||||
import useHeadInfo from './useHeadInfo';
|
|
||||||
import useUserModal from './useUserModal';
|
import useUserModal from './useUserModal';
|
||||||
import useChangePasswordModal from './useChangePasswordModal';
|
import useChangePasswordModal from './useChangePasswordModal';
|
||||||
|
import usePageTags from './usePageTags';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useTagModal,
|
useTagModal,
|
||||||
|
@ -17,7 +17,7 @@ export {
|
||||||
useChangeModal,
|
useChangeModal,
|
||||||
useEditStatusModal,
|
useEditStatusModal,
|
||||||
useChangeUserRoleModal,
|
useChangeUserRoleModal,
|
||||||
useHeadInfo,
|
|
||||||
useUserModal,
|
useUserModal,
|
||||||
useChangePasswordModal,
|
useChangePasswordModal,
|
||||||
|
usePageTags,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
favicon: formData.favicon.value,
|
||||||
};
|
};
|
||||||
brandSetting(params)
|
brandSetting(params)
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
console.log(res);
|
|
||||||
update(params);
|
update(params);
|
||||||
Toast.onShow({
|
Toast.onShow({
|
||||||
msg: t('update', { keyPrefix: 'toast' }),
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
|
|
@ -46,7 +46,8 @@ const Smtp: FC = () => {
|
||||||
},
|
},
|
||||||
smtp_authentication: {
|
smtp_authentication: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('smtp_authentication.label'),
|
title: t('smtp_authentication.title'),
|
||||||
|
label: t('smtp_authentication.label'),
|
||||||
enum: [true, false],
|
enum: [true, false],
|
||||||
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
||||||
},
|
},
|
||||||
|
|
|
@ -147,7 +147,9 @@ const Users: FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<th style={{ width: '12%' }}>{t('status')}</th>
|
<th style={{ width: '12%' }}>{t('status')}</th>
|
||||||
<th style={{ width: '12%' }}>{t('role')}</th>
|
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
|
||||||
|
<th style={{ width: '12%' }}>{t('role')}</th>
|
||||||
|
)}
|
||||||
{curFilter !== 'deleted' ? (
|
{curFilter !== 'deleted' ? (
|
||||||
<th style={{ width: '8%' }} className="text-end">
|
<th style={{ width: '8%' }} className="text-end">
|
||||||
{t('action')}
|
{t('action')}
|
||||||
|
@ -189,11 +191,13 @@ const Users: FC = () => {
|
||||||
{t(user.status)}
|
{t(user.status)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
{curFilter !== 'suspended' && curFilter !== 'deleted' && (
|
||||||
<span className="badge text-bg-light">
|
<td>
|
||||||
{t(user.role_name)}
|
<span className="badge text-bg-light">
|
||||||
</span>
|
{t(user.role_name)}
|
||||||
</td>
|
</span>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
{curFilter !== 'deleted' ? (
|
{curFilter !== 'deleted' ? (
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { AccordionNav, PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { AccordionNav } from '@/components';
|
||||||
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
@ -20,10 +21,11 @@ const formPaths = [
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
usePageTags({
|
||||||
|
title: t('admin'),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={t('admin')} />
|
|
||||||
<div className="bg-light py-2">
|
<div className="bg-light py-2">
|
||||||
<Container className="py-1">
|
<Container className="py-1">
|
||||||
<h6 className="mb-0 fw-bold lh-base">
|
<h6 className="mb-0 fw-bold lh-base">
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { FC, useState, useEffect } from 'react';
|
||||||
import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
|
import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
import {
|
import {
|
||||||
dbCheck,
|
dbCheck,
|
||||||
installInit,
|
installInit,
|
||||||
|
@ -103,7 +103,6 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (params: FormDataType) => {
|
const handleChange = (params: FormDataType) => {
|
||||||
// console.log(params);
|
|
||||||
setErrorData({
|
setErrorData({
|
||||||
msg: '',
|
msg: '',
|
||||||
});
|
});
|
||||||
|
@ -240,13 +239,15 @@ const Index: FC = () => {
|
||||||
configYmlCheck();
|
configYmlCheck();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
usePageTags({
|
||||||
|
title: t('install', { keyPrefix: 'page_title' })
|
||||||
|
});
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-f5 py-5 flex-grow-1">
|
<div className="bg-f5 py-5 flex-grow-1">
|
||||||
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
|
|
||||||
<Container className='py-3'>
|
<Container className='py-3'>
|
||||||
<Row className="justify-content-center">
|
<Row className="justify-content-center">
|
||||||
<Col lg={6}>
|
<Col lg={6}>
|
||||||
|
|
|
@ -4,29 +4,18 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||||
|
|
||||||
import { SWRConfig } from 'swr';
|
import { SWRConfig } from 'swr';
|
||||||
|
|
||||||
import {
|
import { toastStore, brandingStore, pageTagStore } from '@/stores';
|
||||||
siteInfoStore,
|
|
||||||
toastStore,
|
|
||||||
brandingStore,
|
|
||||||
headInfoStore,
|
|
||||||
} from '@/stores';
|
|
||||||
import { Header, Footer, Toast } from '@/components';
|
import { Header, Footer, Toast } from '@/components';
|
||||||
|
|
||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||||
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
|
||||||
const headInfo = headInfoStore((state) => state.headInfo);
|
|
||||||
const { favicon, square_icon } = brandingStore((state) => state.branding);
|
|
||||||
|
|
||||||
const closeToast = () => {
|
const closeToast = () => {
|
||||||
toastClear();
|
toastClear();
|
||||||
};
|
};
|
||||||
const {
|
const { favicon, square_icon } = brandingStore((state) => state.branding);
|
||||||
title,
|
const { pageTitle, keywords, description } = pageTagStore(
|
||||||
keywords,
|
(state) => state.items,
|
||||||
description = siteInfo.description,
|
);
|
||||||
ldJSON,
|
|
||||||
} = headInfo;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
|
@ -38,10 +27,9 @@ const Layout: FC = () => {
|
||||||
/>
|
/>
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href={square_icon} />
|
<link rel="icon" type="image/png" sizes="192x192" href={square_icon} />
|
||||||
<link rel="apple-touch-icon" type="image/png" href={square_icon} />
|
<link rel="apple-touch-icon" type="image/png" href={square_icon} />
|
||||||
{title && <title>{title}</title>}
|
<title>{pageTitle}</title>
|
||||||
{keywords && <meta name="keywords" content={keywords} />}
|
{keywords && <meta name="keywords" content={keywords} />}
|
||||||
{description && <meta name="description" content={description} />}
|
{description && <meta name="description" content={description} />}
|
||||||
{ldJSON && <script type="application/ld+json">{ldJSON}</script>}
|
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
value={{
|
value={{
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { useLegalPrivacy } from '@/services';
|
import { useLegalPrivacy } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||||
|
usePageTags({
|
||||||
|
title: t('privacy'),
|
||||||
|
});
|
||||||
const { data: privacy } = useLegalPrivacy();
|
const { data: privacy } = useLegalPrivacy();
|
||||||
const contentText = privacy?.privacy_policy_original_text;
|
const contentText = privacy?.privacy_policy_original_text;
|
||||||
let matchUrl: URL | undefined;
|
let matchUrl: URL | undefined;
|
||||||
|
@ -19,10 +22,8 @@ const Index: FC = () => {
|
||||||
window.location.replace(matchUrl.toString());
|
window.location.replace(matchUrl.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={t('privacy')} />
|
|
||||||
<h3 className="mb-4">{t('privacy')}</h3>
|
<h3 className="mb-4">{t('privacy')}</h3>
|
||||||
<div
|
<div
|
||||||
className="fmt"
|
className="fmt"
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { useLegalTos } from '@/services';
|
import { useLegalTos } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||||
|
usePageTags({
|
||||||
|
title: t('tos'),
|
||||||
|
});
|
||||||
const { data: tos } = useLegalTos();
|
const { data: tos } = useLegalTos();
|
||||||
const contentText = tos?.terms_of_service_original_text;
|
const contentText = tos?.terms_of_service_original_text;
|
||||||
let matchUrl: URL | undefined;
|
let matchUrl: URL | undefined;
|
||||||
|
@ -21,7 +24,6 @@ const Index: FC = () => {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={t('tos')} />
|
|
||||||
<h3 className="mb-4">{t('tos')}</h3>
|
<h3 className="mb-4">{t('tos')}</h3>
|
||||||
<div
|
<div
|
||||||
className="fmt"
|
className="fmt"
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'page_maintenance',
|
keyPrefix: 'page_maintenance',
|
||||||
});
|
});
|
||||||
|
usePageTags({
|
||||||
|
title: t('maintenance', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className="bg-f5">
|
<div className="bg-f5">
|
||||||
<Container
|
<Container
|
||||||
className="d-flex flex-column justify-content-center align-items-center"
|
className="d-flex flex-column justify-content-center align-items-center"
|
||||||
style={{ minHeight: '100vh' }}>
|
style={{ minHeight: '100vh' }}>
|
||||||
<PageTitle title={t('maintenance', { keyPrefix: 'page_title' })} />
|
|
||||||
<div
|
<div
|
||||||
className="mb-4 text-secondary"
|
className="mb-4 text-secondary"
|
||||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||||
(=‘_‘=)
|
(=‘_‘=)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mb-4">{t('description')}</div>
|
<div className="text-center mb-4">{t('desc')}</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Editor, EditorRef, TagSelector, PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Editor, EditorRef, TagSelector } from '@/components';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import {
|
import {
|
||||||
saveQuestion,
|
saveQuestion,
|
||||||
|
@ -140,22 +141,22 @@ const Ask = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkValidated = (): boolean => {
|
const checkValidated = (): boolean => {
|
||||||
let bol = true;
|
const bol = true;
|
||||||
const { title, content, tags, answer } = formData;
|
const { title, content, tags, answer } = formData;
|
||||||
if (!title.value) {
|
if (!title.value) {
|
||||||
bol = false;
|
// bol = false;
|
||||||
formData.title = {
|
// formData.title = {
|
||||||
value: '',
|
// value: '',
|
||||||
isInvalid: true,
|
// isInvalid: true,
|
||||||
errorMsg: t('form.fields.title.msg.empty'),
|
// errorMsg: t('form.fields.title.msg.empty'),
|
||||||
};
|
// };
|
||||||
} else if (Array.from(title.value).length > 150) {
|
} else if (Array.from(title.value).length > 150) {
|
||||||
bol = false;
|
// bol = false;
|
||||||
formData.title = {
|
// formData.title = {
|
||||||
value: title.value,
|
// value: title.value,
|
||||||
isInvalid: true,
|
// isInvalid: true,
|
||||||
errorMsg: t('form.fields.title.msg.range'),
|
// errorMsg: t('form.fields.title.msg.range'),
|
||||||
};
|
// };
|
||||||
} else {
|
} else {
|
||||||
formData.title = {
|
formData.title = {
|
||||||
value: title.value,
|
value: title.value,
|
||||||
|
@ -165,12 +166,12 @@ const Ask = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.value) {
|
if (!content.value) {
|
||||||
bol = false;
|
// bol = false;
|
||||||
formData.content = {
|
// formData.content = {
|
||||||
value: '',
|
// value: '',
|
||||||
isInvalid: true,
|
// isInvalid: true,
|
||||||
errorMsg: t('form.fields.body.msg.empty'),
|
// errorMsg: t('form.fields.body.msg.empty'),
|
||||||
};
|
// };
|
||||||
} else {
|
} else {
|
||||||
formData.content = {
|
formData.content = {
|
||||||
value: content.value,
|
value: content.value,
|
||||||
|
@ -180,12 +181,12 @@ const Ask = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.value.length === 0) {
|
if (tags.value.length === 0) {
|
||||||
bol = false;
|
// bol = false;
|
||||||
formData.tags = {
|
// formData.tags = {
|
||||||
value: [],
|
// value: [],
|
||||||
isInvalid: true,
|
// isInvalid: true,
|
||||||
errorMsg: t('form.fields.tags.msg.empty'),
|
// errorMsg: t('form.fields.tags.msg.empty'),
|
||||||
};
|
// };
|
||||||
} else {
|
} else {
|
||||||
formData.tags = {
|
formData.tags = {
|
||||||
value: tags.value,
|
value: tags.value,
|
||||||
|
@ -195,12 +196,12 @@ const Ask = () => {
|
||||||
}
|
}
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (!answer.value) {
|
if (!answer.value) {
|
||||||
bol = false;
|
// bol = false;
|
||||||
formData.answer = {
|
// formData.answer = {
|
||||||
value: '',
|
// value: '',
|
||||||
isInvalid: true,
|
// isInvalid: true,
|
||||||
errorMsg: t('form.fields.answer.msg.empty'),
|
// errorMsg: t('form.fields.answer.msg.empty'),
|
||||||
};
|
// };
|
||||||
} else {
|
} else {
|
||||||
formData.answer = {
|
formData.answer = {
|
||||||
value: answer.value,
|
value: answer.value,
|
||||||
|
@ -292,189 +293,185 @@ const Ask = () => {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
pageTitle = t('edit_question', { keyPrefix: 'page_title' });
|
pageTitle = t('edit_question', { keyPrefix: 'page_title' });
|
||||||
}
|
}
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={10} md={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
|
||||||
<Col xxl={10} md={12}>
|
</Col>
|
||||||
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
|
</Row>
|
||||||
</Col>
|
<Row className="justify-content-center">
|
||||||
</Row>
|
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||||
<Row className="justify-content-center">
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
{isEdit && (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form.Group controlId="revision" className="mb-3">
|
||||||
{isEdit && (
|
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||||
<Form.Group controlId="revision" className="mb-3">
|
<Form.Select onChange={handleSelectedRevision}>
|
||||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
{revisions.map(({ reason, create_at, user_info }, index) => {
|
||||||
<Form.Select onChange={handleSelectedRevision}>
|
const date = dayjs(create_at * 1000)
|
||||||
{revisions.map(
|
.tz()
|
||||||
({ reason, create_at, user_info }, index) => {
|
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
|
||||||
const date = dayjs(create_at * 1000)
|
return (
|
||||||
.tz()
|
<option key={`${create_at}`} value={index}>
|
||||||
.format(
|
{`${date} - ${user_info.display_name} - ${
|
||||||
t('long_date_with_time', { keyPrefix: 'dates' }),
|
reason || t('default_reason')
|
||||||
);
|
}`}
|
||||||
return (
|
</option>
|
||||||
<option key={`${create_at}`} value={index}>
|
);
|
||||||
{`${date} - ${user_info.display_name} - ${
|
})}
|
||||||
reason || t('default_reason')
|
</Form.Select>
|
||||||
}`}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</Form.Select>
|
|
||||||
</Form.Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Group controlId="title" className="mb-3">
|
|
||||||
<Form.Label>{t('form.fields.title.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
value={formData.title.value}
|
|
||||||
isInvalid={formData.title.isInvalid}
|
|
||||||
onChange={handleTitleChange}
|
|
||||||
placeholder={t('form.fields.title.placeholder')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.title.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{bool && <SearchQuestion similarQuestions={similarQuestions} />}
|
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group controlId="body">
|
)}
|
||||||
<Form.Label>{t('form.fields.body.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
defaultValue={formData.content.value}
|
|
||||||
isInvalid={formData.content.isInvalid}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Editor
|
|
||||||
value={formData.content.value}
|
|
||||||
onChange={handleContentChange}
|
|
||||||
className={classNames(
|
|
||||||
'form-control p-0',
|
|
||||||
focusType === 'content' && 'focus',
|
|
||||||
)}
|
|
||||||
onFocus={() => {
|
|
||||||
setForceType('content');
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setForceType('');
|
|
||||||
}}
|
|
||||||
ref={editorRef}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.content.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="tags" className="my-3">
|
|
||||||
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
defaultValue={JSON.stringify(formData.tags.value)}
|
|
||||||
isInvalid={formData.tags.isInvalid}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<TagSelector
|
|
||||||
value={formData.tags.value}
|
|
||||||
onChange={handleTagsChange}
|
|
||||||
showRequiredTagText
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.tags.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
{isEdit && (
|
|
||||||
<Form.Group controlId="edit_summary" className="my-3">
|
|
||||||
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
defaultValue={formData.edit_summary.value}
|
|
||||||
isInvalid={formData.edit_summary.isInvalid}
|
|
||||||
placeholder={t('form.fields.edit_summary.placeholder')}
|
|
||||||
onChange={handleSummaryChange}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.edit_summary.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
)}
|
|
||||||
{!checked && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Button type="submit" className="me-2">
|
|
||||||
{isEdit ? t('btn_save_edits') : t('btn_post_question')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="link" onClick={backPage}>
|
<Form.Group controlId="title" className="mb-3">
|
||||||
{t('cancel', { keyPrefix: 'btns' })}
|
<Form.Label>{t('form.fields.title.label')}</Form.Label>
|
||||||
</Button>
|
<Form.Control
|
||||||
</div>
|
value={formData.title.value}
|
||||||
)}
|
isInvalid={formData.title.isInvalid}
|
||||||
{!isEdit && (
|
onChange={handleTitleChange}
|
||||||
<>
|
placeholder={t('form.fields.title.placeholder')}
|
||||||
<Form.Check
|
autoFocus
|
||||||
className="mt-5"
|
|
||||||
checked={checked}
|
|
||||||
type="checkbox"
|
|
||||||
label={t('answer_question')}
|
|
||||||
onChange={(e) => setCheckState(e.target.checked)}
|
|
||||||
id="radio-answer"
|
|
||||||
/>
|
|
||||||
{checked && (
|
|
||||||
<Form.Group controlId="answer" className="mt-4">
|
|
||||||
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
|
||||||
<Editor
|
|
||||||
value={formData.answer.value}
|
|
||||||
onChange={handleAnswerChange}
|
|
||||||
ref={editorRef2}
|
|
||||||
className={classNames(
|
|
||||||
'form-control p-0',
|
|
||||||
focusType === 'answer' && 'focus',
|
|
||||||
)}
|
|
||||||
onFocus={() => {
|
|
||||||
setForceType('answer');
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setForceType('');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Control
|
|
||||||
value={formData.answer.value}
|
|
||||||
type="text"
|
|
||||||
isInvalid={formData.answer.isInvalid}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.answer.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{checked && (
|
|
||||||
<Button type="submit" className="mt-3">
|
|
||||||
{t('post_question&answer')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
|
||||||
<Card className="mb-4">
|
|
||||||
<Card.Header>
|
|
||||||
{t('title', { keyPrefix: 'how_to_format' })}
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body
|
|
||||||
className="fmt small"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
</Col>
|
<Form.Control.Feedback type="invalid">
|
||||||
</Row>
|
{formData.title.errorMsg}
|
||||||
</Container>
|
</Form.Control.Feedback>
|
||||||
</>
|
{bool && <SearchQuestion similarQuestions={similarQuestions} />}
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="body">
|
||||||
|
<Form.Label>{t('form.fields.body.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
defaultValue={formData.content.value}
|
||||||
|
isInvalid={formData.content.isInvalid}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Editor
|
||||||
|
value={formData.content.value}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
className={classNames(
|
||||||
|
'form-control p-0',
|
||||||
|
focusType === 'content' && 'focus',
|
||||||
|
)}
|
||||||
|
onFocus={() => {
|
||||||
|
setForceType('content');
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setForceType('');
|
||||||
|
}}
|
||||||
|
ref={editorRef}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.content.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="tags" className="my-3">
|
||||||
|
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
defaultValue={JSON.stringify(formData.tags.value)}
|
||||||
|
isInvalid={formData.tags.isInvalid}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<TagSelector
|
||||||
|
value={formData.tags.value}
|
||||||
|
onChange={handleTagsChange}
|
||||||
|
showRequiredTagText
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.tags.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
{isEdit && (
|
||||||
|
<Form.Group controlId="edit_summary" className="my-3">
|
||||||
|
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
defaultValue={formData.edit_summary.value}
|
||||||
|
isInvalid={formData.edit_summary.isInvalid}
|
||||||
|
placeholder={t('form.fields.edit_summary.placeholder')}
|
||||||
|
onChange={handleSummaryChange}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.edit_summary.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
{!checked && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button type="submit" className="me-2">
|
||||||
|
{isEdit ? t('btn_save_edits') : t('btn_post_question')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="link" onClick={backPage}>
|
||||||
|
{t('cancel', { keyPrefix: 'btns' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isEdit && (
|
||||||
|
<>
|
||||||
|
<Form.Check
|
||||||
|
className="mt-5"
|
||||||
|
checked={checked}
|
||||||
|
type="checkbox"
|
||||||
|
label={t('answer_question')}
|
||||||
|
onChange={(e) => setCheckState(e.target.checked)}
|
||||||
|
id="radio-answer"
|
||||||
|
/>
|
||||||
|
{checked && (
|
||||||
|
<Form.Group controlId="answer" className="mt-4">
|
||||||
|
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
||||||
|
<Editor
|
||||||
|
value={formData.answer.value}
|
||||||
|
onChange={handleAnswerChange}
|
||||||
|
ref={editorRef2}
|
||||||
|
className={classNames(
|
||||||
|
'form-control p-0',
|
||||||
|
focusType === 'answer' && 'focus',
|
||||||
|
)}
|
||||||
|
onFocus={() => {
|
||||||
|
setForceType('answer');
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setForceType('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.answer.value}
|
||||||
|
type="text"
|
||||||
|
isInvalid={formData.answer.isInvalid}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.answer.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{checked && (
|
||||||
|
<Button type="submit" className="mt-3">
|
||||||
|
{t('post_question&answer')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<Card>
|
||||||
|
<Card.Header>
|
||||||
|
{t('title', { keyPrefix: 'how_to_format' })}
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body
|
||||||
|
className="fmt small"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ import {
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Pattern from '@/common/pattern';
|
import Pattern from '@/common/pattern';
|
||||||
import { Pagination, PageTitle } from '@/components';
|
import { Pagination } from '@/components';
|
||||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||||
import { scrollTop } from '@/utils';
|
import { scrollTop } from '@/utils';
|
||||||
import { usePageUsers } from '@/hooks';
|
import { usePageTags, usePageUsers } from '@/hooks';
|
||||||
import type {
|
import type {
|
||||||
ListResult,
|
ListResult,
|
||||||
QuestionDetailRes,
|
QuestionDetailRes,
|
||||||
|
@ -143,68 +143,69 @@ const Index = () => {
|
||||||
requestAnswers();
|
requestAnswers();
|
||||||
}
|
}
|
||||||
}, [page, order]);
|
}, [page, order]);
|
||||||
|
usePageTags({
|
||||||
|
title: question?.title,
|
||||||
|
description: question?.description,
|
||||||
|
keywords: question?.tags.map((_) => _.slug_name).join(','),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
|
||||||
<PageTitle title={question?.title} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5 questionDetailPage">
|
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
|
||||||
<Row className="justify-content-center">
|
{question?.operation?.operation_type && (
|
||||||
<Col xxl={7} lg={8} sm={12} className="mb-5 mb-md-0">
|
<Alert data={question.operation} />
|
||||||
{question?.operation?.operation_type && (
|
)}
|
||||||
<Alert data={question.operation} />
|
<Question
|
||||||
)}
|
data={question}
|
||||||
<Question
|
initPage={initPage}
|
||||||
data={question}
|
hasAnswer={answers.count > 0}
|
||||||
initPage={initPage}
|
isLogged={isLogged}
|
||||||
hasAnswer={answers.count > 0}
|
/>
|
||||||
isLogged={isLogged}
|
{answers.count > 0 && (
|
||||||
/>
|
<>
|
||||||
{answers.count > 0 && (
|
<AnswerHead count={answers.count} order={order} />
|
||||||
<>
|
{answers?.list?.map((item) => {
|
||||||
<AnswerHead count={answers.count} order={order} />
|
return (
|
||||||
{answers?.list?.map((item) => {
|
<Answer
|
||||||
return (
|
aid={aid}
|
||||||
<Answer
|
key={item?.id}
|
||||||
aid={aid}
|
data={item}
|
||||||
key={item?.id}
|
questionTitle={question?.title || ''}
|
||||||
data={item}
|
isAuthor={isAuthor}
|
||||||
questionTitle={question?.title || ''}
|
callback={initPage}
|
||||||
isAuthor={isAuthor}
|
isLogged={isLogged}
|
||||||
callback={initPage}
|
/>
|
||||||
isLogged={isLogged}
|
);
|
||||||
/>
|
})}
|
||||||
);
|
</>
|
||||||
})}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Math.ceil(answers.count / 15) > 1 && (
|
{Math.ceil(answers.count / 15) > 1 && (
|
||||||
<div className="d-flex justify-content-center answer-item pt-4">
|
<div className="d-flex justify-content-center answer-item pt-4">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={Number(page || 1)}
|
currentPage={Number(page || 1)}
|
||||||
pageSize={15}
|
pageSize={15}
|
||||||
totalSize={answers?.count || 0}
|
totalSize={answers?.count || 0}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!question?.operation?.operation_type && (
|
|
||||||
<WriteAnswer
|
|
||||||
visible={answers.count === 0}
|
|
||||||
data={{
|
|
||||||
qid,
|
|
||||||
answered: question?.answered,
|
|
||||||
}}
|
|
||||||
callback={writeAnswerCallback}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Col>
|
)}
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
|
||||||
<RelatedQuestions id={question?.id || ''} />
|
{!question?.operation?.operation_type && (
|
||||||
</Col>
|
<WriteAnswer
|
||||||
</Row>
|
visible={answers.count === 0}
|
||||||
</Container>
|
data={{
|
||||||
</>
|
qid,
|
||||||
|
answered: question?.answered,
|
||||||
|
}}
|
||||||
|
callback={writeAnswerCallback}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<RelatedQuestions id={question?.id || ''} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,9 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { pathFactory } from '@/router/pathFactory';
|
import { pathFactory } from '@/router/pathFactory';
|
||||||
import { Editor, EditorRef, Icon, PageTitle } from '@/components';
|
import { Editor, EditorRef, Icon } from '@/components';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import {
|
import {
|
||||||
useQueryAnswerInfo,
|
useQueryAnswerInfo,
|
||||||
|
@ -33,7 +34,7 @@ const initFormData = {
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const Ask = () => {
|
const Index = () => {
|
||||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||||
const { aid = '', qid = '' } = useParams();
|
const { aid = '', qid = '' } = useParams();
|
||||||
const [focusType, setForceType] = useState('');
|
const [focusType, setForceType] = useState('');
|
||||||
|
@ -133,128 +134,127 @@ const Ask = () => {
|
||||||
const backPage = () => {
|
const backPage = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('edit_answer', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
||||||
<PageTitle title={t('edit_answer', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
<Col xxl={10} md={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<Col xxl={10} md={12}>
|
</Col>
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
</Row>
|
||||||
</Col>
|
<Row className="justify-content-center">
|
||||||
</Row>
|
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||||
<Row className="justify-content-center">
|
<a
|
||||||
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
href={pathFactory.questionLanding(qid, data?.question.title)}
|
||||||
<a
|
target="_blank"
|
||||||
href={pathFactory.questionLanding(qid, data?.question.title)}
|
rel="noreferrer">
|
||||||
target="_blank"
|
<h5 className="mb-3">{data?.question.title}</h5>
|
||||||
rel="noreferrer">
|
</a>
|
||||||
<h5 className="mb-3">{data?.question.title}</h5>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className="question-content-wrap">
|
<div className="question-content-wrap">
|
||||||
<div
|
<div
|
||||||
ref={questionContentRef}
|
ref={questionContentRef}
|
||||||
className="content position-absolute top-0 w-100"
|
className="content position-absolute top-0 w-100"
|
||||||
dangerouslySetInnerHTML={{ __html: data?.question.html }}
|
dangerouslySetInnerHTML={{ __html: data?.question.html }}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="resize-bottom"
|
className="resize-bottom"
|
||||||
style={{ maxHeight: questionContentRef?.current?.scrollHeight }}
|
style={{ maxHeight: questionContentRef?.current?.scrollHeight }}
|
||||||
/>
|
/>
|
||||||
<div className="line bg-light d-flex justify-content-center align-items-center">
|
<div className="line bg-light d-flex justify-content-center align-items-center">
|
||||||
<Icon name="three-dots" />
|
<Icon name="three-dots" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group controlId="revision" className="mb-3">
|
<Form.Group controlId="revision" className="mb-3">
|
||||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||||
<Form.Select onChange={handleSelectedRevision}>
|
<Form.Select onChange={handleSelectedRevision}>
|
||||||
{revisions.map(({ create_at, reason, user_info }, index) => {
|
{revisions.map(({ create_at, reason, user_info }, index) => {
|
||||||
const date = dayjs(create_at * 1000)
|
const date = dayjs(create_at * 1000)
|
||||||
.tz()
|
.tz()
|
||||||
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
|
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
|
||||||
return (
|
return (
|
||||||
<option key={`${create_at}`} value={index}>
|
<option key={`${create_at}`} value={index}>
|
||||||
{`${date} - ${user_info.display_name} - ${
|
{`${date} - ${user_info.display_name} - ${
|
||||||
reason || t('default_reason')
|
reason || t('default_reason')
|
||||||
}`}
|
}`}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="answer" className="mt-3">
|
<Form.Group controlId="answer" className="mt-3">
|
||||||
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
||||||
<Editor
|
<Editor
|
||||||
value={formData.answer.value}
|
value={formData.answer.value}
|
||||||
onChange={handleAnswerChange}
|
onChange={handleAnswerChange}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'form-control p-0',
|
'form-control p-0',
|
||||||
focusType === 'answer' && 'focus',
|
focusType === 'answer' && 'focus',
|
||||||
)}
|
)}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
setForceType('answer');
|
setForceType('answer');
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setForceType('');
|
|
||||||
}}
|
|
||||||
ref={editorRef}
|
|
||||||
/>
|
|
||||||
<Form.Control
|
|
||||||
value={formData.answer.value}
|
|
||||||
type="text"
|
|
||||||
isInvalid={formData.answer.isInvalid}
|
|
||||||
readOnly
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.answer.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="edit_summary" className="my-3">
|
|
||||||
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
onChange={handleSummaryChange}
|
|
||||||
defaultValue={formData.description.value}
|
|
||||||
isInvalid={formData.description.isInvalid}
|
|
||||||
placeholder={t('form.fields.edit_summary.placeholder')}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.description.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<div className="mt-3">
|
|
||||||
<Button type="submit" className="me-2">
|
|
||||||
{t('btn_save_edits')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="link" onClick={backPage}>
|
|
||||||
{t('btn_cancel')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
|
||||||
<Card>
|
|
||||||
<Card.Header>
|
|
||||||
{t('title', { keyPrefix: 'how_to_format' })}
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body
|
|
||||||
className="fmt small"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: t('description', { keyPrefix: 'how_to_format' }),
|
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setForceType('');
|
||||||
|
}}
|
||||||
|
ref={editorRef}
|
||||||
/>
|
/>
|
||||||
</Card>
|
<Form.Control
|
||||||
</Col>
|
value={formData.answer.value}
|
||||||
</Row>
|
type="text"
|
||||||
</Container>
|
isInvalid={formData.answer.isInvalid}
|
||||||
</>
|
readOnly
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.answer.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="edit_summary" className="my-3">
|
||||||
|
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
onChange={handleSummaryChange}
|
||||||
|
defaultValue={formData.description.value}
|
||||||
|
isInvalid={formData.description.isInvalid}
|
||||||
|
placeholder={t('form.fields.edit_summary.placeholder')}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.description.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button type="submit" className="me-2">
|
||||||
|
{t('btn_save_edits')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="link" onClick={backPage}>
|
||||||
|
{t('btn_cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<Card>
|
||||||
|
<Card.Header>
|
||||||
|
{t('title', { keyPrefix: 'how_to_format' })}
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body
|
||||||
|
className="fmt small"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Ask;
|
export default Index;
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useMatch } from 'react-router-dom';
|
import { useMatch } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle, FollowingTags } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { FollowingTags } from '@/components';
|
||||||
import QuestionList from '@/components/QuestionList';
|
import QuestionList from '@/components/QuestionList';
|
||||||
import HotQuestions from '@/components/HotQuestions';
|
import HotQuestions from '@/components/HotQuestions';
|
||||||
import { siteInfoStore } from '@/stores';
|
import { siteInfoStore } from '@/stores';
|
||||||
|
@ -20,21 +21,19 @@ const Questions: FC = () => {
|
||||||
slogan = `${siteInfo.short_description}`;
|
slogan = `${siteInfo.short_description}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usePageTags({ title: pageTitle, subtitle: slogan });
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} suffix={slogan} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={7} lg={8} sm={12}>
|
||||||
<Row className="justify-content-center">
|
<QuestionList source="questions" />
|
||||||
<Col xxl={7} lg={8} sm={12}>
|
</Col>
|
||||||
<QuestionList source="questions" />
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
</Col>
|
<FollowingTags />
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
<HotQuestions />
|
||||||
<FollowingTags />
|
</Col>
|
||||||
<HotQuestions />
|
</Row>
|
||||||
</Col>
|
</Container>
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,8 @@ import { Container, Row, Col, Alert, Stack, Button } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import { usePageTags } from '@/hooks';
|
||||||
BaseUserCard,
|
import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
|
||||||
FormatTime,
|
|
||||||
Empty,
|
|
||||||
DiffContent,
|
|
||||||
PageTitle,
|
|
||||||
} from '@/components';
|
|
||||||
import { getReviewList, revisionAudit } from '@/services';
|
import { getReviewList, revisionAudit } from '@/services';
|
||||||
import { pathFactory } from '@/router/pathFactory';
|
import { pathFactory } from '@/router/pathFactory';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
@ -124,9 +119,11 @@ const Index: FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queryNextOne(page);
|
queryNextOne(page);
|
||||||
}, []);
|
}, []);
|
||||||
|
usePageTags({
|
||||||
|
title: t('review'),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container className="pt-2 mt-4 mb-5">
|
<Container className="pt-2 mt-4 mb-5">
|
||||||
<PageTitle title={t('review')} />
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={{ span: 7, offset: 1 }}>
|
<Col lg={{ span: 7, offset: 1 }}>
|
||||||
<h3 className="mb-4">{t('review')}</h3>
|
<h3 className="mb-4">{t('review')}</h3>
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Pagination, PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Pagination } from '@/components';
|
||||||
import { useSearch } from '@/services';
|
import { useSearch } from '@/services';
|
||||||
|
|
||||||
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
|
||||||
|
@ -27,38 +28,38 @@ const Index = () => {
|
||||||
if (q) {
|
if (q) {
|
||||||
pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
|
pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
|
||||||
}
|
}
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={7} lg={8} sm={12} className="mb-3">
|
||||||
<Row className="justify-content-center">
|
<Head data={extra} />
|
||||||
<Col xxl={7} lg={8} sm={12} className="mb-3">
|
|
||||||
<Head data={extra} />
|
|
||||||
|
|
||||||
<ListGroup variant="flush" className="mb-5">
|
<ListGroup variant="flush" className="mb-5">
|
||||||
<SearchHead sort={order} count={count} />
|
<SearchHead sort={order} count={count} />
|
||||||
|
|
||||||
{list?.map((item) => {
|
{list?.map((item) => {
|
||||||
return <SearchItem key={item.object.id} data={item} />;
|
return <SearchItem key={item.object.id} data={item} />;
|
||||||
})}
|
})}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
{!isLoading && !list?.length && <Empty />}
|
{!isLoading && !list?.length && <Empty />}
|
||||||
|
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={Number(page)}
|
currentPage={Number(page)}
|
||||||
pageSize={20}
|
pageSize={20}
|
||||||
totalSize={count}
|
totalSize={count}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
<Tips />
|
<Tips />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import * as Type from '@/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import { PageTitle, FollowingTags } from '@/components';
|
import { FollowingTags } from '@/components';
|
||||||
import { useTagInfo, useFollow } from '@/services';
|
import { useTagInfo, useFollow, useQuerySynonymsTags } from '@/services';
|
||||||
import QuestionList from '@/components/QuestionList';
|
import QuestionList from '@/components/QuestionList';
|
||||||
import HotQuestions from '@/components/HotQuestions';
|
import HotQuestions from '@/components/HotQuestions';
|
||||||
import { escapeRemove } from '@/utils';
|
import { escapeRemove } from '@/utils';
|
||||||
|
@ -20,7 +21,7 @@ const Questions: FC = () => {
|
||||||
const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
|
const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
|
||||||
const { data: tagResp } = useTagInfo({ name: curTagName });
|
const { data: tagResp } = useTagInfo({ name: curTagName });
|
||||||
const { data: followResp } = useFollow(tagFollow);
|
const { data: followResp } = useFollow(tagFollow);
|
||||||
|
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
|
||||||
const toggleFollow = () => {
|
const toggleFollow = () => {
|
||||||
setTagFollow({
|
setTagFollow({
|
||||||
is_cancel: tagInfo.is_follower,
|
is_cancel: tagInfo.is_follower,
|
||||||
|
@ -51,57 +52,66 @@ const Questions: FC = () => {
|
||||||
}
|
}
|
||||||
}, [tagResp, followResp]);
|
}, [tagResp, followResp]);
|
||||||
let pageTitle = '';
|
let pageTitle = '';
|
||||||
if (tagInfo) {
|
if (tagInfo?.display_name) {
|
||||||
pageTitle = `'${tagInfo.display_name}' ${t('questions', {
|
pageTitle = `'${tagInfo.display_name}' ${t('questions', {
|
||||||
keyPrefix: 'page_title',
|
keyPrefix: 'page_title',
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
|
const keywords: string[] = [];
|
||||||
|
if (tagInfo?.slug_name) {
|
||||||
|
keywords.push(tagInfo.slug_name);
|
||||||
|
}
|
||||||
|
synonymsRes?.synonyms.forEach((o) => {
|
||||||
|
keywords.push(o.slug_name);
|
||||||
|
});
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
description: tagInfo?.description,
|
||||||
|
keywords: keywords.join(','),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={7} lg={8} sm={12}>
|
||||||
<Row className="justify-content-center">
|
<div className="tag-box mb-5">
|
||||||
<Col xxl={7} lg={8} sm={12}>
|
<h3 className="mb-3">
|
||||||
<div className="tag-box mb-5">
|
<Link
|
||||||
<h3 className="mb-3">
|
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||||
<Link
|
replace
|
||||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
className="link-dark">
|
||||||
replace
|
{tagInfo.display_name}
|
||||||
className="link-dark">
|
</Link>
|
||||||
{tagInfo.display_name}
|
</h3>
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="text-break">
|
<p className="text-break">
|
||||||
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
|
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
|
||||||
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
|
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
|
||||||
[{t('more')}]
|
[{t('more')}]
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="box-ft">
|
<div className="box-ft">
|
||||||
{tagInfo.is_follower ? (
|
{tagInfo.is_follower ? (
|
||||||
<Button variant="primary" onClick={() => toggleFollow()}>
|
<Button variant="primary" onClick={() => toggleFollow()}>
|
||||||
{t('button_following')}
|
{t('button_following')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
onClick={() => toggleFollow()}>
|
onClick={() => toggleFollow()}>
|
||||||
{t('button_follow')}
|
{t('button_follow')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<QuestionList source="tag" />
|
</div>
|
||||||
</Col>
|
<QuestionList source="tag" />
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
</Col>
|
||||||
<FollowingTags />
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
<HotQuestions />
|
<FollowingTags />
|
||||||
</Col>
|
<HotQuestions />
|
||||||
</Row>
|
</Col>
|
||||||
</Container>
|
</Row>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Editor, EditorRef, PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Editor, EditorRef } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
|
import { useTagInfo, modifyTag, useQueryRevisions } from '@/services';
|
||||||
|
@ -39,7 +40,7 @@ const initFormData = {
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const Ask = () => {
|
const Index = () => {
|
||||||
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||||
|
|
||||||
const { tagId } = useParams();
|
const { tagId } = useParams();
|
||||||
|
@ -143,132 +144,129 @@ const Ask = () => {
|
||||||
const backPage = () => {
|
const backPage = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('edit_tag', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
||||||
<PageTitle title={t('edit_tag', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
<Col xxl={10} md={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<Col xxl={10} md={12}>
|
</Col>
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
</Row>
|
||||||
</Col>
|
<Row className="justify-content-center">
|
||||||
</Row>
|
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||||
<Row className="justify-content-center">
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
<Form.Group controlId="revision" className="mb-3">
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||||
<Form.Group controlId="revision" className="mb-3">
|
<Form.Select onChange={handleSelectedRevision}>
|
||||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
{revisions.map(({ create_at, reason, user_info }, index) => {
|
||||||
<Form.Select onChange={handleSelectedRevision}>
|
const date = dayjs(create_at * 1000)
|
||||||
{revisions.map(({ create_at, reason, user_info }, index) => {
|
.tz()
|
||||||
const date = dayjs(create_at * 1000)
|
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
|
||||||
.tz()
|
return (
|
||||||
.format(t('long_date_with_time', { keyPrefix: 'dates' }));
|
<option key={`${create_at}`} value={index}>
|
||||||
return (
|
{`${date} - ${user_info.display_name} - ${
|
||||||
<option key={`${create_at}`} value={index}>
|
reason || t('default_reason')
|
||||||
{`${date} - ${user_info.display_name} - ${
|
}`}
|
||||||
reason || t('default_reason')
|
</option>
|
||||||
}`}
|
);
|
||||||
</option>
|
})}
|
||||||
);
|
</Form.Select>
|
||||||
})}
|
</Form.Group>
|
||||||
</Form.Select>
|
<Form.Group controlId="display_name" className="mb-3">
|
||||||
</Form.Group>
|
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||||
<Form.Group controlId="display_name" className="mb-3">
|
<Form.Control
|
||||||
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
value={formData.displayName.value}
|
||||||
<Form.Control
|
isInvalid={formData.displayName.isInvalid}
|
||||||
value={formData.displayName.value}
|
disabled={!is_admin}
|
||||||
isInvalid={formData.displayName.isInvalid}
|
onChange={handleDisplayNameChange}
|
||||||
disabled={!is_admin}
|
|
||||||
onChange={handleDisplayNameChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.displayName.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="slug_name" className="mb-3">
|
|
||||||
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
value={formData.slugName.value}
|
|
||||||
isInvalid={formData.slugName.isInvalid}
|
|
||||||
disabled={!is_admin}
|
|
||||||
onChange={handleSlugNameChange}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">
|
|
||||||
{t('form.fields.slug_name.info')}
|
|
||||||
</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.slugName.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="description" className="mt-4">
|
|
||||||
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
|
||||||
<Editor
|
|
||||||
value={formData.description.value}
|
|
||||||
onChange={handleDescriptionChange}
|
|
||||||
className={classNames(
|
|
||||||
'form-control p-0',
|
|
||||||
focusType === 'description' && 'focus',
|
|
||||||
)}
|
|
||||||
onFocus={() => {
|
|
||||||
setForceType('description');
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setForceType('');
|
|
||||||
}}
|
|
||||||
ref={editorRef}
|
|
||||||
/>
|
|
||||||
<Form.Control
|
|
||||||
value={formData.description.value}
|
|
||||||
type="text"
|
|
||||||
isInvalid={formData.description.isInvalid}
|
|
||||||
readOnly
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.description.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="edit_summary" className="my-3">
|
|
||||||
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
defaultValue={formData.editSummary.value}
|
|
||||||
isInvalid={formData.editSummary.isInvalid}
|
|
||||||
onChange={handleEditSummaryChange}
|
|
||||||
placeholder={t('form.fields.edit_summary.placeholder')}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.editSummary.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<div className="mt-3">
|
|
||||||
<Button type="submit">{t('btn_save_edits')}</Button>
|
|
||||||
<Button variant="link" className="ms-2" onClick={backPage}>
|
|
||||||
{t('btn_cancel')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
|
||||||
<Card>
|
|
||||||
<Card.Header>
|
|
||||||
{t('title', { keyPrefix: 'how_to_format' })}
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body
|
|
||||||
className="fmt small"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
</Col>
|
<Form.Control.Feedback type="invalid">
|
||||||
</Row>
|
{formData.displayName.errorMsg}
|
||||||
</Container>
|
</Form.Control.Feedback>
|
||||||
</>
|
</Form.Group>
|
||||||
|
<Form.Group controlId="slug_name" className="mb-3">
|
||||||
|
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.slugName.value}
|
||||||
|
isInvalid={formData.slugName.isInvalid}
|
||||||
|
disabled={!is_admin}
|
||||||
|
onChange={handleSlugNameChange}
|
||||||
|
/>
|
||||||
|
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.slugName.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="description" className="mt-4">
|
||||||
|
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
||||||
|
<Editor
|
||||||
|
value={formData.description.value}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
className={classNames(
|
||||||
|
'form-control p-0',
|
||||||
|
focusType === 'description' && 'focus',
|
||||||
|
)}
|
||||||
|
onFocus={() => {
|
||||||
|
setForceType('description');
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setForceType('');
|
||||||
|
}}
|
||||||
|
ref={editorRef}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.description.value}
|
||||||
|
type="text"
|
||||||
|
isInvalid={formData.description.isInvalid}
|
||||||
|
readOnly
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.description.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="edit_summary" className="my-3">
|
||||||
|
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
defaultValue={formData.editSummary.value}
|
||||||
|
isInvalid={formData.editSummary.isInvalid}
|
||||||
|
onChange={handleEditSummaryChange}
|
||||||
|
placeholder={t('form.fields.edit_summary.placeholder')}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.editSummary.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button type="submit">{t('btn_save_edits')}</Button>
|
||||||
|
<Button variant="link" className="ms-2" onClick={backPage}>
|
||||||
|
{t('btn_cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<Card>
|
||||||
|
<Card.Header>
|
||||||
|
{t('title', { keyPrefix: 'how_to_format' })}
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body
|
||||||
|
className="fmt small"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Ask;
|
export default Index;
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Tag, TagSelector, FormatTime, Modal } from '@/components';
|
||||||
import {
|
import {
|
||||||
useTagInfo,
|
useTagInfo,
|
||||||
useQuerySynonymsTags,
|
useQuerySynonymsTags,
|
||||||
|
@ -26,7 +27,15 @@ const TagIntroduction = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'tag_info' });
|
const { t } = useTranslation('translation', { keyPrefix: 'tag_info' });
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: synonymsData, mutate } = useQuerySynonymsTags(tagInfo?.tag_id);
|
const { data: synonymsData, mutate } = useQuerySynonymsTags(tagInfo?.tag_id);
|
||||||
|
let pageTitle = '';
|
||||||
|
if (tagInfo) {
|
||||||
|
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
|
||||||
|
keyPrefix: 'page_title',
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (locationState?.isReview) {
|
if (locationState?.isReview) {
|
||||||
toastStore.getState().show({
|
toastStore.getState().show({
|
||||||
|
@ -35,7 +44,6 @@ const TagIntroduction = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [locationState]);
|
}, [locationState]);
|
||||||
|
|
||||||
if (!tagInfo) {
|
if (!tagInfo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -100,145 +108,132 @@ const TagIntroduction = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let pageTitle = '';
|
|
||||||
if (tagInfo) {
|
|
||||||
pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {
|
|
||||||
keyPrefix: 'page_title',
|
|
||||||
})}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={7} lg={8} sm={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-3">
|
||||||
<Col xxl={7} lg={8} sm={12}>
|
<Link
|
||||||
<h3 className="mb-3">
|
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||||
<Link
|
replace
|
||||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
className="link-dark">
|
||||||
replace
|
{tagInfo.display_name}
|
||||||
className="link-dark">
|
</Link>
|
||||||
{tagInfo.display_name}
|
</h3>
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="text-secondary mb-4 fs-14">
|
<div className="text-secondary mb-4 fs-14">
|
||||||
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
|
<FormatTime preFix={t('created_at')} time={tagInfo.created_at} />
|
||||||
<FormatTime
|
<FormatTime
|
||||||
preFix={t('edited_at')}
|
preFix={t('edited_at')}
|
||||||
className="ms-3"
|
className="ms-3"
|
||||||
time={tagInfo.updated_at}
|
time={tagInfo.updated_at}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="content text-break"
|
|
||||||
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
</div>
|
||||||
{tagInfo?.member_actions.map((action, index) => {
|
|
||||||
return (
|
<div
|
||||||
<Button
|
className="content text-break"
|
||||||
key={action.name}
|
dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}
|
||||||
variant="link"
|
/>
|
||||||
className={classNames(
|
<div className="mt-4">
|
||||||
'link-secondary btn-no-border p-0 fs-14',
|
{tagInfo?.member_actions.map((action, index) => {
|
||||||
index > 0 && 'ms-3',
|
return (
|
||||||
)}
|
<Button
|
||||||
onClick={() => onAction(action)}>
|
key={action.name}
|
||||||
{action.name}
|
variant="link"
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLogged && (
|
|
||||||
<Link
|
|
||||||
to={`/tags/${tagInfo?.tag_id}/timeline`}
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'link-secondary btn-no-border p-0 fs-14',
|
'link-secondary btn-no-border p-0 fs-14',
|
||||||
tagInfo?.member_actions?.length > 0 && 'ms-3',
|
index > 0 && 'ms-3',
|
||||||
)}>
|
)}
|
||||||
{t('history')}
|
onClick={() => onAction(action)}>
|
||||||
</Link>
|
{action.name}
|
||||||
)}
|
</Button>
|
||||||
</div>
|
);
|
||||||
</Col>
|
})}
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
{isLogged && (
|
||||||
<Card>
|
<Link
|
||||||
<Card.Header className="d-flex justify-content-between">
|
to={`/tags/${tagInfo?.tag_id}/timeline`}
|
||||||
<span>{t('synonyms.title')}</span>
|
className={classNames(
|
||||||
{isEdit ? (
|
'link-secondary btn-no-border p-0 fs-14',
|
||||||
<Button
|
tagInfo?.member_actions?.length > 0 && 'ms-3',
|
||||||
variant="link"
|
)}>
|
||||||
className="p-0 btn-no-border"
|
{t('history')}
|
||||||
onClick={handleSave}>
|
</Link>
|
||||||
{t('synonyms.btn_save')}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
) : synonymsData?.member_actions?.find(
|
</Col>
|
||||||
(v) => v.action === 'edit',
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
) ? (
|
<Card>
|
||||||
<Button
|
<Card.Header className="d-flex justify-content-between">
|
||||||
variant="link"
|
<span>{t('synonyms.title')}</span>
|
||||||
className="p-0 btn-no-border"
|
{isEdit ? (
|
||||||
onClick={handleEdit}>
|
<Button
|
||||||
{t('synonyms.btn_edit')}
|
variant="link"
|
||||||
</Button>
|
className="p-0 btn-no-border"
|
||||||
) : null}
|
onClick={handleSave}>
|
||||||
</Card.Header>
|
{t('synonyms.btn_save')}
|
||||||
<Card.Body>
|
</Button>
|
||||||
{isEdit && (
|
) : synonymsData?.member_actions?.find(
|
||||||
<>
|
(v) => v.action === 'edit',
|
||||||
<div className="mb-3">
|
) ? (
|
||||||
{t('synonyms.text')}{' '}
|
<Button
|
||||||
<Tag
|
variant="link"
|
||||||
data={{
|
className="p-0 btn-no-border"
|
||||||
slug_name: tagName || '',
|
onClick={handleEdit}>
|
||||||
main_tag_slug_name: '',
|
{t('synonyms.btn_edit')}
|
||||||
display_name: '',
|
</Button>
|
||||||
recommend: false,
|
) : null}
|
||||||
reserved: false,
|
</Card.Header>
|
||||||
}}
|
<Card.Body>
|
||||||
/>
|
{isEdit && (
|
||||||
</div>
|
<>
|
||||||
<TagSelector
|
<div className="mb-3">
|
||||||
value={synonymsData?.synonyms}
|
{t('synonyms.text')}{' '}
|
||||||
onChange={handleTagsChange}
|
<Tag
|
||||||
hiddenDescription
|
data={{
|
||||||
|
slug_name: tagName || '',
|
||||||
|
main_tag_slug_name: '',
|
||||||
|
display_name: '',
|
||||||
|
recommend: false,
|
||||||
|
reserved: false,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<TagSelector
|
||||||
|
value={synonymsData?.synonyms}
|
||||||
|
onChange={handleTagsChange}
|
||||||
|
hiddenDescription
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isEdit &&
|
||||||
|
(synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? (
|
||||||
|
<div className="m-n1">
|
||||||
|
{synonymsData.synonyms.map((item) => {
|
||||||
|
return (
|
||||||
|
<Tag key={item.tag_id} className="m-1" data={item} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-muted mb-3">{t('synonyms.empty')}</div>
|
||||||
|
{synonymsData?.member_actions?.find(
|
||||||
|
(v) => v.action === 'edit',
|
||||||
|
) && (
|
||||||
|
<Button
|
||||||
|
variant="outline-primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleEdit}>
|
||||||
|
{t('synonyms.btn_add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
))}
|
||||||
{!isEdit &&
|
</Card.Body>
|
||||||
(synonymsData?.synonyms &&
|
</Card>
|
||||||
synonymsData.synonyms.length > 0 ? (
|
</Col>
|
||||||
<div className="m-n1">
|
</Row>
|
||||||
{synonymsData.synonyms.map((item) => {
|
</Container>
|
||||||
return (
|
|
||||||
<Tag key={item.tag_id} className="m-1" data={item} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="text-muted mb-3">
|
|
||||||
{t('synonyms.empty')}
|
|
||||||
</div>
|
|
||||||
{synonymsData?.member_actions?.find(
|
|
||||||
(v) => v.action === 'edit',
|
|
||||||
) && (
|
|
||||||
<Button
|
|
||||||
variant="outline-primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleEdit}>
|
|
||||||
{t('synonyms.btn_add')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Tag, Pagination, PageTitle, QueryGroup } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Tag, Pagination, QueryGroup } from '@/components';
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
import { useQueryTags, following } from '@/services';
|
import { useQueryTags, following } from '@/services';
|
||||||
|
|
||||||
|
@ -37,81 +38,81 @@ const Tags = () => {
|
||||||
mutate();
|
mutate();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('tags', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="py-3 my-3">
|
||||||
<PageTitle title={t('tags', { keyPrefix: 'page_title' })} />
|
<Row className="mb-4 d-flex justify-content-center">
|
||||||
<Container className="py-3 my-3">
|
<Col xxl={10} sm={12}>
|
||||||
<Row className="mb-4 d-flex justify-content-center">
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<Col xxl={10} sm={12}>
|
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
<Form>
|
||||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
<Form.Group controlId="formBasicEmail">
|
||||||
<Form>
|
<Form.Control
|
||||||
<Form.Group controlId="formBasicEmail">
|
value={searchTag}
|
||||||
<Form.Control
|
placeholder={t('search_placeholder')}
|
||||||
value={searchTag}
|
type="text"
|
||||||
placeholder={t('search_placeholder')}
|
onChange={handleChange}
|
||||||
type="text"
|
size="sm"
|
||||||
onChange={handleChange}
|
/>
|
||||||
size="sm"
|
</Form.Group>
|
||||||
/>
|
</Form>
|
||||||
</Form.Group>
|
<QueryGroup
|
||||||
</Form>
|
data={sortBtns}
|
||||||
<QueryGroup
|
currentSort={sort || 'popular'}
|
||||||
data={sortBtns}
|
sortKey="sort"
|
||||||
currentSort={sort || 'popular'}
|
i18nKeyPrefix="tags.sort_buttons"
|
||||||
sortKey="sort"
|
/>
|
||||||
i18nKeyPrefix="tags.sort_buttons"
|
</div>
|
||||||
/>
|
</Col>
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col className="mt-4" xxl={10} sm={12}>
|
<Col className="mt-4" xxl={10} sm={12}>
|
||||||
<Row>
|
<Row>
|
||||||
{tags?.list?.map((tag) => (
|
{tags?.list?.map((tag) => (
|
||||||
<Col
|
<Col
|
||||||
key={tag.slug_name}
|
key={tag.slug_name}
|
||||||
xs={12}
|
xs={12}
|
||||||
lg={3}
|
lg={3}
|
||||||
md={4}
|
md={4}
|
||||||
sm={6}
|
sm={6}
|
||||||
className="mb-4">
|
className="mb-4">
|
||||||
<Card className="h-100">
|
<Card className="h-100">
|
||||||
<Card.Body className="d-flex flex-column align-items-start">
|
<Card.Body className="d-flex flex-column align-items-start">
|
||||||
<Tag className="mb-3" data={tag} />
|
<Tag className="mb-3" data={tag} />
|
||||||
|
|
||||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
|
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
|
||||||
{tag.original_text}
|
{tag.original_text}
|
||||||
</p>
|
</p>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Button
|
<Button
|
||||||
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
|
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleFollow(tag)}>
|
onClick={() => handleFollow(tag)}>
|
||||||
{tag.is_follower
|
{tag.is_follower
|
||||||
? t('button_following')
|
? t('button_following')
|
||||||
: t('button_follow')}
|
: t('button_follow')}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-secondary fs-14 text-nowrap">
|
<span className="text-secondary fs-14 text-nowrap">
|
||||||
{formatCount(tag.question_count)} {t('tag_label')}
|
{formatCount(tag.question_count)} {t('tag_label')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
totalSize={tags?.count || 0}
|
totalSize={tags?.count || 0}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@ import { Container, Row, Col, Form, Table } from 'react-bootstrap';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { pathFactory } from '@/router/pathFactory';
|
import { pathFactory } from '@/router/pathFactory';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { getTimelineData } from '@/services';
|
import { getTimelineData } from '@/services';
|
||||||
import { PageTitle, Empty } from '@/components';
|
import { Empty } from '@/components';
|
||||||
import * as Type from '@/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
|
|
||||||
import HistoryItem from './components/Item';
|
import HistoryItem from './components/Item';
|
||||||
|
@ -74,10 +75,11 @@ const Index: FC = () => {
|
||||||
|
|
||||||
const revisionList =
|
const revisionList =
|
||||||
timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
|
timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container className="py-3">
|
<Container className="py-3">
|
||||||
<PageTitle title={pageTitle} />
|
|
||||||
<Row className="py-3 justify-content-center">
|
<Row className="py-3 justify-content-center">
|
||||||
<Col xxl={10}>
|
<Col xxl={10}>
|
||||||
<h5 className="mb-4">
|
<h5 className="mb-4">
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { Container, Col } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
|
||||||
import SendEmail from './components/sendEmail';
|
import SendEmail from './components/sendEmail';
|
||||||
|
|
||||||
|
@ -15,32 +15,31 @@ const Index: React.FC = () => {
|
||||||
setStep(param);
|
setStep(param);
|
||||||
setEmail(mail);
|
setEmail(mail);
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('account_recovery', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||||
<PageTitle title={t('account_recovery', { keyPrefix: 'page_title' })} />
|
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
{step === 1 && (
|
||||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
<Col className="mx-auto" md={3}>
|
||||||
{step === 1 && (
|
<SendEmail visible={step === 1} callback={callback} />
|
||||||
<Col className="mx-auto" md={3}>
|
</Col>
|
||||||
<SendEmail visible={step === 1} callback={callback} />
|
)}
|
||||||
</Col>
|
{step === 2 && (
|
||||||
)}
|
<Col className="mx-auto px-4" md={6}>
|
||||||
{step === 2 && (
|
<div className="text-center">
|
||||||
<Col className="mx-auto px-4" md={6}>
|
<p>
|
||||||
<div className="text-center">
|
<Trans
|
||||||
<p>
|
i18nKey="account_forgot.send_success"
|
||||||
<Trans
|
values={{ mail: email }}
|
||||||
i18nKey="account_forgot.send_success"
|
components={{ bold: <strong /> }}
|
||||||
values={{ mail: email }}
|
/>
|
||||||
components={{ bold: <strong /> }}
|
</p>
|
||||||
/>
|
</div>
|
||||||
</p>
|
</Col>
|
||||||
</div>
|
)}
|
||||||
</Col>
|
</Container>
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,34 +3,34 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
usePageTags({
|
||||||
|
title: t('account_activation', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={t('account_activation', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col lg={6}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
||||||
<Col lg={6}>
|
{location.pathname?.includes('success') && (
|
||||||
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
<>
|
||||||
{location.pathname?.includes('success') && (
|
<p className="text-center">{t('success')}</p>
|
||||||
<>
|
<div className="text-center">
|
||||||
<p className="text-center">{t('success')}</p>
|
<Link to="/">{t('link')}</Link>
|
||||||
<div className="text-center">
|
</div>
|
||||||
<Link to="/">{t('link')}</Link>
|
</>
|
||||||
</div>
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{location.pathname?.includes('failed') && (
|
{location.pathname?.includes('failed') && (
|
||||||
<p className="text-center">{t('invalid')}</p>
|
<p className="text-center">{t('invalid')}</p>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { FC, memo, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { activateAccount } from '@/services';
|
import { activateAccount } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
|
@ -25,7 +25,10 @@ const Index: FC = () => {
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
return <PageTitle title={t('account_activation')} />;
|
usePageTags({
|
||||||
|
title: t('account_activation'),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Index);
|
export default memo(Index);
|
||||||
|
|
|
@ -2,23 +2,22 @@ import { FC, memo } from 'react';
|
||||||
import { Container, Col } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
|
||||||
import SendEmail from './components/sendEmail';
|
import SendEmail from './components/sendEmail';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
||||||
|
usePageTags({
|
||||||
|
title: t('change_email', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||||
<PageTitle title={t('change_email', { keyPrefix: 'page_title' })} />
|
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
<Col className="mx-auto" md={3}>
|
||||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
<SendEmail />
|
||||||
<Col className="mx-auto" md={3}>
|
</Col>
|
||||||
<SendEmail />
|
</Container>
|
||||||
</Col>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
|
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||||
|
@ -31,30 +31,29 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
usePageTags({
|
||||||
|
title: t('confirm_email', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={t('confirm_email', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col lg={6}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
||||||
<Col lg={6}>
|
{step === 'success' && (
|
||||||
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
<>
|
||||||
{step === 'success' && (
|
<p className="text-center">{t('confirm_new_email')}</p>
|
||||||
<>
|
<div className="text-center">
|
||||||
<p className="text-center">{t('confirm_new_email')}</p>
|
<Link to="/">{t('link')}</Link>
|
||||||
<div className="text-center">
|
</div>
|
||||||
<Link to="/">{t('link')}</Link>
|
</>
|
||||||
</div>
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'invalid' && (
|
{step === 'invalid' && (
|
||||||
<p className="text-center">{t('confirm_new_email_invalid')}</p>
|
<p className="text-center">{t('confirm_new_email_invalid')}</p>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import type {
|
import type {
|
||||||
LoginReqParams,
|
LoginReqParams,
|
||||||
ImgCodeRes,
|
ImgCodeRes,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { PageTitle, Unactivate } from '@/components';
|
import { Unactivate } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||||
import { login, checkImgCode } from '@/services';
|
import { login, checkImgCode } from '@/services';
|
||||||
|
@ -167,11 +168,12 @@ const Index: React.FC = () => {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
usePageTags({
|
||||||
|
title: t('login', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||||
<PageTitle title={t('login', { keyPrefix: 'page_title' })} />
|
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<Col className="mx-auto" md={3}>
|
<Col className="mx-auto" md={3}>
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { PageTitle } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
useQueryNotifications,
|
useQueryNotifications,
|
||||||
clearUnreadNotification,
|
clearUnreadNotification,
|
||||||
|
@ -66,67 +66,66 @@ const Notifications = () => {
|
||||||
const handleReadNotification = (id) => {
|
const handleReadNotification = (id) => {
|
||||||
readNotification(id);
|
readNotification(id);
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('notifications', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={t('notifications', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Col xxl={7} lg={8} sm={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<Col xxl={7} lg={8} sm={12}>
|
<div className="d-flex justify-content-between mb-3">
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
<ButtonGroup size="sm">
|
||||||
<div className="d-flex justify-content-between mb-3">
|
|
||||||
<ButtonGroup size="sm">
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
href="/users/notifications/inbox"
|
|
||||||
variant="outline-secondary"
|
|
||||||
active={type === 'inbox'}
|
|
||||||
onClick={(evt) => handleTypeChange(evt, 'inbox')}>
|
|
||||||
{t('inbox')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
href="/users/notifications/achievement"
|
|
||||||
variant="outline-secondary"
|
|
||||||
active={type === 'achievement'}
|
|
||||||
onClick={(evt) => handleTypeChange(evt, 'achievement')}>
|
|
||||||
{t('achievement')}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
as="a"
|
||||||
|
href="/users/notifications/inbox"
|
||||||
variant="outline-secondary"
|
variant="outline-secondary"
|
||||||
onClick={handleUnreadNotification}>
|
active={type === 'inbox'}
|
||||||
{t('all_read')}
|
onClick={(evt) => handleTypeChange(evt, 'inbox')}>
|
||||||
|
{t('inbox')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
href="/users/notifications/achievement"
|
||||||
|
variant="outline-secondary"
|
||||||
|
active={type === 'achievement'}
|
||||||
|
onClick={(evt) => handleTypeChange(evt, 'achievement')}>
|
||||||
|
{t('achievement')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline-secondary"
|
||||||
|
onClick={handleUnreadNotification}>
|
||||||
|
{t('all_read')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{type === 'inbox' && (
|
||||||
|
<Inbox
|
||||||
|
data={notificationData}
|
||||||
|
handleReadNotification={handleReadNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === 'achievement' && (
|
||||||
|
<Achievements
|
||||||
|
data={notificationData}
|
||||||
|
handleReadNotification={handleReadNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(data?.count || 0) > PAGE_SIZE * page && (
|
||||||
|
<div className="d-flex justify-content-center align-items-center py-3">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="btn-no-border"
|
||||||
|
onClick={handleLoadMore}>
|
||||||
|
{t('show_more')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{type === 'inbox' && (
|
)}
|
||||||
<Inbox
|
</Col>
|
||||||
data={notificationData}
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
|
||||||
handleReadNotification={handleReadNotification}
|
</Row>
|
||||||
/>
|
</Container>
|
||||||
)}
|
|
||||||
{type === 'achievement' && (
|
|
||||||
<Achievements
|
|
||||||
data={notificationData}
|
|
||||||
handleReadNotification={handleReadNotification}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(data?.count || 0) > PAGE_SIZE * page && (
|
|
||||||
<div className="d-flex justify-content-center align-items-center py-3">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="btn-no-border"
|
|
||||||
onClick={handleLoadMore}>
|
|
||||||
{t('show_more')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0" />
|
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { Container, Col, Form, Button } from 'react-bootstrap';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { replacementPassword } from '@/services';
|
import { replacementPassword } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
import { handleFormError } from '@/utils';
|
import { handleFormError } from '@/utils';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
|
@ -112,89 +112,88 @@ const Index: React.FC = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('account_recovery', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||||
<PageTitle title={t('account_recovery', { keyPrefix: 'page_title' })} />
|
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
{step === 1 && (
|
||||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
<Col className="mx-auto" md={3}>
|
||||||
{step === 1 && (
|
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||||
<Col className="mx-auto" md={3}>
|
<Form.Group controlId="email" className="mb-3">
|
||||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
<Form.Label>{t('password.label')}</Form.Label>
|
||||||
<Form.Group controlId="email" className="mb-3">
|
<Form.Control
|
||||||
<Form.Label>{t('password.label')}</Form.Label>
|
autoComplete="off"
|
||||||
<Form.Control
|
required
|
||||||
autoComplete="off"
|
type="password"
|
||||||
required
|
maxLength={32}
|
||||||
type="password"
|
isInvalid={formData.pass.isInvalid}
|
||||||
maxLength={32}
|
onChange={(e) => {
|
||||||
isInvalid={formData.pass.isInvalid}
|
handleChange({
|
||||||
onChange={(e) => {
|
pass: {
|
||||||
handleChange({
|
value: e.target.value,
|
||||||
pass: {
|
isInvalid: false,
|
||||||
value: e.target.value,
|
errorMsg: '',
|
||||||
isInvalid: false,
|
},
|
||||||
errorMsg: '',
|
});
|
||||||
},
|
}}
|
||||||
});
|
/>
|
||||||
}}
|
<Form.Control.Feedback type="invalid">
|
||||||
/>
|
{formData.pass.errorMsg}
|
||||||
<Form.Control.Feedback type="invalid">
|
</Form.Control.Feedback>
|
||||||
{formData.pass.errorMsg}
|
</Form.Group>
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="password" className="mb-3">
|
<Form.Group controlId="password" className="mb-3">
|
||||||
<Form.Label>{t('password_confirm.label')}</Form.Label>
|
<Form.Label>{t('password_confirm.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
isInvalid={formData.passSecond.isInvalid}
|
isInvalid={formData.passSecond.isInvalid}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange({
|
handleChange({
|
||||||
passSecond: {
|
passSecond: {
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData.passSecond.errorMsg}
|
{formData.passSecond.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<div className="d-grid mb-3">
|
<div className="d-grid mb-3">
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t('btn_name')}
|
{t('btn_name')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<Col className="mx-auto px-4" md={6}>
|
|
||||||
<div className="text-center">
|
|
||||||
<p>{t('reset_success')}</p>
|
|
||||||
<Link to="/users/login">{t('to_login')}</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Form>
|
||||||
)}
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === 3 && (
|
{step === 2 && (
|
||||||
<Col className="mx-auto px-4" md={6}>
|
<Col className="mx-auto px-4" md={6}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p>{t('link_invalid')}</p>
|
<p>{t('reset_success')}</p>
|
||||||
<Link to="/users/login">{t('to_login')}</Link>
|
<Link to="/users/login">{t('to_login')}</Link>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Container>
|
|
||||||
</>
|
{step === 3 && (
|
||||||
|
<Col className="mx-auto px-4" md={6}>
|
||||||
|
<div className="text-center">
|
||||||
|
<p>{t('link_invalid')}</p>
|
||||||
|
<Link to="/users/login">{t('to_login')}</Link>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Pagination, FormatTime, PageTitle, Empty } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Pagination, FormatTime, Empty } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import {
|
import {
|
||||||
usePersonalInfoByName,
|
usePersonalInfoByName,
|
||||||
|
@ -50,9 +51,11 @@ const Personal: FC = () => {
|
||||||
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`;
|
pageTitle = `${userInfo.info.display_name} (${userInfo.info.username})`;
|
||||||
}
|
}
|
||||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||||
|
usePageTags({
|
||||||
|
title: pageTitle,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<PageTitle title={pageTitle} />
|
|
||||||
<Row className="justify-content-center">
|
<Row className="justify-content-center">
|
||||||
{userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && (
|
{userInfo?.info?.status !== 'normal' && userInfo?.info?.status_msg && (
|
||||||
<Alert data={userInfo?.info.status_msg} />
|
<Alert data={userInfo?.info.status_msg} />
|
||||||
|
|
|
@ -2,7 +2,8 @@ import React, { useState } from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageTitle, Unactivate } from '@/components';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { Unactivate } from '@/components';
|
||||||
|
|
||||||
import SignUpForm from './components/SignUpForm';
|
import SignUpForm from './components/SignUpForm';
|
||||||
|
|
||||||
|
@ -13,11 +14,12 @@ const Index: React.FC = () => {
|
||||||
const onStep = () => {
|
const onStep = () => {
|
||||||
setShowForm((bol) => !bol);
|
setShowForm((bol) => !bol);
|
||||||
};
|
};
|
||||||
|
usePageTags({
|
||||||
|
title: t('sign_up', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||||
<PageTitle title={t('sign_up', { keyPrefix: 'page_title' })} />
|
|
||||||
{showForm ? (
|
{showForm ? (
|
||||||
<SignUpForm callback={onStep} />
|
<SignUpForm callback={onStep} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ModifyEmail from './components/ModifyEmail';
|
import ModifyEmail from './components/ModifyEmail';
|
||||||
import ModifyPassword from './components/ModifyPass';
|
import ModifyPassword from './components/ModifyPass';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'settings.account',
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<h3 className="mb-4">{t('heading')}</h3>
|
||||||
<ModifyEmail />
|
<ModifyEmail />
|
||||||
<ModifyPassword />
|
<ModifyPassword />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useEffect, useState, FormEvent } from 'react';
|
import React, { useEffect, useState, FormEvent } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { LangsType, FormDataType } from '@/common/interface';
|
import type { LangsType, FormDataType } from '@/common/interface';
|
||||||
|
@ -7,6 +6,7 @@ import { useToast } from '@/hooks';
|
||||||
import { updateUserInterface } from '@/services';
|
import { updateUserInterface } from '@/services';
|
||||||
import { localize } from '@/utils';
|
import { localize } from '@/utils';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
|
@ -15,19 +15,34 @@ const Index = () => {
|
||||||
const loggedUserInfo = loggedUserInfoStore.getState().user;
|
const loggedUserInfo = loggedUserInfoStore.getState().user;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [langs, setLangs] = useState<LangsType[]>();
|
const [langs, setLangs] = useState<LangsType[]>();
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const schema: JSONSchema = {
|
||||||
lang: {
|
title: t('heading'),
|
||||||
value: loggedUserInfo.language,
|
properties: {
|
||||||
isInvalid: false,
|
lang: {
|
||||||
errorMsg: '',
|
type: 'string',
|
||||||
|
title: t('lang.label'),
|
||||||
|
description: t('lang.text'),
|
||||||
|
enum: langs?.map((_) => _.value),
|
||||||
|
enumNames: langs?.map((_) => _.label),
|
||||||
|
default: loggedUserInfo.language,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
lang: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||||
|
|
||||||
const getLangs = async () => {
|
const getLangs = async () => {
|
||||||
const res: LangsType[] = await localize.loadLanguageOptions();
|
const res: LangsType[] = await localize.loadLanguageOptions();
|
||||||
setLangs(res);
|
setLangs(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (d) => {
|
||||||
|
setFormData(d);
|
||||||
|
};
|
||||||
const handleSubmit = (event: FormEvent) => {
|
const handleSubmit = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const lang = formData.lang.value;
|
const lang = formData.lang.value;
|
||||||
|
@ -48,39 +63,16 @@ const Index = () => {
|
||||||
getLangs();
|
getLangs();
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<>
|
||||||
<Form.Group controlId="emailSend" className="mb-3">
|
<h3 className="mb-4">{t('heading')}</h3>
|
||||||
<Form.Label>{t('lang.label')}</Form.Label>
|
<SchemaForm
|
||||||
<Form.Select
|
schema={schema}
|
||||||
value={formData.lang.value}
|
uiSchema={uiSchema}
|
||||||
isInvalid={formData.lang.isInvalid}
|
formData={formData}
|
||||||
onChange={(e) => {
|
onChange={handleOnChange}
|
||||||
setFormData({
|
onSubmit={handleSubmit}
|
||||||
lang: {
|
/>
|
||||||
value: e.target.value,
|
</>
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
{langs?.map((item) => {
|
|
||||||
return (
|
|
||||||
<option value={item.value} key={item.label}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Form.Select>
|
|
||||||
<Form.Text as="div">{t('lang.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.lang.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,33 @@
|
||||||
import React, { useState, FormEvent, useEffect } from 'react';
|
import React, { useState, FormEvent, useEffect } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { setNotice, getLoggedUserInfo } from '@/services';
|
import { setNotice, getLoggedUserInfo } from '@/services';
|
||||||
|
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'settings.notification',
|
keyPrefix: 'settings.notification',
|
||||||
});
|
});
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const schema: JSONSchema = {
|
||||||
notice_switch: {
|
title: t('heading'),
|
||||||
value: false,
|
properties: {
|
||||||
isInvalid: false,
|
notice_switch: {
|
||||||
errorMsg: '',
|
type: 'boolean',
|
||||||
|
title: t('email.label'),
|
||||||
|
label: t('email.radio'),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
notice_switch: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||||
|
|
||||||
const getProfile = () => {
|
const getProfile = () => {
|
||||||
getLoggedUserInfo().then((res) => {
|
getLoggedUserInfo().then((res) => {
|
||||||
|
@ -47,34 +57,20 @@ const Index = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getProfile();
|
getProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleChange = (ud) => {
|
||||||
|
setFormData(ud);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<>
|
||||||
<Form.Group controlId="emailSend" className="mb-3">
|
<h3 className="mb-4">{t('heading')}</h3>
|
||||||
<Form.Label>{t('email.label')}</Form.Label>
|
<SchemaForm
|
||||||
<Form.Check
|
schema={schema}
|
||||||
required
|
uiSchema={uiSchema}
|
||||||
type="checkbox"
|
formData={formData}
|
||||||
label={t('email.radio')}
|
onChange={handleChange}
|
||||||
checked={formData.notice_switch.value}
|
onSubmit={handleSubmit}
|
||||||
onChange={(e) => {
|
/>
|
||||||
setFormData({
|
</>
|
||||||
notice_switch: {
|
|
||||||
value: e.target.checked,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.notice_switch.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { FormEvent, useState, useEffect } from 'react';
|
import React, { FormEvent, useState, useEffect } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import MD5 from 'md5';
|
import MD5 from 'md5';
|
||||||
|
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { UploadImg, Avatar } from '@/components';
|
import { UploadImg, Avatar, Icon } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
||||||
|
@ -19,7 +19,7 @@ const Index: React.FC = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { user, update } = loggedUserInfoStore();
|
const { user, update } = loggedUserInfoStore();
|
||||||
const [mailHash, setMailHash] = useState('');
|
const [mailHash, setMailHash] = useState('');
|
||||||
const [count, setCount] = useState(0);
|
const [count] = useState(0);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
display_name: {
|
display_name: {
|
||||||
|
@ -60,6 +60,40 @@ const Index: React.FC = () => {
|
||||||
const handleChange = (params: FormDataType) => {
|
const handleChange = (params: FormDataType) => {
|
||||||
setFormData({ ...formData, ...params });
|
setFormData({ ...formData, ...params });
|
||||||
};
|
};
|
||||||
|
const handleAvatarChange = (evt) => {
|
||||||
|
const { value: v } = evt.currentTarget;
|
||||||
|
if (v === 'gravatar') {
|
||||||
|
handleChange({
|
||||||
|
avatar: {
|
||||||
|
...formData.avatar,
|
||||||
|
type: 'gravatar',
|
||||||
|
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (v === 'custom') {
|
||||||
|
handleChange({
|
||||||
|
avatar: {
|
||||||
|
...formData.avatar,
|
||||||
|
type: 'custom',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (v === 'default') {
|
||||||
|
handleChange({
|
||||||
|
avatar: {
|
||||||
|
...formData.avatar,
|
||||||
|
type: 'default',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const avatarUpload = (path: string) => {
|
const avatarUpload = (path: string) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
@ -73,6 +107,17 @@ const Index: React.FC = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const removeCustomAvatar = () => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
avatar: {
|
||||||
|
...formData.avatar,
|
||||||
|
custom: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const checkValidated = (): boolean => {
|
const checkValidated = (): boolean => {
|
||||||
let bol = true;
|
let bol = true;
|
||||||
|
@ -201,265 +246,220 @@ const Index: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshGravatar = () => {
|
// const refreshGravatar = () => {
|
||||||
setCount((pre) => pre + 1);
|
// setCount((pre) => pre + 1);
|
||||||
};
|
// };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getProfile();
|
getProfile();
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<>
|
||||||
<Form.Group controlId="displayName" className="mb-3">
|
<h3 className="mb-4">{t('heading')}</h3>
|
||||||
<Form.Label>{t('display_name.label')}</Form.Label>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Control
|
<Form.Group controlId="displayName" className="mb-3">
|
||||||
required
|
<Form.Label>{t('display_name.label')}</Form.Label>
|
||||||
type="text"
|
<Form.Control
|
||||||
value={formData.display_name.value}
|
required
|
||||||
isInvalid={formData.display_name.isInvalid}
|
type="text"
|
||||||
onChange={(e) =>
|
value={formData.display_name.value}
|
||||||
handleChange({
|
isInvalid={formData.display_name.isInvalid}
|
||||||
display_name: {
|
onChange={(e) =>
|
||||||
value: e.target.value,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.display_name.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="userName" className="mb-3">
|
|
||||||
<Form.Label>{t('username.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.username.value}
|
|
||||||
isInvalid={formData.username.isInvalid}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange({
|
|
||||||
username: {
|
|
||||||
value: e.target.value,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.username.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group className="mb-3">
|
|
||||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
|
||||||
<div className="mb-2">
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
type="radio"
|
|
||||||
id="gravatar"
|
|
||||||
label={t('avatar.gravatar')}
|
|
||||||
className="mb-0"
|
|
||||||
checked={formData.avatar.type === 'gravatar'}
|
|
||||||
onChange={() =>
|
|
||||||
handleChange({
|
handleChange({
|
||||||
avatar: {
|
display_name: {
|
||||||
...formData.avatar,
|
value: e.target.value,
|
||||||
type: 'gravatar',
|
|
||||||
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
|
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Control.Feedback type="invalid">
|
||||||
inline
|
{formData.display_name.errorMsg}
|
||||||
type="radio"
|
</Form.Control.Feedback>
|
||||||
label={t('avatar.custom')}
|
</Form.Group>
|
||||||
id="custom"
|
|
||||||
className="mb-0"
|
|
||||||
checked={formData.avatar.type === 'custom'}
|
|
||||||
onChange={() =>
|
|
||||||
handleChange({
|
|
||||||
avatar: {
|
|
||||||
...formData.avatar,
|
|
||||||
type: 'custom',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
type="radio"
|
|
||||||
id="default"
|
|
||||||
label={t('avatar.default')}
|
|
||||||
className="mb-0"
|
|
||||||
checked={formData.avatar.type === 'default'}
|
|
||||||
onChange={() =>
|
|
||||||
handleChange({
|
|
||||||
avatar: {
|
|
||||||
...formData.avatar,
|
|
||||||
type: 'default',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
{formData.avatar.type === 'gravatar' && (
|
|
||||||
<>
|
|
||||||
<Avatar
|
|
||||||
size="128px"
|
|
||||||
avatar={formData.avatar.gravatar}
|
|
||||||
searchStr={`s=256&d=identicon${
|
|
||||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
|
||||||
}`}
|
|
||||||
className="me-3 rounded"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
variant="outline-secondary"
|
|
||||||
className="mb-2"
|
|
||||||
onClick={refreshGravatar}>
|
|
||||||
{t('avatar.btn_refresh')}
|
|
||||||
</Button>
|
|
||||||
<div>
|
|
||||||
<Form.Text className="text-muted mt-0">
|
|
||||||
<Trans i18nKey="settings.profile.gravatar_text">
|
|
||||||
You can change your image on{' '}
|
|
||||||
<a
|
|
||||||
href="https://gravatar.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">
|
|
||||||
gravatar.com
|
|
||||||
</a>
|
|
||||||
</Trans>
|
|
||||||
</Form.Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{formData.avatar.type === 'custom' && (
|
<Form.Group controlId="userName" className="mb-3">
|
||||||
<>
|
<Form.Label>{t('username.label')}</Form.Label>
|
||||||
<Avatar
|
<Form.Control
|
||||||
size="128px"
|
required
|
||||||
searchStr="s=256"
|
type="text"
|
||||||
avatar={formData.avatar.custom}
|
value={formData.username.value}
|
||||||
className="me-3 rounded"
|
isInvalid={formData.username.isInvalid}
|
||||||
/>
|
onChange={(e) =>
|
||||||
<div>
|
handleChange({
|
||||||
<UploadImg
|
username: {
|
||||||
type="avatar"
|
value: e.target.value,
|
||||||
uploadCallback={avatarUpload}
|
isInvalid: false,
|
||||||
className="mb-2"
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.username.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className="mb-3">
|
||||||
|
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||||
|
<div className="mb-3">
|
||||||
|
<Form.Select
|
||||||
|
name="avatar.type"
|
||||||
|
value={formData.avatar.type}
|
||||||
|
onChange={handleAvatarChange}>
|
||||||
|
<option value="gravatar" key="gravatar">
|
||||||
|
{t('avatar.gravatar')}
|
||||||
|
</option>
|
||||||
|
<option value="default" key="default">
|
||||||
|
{t('avatar.default')}
|
||||||
|
</option>
|
||||||
|
<option value="custom" key="custom">
|
||||||
|
{t('avatar.custom')}
|
||||||
|
</option>
|
||||||
|
</Form.Select>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex">
|
||||||
|
{formData.avatar.type === 'gravatar' && (
|
||||||
|
<Stack>
|
||||||
|
<Avatar
|
||||||
|
size="160px"
|
||||||
|
avatar={formData.avatar.gravatar}
|
||||||
|
searchStr={`s=256&d=identicon${
|
||||||
|
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||||
|
}`}
|
||||||
|
className="me-3 rounded"
|
||||||
/>
|
/>
|
||||||
<div>
|
<Form.Text className="text-muted mt-1">
|
||||||
<Form.Text className="text-muted mt-0">
|
<Trans i18nKey="settings.profile.avatar.gravatar_text">
|
||||||
<Trans i18nKey="settings.profile.avatar.text">
|
You can change image on
|
||||||
You can upload your image.
|
<a
|
||||||
</Trans>
|
href="https://gravatar.com"
|
||||||
</Form.Text>
|
target="_blank"
|
||||||
</div>
|
rel="noreferrer">
|
||||||
</div>
|
gravatar.com
|
||||||
</>
|
</a>
|
||||||
)}
|
</Trans>
|
||||||
{formData.avatar.type === 'default' && (
|
</Form.Text>
|
||||||
<Avatar size="128px" avatar="" className="me-3 rounded" />
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<Form.Control
|
|
||||||
isInvalid={formData.avatar.isInvalid}
|
|
||||||
className="d-none"
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.avatar.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="bio" className="mb-3">
|
{formData.avatar.type === 'custom' && (
|
||||||
<Form.Label>{t('bio.label')}</Form.Label>
|
<Stack>
|
||||||
<Form.Control
|
<Stack direction="horizontal" className="align-items-start">
|
||||||
className="font-monospace"
|
<Avatar
|
||||||
required
|
size="160px"
|
||||||
as="textarea"
|
searchStr="s=256"
|
||||||
rows={5}
|
avatar={formData.avatar.custom}
|
||||||
value={formData.bio.value}
|
className="me-2 bg-gray-300 "
|
||||||
isInvalid={formData.bio.isInvalid}
|
/>
|
||||||
onChange={(e) =>
|
<ButtonGroup vertical className="fit-content">
|
||||||
handleChange({
|
<UploadImg type="avatar" uploadCallback={avatarUpload}>
|
||||||
bio: {
|
<Icon name="cloud-upload" />
|
||||||
value: e.target.value,
|
</UploadImg>
|
||||||
isInvalid: false,
|
<Button
|
||||||
errorMsg: '',
|
variant="outline-secondary"
|
||||||
},
|
onClick={removeCustomAvatar}>
|
||||||
})
|
<Icon name="trash" />
|
||||||
}
|
</Button>
|
||||||
/>
|
</ButtonGroup>
|
||||||
<Form.Control.Feedback type="invalid">
|
</Stack>
|
||||||
{formData.bio.errorMsg}
|
<Form.Text className="text-muted mt-1">
|
||||||
</Form.Control.Feedback>
|
<Trans i18nKey="settings.profile.avatar.text">
|
||||||
</Form.Group>
|
You can upload your image.
|
||||||
|
</Trans>
|
||||||
|
</Form.Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{formData.avatar.type === 'default' && (
|
||||||
|
<Avatar size="160px" avatar="" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Form.Control
|
||||||
|
isInvalid={formData.avatar.isInvalid}
|
||||||
|
className="d-none"
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.avatar.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="website" className="mb-3">
|
<Form.Group controlId="bio" className="mb-3">
|
||||||
<Form.Label>{t('website.label')}</Form.Label>
|
<Form.Label>{t('bio.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
required
|
className="font-monospace"
|
||||||
type="text"
|
required
|
||||||
placeholder={t('website.placeholder')}
|
as="textarea"
|
||||||
value={formData.website.value}
|
rows={5}
|
||||||
isInvalid={formData.website.isInvalid}
|
value={formData.bio.value}
|
||||||
onChange={(e) =>
|
isInvalid={formData.bio.isInvalid}
|
||||||
handleChange({
|
onChange={(e) =>
|
||||||
website: {
|
handleChange({
|
||||||
value: e.target.value,
|
bio: {
|
||||||
isInvalid: false,
|
value: e.target.value,
|
||||||
errorMsg: '',
|
isInvalid: false,
|
||||||
},
|
errorMsg: '',
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
<Form.Control.Feedback type="invalid">
|
/>
|
||||||
{formData.website.errorMsg}
|
<Form.Control.Feedback type="invalid">
|
||||||
</Form.Control.Feedback>
|
{formData.bio.errorMsg}
|
||||||
</Form.Group>
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="email" className="mb-3">
|
<Form.Group controlId="website" className="mb-3">
|
||||||
<Form.Label>{t('location.label')}</Form.Label>
|
<Form.Label>{t('website.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('location.placeholder')}
|
placeholder={t('website.placeholder')}
|
||||||
value={formData.location.value}
|
value={formData.website.value}
|
||||||
isInvalid={formData.location.isInvalid}
|
isInvalid={formData.website.isInvalid}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
location: {
|
website: {
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData.location.errorMsg}
|
{formData.website.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
<Form.Group controlId="email" className="mb-3">
|
||||||
{t('btn_name')}
|
<Form.Label>{t('location.label')}</Form.Label>
|
||||||
</Button>
|
<Form.Control
|
||||||
</Form>
|
required
|
||||||
|
type="text"
|
||||||
|
placeholder={t('location.placeholder')}
|
||||||
|
value={formData.location.value}
|
||||||
|
isInvalid={formData.location.isInvalid}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange({
|
||||||
|
location: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.location.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{t('btn_name')}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { getLoggedUserInfo } from '@/services';
|
import { getLoggedUserInfo } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
import Nav from './components/Nav';
|
import Nav from './components/Nav';
|
||||||
|
|
||||||
|
@ -55,29 +55,27 @@ const Index: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getProfile();
|
getProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
usePageTags({
|
||||||
|
title: t('settings', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="mt-4 mb-5 pb-5">
|
||||||
<PageTitle title={t('settings', { keyPrefix: 'page_title' })} />
|
<Row className="justify-content-center">
|
||||||
<Container className="mt-4 mb-5 pb-5">
|
<Col xxl={10} md={12}>
|
||||||
<Row className="justify-content-center">
|
<h3 className="mb-4">{t('page_title', { keyPrefix: 'settings' })}</h3>
|
||||||
<Col xxl={10} md={12}>
|
</Col>
|
||||||
<h3 className="mb-4">
|
</Row>
|
||||||
{t('page_title', { keyPrefix: 'settings' })}
|
|
||||||
</h3>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col xxl={1} />
|
<Col xxl={1} />
|
||||||
<Col md={3} lg={2} className="mb-3">
|
<Col md={3} lg={2} className="mb-3">
|
||||||
<Nav />
|
<Nav />
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={9} lg={6}>
|
<Col md={9} lg={6}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,27 @@
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { PageTitle } from '@/components';
|
|
||||||
|
|
||||||
const Suspended = () => {
|
const Suspended = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'suspended' });
|
const { t } = useTranslation('translation', { keyPrefix: 'suspended' });
|
||||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||||
|
usePageTags({
|
||||||
|
title: t('account_suspended', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
if (userInfo.status !== 'forbidden') {
|
if (userInfo.status !== 'forbidden') {
|
||||||
window.location.replace('/');
|
window.location.replace('/');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="d-flex flex-column align-items-center mt-5 pt-3">
|
||||||
<PageTitle title={t('account_suspended', { keyPrefix: 'page_title' })} />
|
<h3 className="mb-5">{t('title')}</h3>
|
||||||
<div className="d-flex flex-column align-items-center mt-5 pt-3">
|
<p className="text-center">
|
||||||
<h3 className="mb-5">{t('title')}</h3>
|
{t('forever')}
|
||||||
<p className="text-center">
|
<br />
|
||||||
{t('forever')}
|
{t('end')}
|
||||||
<br />
|
</p>
|
||||||
{t('end')}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ export const getBrandSetting = () => {
|
||||||
return request.get('/answer/admin/api/siteinfo/branding');
|
return request.get('/answer/admin/api/siteinfo/branding');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const brandSetting = (params: Type.AdmingSettingBranding) => {
|
export const brandSetting = (params: Type.AdminSettingBranding) => {
|
||||||
return request.put('/answer/admin/api/siteinfo/branding', params);
|
return request.put('/answer/admin/api/siteinfo/branding', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,10 @@ export const useFollowingTags = () => {
|
||||||
export const useTagInfo = ({ id = '', name = '' }) => {
|
export const useTagInfo = ({ id = '', name = '' }) => {
|
||||||
let apiUrl;
|
let apiUrl;
|
||||||
if (id) {
|
if (id) {
|
||||||
apiUrl = `/answer/api/v1/tag/?id=${id}`;
|
apiUrl = `/answer/api/v1/tag?id=${id}`;
|
||||||
} else if (name) {
|
} else if (name) {
|
||||||
name = encodeURIComponent(name);
|
name = encodeURIComponent(name);
|
||||||
apiUrl = `/answer/api/v1/tag/?name=${name}`;
|
apiUrl = `/answer/api/v1/tag?name=${name}`;
|
||||||
}
|
}
|
||||||
const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get);
|
const { data, error } = useSWR<Type.TagInfo>(apiUrl, request.instance.get);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
|
|
||||||
import { AdmingSettingBranding } from '@/common/interface';
|
import { AdminSettingBranding } from '@/common/interface';
|
||||||
import { DEFAULT_LANG } from '@/common/constants';
|
import { DEFAULT_LANG } from '@/common/constants';
|
||||||
|
|
||||||
interface InterfaceType {
|
interface InterfaceType {
|
||||||
branding: AdmingSettingBranding;
|
branding: AdminSettingBranding;
|
||||||
update: (params: AdmingSettingBranding) => void;
|
update: (params: AdminSettingBranding) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interfaceSetting = create<InterfaceType>((set) => ({
|
const interfaceSetting = create<InterfaceType>((set) => ({
|
||||||
|
|
|
@ -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 siteInfoStore from './siteInfo';
|
||||||
import interfaceStore from './interface';
|
import interfaceStore from './interface';
|
||||||
import brandingStore from './branding';
|
import brandingStore from './branding';
|
||||||
import headInfoStore from './headInfo';
|
import pageTagStore from './pageTags';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
toastStore,
|
toastStore,
|
||||||
|
@ -13,5 +13,5 @@ export {
|
||||||
siteInfoStore,
|
siteInfoStore,
|
||||||
interfaceStore,
|
interfaceStore,
|
||||||
brandingStore,
|
brandingStore,
|
||||||
headInfoStore,
|
pageTagStore,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
?.replace(/<input/gi, '<input');
|
||||||
}
|
}
|
||||||
const diff = Diff.diffChars(oldText, newText);
|
const diff = Diff.diffChars(oldText, newText);
|
||||||
// console.log(diff);
|
|
||||||
const result = diff.map((part) => {
|
const result = diff.map((part) => {
|
||||||
if (part.added) {
|
if (part.added) {
|
||||||
if (part.value.replace(/\n/g, '').length <= 0) {
|
if (part.value.replace(/\n/g, '').length <= 0) {
|
||||||
|
|
Loading…
Reference in New Issue