mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-0.6.0' into feat/0.6.0/seo
This commit is contained in:
commit
6904c9775c
|
@ -13,8 +13,6 @@ stages:
|
|||
"compile the html and other static files":
|
||||
image: node:16
|
||||
stage: compile-html
|
||||
tags:
|
||||
- runner-nanjing
|
||||
before_script:
|
||||
- npm config set registry https://repo.huaweicloud.com/repository/npm/
|
||||
- make install-ui-packages
|
||||
|
|
|
@ -147,32 +147,32 @@ backend:
|
|||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
desc:
|
||||
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
|
||||
rude:
|
||||
name:
|
||||
other: "rude or abusive"
|
||||
description:
|
||||
desc:
|
||||
other: "A reasonable person would find this content inappropriate for respectful discourse."
|
||||
duplicate:
|
||||
name:
|
||||
other: "a duplicate"
|
||||
description:
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
not_answer:
|
||||
name:
|
||||
other: "not an answer"
|
||||
description:
|
||||
desc:
|
||||
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
|
||||
not_need:
|
||||
name:
|
||||
other: "no longer needed"
|
||||
description:
|
||||
desc:
|
||||
other: "This comment is outdated, conversational or not relevant to this post."
|
||||
other:
|
||||
name:
|
||||
other: "something else"
|
||||
description:
|
||||
desc:
|
||||
other: "This post requires staff attention for another reason not listed above."
|
||||
|
||||
question:
|
||||
|
@ -180,22 +180,22 @@ backend:
|
|||
duplicate:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
guideline:
|
||||
name:
|
||||
other: "a community-specific reason"
|
||||
description:
|
||||
desc:
|
||||
other: "This question doesn't meet a community guideline."
|
||||
multiple:
|
||||
name:
|
||||
other: "needs details or clarity"
|
||||
description:
|
||||
desc:
|
||||
other: "This question currently includes multiple questions in one. It should focus on one problem only."
|
||||
other:
|
||||
name:
|
||||
other: "something else"
|
||||
description:
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
|
||||
notification:
|
||||
|
@ -229,7 +229,7 @@ backend:
|
|||
ui:
|
||||
how_to_format:
|
||||
title: How to Format
|
||||
description: >-
|
||||
desc: >-
|
||||
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre
|
||||
class="mb-2"><code><https://url.com><br/><br/>[Title](https://url.com)</code></pre></li><li><p
|
||||
class="mb-2">put returns between paragraphs</p></li><li><p
|
||||
|
@ -338,7 +338,7 @@ ui:
|
|||
empty: File cannot be empty.
|
||||
only_image: Only image files are allowed.
|
||||
max_size: File size cannot exceed 4MB.
|
||||
description:
|
||||
desc:
|
||||
label: Description (optional)
|
||||
tab_url: Image URL
|
||||
form_url:
|
||||
|
@ -410,12 +410,12 @@ ui:
|
|||
range: Display name up to 35 characters.
|
||||
slug_name:
|
||||
label: URL Slug
|
||||
description: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
msg: "todo"
|
||||
# empty: URL slug cannot be empty.
|
||||
# range: URL slug up to 35 characters.
|
||||
# character: URL slug contains unallowed character set.
|
||||
description:
|
||||
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
msg:
|
||||
empty: URL slug cannot be empty.
|
||||
range: URL slug up to 35 characters.
|
||||
character: URL slug contains unallowed character set.
|
||||
desc:
|
||||
label: Description (optional)
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
|
@ -450,7 +450,7 @@ ui:
|
|||
slug_name:
|
||||
label: URL Slug
|
||||
info: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
description:
|
||||
desc:
|
||||
label: Description
|
||||
edit_summary:
|
||||
label: Edit Summary
|
||||
|
@ -511,7 +511,7 @@ ui:
|
|||
button_following: Following
|
||||
tag_label: questions
|
||||
search_placeholder: Filter by tag name
|
||||
no_description: The tag has no description.
|
||||
no_desc: The tag has no description.
|
||||
more: More
|
||||
ask:
|
||||
title: Add Question
|
||||
|
@ -752,6 +752,10 @@ ui:
|
|||
<p>Are you sure you want to add another answer?</p><p>You could use the
|
||||
edit link to refine and improve your existing answer, instead.</p>
|
||||
empty: Answer cannot be empty.
|
||||
reopen:
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
success: This post has been reopened
|
||||
delete:
|
||||
title: Delete this post
|
||||
question: >-
|
||||
|
@ -912,7 +916,7 @@ ui:
|
|||
config_yaml:
|
||||
title: Create config.yaml
|
||||
label: The config.yaml file created.
|
||||
description: >-
|
||||
desc: >-
|
||||
You can create the <1>config.yaml</1> file manually in the
|
||||
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
|
||||
info: "After you’ve done that, click “Next” button."
|
||||
|
@ -949,31 +953,31 @@ ui:
|
|||
empty: Email cannot be empty.
|
||||
incorrect: Email incorrect format.
|
||||
ready_title: Your Answer is Ready!
|
||||
ready_description: >-
|
||||
ready_desc: >-
|
||||
If you ever feel like changing more settings, visit <1>admin section</1>;
|
||||
find it in the site menu.
|
||||
good_luck: "Have fun, and good luck!"
|
||||
warn_title: Warning
|
||||
warn_description: >-
|
||||
warn_desc: >-
|
||||
The file <1>config.yaml</1> already exists. If you need to reset any of the
|
||||
configuration items in this file, please delete it first.
|
||||
install_now: You may try <1>installing now</1>.
|
||||
installed: Already installed
|
||||
installed_description: >-
|
||||
installed_desc: >-
|
||||
You appear to have already installed. To reinstall please clear your old
|
||||
database tables first.
|
||||
db_failed: Database connection failed
|
||||
db_failed_description: >-
|
||||
db_failed_desc: >-
|
||||
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
||||
|
||||
page_404:
|
||||
description: "Unfortunately, this page doesn't exist."
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
description: The server encountered an error and could not complete your request.
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
description: "We are under maintenance, we’ll be back soon."
|
||||
desc: "We are under maintenance, we’ll be back soon."
|
||||
nav_menus:
|
||||
dashboard: Dashboard
|
||||
contents: Contents
|
||||
|
@ -990,6 +994,7 @@ ui:
|
|||
write: Write
|
||||
tos: Terms of Service
|
||||
privacy: Privacy
|
||||
seo: SEO
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
@ -1039,13 +1044,13 @@ ui:
|
|||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
normal_name: normal
|
||||
normal_description: A normal user can ask and answer questions.
|
||||
normal_desc: A normal user can ask and answer questions.
|
||||
suspended_name: suspended
|
||||
suspended_description: A suspended user can't log in.
|
||||
suspended_desc: A suspended user can't log in.
|
||||
deleted_name: deleted
|
||||
deleted_description: "Delete profile, authentication associations."
|
||||
deleted_desc: "Delete profile, authentication associations."
|
||||
inactive_name: inactive
|
||||
inactive_description: An inactive user must re-validate their email.
|
||||
inactive_desc: An inactive user must re-validate their email.
|
||||
confirm_title: Delete this user
|
||||
confirm_content: Are you sure you want to delete this user? This is permanent!
|
||||
confirm_btn: Delete
|
||||
|
@ -1054,11 +1059,11 @@ ui:
|
|||
status_modal:
|
||||
title: "Change {{ type }} status to..."
|
||||
normal_name: normal
|
||||
normal_description: A normal post available to everyone.
|
||||
normal_desc: A normal post available to everyone.
|
||||
closed_name: closed
|
||||
closed_description: "A closed question can't answer, but still can edit, vote and comment."
|
||||
closed_desc: "A closed question can't answer, but still can edit, vote and comment."
|
||||
deleted_name: deleted
|
||||
deleted_description: All reputation gained and lost will be restored.
|
||||
deleted_desc: All reputation gained and lost will be restored.
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_next: Next
|
||||
|
@ -1130,11 +1135,11 @@ ui:
|
|||
msg: Site url cannot be empty.
|
||||
validate: Please enter a valid URL.
|
||||
text: The address of your site.
|
||||
short_description:
|
||||
short_desc:
|
||||
label: Short Site Description (optional)
|
||||
msg: Short site description cannot be empty.
|
||||
text: "Short description, as used in the title tag on homepage."
|
||||
description:
|
||||
desc:
|
||||
label: Site Description (optional)
|
||||
msg: Site description cannot be empty.
|
||||
text: "Describe this site in one sentence, as used in the meta description tag."
|
||||
|
@ -1143,6 +1148,9 @@ ui:
|
|||
msg: Contact email cannot be empty.
|
||||
validate: Contact email is not valid.
|
||||
text: Email address of key contact responsible for this site.
|
||||
permalink:
|
||||
label: Permalink
|
||||
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
|
||||
interface:
|
||||
page_title: Interface
|
||||
logo:
|
||||
|
@ -1236,6 +1244,11 @@ ui:
|
|||
reserved_tags:
|
||||
label: Reserved Tags
|
||||
text: "Reserved tags can only be added to a post by moderator."
|
||||
seo:
|
||||
page_title: SEO
|
||||
robots:
|
||||
label: robots.txt
|
||||
text: This will permanently override any related site settings.
|
||||
form:
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"react-router-dom": "^6.4.0",
|
||||
"semver": "^7.3.8",
|
||||
"swr": "^1.3.0",
|
||||
"urlcat": "^3.0.0",
|
||||
"zustand": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -26,6 +26,7 @@ specifiers:
|
|||
customize-cra: ^1.0.0
|
||||
dayjs: ^1.11.5
|
||||
diff: ^5.1.0
|
||||
emoji-regex: ^10.2.1
|
||||
eslint: ^8.0.1
|
||||
eslint-config-airbnb: ^19.0.4
|
||||
eslint-config-airbnb-typescript: ^17.0.0
|
||||
|
@ -63,6 +64,7 @@ specifiers:
|
|||
semver: ^7.3.8
|
||||
swr: ^1.3.0
|
||||
typescript: ^4.8.3
|
||||
urlcat: ^3.0.0
|
||||
yaml-loader: ^0.8.0
|
||||
zustand: ^4.1.1
|
||||
|
||||
|
@ -75,6 +77,7 @@ dependencies:
|
|||
copy-to-clipboard: 3.3.2
|
||||
dayjs: 1.11.5
|
||||
diff: 5.1.0
|
||||
emoji-regex: 10.2.1
|
||||
i18next: 21.9.2
|
||||
katex: 0.16.2
|
||||
lodash: 4.17.21
|
||||
|
@ -91,6 +94,7 @@ dependencies:
|
|||
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
|
||||
semver: 7.3.8
|
||||
swr: 1.3.0_react@18.2.0
|
||||
urlcat: 3.0.0
|
||||
zustand: 4.1.1_react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
|
@ -4903,6 +4907,10 @@ packages:
|
|||
resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/emoji-regex/10.2.1:
|
||||
resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==}
|
||||
dev: false
|
||||
|
||||
/emoji-regex/8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
|
@ -10417,6 +10425,12 @@ packages:
|
|||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
|
||||
/urlcat/3.0.0:
|
||||
resolution: {integrity: sha512-SSXrIzInzKdWjBfm5iOrPfO6E5Nt0aFs5PTZCauxJTjJE3qhfePAWz8tjGm7dnWMYIAdPGjio51aakunyZHMXQ==}
|
||||
dependencies:
|
||||
qs: 6.11.0
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store/1.2.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
|
|
|
@ -62,6 +62,7 @@ export const ADMIN_NAV_MENUS = [
|
|||
{ name: 'smtp' },
|
||||
{ name: 'legal' },
|
||||
{ name: 'write' },
|
||||
{ name: 'seo' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -279,6 +279,19 @@ export interface AdminSettingsGeneral {
|
|||
description: string;
|
||||
site_url: string;
|
||||
contact_email: string;
|
||||
/**
|
||||
* 0: not set
|
||||
* 1:with title
|
||||
* 2: no title
|
||||
*/
|
||||
permalink: number;
|
||||
}
|
||||
|
||||
export interface HeadInfo {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string;
|
||||
ldJSON?;
|
||||
}
|
||||
|
||||
export interface AdminSettingsInterface {
|
||||
|
@ -325,6 +338,10 @@ export interface AdminSettingsWrite {
|
|||
reserved_tags: string[];
|
||||
}
|
||||
|
||||
export interface AdminSettingsSeo {
|
||||
robots: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description interface for Activity
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const pattern = {
|
||||
emoji: emojiRegex(),
|
||||
email:
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
|
||||
isAnswerId: /^1002\d{13}$/,
|
||||
};
|
||||
|
||||
export default pattern;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Icon } from '@/components';
|
||||
import { useHotQuestions } from '@/services';
|
||||
|
||||
|
@ -29,7 +30,7 @@ const HotQuestions: FC = () => {
|
|||
<ListGroupItem
|
||||
key={li.id}
|
||||
as={Link}
|
||||
to={`/questions/${li.id}`}
|
||||
to={pathFactory.questionLanding(li.id, li.title)}
|
||||
action>
|
||||
<div className="link-dark">{li.title}</div>
|
||||
{li.answer_count > 0 ? (
|
||||
|
|
|
@ -6,7 +6,12 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Modal } from '@/components';
|
||||
import { useReportModal, useToast } from '@/hooks';
|
||||
import Share from '../Share';
|
||||
import { deleteQuestion, deleteAnswer, editCheck } from '@/services';
|
||||
import {
|
||||
deleteQuestion,
|
||||
deleteAnswer,
|
||||
editCheck,
|
||||
reopenQuestion,
|
||||
} from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
|
||||
interface IProps {
|
||||
|
@ -33,7 +38,11 @@ const Index: FC<IProps> = ({
|
|||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const reportModal = useReportModal();
|
||||
const closeModal = useReportModal();
|
||||
|
||||
const refershQuestion = () => {
|
||||
callback?.('default');
|
||||
};
|
||||
const closeModal = useReportModal(refershQuestion);
|
||||
const editUrl =
|
||||
type === 'answer' ? `/posts/${qid}/${aid}/edit` : `/posts/${qid}/edit`;
|
||||
|
||||
|
@ -108,6 +117,25 @@ const Index: FC<IProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const handleReopen = () => {
|
||||
Modal.confirm({
|
||||
title: t('title', { keyPrefix: 'question_detail.reopen' }),
|
||||
content: t('content', { keyPrefix: 'question_detail.reopen' }),
|
||||
cancelBtnVariant: 'link',
|
||||
onConfirm: () => {
|
||||
reopenQuestion({
|
||||
question_id: qid,
|
||||
}).then(() => {
|
||||
toast.onShow({
|
||||
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
|
||||
variant: 'success',
|
||||
});
|
||||
refershQuestion();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAction = (action) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
|
@ -123,6 +151,10 @@ const Index: FC<IProps> = ({
|
|||
if (action === 'close') {
|
||||
handleClose();
|
||||
}
|
||||
|
||||
if (action === 'reopen') {
|
||||
handleReopen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ListGroup } from 'react-bootstrap';
|
|||
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
Icon,
|
||||
|
@ -123,7 +124,9 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
key={li.id}
|
||||
className="border-bottom pt-3 pb-2 px-0">
|
||||
<h5 className="text-wrap text-break">
|
||||
<NavLink to={`/questions/${li.id}`} className="link-dark">
|
||||
<NavLink
|
||||
to={pathFactory.questionLanding(li.id, li.title)}
|
||||
className="link-dark">
|
||||
{li.title}
|
||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||
</NavLink>
|
||||
|
|
|
@ -14,13 +14,13 @@ export interface JSONSchema {
|
|||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string' | 'boolean';
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean>;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
default?: string | boolean;
|
||||
default?: string | boolean | number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -477,8 +477,10 @@ const SchemaForm: FC<IProps> = ({
|
|||
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||
const formData: Type.FormDataType = {};
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
const v = schema.properties[key]?.default;
|
||||
// TODO: set default value by property type
|
||||
formData[key] = {
|
||||
value: '',
|
||||
value: typeof v !== 'undefined' ? v : '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import { FacebookShareButton, TwitterShareButton } from 'next-share';
|
|||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
|
@ -23,8 +24,12 @@ const Index: FC<IProps> = ({ type, qid, aid, title }) => {
|
|||
|
||||
let baseUrl =
|
||||
type === 'question'
|
||||
? `${window.location.origin}/questions/${qid}`
|
||||
: `${window.location.origin}/questions/${qid}/${aid}`;
|
||||
? `${window.location.origin}${pathFactory.questionLanding(qid, title)}`
|
||||
: `${window.location.origin}${pathFactory.answerLanding({
|
||||
questionId: qid,
|
||||
questionTitle: title,
|
||||
answerId: aid,
|
||||
})}`;
|
||||
if (user.id) {
|
||||
baseUrl = `${baseUrl}?shareUserId=${user.username}`;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index: FC<IProps> = ({
|
|||
className = '',
|
||||
textClassName = '',
|
||||
}) => {
|
||||
href ||= pathFactory.tagLanding(data);
|
||||
href ||= pathFactory.tagLanding(data?.slug_name);
|
||||
|
||||
return (
|
||||
<a
|
||||
|
|
|
@ -5,6 +5,7 @@ import usePageUsers from './usePageUsers';
|
|||
import useChangeModal from './useChangeModal';
|
||||
import useEditStatusModal from './useEditStatusModal';
|
||||
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||
import useHeadInfo from './useHeadInfo';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -14,4 +15,5 @@ export {
|
|||
useChangeModal,
|
||||
useEditStatusModal,
|
||||
useChangeUserRoleModal,
|
||||
useHeadInfo,
|
||||
};
|
||||
|
|
|
@ -35,22 +35,22 @@ const useChangeModal = ({ callback }: Props) => {
|
|||
{
|
||||
type: 'normal',
|
||||
name: t('normal_name'),
|
||||
description: t('normal_description'),
|
||||
description: t('normal_desc'),
|
||||
},
|
||||
{
|
||||
type: 'suspended',
|
||||
name: t('suspended_name'),
|
||||
description: t('suspended_description'),
|
||||
description: t('suspended_desc'),
|
||||
},
|
||||
{
|
||||
type: 'deleted',
|
||||
name: t('deleted_name'),
|
||||
description: t('deleted_description'),
|
||||
description: t('deleted_desc'),
|
||||
},
|
||||
{
|
||||
type: 'inactive',
|
||||
name: t('inactive_name'),
|
||||
description: t('inactive_description'),
|
||||
description: t('inactive_desc'),
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -27,17 +27,17 @@ const useEditStatusModal = ({
|
|||
{
|
||||
type: 'normal',
|
||||
name: t('normal_name'),
|
||||
description: t('normal_description'),
|
||||
description: t('normal_desc'),
|
||||
},
|
||||
{
|
||||
type: 'closed',
|
||||
name: t('closed_name'),
|
||||
description: t('closed_description'),
|
||||
description: t('closed_desc'),
|
||||
},
|
||||
{
|
||||
type: 'deleted',
|
||||
name: t('deleted_name'),
|
||||
description: t('deleted_description'),
|
||||
description: t('deleted_desc'),
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
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]);
|
||||
}
|
|
@ -204,15 +204,13 @@ const useTagModal = (props: IProps = {}) => {
|
|||
isInvalid={formData.slugName.isInvalid}
|
||||
/>
|
||||
|
||||
<Form.Text as="div">
|
||||
{t('form.fields.slug_name.description')}
|
||||
</Form.Text>
|
||||
<Form.Text as="div">{t('form.fields.slug_name.desc')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.slugName.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="description">
|
||||
<Form.Label>{t('form.fields.description.label')}</Form.Label>
|
||||
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
||||
<Form.Control
|
||||
className="font-monospace"
|
||||
value={formData.description.value}
|
||||
|
|
|
@ -11,7 +11,7 @@ const Index = () => {
|
|||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<div className="text-center mb-4">{t('description')}</div>
|
||||
<div className="text-center mb-4">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
|
|
|
@ -11,7 +11,7 @@ const Index = () => {
|
|||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=T^T=)
|
||||
</div>
|
||||
<div className="text-center mb-3">{t('description')}</div>
|
||||
<div className="text-center mb-3">{t('desc')}</div>
|
||||
<div className="text-center">
|
||||
<Button as={Link} to="/" variant="link">
|
||||
{t('back_home')}
|
||||
|
|
|
@ -19,8 +19,7 @@ import { useEditStatusModal } from '@/hooks';
|
|||
import * as Type from '@/common/interface';
|
||||
import { useAnswerSearch, changeAnswerStatus } from '@/services';
|
||||
import { escapeRemove } from '@/utils';
|
||||
|
||||
import '../index.scss';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
|
||||
|
||||
|
@ -129,7 +128,11 @@ const Answers: FC = () => {
|
|||
<Stack>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<a
|
||||
href={`/questions/${li.question_id}/${li.id}`}
|
||||
href={pathFactory.answerLanding({
|
||||
questionId: li.question_id,
|
||||
questionTitle: li.question_info.title,
|
||||
answerId: li.id,
|
||||
})}
|
||||
target="_blank"
|
||||
className="text-break text-wrap"
|
||||
rel="noreferrer">
|
||||
|
|
|
@ -14,8 +14,7 @@ import { useReportModal } from '@/hooks';
|
|||
import * as Type from '@/common/interface';
|
||||
import { useFlagSearch } from '@/services';
|
||||
import { escapeRemove } from '@/utils';
|
||||
|
||||
import '../index.scss';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
const flagFilterKeys: Type.FlagStatus[] = ['pending', 'completed'];
|
||||
const flagTypeKeys: Type.FlagType[] = ['all', 'question', 'answer', 'comment'];
|
||||
|
@ -101,7 +100,10 @@ const Flags: FC = () => {
|
|||
</small>
|
||||
<BaseUserCard data={li.reported_user} className="mt-2" />
|
||||
<a
|
||||
href={`/questions/${li.question_id}`}
|
||||
href={pathFactory.questionLanding(
|
||||
li.question_id,
|
||||
li.title,
|
||||
)}
|
||||
target="_blank"
|
||||
className="text-wrap text-break mt-2"
|
||||
rel="noreferrer">
|
||||
|
|
|
@ -9,8 +9,6 @@ import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
|||
import Pattern from '@/common/pattern';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const General: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.general',
|
||||
|
@ -35,19 +33,27 @@ const General: FC = () => {
|
|||
},
|
||||
short_description: {
|
||||
type: 'string',
|
||||
title: t('short_description.label'),
|
||||
description: t('short_description.text'),
|
||||
title: t('short_desc.label'),
|
||||
description: t('short_desc.text'),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
title: t('description.label'),
|
||||
description: t('description.text'),
|
||||
title: t('desc.label'),
|
||||
description: t('desc.text'),
|
||||
},
|
||||
contact_email: {
|
||||
type: 'string',
|
||||
title: t('contact_email.label'),
|
||||
description: t('contact_email.text'),
|
||||
},
|
||||
permalink: {
|
||||
type: 'number',
|
||||
title: t('permalink.label'),
|
||||
description: t('permalink.text'),
|
||||
enum: [1, 2],
|
||||
enumNames: ['/questions/123/post-title', '/questions/123'],
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
|
@ -63,7 +69,7 @@ const General: FC = () => {
|
|||
}
|
||||
if (
|
||||
!url ||
|
||||
/^https?:$/.test(url.protocol) === false ||
|
||||
!/^https?:$/.test(url.protocol) ||
|
||||
url.pathname !== '/' ||
|
||||
url.search !== '' ||
|
||||
url.hash !== ''
|
||||
|
@ -86,11 +92,13 @@ const General: FC = () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
permalink: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
);
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
@ -100,6 +108,7 @@ const General: FC = () => {
|
|||
short_description: formData.short_description.value,
|
||||
site_url: formData.site_url.value,
|
||||
contact_email: formData.contact_email.value,
|
||||
permalink: Number(formData.permalink.value),
|
||||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
|
@ -122,10 +131,13 @@ const General: FC = () => {
|
|||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const formMeta = {};
|
||||
Object.keys(setting).forEach((k) => {
|
||||
const formMeta: Type.FormDataType = {};
|
||||
Object.keys(formData).forEach((k) => {
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
});
|
||||
if (formMeta.permalink.value !== 1 && formMeta.permalink.value !== 2) {
|
||||
formMeta.permalink.value = 1;
|
||||
}
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}, [setting]);
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
|||
import { useToast } from '@/hooks';
|
||||
import { getLegalSetting, putLegalSetting } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import '../index.scss';
|
||||
|
||||
const Legal: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
|
|
|
@ -18,8 +18,7 @@ import { ADMIN_LIST_STATUS } from '@/common/constants';
|
|||
import { useEditStatusModal, useReportModal } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useQuestionSearch, changeQuestionStatus } from '@/services';
|
||||
|
||||
import '../index.scss';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
const questionFilterItems: Type.AdminContentsFilterBy[] = [
|
||||
'normal',
|
||||
|
@ -138,7 +137,7 @@ const Questions: FC = () => {
|
|||
<tr key={li.id}>
|
||||
<td>
|
||||
<a
|
||||
href={`/questions/${li.id}`}
|
||||
href={pathFactory.questionLanding(li.id, li.title)}
|
||||
target="_blank"
|
||||
className="text-break text-wrap"
|
||||
rel="noreferrer">
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { getSeoSetting, putSeoSetting } from '@/services';
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import { useToast } from '@/hooks';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.seo',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
properties: {
|
||||
robots: {
|
||||
type: 'string',
|
||||
title: t('robots.label'),
|
||||
description: t('robots.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
robots: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsSeo = {
|
||||
robots: formData.robots.value,
|
||||
};
|
||||
|
||||
putSeoSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSeoSetting().then((setting) => {
|
||||
if (setting) {
|
||||
const formMeta = { ...formData };
|
||||
formMeta.robots.value = setting.robots;
|
||||
setFormData(formMeta);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -5,8 +5,7 @@ import type * as Type from '@/common/interface';
|
|||
import { useToast } from '@/hooks';
|
||||
import { useSmtpSetting, updateSmtpSetting } from '@/services';
|
||||
import pattern from '@/common/pattern';
|
||||
import { SchemaForm, JSONSchema, UISchema } from '@/components';
|
||||
import { initFormData } from '../../../components/SchemaForm/index';
|
||||
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Smtp: FC = () => {
|
||||
|
|
|
@ -18,11 +18,9 @@ import { useChangeModal, useChangeUserRoleModal, useToast } from '@/hooks';
|
|||
import { useQueryUsers } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
'all',
|
||||
// 'staff',
|
||||
'staff',
|
||||
'inactive',
|
||||
'suspended',
|
||||
'deleted',
|
||||
|
@ -119,7 +117,7 @@ const Users: FC = () => {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>{t('name')}</th>
|
||||
{/* <th style={{ width: '12%' }}>{t('reputation')}</th> */}
|
||||
<th style={{ width: '12%' }}>{t('reputation')}</th>
|
||||
<th style={{ width: '20%' }}>{t('email')}</th>
|
||||
<th className="text-nowrap" style={{ width: '15%' }}>
|
||||
{t('created_at')}
|
||||
|
@ -131,7 +129,7 @@ const Users: FC = () => {
|
|||
)}
|
||||
|
||||
<th style={{ width: '12%' }}>{t('status')}</th>
|
||||
{/* <th style={{ width: '12%' }}>{t('role')}</th> */}
|
||||
<th style={{ width: '12%' }}>{t('role')}</th>
|
||||
{curFilter !== 'deleted' ? (
|
||||
<th style={{ width: '8%' }} className="text-end">
|
||||
{t('action')}
|
||||
|
@ -153,7 +151,6 @@ const Users: FC = () => {
|
|||
showReputation={false}
|
||||
/>
|
||||
</td>
|
||||
{/* <td>{user.rank}</td> */}
|
||||
<td className="text-break">{user.e_mail}</td>
|
||||
<td>
|
||||
<FormatTime time={user.created_at} />
|
||||
|
@ -173,11 +170,11 @@ const Users: FC = () => {
|
|||
{t(user.status)}
|
||||
</span>
|
||||
</td>
|
||||
{/* <td> */}
|
||||
{/* <span className="badge text-bg-light"> */}
|
||||
{/* {t(user.role_name)} */}
|
||||
{/* </span> */}
|
||||
{/* </td> */}
|
||||
<td>
|
||||
<span className="badge text-bg-light">
|
||||
{t(user.role_name)}
|
||||
</span>
|
||||
</td>
|
||||
{curFilter !== 'deleted' ? (
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
|
@ -185,28 +182,16 @@ const Users: FC = () => {
|
|||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{/* <Dropdown.Item>{t('set_new_password')}</Dropdown.Item> */}
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
</Dropdown.Item>
|
||||
{/* <Dropdown.Item */}
|
||||
{/* onClick={() => handleAction('role', user)}> */}
|
||||
{/* {t('change_role')} */}
|
||||
{/* </Dropdown.Item> */}
|
||||
{/* <Dropdown.Divider />
|
||||
<Dropdown.Item>{t('show_logs')}</Dropdown.Item> */}
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', user)}>
|
||||
{t('change_role')}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
{/* {user.status !== 'deleted' && (
|
||||
<Button
|
||||
className="p-0 btn-no-border"
|
||||
variant="link"
|
||||
onClick={() => handleClick(user)}>
|
||||
{t('change')}
|
||||
</Button>
|
||||
)} */}
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
|
|
|
@ -10,14 +10,11 @@ import {
|
|||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const Legal: FC = () => {
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.write',
|
||||
});
|
||||
const Toast = useToast();
|
||||
// const updateSiteInfo = siteInfoStore((state) => state.update);
|
||||
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
|
@ -125,4 +122,4 @@ const Legal: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Legal;
|
||||
export default Index;
|
||||
|
|
|
@ -4,17 +4,29 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
|
|||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { siteInfoStore, toastStore, brandingStore } from '@/stores';
|
||||
import {
|
||||
siteInfoStore,
|
||||
toastStore,
|
||||
brandingStore,
|
||||
headInfoStore,
|
||||
} from '@/stores';
|
||||
import { Header, Footer, Toast } from '@/components';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const { siteInfo } = siteInfoStore.getState();
|
||||
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
||||
const headInfo = headInfoStore((state) => state.headInfo);
|
||||
const { favicon, square_icon } = brandingStore((state) => state.branding);
|
||||
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
const {
|
||||
title,
|
||||
keywords,
|
||||
description = siteInfo.description,
|
||||
ldJSON,
|
||||
} = headInfo;
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
|
@ -26,8 +38,10 @@ const Layout: FC = () => {
|
|||
/>
|
||||
<link rel="icon" type="image/png" sizes="192x192" href={square_icon} />
|
||||
<link rel="apple-touch-icon" type="image/png" href={square_icon} />
|
||||
|
||||
{siteInfo && <meta name="description" content={siteInfo.description} />}
|
||||
{title && <title>{title}</title>}
|
||||
{keywords && <meta name="keywords" content={keywords} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{ldJSON && <script type="application/ld+json">{ldJSON}</script>}
|
||||
</Helmet>
|
||||
<SWRConfig
|
||||
value={{
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Accordion, ListGroup } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
@ -28,7 +29,7 @@ const SearchQuestion = ({ similarQuestions }) => {
|
|||
as="a"
|
||||
className="link-dark"
|
||||
key={item.id}
|
||||
href={`/questions/${item.id}`}
|
||||
href={pathFactory.questionLanding(item.id, item.title)}
|
||||
target="_blank">
|
||||
<span className="text-wrap text-break">
|
||||
{item.title}
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
useQueryQuestionByTitle,
|
||||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
import SearchQuestion from './components/SearchQuestion';
|
||||
|
||||
|
@ -235,7 +236,7 @@ const Ask = () => {
|
|||
edit_summary: formData.edit_summary.value,
|
||||
})
|
||||
.then((res) => {
|
||||
navigate(`/questions/${qid}`, {
|
||||
navigate(pathFactory.questionLanding(qid, params.title), {
|
||||
state: { isReview: res?.wait_for_review },
|
||||
});
|
||||
})
|
||||
|
@ -262,7 +263,7 @@ const Ask = () => {
|
|||
html: editorRef2.current.getHtml(),
|
||||
})
|
||||
.then(() => {
|
||||
navigate(`/questions/${id}`);
|
||||
navigate(pathFactory.questionLanding(id, params.title));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
|
@ -271,7 +272,7 @@ const Ask = () => {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
navigate(`/questions/${id}`);
|
||||
navigate(pathFactory.questionLanding(id, params.title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -466,7 +467,7 @@ const Ask = () => {
|
|||
<Card.Body
|
||||
className="fmt small"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('description', { keyPrefix: 'how_to_format' }),
|
||||
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { following } from '@/services';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
@ -57,10 +58,14 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
if (!data?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="h3 mb-3 text-wrap text-break">
|
||||
<Link className="link-dark" reloadDocument to={`/questions/${data.id}`}>
|
||||
<Link
|
||||
className="link-dark"
|
||||
reloadDocument
|
||||
to={pathFactory.questionLanding(data.id, data.title)}>
|
||||
{data.title}
|
||||
{data.status === 2
|
||||
? ` [${t('closed', { keyPrefix: 'question' })}]`
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Icon } from '@/components';
|
||||
import { useSimilarQuestion } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
@ -31,7 +32,7 @@ const Index: FC<Props> = ({ id }) => {
|
|||
action
|
||||
key={item.id}
|
||||
as={Link}
|
||||
to={`/questions/${item.id}`}>
|
||||
to={pathFactory.questionLanding(item.id, item.title)}>
|
||||
<div className="link-dark">{item.title}</div>
|
||||
{item.answer_count > 0 && (
|
||||
<div
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Pattern from '@/common/pattern';
|
||||
import { Pagination, PageTitle } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollTop } from '@/utils';
|
||||
|
@ -33,9 +34,14 @@ import './index.scss';
|
|||
const Index = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('translation');
|
||||
const { qid = '', aid = '' } = useParams();
|
||||
const [urlSearch] = useSearchParams();
|
||||
const { qid = '', slugPermalink = '' } = useParams();
|
||||
// Compatible with Permalink
|
||||
let { aid = '' } = useParams();
|
||||
if (!aid && Pattern.isAnswerId.test(slugPermalink)) {
|
||||
aid = slugPermalink;
|
||||
}
|
||||
|
||||
const [urlSearch] = useSearchParams();
|
||||
const page = Number(urlSearch.get('page') || 0);
|
||||
const order = urlSearch.get('order') || '';
|
||||
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
|
||||
|
@ -108,6 +114,12 @@ const Index = () => {
|
|||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'default') {
|
||||
window.scrollTo(0, 0);
|
||||
getDetail();
|
||||
return;
|
||||
}
|
||||
requestAnswers();
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Editor, EditorRef, Icon, PageTitle } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
|
@ -110,12 +111,18 @@ const Ask = () => {
|
|||
edit_summary: formData.description.value,
|
||||
};
|
||||
modifyAnswer(params).then((res) => {
|
||||
navigate(`/questions/${qid}/${aid}`, {
|
||||
state: { isReview: res?.wait_for_review },
|
||||
});
|
||||
navigate(
|
||||
pathFactory.answerLanding({
|
||||
questionId: qid,
|
||||
questionTitle: data?.question?.title,
|
||||
answerId: aid,
|
||||
}),
|
||||
{
|
||||
state: { isReview: res?.wait_for_review },
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
|
@ -138,7 +145,10 @@ const Ask = () => {
|
|||
</Row>
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||
<a href={`/questions/${qid}`} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href={pathFactory.questionLanding(qid, data?.question.title)}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<h5 className="mb-3">{data?.question.title}</h5>
|
||||
</a>
|
||||
|
||||
|
|
|
@ -100,22 +100,23 @@ const Index: FC = () => {
|
|||
const editor = unreviewed_info?.user_info;
|
||||
const editTime = unreviewed_info?.create_at;
|
||||
if (type === 'question') {
|
||||
itemLink = pathFactory.questionLanding(info?.object_id);
|
||||
itemLink = pathFactory.questionLanding(info?.object_id, info?.title);
|
||||
itemTitle = info?.title;
|
||||
editBadge = t('question_edit');
|
||||
editSummary ||= t('edit_question');
|
||||
} else if (type === 'answer') {
|
||||
itemLink = pathFactory.answerLanding(
|
||||
itemLink = pathFactory.answerLanding({
|
||||
// @ts-ignore
|
||||
unreviewed_info.content.question_id,
|
||||
unreviewed_info.object_id,
|
||||
);
|
||||
questionId: unreviewed_info.content.question_id,
|
||||
questionTitle: info?.title,
|
||||
answerId: unreviewed_info.object_id,
|
||||
});
|
||||
itemTitle = info?.title;
|
||||
editBadge = t('answer_edit');
|
||||
editSummary ||= t('edit_answer');
|
||||
} else if (type === 'tag') {
|
||||
const tagInfo = unreviewed_info.content as Type.Tag;
|
||||
itemLink = pathFactory.tagLanding(tagInfo);
|
||||
itemLink = pathFactory.tagLanding(tagInfo.slug_name);
|
||||
itemTitle = tagInfo.display_name;
|
||||
editBadge = t('tag_edit');
|
||||
editSummary ||= t('edit_tag');
|
||||
|
|
|
@ -2,6 +2,7 @@ import { memo, FC } from 'react';
|
|||
import { ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
||||
import type { SearchResItem } from '@/common/interface';
|
||||
import { escapeRemove } from '@/utils';
|
||||
|
@ -14,9 +15,13 @@ const Index: FC<Props> = ({ data }) => {
|
|||
if (!data?.object_type) {
|
||||
return null;
|
||||
}
|
||||
let itemUrl = `/questions/${data.object.id}`;
|
||||
if (data.object_type === 'answer') {
|
||||
itemUrl = `/questions/${data.object.question_id}/${data.object.id}`;
|
||||
let itemUrl = pathFactory.questionLanding(data.object.id, data.object.title);
|
||||
if (data.object_type === 'answer' && data.object.question_id) {
|
||||
itemUrl = pathFactory.answerLanding({
|
||||
questionId: data.object.question_id,
|
||||
questionTitle: data.object.title,
|
||||
answerId: data.object.id,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<ListGroupItem className="py-3 px-0">
|
||||
|
|
|
@ -31,7 +31,9 @@ const Questions: FC = () => {
|
|||
if (tagResp) {
|
||||
const info = { ...tagResp };
|
||||
if (info.main_tag_slug_name) {
|
||||
navigate(pathFactory.tagLanding(info), { replace: true });
|
||||
navigate(pathFactory.tagLanding(info.main_tag_slug_name), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (followResp) {
|
||||
|
@ -63,7 +65,7 @@ const Questions: FC = () => {
|
|||
<div className="tag-box mb-5">
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={pathFactory.tagLanding(tagInfo)}
|
||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||
replace
|
||||
className="link-dark">
|
||||
{tagInfo.display_name}
|
||||
|
@ -71,7 +73,7 @@ const Questions: FC = () => {
|
|||
</h3>
|
||||
|
||||
<p className="text-break">
|
||||
{escapeRemove(tagInfo.excerpt) || t('no_description')}
|
||||
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
|
||||
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
|
||||
[{t('more')}]
|
||||
</Link>
|
||||
|
|
|
@ -203,7 +203,7 @@ const Ask = () => {
|
|||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="description" className="mt-4">
|
||||
<Form.Label>{t('form.fields.description.label')}</Form.Label>
|
||||
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
||||
<Editor
|
||||
value={formData.description.value}
|
||||
onChange={handleDescriptionChange}
|
||||
|
@ -260,7 +260,7 @@ const Ask = () => {
|
|||
<Card.Body
|
||||
className="fmt small"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('description', { keyPrefix: 'how_to_format' }),
|
||||
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
|
|
@ -106,6 +106,7 @@ const TagIntroduction = () => {
|
|||
keyPrefix: 'page_title',
|
||||
})}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={pageTitle} />
|
||||
|
@ -114,7 +115,7 @@ const TagIntroduction = () => {
|
|||
<Col xxl={7} lg={8} sm={12}>
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={pathFactory.tagLanding(tagInfo)}
|
||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||
replace
|
||||
className="link-dark">
|
||||
{tagInfo.display_name}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Container, Row, Col, Form, Table } from 'react-bootstrap';
|
|||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { getTimelineData } from '@/services';
|
||||
import { PageTitle, Empty } from '@/components';
|
||||
|
@ -44,12 +45,19 @@ const Index: FC = () => {
|
|||
let linkUrl = '';
|
||||
let pageTitle = '';
|
||||
if (timelineData?.object_info.object_type === 'question') {
|
||||
linkUrl = `/questions/${timelineData?.object_info.question_id}`;
|
||||
linkUrl = pathFactory.questionLanding(
|
||||
timelineData?.object_info.question_id,
|
||||
timelineData?.object_info.title,
|
||||
);
|
||||
pageTitle = `${t('title_for_question')} ${timelineData?.object_info.title}`;
|
||||
}
|
||||
|
||||
if (timelineData?.object_info.object_type === 'answer') {
|
||||
linkUrl = `/questions/${timelineData?.object_info.question_id}/${timelineData?.object_info.answer_id}`;
|
||||
linkUrl = pathFactory.answerLanding({
|
||||
questionId: timelineData?.object_info.question_id,
|
||||
questionTitle: timelineData?.object_info.title,
|
||||
answerId: timelineData?.object_info.answer_id,
|
||||
});
|
||||
pageTitle = `${t('title_for_answer', {
|
||||
title: timelineData?.object_info.title,
|
||||
author: timelineData?.object_info.display_name,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, FormatTime, Tag } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
@ -20,7 +21,11 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
<ListGroupItem className="py-3 px-0" key={item.answer_id}>
|
||||
<h6 className="mb-2">
|
||||
<a
|
||||
href={`/questions/${item.question_id}/${item.answer_id}`}
|
||||
href={pathFactory.answerLanding({
|
||||
questionId: item.question_id,
|
||||
questionTitle: item.question_info?.title,
|
||||
answerId: item.answer_id,
|
||||
})}
|
||||
className="text-break">
|
||||
{item.question_info?.title}
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { FormatTime } from '@/components';
|
||||
|
||||
interface Props {
|
||||
|
@ -19,11 +20,15 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
<ListGroupItem className="py-3 px-0" key={item.comment_id}>
|
||||
<a
|
||||
className="text-break"
|
||||
href={`/questions/${
|
||||
href={
|
||||
item.object_type === 'question'
|
||||
? item.object_id
|
||||
: `${item.question_id}/${item.object_id}`
|
||||
}`}>
|
||||
? pathFactory.questionLanding(item.object_id, item.title)
|
||||
: pathFactory.answerLanding({
|
||||
questionId: item.question_id,
|
||||
questionTitle: item.title,
|
||||
answerId: item.object_id,
|
||||
})
|
||||
}>
|
||||
{item.title}
|
||||
</a>
|
||||
<div
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, FormatTime, Tag, BaseUserCard } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
@ -15,6 +16,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
|||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListGroup variant="flush">
|
||||
{data.map((item) => {
|
||||
|
@ -25,9 +27,10 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
|||
<h6 className="mb-2">
|
||||
<a
|
||||
className="text-break"
|
||||
href={`/questions/${
|
||||
tabName === 'questions' ? item.question_id : item.id
|
||||
}`}>
|
||||
href={pathFactory.questionLanding(
|
||||
tabName === 'questions' ? item.question_id : item.id,
|
||||
item.title,
|
||||
)}>
|
||||
{item.title}
|
||||
{tabName === 'questions' && item.status === 'closed'
|
||||
? ` [${t('closed', { keyPrefix: 'question' })}]`
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormatTime } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
@ -30,11 +31,15 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
<div>
|
||||
<a
|
||||
className="text-break"
|
||||
href={`/questions/${
|
||||
href={
|
||||
item.object_type === 'question'
|
||||
? item.object_id
|
||||
: `${item.question_id}/${item.object_id}`
|
||||
}`}>
|
||||
? pathFactory.questionLanding(item.object_id, item.title)
|
||||
: pathFactory.answerLanding({
|
||||
questionId: item.question_id,
|
||||
questionTitle: item.title,
|
||||
answerId: item.object_id,
|
||||
})
|
||||
}>
|
||||
{item.title}
|
||||
</a>
|
||||
<div className="d-flex align-items-center fs-14 text-secondary">
|
||||
|
|
|
@ -2,6 +2,7 @@ import { FC, memo } from 'react';
|
|||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
interface Props {
|
||||
|
@ -10,7 +11,6 @@ interface Props {
|
|||
}
|
||||
const Index: FC<Props> = ({ data, type }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||
|
||||
return (
|
||||
<ListGroup variant="flush" className="mb-4">
|
||||
{data?.map((item) => {
|
||||
|
@ -19,11 +19,15 @@ const Index: FC<Props> = ({ data, type }) => {
|
|||
className="p-0 border-0 mb-2"
|
||||
key={type === 'answer' ? item.answer_id : item.question_id}>
|
||||
<a
|
||||
href={`/questions/${
|
||||
href={
|
||||
type === 'answer'
|
||||
? `${item.question_id}/${item.answer_id}`
|
||||
: item.question_id
|
||||
}`}>
|
||||
? pathFactory.answerLanding({
|
||||
questionId: item.question_id,
|
||||
questionTitle: item.question_info?.title,
|
||||
answerId: item.answer_id,
|
||||
})
|
||||
: pathFactory.questionLanding(item.question_id, item.title)
|
||||
}>
|
||||
{type === 'answer' ? item.question_info.title : item.title}
|
||||
</a>
|
||||
<div className="d-inline-block text-secondary ms-3 fs-14">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { FormatTime } from '@/components';
|
||||
|
||||
interface Props {
|
||||
|
@ -12,6 +13,7 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
if (!visible || !data?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListGroup variant="flush">
|
||||
{data.map((item) => {
|
||||
|
@ -25,11 +27,15 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
<div>
|
||||
<a
|
||||
className="text-break"
|
||||
href={`/questions/${
|
||||
href={
|
||||
item.object_type === 'question'
|
||||
? item.question_id
|
||||
: `${item.question_id}/${item.answer_id}`
|
||||
}`}>
|
||||
? pathFactory.questionLanding(item.question_id, item.title)
|
||||
: pathFactory.answerLanding({
|
||||
questionId: item.question_id,
|
||||
questionTitle: item.title,
|
||||
answerId: item.answer_id,
|
||||
})
|
||||
}>
|
||||
{item.title}
|
||||
</a>
|
||||
<div className="d-flex align-items-center fs-14 text-secondary">
|
||||
|
|
|
@ -1,22 +1,53 @@
|
|||
import type * as Type from '@/common/interface';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
const tagLanding = (tag: Type.Tag) => {
|
||||
let slugName = tag.slug_name || '';
|
||||
import Pattern from '@/common/pattern';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
const tagLanding = (slugName: string) => {
|
||||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return `/tags/${encodeURIComponent(slugName)}`;
|
||||
return urlcat('/tags/:slugName', { slugName });
|
||||
};
|
||||
const tagInfo = (slugName: string) => {
|
||||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return `/tags/${encodeURIComponent(slugName)}/info`;
|
||||
return urlcat('/tags/:slugName/info', { slugName });
|
||||
};
|
||||
const tagEdit = (tagId: string) => {
|
||||
return `/tags/${tagId}/edit`;
|
||||
return urlcat('/tags/:tagId/edit', { tagId });
|
||||
};
|
||||
const questionLanding = (question_id: string) => {
|
||||
return `/questions/${question_id}`;
|
||||
const questionLanding = (questionId: string, title: string = '') => {
|
||||
const { siteInfo } = siteInfoStore.getState();
|
||||
if (siteInfo.permalink === 1) {
|
||||
title = title.toLowerCase();
|
||||
title = title.trim().replace(/\s+/g, '-');
|
||||
title = title.replace(Pattern.emoji, '');
|
||||
if (title) {
|
||||
return urlcat('/questions/:questionId/:slugPermalink', {
|
||||
questionId,
|
||||
slugPermalink: title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urlcat('/questions/:questionId', { questionId });
|
||||
};
|
||||
const answerLanding = (question_id: string, answer_id: string) => {
|
||||
return `/questions/${question_id}/${answer_id}`;
|
||||
const answerLanding = (params: {
|
||||
questionId: string;
|
||||
questionTitle?: string;
|
||||
answerId: string;
|
||||
}) => {
|
||||
const questionLandingUrl = questionLanding(
|
||||
params.questionId,
|
||||
params.questionTitle,
|
||||
);
|
||||
return urlcat(`${questionLandingUrl}/:answerId`, {
|
||||
answerId: params.answerId,
|
||||
});
|
||||
};
|
||||
|
||||
export const pathFactory = {
|
||||
|
|
|
@ -33,14 +33,6 @@ const routes: RouteNode[] = [
|
|||
path: 'questions',
|
||||
page: 'pages/Questions',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid/:aid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'questions/ask',
|
||||
page: 'pages/Questions/Ask',
|
||||
|
@ -48,6 +40,18 @@ const routes: RouteNode[] = [
|
|||
return guard.activated();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid/:slugPermalink',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid/:slugPermalink/:aid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'posts/:qid/edit',
|
||||
page: 'pages/Questions/Ask',
|
||||
|
@ -269,6 +273,10 @@ const routes: RouteNode[] = [
|
|||
path: 'write',
|
||||
page: 'pages/Admin/Write',
|
||||
},
|
||||
{
|
||||
path: 'seo',
|
||||
page: 'pages/Admin/Seo',
|
||||
},
|
||||
],
|
||||
},
|
||||
// for review
|
||||
|
|
|
@ -101,3 +101,11 @@ export const getLegalSetting = () => {
|
|||
export const putLegalSetting = (params: Type.AdminSettingsLegal) => {
|
||||
return request.put('/answer/admin/api/siteinfo/legal', params);
|
||||
};
|
||||
|
||||
export const getSeoSetting = () => {
|
||||
return request.get<Type.AdminSettingsSeo>('/answer/admin/api/siteinfo/seo');
|
||||
};
|
||||
|
||||
export const putSeoSetting = (params: Type.AdminSettingsSeo) => {
|
||||
return request.put('/answer/admin/api/siteinfo/seo', params);
|
||||
};
|
||||
|
|
|
@ -248,3 +248,7 @@ export const changeEmailVerify = (params: { code: string }) => {
|
|||
export const getAppSettings = () => {
|
||||
return request.get<Type.SiteSettings>('/answer/api/v1/siteinfo');
|
||||
};
|
||||
|
||||
export const reopenQuestion = (params: { question_id: string }) => {
|
||||
return request.put('/answer/api/v1/question/reopen', params);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
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,6 +4,7 @@ import globalStore from './global';
|
|||
import siteInfoStore from './siteInfo';
|
||||
import interfaceStore from './interface';
|
||||
import brandingStore from './branding';
|
||||
import headInfoStore from './headInfo';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
|
@ -12,4 +13,5 @@ export {
|
|||
siteInfoStore,
|
||||
interfaceStore,
|
||||
brandingStore,
|
||||
headInfoStore,
|
||||
};
|
||||
|
|
|
@ -14,11 +14,16 @@ const siteInfo = create<SiteInfoType>((set) => ({
|
|||
short_description: '',
|
||||
site_url: '',
|
||||
contact_email: '',
|
||||
permalink: 1,
|
||||
},
|
||||
update: (params) =>
|
||||
set(() => {
|
||||
set((_) => {
|
||||
const o = { ..._.siteInfo, ...params };
|
||||
if (o.permalink !== 1 && o.permalink !== 2) {
|
||||
o.permalink = 1;
|
||||
}
|
||||
return {
|
||||
siteInfo: params,
|
||||
siteInfo: o,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue