Commit cff4d2fb authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

fix

parent 4489f507
...@@ -15,15 +15,14 @@ import { ...@@ -15,15 +15,14 @@ import {
HEADER_CONFIG_STORAGE_KEY, HEADER_CONFIG_STORAGE_KEY,
HeaderCategoryItem, HeaderCategoryItem,
normalizeHeaderCategories, normalizeHeaderCategories,
toSlug,
} from '@/mockdata/header-config'; } from '@/mockdata/header-config';
import { import {
createHeaderCategoryPostId,
EMPTY_HEADER_CATEGORY_POST_FORM, EMPTY_HEADER_CATEGORY_POST_FORM,
getHeaderCategoryPostSeed, getHeaderCategoryPostSeed,
HEADER_CATEGORY_POSTS_STORAGE_KEY, HEADER_CATEGORY_POSTS_STORAGE_KEY,
HeaderCategoryPostFormValues, HeaderCategoryPostFormValues,
HeaderCategoryPostItem, HeaderCategoryPostItem,
makeHeaderCategoryPostSlug,
normalizeHeaderCategoryPosts, normalizeHeaderCategoryPosts,
} from '@/mockdata/header-category-posts'; } from '@/mockdata/header-category-posts';
import { ArrowLeft, Save } from 'lucide-react'; import { ArrowLeft, Save } from 'lucide-react';
...@@ -97,14 +96,6 @@ function persistHeaderCategoryPosts(items: HeaderCategoryPostItem[]) { ...@@ -97,14 +96,6 @@ function persistHeaderCategoryPosts(items: HeaderCategoryPostItem[]) {
); );
} }
function upsertPost(items: HeaderCategoryPostItem[], post: HeaderCategoryPostItem) {
const exists = items.some((item) => item.id === post.id);
return normalizeHeaderCategoryPosts(
exists ? items.map((item) => (item.id === post.id ? post : item)) : [...items, post],
);
}
function useHeaderCategoryPostsModule() { function useHeaderCategoryPostsModule() {
const [items, setItems] = React.useState<HeaderCategoryPostItem[]>([]); const [items, setItems] = React.useState<HeaderCategoryPostItem[]>([]);
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
...@@ -134,10 +125,10 @@ function useHeaderCategoryPostsModule() { ...@@ -134,10 +125,10 @@ function useHeaderCategoryPostsModule() {
const now = new Date().toISOString(); const now = new Date().toISOString();
const nextPost: HeaderCategoryPostItem = { const nextPost: HeaderCategoryPostItem = {
id: createHeaderCategoryPostId(), id: `header-post-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
category_id: categoryId, category_id: categoryId,
title: values.title.trim(), title: values.title.trim(),
slug: values.slug.trim() || makeHeaderCategoryPostSlug(values.title), slug: values.slug.trim() || toSlug(values.title),
excerpt: values.excerpt.trim(), excerpt: values.excerpt.trim(),
content: values.content.trim(), content: values.content.trim(),
thumbnail: values.thumbnail.trim(), thumbnail: values.thumbnail.trim(),
...@@ -147,35 +138,32 @@ function useHeaderCategoryPostsModule() { ...@@ -147,35 +138,32 @@ function useHeaderCategoryPostsModule() {
updated_at: now, updated_at: now,
}; };
setItems((current) => upsertPost(current, nextPost)); setItems((current) => normalizeHeaderCategoryPosts([...current, nextPost]));
return nextPost; return nextPost;
}, },
[], [],
); );
const updatePost = React.useCallback((postId: string, values: HeaderCategoryPostFormValues) => { const updatePost = React.useCallback((postId: string, values: HeaderCategoryPostFormValues) => {
let updatedPost: HeaderCategoryPostItem | null = null; setItems((current) =>
normalizeHeaderCategoryPosts(
setItems((current) => { current.map((item) =>
const existing = current.find((item) => item.id === postId); item.id === postId
if (!existing) return current; ? {
...item,
updatedPost = { title: values.title.trim(),
...existing, slug: values.slug.trim() || toSlug(values.title),
title: values.title.trim(), excerpt: values.excerpt.trim(),
slug: values.slug.trim() || makeHeaderCategoryPostSlug(values.title), content: values.content.trim(),
excerpt: values.excerpt.trim(), thumbnail: values.thumbnail.trim(),
content: values.content.trim(), published_at: values.published_at || item.published_at,
thumbnail: values.thumbnail.trim(), is_active: values.is_active,
published_at: values.published_at || existing.published_at, updated_at: new Date().toISOString(),
is_active: values.is_active, }
updated_at: new Date().toISOString(), : item,
}; ),
),
return upsertPost(current, updatedPost); );
});
return updatedPost;
}, []); }, []);
const toFormValues = React.useCallback( const toFormValues = React.useCallback(
...@@ -313,17 +301,7 @@ export default function HeaderCategoryPostFormPage() { ...@@ -313,17 +301,7 @@ export default function HeaderCategoryPostFormPage() {
setForm((previous) => ({ setForm((previous) => ({
...previous, ...previous,
title: value, title: value,
slug: slug: toSlug(value) || previous.slug,
value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/đ/g, 'd')
.replace(/Đ/g, 'D')
.toLowerCase()
.trim()
.replace(/[^a-z0-9\\s-]/g, '')
.replace(/\\s+/g, '-')
.replace(/-+/g, '-') || previous.slug,
})); }));
}; };
......
...@@ -35,12 +35,9 @@ import { ...@@ -35,12 +35,9 @@ import {
normalizeHeaderCategories, normalizeHeaderCategories,
} from '@/mockdata/header-config'; } from '@/mockdata/header-config';
import { import {
createHeaderCategoryPostId,
getHeaderCategoryPostSeed, getHeaderCategoryPostSeed,
HEADER_CATEGORY_POSTS_STORAGE_KEY, HEADER_CATEGORY_POSTS_STORAGE_KEY,
HeaderCategoryPostFormValues,
HeaderCategoryPostItem, HeaderCategoryPostItem,
makeHeaderCategoryPostSlug,
normalizeHeaderCategoryPosts, normalizeHeaderCategoryPosts,
} from '@/mockdata/header-category-posts'; } from '@/mockdata/header-category-posts';
import { import {
...@@ -120,14 +117,6 @@ function persistHeaderCategoryPosts(items: HeaderCategoryPostItem[]) { ...@@ -120,14 +117,6 @@ function persistHeaderCategoryPosts(items: HeaderCategoryPostItem[]) {
); );
} }
function upsertPost(items: HeaderCategoryPostItem[], post: HeaderCategoryPostItem) {
const exists = items.some((item) => item.id === post.id);
return normalizeHeaderCategoryPosts(
exists ? items.map((item) => (item.id === post.id ? post : item)) : [...items, post],
);
}
function useHeaderCategoryPostsModule() { function useHeaderCategoryPostsModule() {
const [items, setItems] = React.useState<HeaderCategoryPostItem[]>([]); const [items, setItems] = React.useState<HeaderCategoryPostItem[]>([]);
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
...@@ -147,60 +136,6 @@ function useHeaderCategoryPostsModule() { ...@@ -147,60 +136,6 @@ function useHeaderCategoryPostsModule() {
[items], [items],
); );
const getPostById = React.useCallback(
(postId: string) => items.find((item) => item.id === postId) ?? null,
[items],
);
const createPost = React.useCallback(
(categoryId: string, values: HeaderCategoryPostFormValues) => {
const now = new Date().toISOString();
const nextPost: HeaderCategoryPostItem = {
id: createHeaderCategoryPostId(),
category_id: categoryId,
title: values.title.trim(),
slug: values.slug.trim() || makeHeaderCategoryPostSlug(values.title),
excerpt: values.excerpt.trim(),
content: values.content.trim(),
thumbnail: values.thumbnail.trim(),
published_at: values.published_at || now.slice(0, 10),
is_active: values.is_active,
created_at: now,
updated_at: now,
};
setItems((current) => upsertPost(current, nextPost));
return nextPost;
},
[],
);
const updatePost = React.useCallback((postId: string, values: HeaderCategoryPostFormValues) => {
let updatedPost: HeaderCategoryPostItem | null = null;
setItems((current) => {
const existing = current.find((item) => item.id === postId);
if (!existing) return current;
updatedPost = {
...existing,
title: values.title.trim(),
slug: values.slug.trim() || makeHeaderCategoryPostSlug(values.title),
excerpt: values.excerpt.trim(),
content: values.content.trim(),
thumbnail: values.thumbnail.trim(),
published_at: values.published_at || existing.published_at,
is_active: values.is_active,
updated_at: new Date().toISOString(),
};
return upsertPost(current, updatedPost);
});
return updatedPost;
}, []);
const removePost = React.useCallback((postId: string) => { const removePost = React.useCallback((postId: string) => {
setItems((current) => current.filter((item) => item.id !== postId)); setItems((current) => current.filter((item) => item.id !== postId));
}, []); }, []);
...@@ -208,9 +143,6 @@ function useHeaderCategoryPostsModule() { ...@@ -208,9 +143,6 @@ function useHeaderCategoryPostsModule() {
return { return {
isReady, isReady,
getPostsByCategory, getPostsByCategory,
getPostById,
createPost,
updatePost,
removePost, removePost,
}; };
} }
...@@ -417,13 +349,13 @@ export default function HeaderCategoryPostsPage() { ...@@ -417,13 +349,13 @@ export default function HeaderCategoryPostsPage() {
</div> </div>
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<p className="truncate font-medium text-black">{post.title}</p> <p className="truncate font-medium text-black">{post.title}</p>
<p className="line-clamp-2 text-sm text-gray-700">{post.excerpt || ''}</p> <p className="line-clamp-2 text-sm text-gray-700">{post.excerpt || '-'}</p>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-center text-sm text-gray-700">{post.slug}</TableCell> <TableCell className="text-center text-sm text-gray-700">{post.slug}</TableCell>
<TableCell className="text-center text-sm text-gray-700"> <TableCell className="text-center text-sm text-gray-700">
{post.published_at ? dayjs(post.published_at).format('DD/MM/YYYY') : ''} {post.published_at ? dayjs(post.published_at).format('DD/MM/YYYY') : '-'}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
{post.is_active ? ( {post.is_active ? (
......
...@@ -8,22 +8,31 @@ interface HeaderCategoryStatsProps { ...@@ -8,22 +8,31 @@ interface HeaderCategoryStatsProps {
total: number; total: number;
root: number; root: number;
nested: number; nested: number;
grouped: number;
} }
export function HeaderCategoryStats({ export function HeaderCategoryStats({
total, total,
root, root,
nested, nested,
grouped,
}: HeaderCategoryStatsProps) { }: HeaderCategoryStatsProps) {
return ( return (
<AdminStatsGrid <AdminStatsGrid
items={[ items={[
{ label: "Tổng danh mục", value: total, icon: <FolderTree className="h-4 w-4 text-[#063e8e]" /> }, {
{ label: "Danh mục cha", value: root, icon: <FolderTree className="h-4 w-4 text-[#063e8e]" /> }, label: "Tổng danh mục",
{ label: "Danh mục con", value: nested, icon: <FolderTree className="h-4 w-4 text-[#063e8e]" /> }, value: total,
{ label: "Có danh mục con", value: grouped, icon: <FolderTree className="h-4 w-4 text-[#063e8e]" /> }, icon: <FolderTree className="h-4 w-4 text-[#063e8e]" />,
},
{
label: "Danh mục cha",
value: root,
icon: <FolderTree className="h-4 w-4 text-[#063e8e]" />,
},
{
label: "Danh mục con",
value: nested,
icon: <FolderTree className="h-4 w-4 text-[#063e8e]" />,
},
]} ]}
/> />
); );
......
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Plus } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
HeaderCategoryDeleteDialog, HeaderCategoryDeleteDialog,
...@@ -12,7 +11,6 @@ import { ...@@ -12,7 +11,6 @@ import {
HeaderCategoryStats, HeaderCategoryStats,
HeaderCategoryTable, HeaderCategoryTable,
} from './components'; } from './components';
import { Button } from '@/components/ui/button';
import { import {
buildHeaderCategoryTree, buildHeaderCategoryTree,
createHeaderCategoryId, createHeaderCategoryId,
...@@ -252,11 +250,6 @@ export default function HeaderConfigPage() { ...@@ -252,11 +250,6 @@ export default function HeaderConfigPage() {
[tree], [tree],
); );
const groupedCount = React.useMemo(
() => flatRows.filter((item) => item.children.length > 0).length,
[flatRows],
);
const editingItem = React.useMemo( const editingItem = React.useMemo(
() => flatRows.find((item) => item.id === formValues.id) ?? null, () => flatRows.find((item) => item.id === formValues.id) ?? null,
[flatRows, formValues.id], [flatRows, formValues.id],
...@@ -347,7 +340,6 @@ export default function HeaderConfigPage() { ...@@ -347,7 +340,6 @@ export default function HeaderConfigPage() {
total={flatRows.length} total={flatRows.length}
root={flatRows.filter((item) => !item.parentId).length} root={flatRows.filter((item) => !item.parentId).length}
nested={flatRows.filter((item) => item.parentId).length} nested={flatRows.filter((item) => item.parentId).length}
grouped={groupedCount}
/> />
<HeaderCategoryTable <HeaderCategoryTable
...@@ -355,19 +347,11 @@ export default function HeaderConfigPage() { ...@@ -355,19 +347,11 @@ export default function HeaderConfigPage() {
expanded={expanded} expanded={expanded}
isLoading={!isReady} isLoading={!isReady}
searchValue={search} searchValue={search}
action={
<Button
className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
onClick={openCreateRoot}
>
<Plus className="mr-2 h-4 w-4" />
Thêm danh mục
</Button>
}
onSearchChange={setSearch} onSearchChange={setSearch}
onToggle={(id) => onToggle={(id) =>
setExpanded((previous) => ({ ...previous, [id]: !(previous[id] ?? true) })) setExpanded((previous) => ({ ...previous, [id]: !(previous[id] ?? true) }))
} }
onCreateRoot={openCreateRoot}
onCreateChild={openCreateChild} onCreateChild={openCreateChild}
onEdit={openEdit} onEdit={openEdit}
onDelete={setDeleteTarget} onDelete={setDeleteTarget}
......
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface AdminStatsGridItem { interface AdminStatsGridItem {
label: string; label: string;
...@@ -15,20 +14,31 @@ interface AdminStatsGridProps { ...@@ -15,20 +14,31 @@ interface AdminStatsGridProps {
} }
export function AdminStatsGrid({ items, className }: AdminStatsGridProps) { export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
const gridClassName =
className ??
(items.length === 3
? "grid grid-cols-1 gap-4 md:grid-cols-3"
: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4");
return ( return (
<div className={className ?? "grid grid-cols-2 gap-4 lg:grid-cols-4"}> <div className={gridClassName}>
{items.map((item) => ( {items.map((item) => (
<Card key={item.label} className="border-slate-200 shadow-sm"> <div
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> key={item.label}
<CardTitle className="text-sm font-medium text-gray-700"> className="rounded-2xl border border-[#063e8e]/15 bg-white px-5 py-4 shadow-sm"
{item.label} >
</CardTitle> <div className="flex items-start justify-between gap-4">
{item.icon ?? null} <div className="space-y-2">
</CardHeader> <p className="text-sm font-medium text-gray-700">{item.label}</p>
<CardContent> <div className="text-3xl font-semibold leading-none text-black">{item.value}</div>
<div className="text-2xl font-bold text-black">{item.value}</div> </div>
</CardContent> {item.icon ? (
</Card> <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#063e8e]/10">
{item.icon}
</div>
) : null}
</div>
</div>
))} ))}
</div> </div>
); );
......
"use client";
import * as React from "react";
import { Plus } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
interface AdminTableLayoutProps {
searchValue: string;
searchPlaceholder?: string;
actionLabel?: string;
actionIcon?: React.ReactNode;
actionDisabled?: boolean;
children: React.ReactNode;
onSearchChange: (value: string) => void;
onActionClick?: () => void;
}
export function AdminTableLayout({
searchValue,
searchPlaceholder = "Tìm kiếm...",
actionLabel,
actionIcon,
actionDisabled = false,
children,
onSearchChange,
onActionClick,
}: AdminTableLayoutProps) {
return (
<div className="space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<Input
value={searchValue}
placeholder={searchPlaceholder}
onChange={(event) => onSearchChange(event.target.value)}
className="max-w-sm border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700"
/>
{actionLabel ? (
<Button
type="button"
disabled={actionDisabled}
onClick={onActionClick}
className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
>
{actionIcon ?? <Plus className="mr-2 h-4 w-4" />}
{actionLabel}
</Button>
) : null}
</div>
<div className="overflow-hidden rounded-xl border border-[#063e8e]/15 bg-white shadow-sm">
{children}
</div>
</div>
);
}
...@@ -19,6 +19,88 @@ export interface HeaderCategoryPostItem { ...@@ -19,6 +19,88 @@ export interface HeaderCategoryPostItem {
export const HEADER_CATEGORY_POSTS_STORAGE_KEY = export const HEADER_CATEGORY_POSTS_STORAGE_KEY =
"vcci-news.header-category-posts.data.v1"; "vcci-news.header-category-posts.data.v1";
function normalizeVietnameseText(value: string) {
return value
.replace(/Ä‘/g, "đ")
.replace(/Đ/g, "Đ")
.replace(/à/g, "à")
.replace(/á/g, "á")
.replace(/ả/g, "ả")
.replace(/ã/g, "ã")
.replace(/ạ/g, "ạ")
.replace(/ă/g, "ă")
.replace(/â/g, "â")
.replace(/è/g, "è")
.replace(/é/g, "é")
.replace(/ẻ/g, "ẻ")
.replace(/ẽ/g, "ẽ")
.replace(/ẹ/g, "ẹ")
.replace(/ê/g, "ê")
.replace(/ì/g, "ì")
.replace(/í/g, "í")
.replace(/Ä©/g, "ĩ")
.replace(/ò/g, "ò")
.replace(/ó/g, "ó")
.replace(/õ/g, "õ")
.replace(/ô/g, "ô")
.replace(/Æ¡/g, "ơ")
.replace(/ù/g, "ù")
.replace(/ú/g, "ú")
.replace(/Å©/g, "ũ")
.replace(/ư/g, "ư")
.replace(/ề/g, "ề")
.replace(/ế/g, "ế")
.replace(/ể/g, "ể")
.replace(/á»…/g, "ễ")
.replace(/ệ/g, "ệ")
.replace(/ờ/g, "ờ")
.replace(/á»›/g, "ớ")
.replace(/ở/g, "ở")
.replace(/ỡ/g, "ỡ")
.replace(/ợ/g, "ợ")
.replace(/ừ/g, "ừ")
.replace(/ứ/g, "ứ")
.replace(/á»­/g, "ử")
.replace(/ữ/g, "ữ")
.replace(/á»±/g, "ự")
.replace(/ỳ/g, "ỳ")
.replace(/ý/g, "ý")
.replace(/á»·/g, "ỷ")
.replace(/ỹ/g, "ỹ")
.replace(/ỵ/g, "ỵ")
.replace(/ổ/g, "ổ")
.replace(/ố/g, "ố")
.replace(/ồ/g, "ồ")
.replace(/á»—/g, "ỗ")
.replace(/á»™/g, "ộ")
.replace(/ầ/g, "ầ")
.replace(/ấ/g, "ấ")
.replace(/ẩ/g, "ẩ")
.replace(/ẫ/g, "ẫ")
.replace(/ậ/g, "ậ")
.replace(/ằ/g, "ằ")
.replace(/ắ/g, "ắ")
.replace(/ẳ/g, "ẳ")
.replace(/ẵ/g, "ẵ")
.replace(/ặ/g, "ặ")
.replace(/ỏ/g, "ỏ")
.replace(/ọ/g, "ọ")
.replace(/á»§/g, "ủ")
.replace(/ụ/g, "ụ")
.replace(/ỉ/g, "ỉ")
.replace(/ị/g, "ị")
.replace(/ời/g, "ời");
}
function normalizePostItem(item: HeaderCategoryPostItem): HeaderCategoryPostItem {
return {
...item,
title: normalizeVietnameseText(item.title),
excerpt: normalizeVietnameseText(item.excerpt),
content: normalizeVietnameseText(item.content),
};
}
export const headerCategoryPostSeed: HeaderCategoryPostItem[] = [ export const headerCategoryPostSeed: HeaderCategoryPostItem[] = [
{ {
id: "header-post-intro-about", id: "header-post-intro-about",
...@@ -124,12 +206,14 @@ export const EMPTY_HEADER_CATEGORY_POST_FORM: HeaderCategoryPostFormValues = { ...@@ -124,12 +206,14 @@ export const EMPTY_HEADER_CATEGORY_POST_FORM: HeaderCategoryPostFormValues = {
}; };
export function normalizeHeaderCategoryPosts(items: HeaderCategoryPostItem[]) { export function normalizeHeaderCategoryPosts(items: HeaderCategoryPostItem[]) {
return [...items].sort((left, right) => { return [...items]
const leftTime = new Date(left.published_at || left.updated_at).getTime(); .map((item) => normalizePostItem(item))
const rightTime = new Date(right.published_at || right.updated_at).getTime(); .sort((left, right) => {
const leftTime = new Date(left.published_at || left.updated_at).getTime();
const rightTime = new Date(right.published_at || right.updated_at).getTime();
return rightTime - leftTime || right.updated_at.localeCompare(left.updated_at); return rightTime - leftTime || right.updated_at.localeCompare(left.updated_at);
}); });
} }
export function createHeaderCategoryPostId() { export function createHeaderCategoryPostId() {
......
...@@ -157,8 +157,122 @@ export const headerArticleCategoryOptions: HeaderArticleCategoryOption[] = [ ...@@ -157,8 +157,122 @@ export const headerArticleCategoryOptions: HeaderArticleCategoryOption[] = [
{ id: "cat-gallery", name: "Ảnh nổi bật" }, { id: "cat-gallery", name: "Ảnh nổi bật" },
]; ];
export function toSlug(value: string) { function normalizeVietnameseText(value: string) {
return value return value
.replace(/Tìm/g, "Tìm")
.replace(/Tên/g, "Tên")
.replace(/Tổng/g, "Tổng")
.replace(/Thể/g, "Thể")
.replace(/Thứ/g, "Thứ")
.replace(/Liên/g, "Liên")
.replace(/Không/g, "Không")
.replace(/Danh mục/g, "Danh mục")
.replace(/danh mục/g, "danh mục")
.replace(/Bài viết/g, "Bài viết")
.replace(/Tin tức/g, "Tin tức")
.replace(/Ảnh/g, "Ảnh")
.replace(/Giá»›i thiệu/g, "Giới thiệu")
.replace(/Về/g, "Về")
.replace(/CÆ¡ cấu tổ chức/g, "Cơ cấu tổ chức")
.replace(/Hoạt động/g, "Hoạt động")
.replace(/Sá»± kiện/g, "Sự kiện")
.replace(/Thư viện ảnh/g, "Thư viện ảnh")
.replace(/nổi bật/g, "nổi bật")
.replace(/Nhóm/g, "Nhóm")
.replace(/ná»™i dung/g, "nội dung")
.replace(/thông tin/g, "thông tin")
.replace(/tổng hợp/g, "tổng hợp")
.replace(/Chính sách/g, "Chính sách")
.replace(/Ä‘/g, "đ")
.replace(/Đ/g, "Đ")
.replace(/à/g, "à")
.replace(/á/g, "á")
.replace(/ả/g, "ả")
.replace(/ã/g, "ã")
.replace(/ạ/g, "ạ")
.replace(/ă/g, "ă")
.replace(/ằ/g, "ằ")
.replace(/ắ/g, "ắ")
.replace(/ẳ/g, "ẳ")
.replace(/ẵ/g, "ẵ")
.replace(/ặ/g, "ặ")
.replace(/â/g, "â")
.replace(/ầ/g, "ầ")
.replace(/ấ/g, "ấ")
.replace(/ẩ/g, "ẩ")
.replace(/ẫ/g, "ẫ")
.replace(/ậ/g, "ậ")
.replace(/è/g, "è")
.replace(/é/g, "é")
.replace(/ẻ/g, "ẻ")
.replace(/ẽ/g, "ẽ")
.replace(/ẹ/g, "ẹ")
.replace(/ê/g, "ê")
.replace(/ề/g, "ề")
.replace(/ế/g, "ế")
.replace(/ể/g, "ể")
.replace(/á»…/g, "ễ")
.replace(/ệ/g, "ệ")
.replace(/ì/g, "ì")
.replace(/í/g, "í")
.replace(/ỉ/g, "ỉ")
.replace(/Ä©/g, "ĩ")
.replace(/ị/g, "ị")
.replace(/ò/g, "ò")
.replace(/ó/g, "ó")
.replace(/ỏ/g, "ỏ")
.replace(/õ/g, "õ")
.replace(/ọ/g, "ọ")
.replace(/ô/g, "ô")
.replace(/ồ/g, "ồ")
.replace(/ố/g, "ố")
.replace(/ổ/g, "ổ")
.replace(/á»—/g, "ỗ")
.replace(/á»™/g, "ộ")
.replace(/Æ¡/g, "ơ")
.replace(/ờ/g, "ờ")
.replace(/á»›/g, "ớ")
.replace(/ở/g, "ở")
.replace(/ỡ/g, "ỡ")
.replace(/ợ/g, "ợ")
.replace(/ù/g, "ù")
.replace(/ú/g, "ú")
.replace(/á»§/g, "ủ")
.replace(/Å©/g, "ũ")
.replace(/ụ/g, "ụ")
.replace(/ư/g, "ư")
.replace(/ừ/g, "ừ")
.replace(/ứ/g, "ứ")
.replace(/á»­/g, "ử")
.replace(/ữ/g, "ữ")
.replace(/á»±/g, "ự")
.replace(/ỳ/g, "ỳ")
.replace(/ý/g, "ý")
.replace(/á»·/g, "ỷ")
.replace(/ỹ/g, "ỹ")
.replace(/ỵ/g, "ỵ");
}
function normalizeHeaderCategoryText<T extends HeaderCategoryItem | HeaderArticleCategoryOption>(
item: T,
): T {
const normalized = {
...item,
name: normalizeVietnameseText(item.name),
} as T;
if ("description" in item && typeof item.description === "string") {
return {
...normalized,
description: normalizeVietnameseText(item.description),
};
}
return normalized;
}
export function toSlug(value: string) {
return normalizeVietnameseText(value)
.normalize("NFD") .normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") .replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d") .replace(/đ/g, "d")
...@@ -206,11 +320,14 @@ function assignLevel(item: HeaderCategoryItem, items: HeaderCategoryItem[]) { ...@@ -206,11 +320,14 @@ function assignLevel(item: HeaderCategoryItem, items: HeaderCategoryItem[]) {
} }
export function normalizeHeaderCategories(items: HeaderCategoryItem[]) { export function normalizeHeaderCategories(items: HeaderCategoryItem[]) {
const sanitizedItems = items.map((item) => normalizeHeaderCategoryText(item));
const parentIds = new Set( const parentIds = new Set(
items.filter((item) => item.parent_id).map((item) => item.parent_id as string), sanitizedItems
.filter((item) => item.parent_id)
.map((item) => item.parent_id as string),
); );
return items.map((item) => { return sanitizedItems.map((item) => {
const next = { ...item }; const next = { ...item };
if (parentIds.has(next.id)) { if (parentIds.has(next.id)) {
...@@ -218,8 +335,8 @@ export function normalizeHeaderCategories(items: HeaderCategoryItem[]) { ...@@ -218,8 +335,8 @@ export function normalizeHeaderCategories(items: HeaderCategoryItem[]) {
next.category_ids = []; next.category_ids = [];
} }
next.level = assignLevel(next, items); next.level = assignLevel(next, sanitizedItems);
next.static_link = next.slug === "" && !next.parent_id ? "/" : buildStaticLink(next, items); next.static_link = next.slug === "" && !next.parent_id ? "/" : buildStaticLink(next, sanitizedItems);
next.is_article = next.type === "news"; next.is_article = next.type === "news";
if (next.type !== "news") { if (next.type !== "news") {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment