feat(permalink): permalink factory method done

This commit is contained in:
haitao(lj) 2022-12-02 11:50:13 +08:00
parent 9ceb9e0a07
commit ca576d9b12
11 changed files with 73 additions and 15 deletions

View File

@ -1104,6 +1104,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:

View File

@ -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": {

View File

@ -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:

View File

@ -279,6 +279,7 @@ export interface AdminSettingsGeneral {
description: string;
site_url: string;
contact_email: string;
permalink: boolean;
}
export interface HeadInfo {

View File

@ -1,4 +1,7 @@
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,}))$/,
};

View File

@ -48,6 +48,13 @@ const General: FC = () => {
title: t('contact_email.label'),
description: t('contact_email.text'),
},
permalink: {
type: 'boolean',
title: t('permalink.label'),
description: t('permalink.text'),
enum: [true, false],
enumNames: ['/questions/123/post-title', '/questions/123'],
},
},
};
const uiSchema: UISchema = {
@ -63,7 +70,7 @@ const General: FC = () => {
}
if (
!url ||
/^https?:$/.test(url.protocol) === false ||
!/^https?:$/.test(url.protocol) ||
url.pathname !== '/' ||
url.search !== '' ||
url.hash !== ''
@ -86,6 +93,9 @@ const General: FC = () => {
},
},
},
permalink: {
'ui:widget': 'select',
},
};
const [formData, setFormData] = useState<Type.FormDataType>(
initFormData(schema),
@ -100,6 +110,7 @@ const General: FC = () => {
short_description: formData.short_description.value,
site_url: formData.site_url.value,
contact_email: formData.contact_email.value,
permalink: formData.permalink.value,
};
updateGeneralSetting(reqParams)

View File

@ -28,7 +28,6 @@ const Index = () => {
const navigate = useNavigate();
const { qid = '', aid = '' } = useParams();
const [urlSearch] = useSearchParams();
const page = Number(urlSearch.get('page') || 0);
const order = urlSearch.get('order') || '';
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);

View File

@ -100,7 +100,7 @@ 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');
@ -108,6 +108,7 @@ const Index: FC = () => {
itemLink = pathFactory.answerLanding(
// @ts-ignore
unreviewed_info.content.question_id,
info.title,
unreviewed_info.object_id,
);
itemTitle = info?.title;

View File

@ -1,22 +1,41 @@
import urlcat from 'urlcat';
import type * as Type from '@/common/interface';
import Pattern from '@/common/pattern';
import { siteInfoStore } from '@/stores';
const tagLanding = (tag: Type.Tag) => {
let slugName = tag.main_tag_slug_name || tag.slug_name || '';
slugName = slugName.toLowerCase();
return `/tags/${encodeURIComponent(slugName)}`;
return urlcat('/tags/:slugName', { slugName });
};
const tagInfo = (slugName: string) => {
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) {
title = title.toLowerCase();
title = title.trim().replace(/\s+/g, '-');
title = title.replace(Pattern.emoji, '');
return urlcat('/questions/:questionId/:title', { questionId, title });
}
return urlcat('/questions/:questionId', { questionId });
};
const answerLanding = (question_id: string, answer_id: string) => {
return `/questions/${question_id}/${answer_id}`;
const answerLanding = (
questionId: string,
questionTitle: string = '',
answerId: string,
) => {
const questionLandingUrl = questionLanding(questionId, questionTitle);
return urlcat(`${questionLandingUrl}/:answerId`, {
answerId,
});
};
export const pathFactory = {

View File

@ -1,5 +1,6 @@
import { LoaderFunctionArgs, RouteObject } from 'react-router-dom';
import { siteInfoStore } from '@/stores';
import { guard } from '@/utils';
import type { TGuardResult } from '@/utils/guard';
@ -15,7 +16,7 @@ export interface RouteNode extends RouteObject {
*/
guard?: (args: LoaderFunctionArgs) => Promise<TGuardResult>;
}
const { siteInfo } = siteInfoStore.getState();
const routes: RouteNode[] = [
{
path: '/',
@ -34,11 +35,13 @@ const routes: RouteNode[] = [
page: 'pages/Questions',
},
{
path: 'questions/:qid',
path: siteInfo.permalink ? 'questions/:qid/:title' : 'questions/:qid',
page: 'pages/Questions/Detail',
},
{
path: 'questions/:qid/:aid',
path: siteInfo.permalink
? 'questions/:qid/:title/:aid'
: 'questions/:qid/:aid',
page: 'pages/Questions/Detail',
},
{

View File

@ -14,11 +14,13 @@ const siteInfo = create<SiteInfoType>((set) => ({
short_description: '',
site_url: '',
contact_email: '',
permalink: true,
},
update: (params) =>
set(() => {
set((_) => {
const o = { ..._.siteInfo, ...params };
return {
siteInfo: params,
siteInfo: o,
};
}),
}));