This commit is contained in:
2025-12-30 16:47:24 +07:00
parent 9fa4b50b68
commit 15240ff81f
65 changed files with 6550 additions and 217 deletions

View File

@@ -3,9 +3,11 @@ import { useParams } from 'next/navigation';
import NotFound from '../pages/404';
import { resolvePageType } from '@/lib/resolvePageType';
import CategoryPage from '@/components/Product/Category';
import ProductDetailPage from '@/components/Product/ProductDetail';
import ArticlePage from '@/components/Article';
import CategoryPage from '@/app/pages/Product/Category';
import ProductDetailPage from '@/app/pages/Product/ProductDetail';
import ArticlePage from '@/app/pages/Article/HomeArticlePage';
import ArticleCategoryPage from '@/app/pages/Article/CategoryPage';
import ArticleDetailPage from '@/app/pages/Article/DetailPage';
export default function DynamicPage() {
const { slug } = useParams();
@@ -20,6 +22,10 @@ export default function DynamicPage() {
return <ProductDetailPage slug={fullSlug} />;
case 'article-home':
return <ArticlePage />;
case 'article-category':
return <ArticleCategoryPage slug={fullSlug} />;
case 'article-detail':
return <ArticleDetailPage slug={fullSlug} />;
default:
return <NotFound />;
}

127
src/app/deal/page.tsx Normal file
View File

@@ -0,0 +1,127 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Metadata } from 'next';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { bannerData } from '@/data/banner';
import { ListDealData } from '@/data/deal';
import { formatCurrency } from '@/lib/formatPrice';
import CounDown from '@/components/Common/CounDown';
export const metadata: Metadata = {
title: 'Danh sách deal',
description: 'Sản phẩm khuyễn mãi giá ưu đãi',
};
export default function DealPage() {
const breadcrumbItems = [{ name: 'Danh sách deal', url: '/deal' }];
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-deal container">
<div className="box-product-deal">
{bannerData[0].header.banner_page_deal_2023 && (
<div className="banner-deal-page mb-5">
{bannerData[0].header.banner_page_deal_2023.map((item, index) => (
<Link href={item.desUrl} className="item-banner" key={index}>
<Image
src={item.fileUrl}
width={1200}
height={325}
alt={item.title}
style={{ display: 'block' }}
/>
</Link>
))}
</div>
)}
<div className="box-list-item-deal grid grid-cols-4 gap-3" id="js-deal-page">
{ListDealData.map((Item, index) => (
<div className="product-item" key={index}>
<div className="item-deal">
<Link
href={Item.product_info.productUrl}
className="product-image position-relative"
>
<Image
src={Item.product_info.productImage.large}
width={250}
height={250}
alt={Item.product_info.productName}
/>
</Link>
<div className="product-info flex-1">
<Link href={Item.product_info.productUrl}>
<h3 className="product-title line-clamp-3">
{Item.product_info.productName}
</h3>
</Link>
<div className="product-martket-main flex items-center">
{Item.product_info.marketPrice > 0 && (
<>
<p className="product-market-price">
{Item.product_info.marketPrice.toLocaleString()}
</p>
<div className="product-percent-price">
-{Item.product_info.price_off || 0}%
</div>
</>
)}
</div>
<div className="product-price-main font-bold">
{Item.product_info.price > '0'
? `${formatCurrency(Item.product_info.price)}đ`
: 'Liên hệ'}
</div>
<div className="p-quantity-sale">
<i className="sprite sprite-fire-deal"></i>
<div className="bg-gradient"></div>
{(() => {
const percentRemaining =
((Number(Item.quantity) - Number(Item.sale_quantity)) /
Number(Item.quantity)) *
100;
return (
<>
<p
className="js-line-deal-left"
style={{ width: `${percentRemaining}%` }}
></p>
</>
);
})()}
<span>
Còn {Number(Item.quantity) - Number(Item.sale_quantity)}/
{Number(Item.quantity)} sản phẩm
</span>
</div>
<div className="js-item-deal-time js-item-time-25404">
<div className="time-deal-page flex items-center">
<div>Kết thúc sau </div>
<CounDown deadline={new Date(Item.to_time)} />
</div>
</div>
<a href="javascript:buyNow(25404)" className="buy-now-deal">
Mua giá sốc
</a>
<Link
href="/bts-gaming-02"
className="text-deal-item color-primary mt-3 hidden font-bold"
>
Xem sản phẩm
</Link>
</div>
</div>
</div>
))}
</div>
</div>
</section>
</>
);
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import Home from '@/components/Home';
import Home from '@/app/pages/Home';
import { Metadata } from 'next';
export const metadata: Metadata = {

View File

@@ -0,0 +1,116 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import type { TypeArticleCatePage } from '@/types/article/TypeArticleCatePage';
import { ArticleCateDetailPageData } from '@/data/article/ArticleCateDetailPageData';
import { DataArticleCategory } from '@/data/article/ListCategory';
import { DataListArticleNews } from '@/data/article/ListArticleNews';
import { findCategoryBySlug } from '@/lib/article/category';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { ErrorLink } from '@components/Common/error';
import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from '../ArticleTopRight';
import ItemArticle from '@/components/Common/ItemArticle';
interface CategoryPageProps {
slug: string; // khai báo prop slug
}
const ArticleCategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
// Ép kiểu dữ liệu từ index.ts về CategoryData[]
const categories = ArticleCateDetailPageData as TypeArticleCatePage[];
const currentCategory = findCategoryBySlug(slug, categories);
const breadcrumbItems = [
{ name: 'Tin tức', url: '/tin-tuc' },
{ name: currentCategory?.category_info.name, url: currentCategory?.category_info.request_path },
];
// Trường hợp không tìm thấy danh mục
if (!currentCategory) {
return <ErrorLink />;
}
// lấy danh sách tin tức
const articleList = Object.values(currentCategory.article_list);
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-article page-article-category container">
<div className="tabs-category-article flex items-center">
{DataArticleCategory.map((item, index) => (
<Link
href={item.url}
key={index}
className={`item-tab-article ${currentCategory.title === item.title ? 'active' : ''}`}
>
<h2 className="title-cate-article font-[400]">{item.title}</h2>
</Link>
))}
</div>
<div className="box-article-home-top grid grid-cols-3 gap-3">
<div className="col-left-article border-box-article box-new-article boder-radius-10 col-span-2">
<ArticleTopLeft />
</div>
<ArticleTopRight />
</div>
<div className="box-article-home-middle mt-5 grid grid-cols-3 gap-3">
<div className="box-article-tech col-left-article boder-radius-10 border-box-article col-span-2">
<p className="title-box-article font-[600]">{currentCategory.title}</p>
<div className="list-article-tech">
{articleList.slice(0, 9).map((item, index) => (
<ItemArticle item={item} key={index} />
))}
</div>
<Link
href="/tin-cong-nghe"
className="btn-article-col flex items-center justify-center gap-2 font-[500]"
>
Xem tất cả
</Link>
</div>
<div className="col-right-article page-hompage flex-1">
<div className="box-article-global border-box-article boder-radius-10">
<p className="title-box-article font-bold">Tin nổi bật</p>
<div className="list-article-global flex flex-col gap-2">
{DataListArticleNews.slice(0, 5).map((item, index) => (
<div className="item-article flex gap-4" key={index}>
<Link href={item.url} className="img-article boder-radius-10 relative">
<Image
className="boder-radius-10"
src={item.image.original}
fill
alt={item.title}
/>
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
</Link>
<div className="content-article content-article-item flex flex-1 flex-col">
<Link href="/tuyen-dung-nhan-vien-ky-thuat-1-2" className="title-article">
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
</Link>
<p className="time-article flex items-center gap-2">
<i className="sprite sprite-clock-item-article"></i>
<span>{item.createDate}</span>
</p>
<p className="descreption-article line-clamp-2">{item.summary}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
</>
);
};
export default ArticleCategoryPage;

View File

@@ -0,0 +1,113 @@
'use client';
import { useMemo } from 'react';
type HeadingItem = {
id: string;
text: string;
level: number;
children?: HeadingItem[];
};
function convertToSlug(text: string) {
return text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/đ/g, 'd')
.replace(/[^\w ]+/g, '')
.trim()
.replace(/\s+/g, '-');
}
// Hàm xây dựng cây TOC từ danh sách heading
function buildTree(headings: HeadingItem[]): HeadingItem[] {
const root: HeadingItem[] = [];
const stack: HeadingItem[] = [];
headings.forEach((h) => {
const node = { ...h, children: [] };
while (stack.length && stack[stack.length - 1].level >= node.level) {
stack.pop();
}
if (stack.length === 0) {
root.push(node);
} else {
stack[stack.length - 1].children!.push(node);
}
stack.push(node);
});
return root;
}
function renderTree(nodes: HeadingItem[]) {
return (
<ol>
{nodes.map((n) => (
<li key={n.id}>
<a
href={`#${n.id}`}
onClick={(e) => {
e.preventDefault();
const el = document.getElementById(n.id);
if (el) {
const y = el.getBoundingClientRect().top + window.scrollY - 120;
window.scrollTo({ top: y, behavior: 'smooth' });
}
}}
className="text-blue-600 hover:underline"
>
{n.text}
</a>
{n.children && n.children.length > 0 && renderTree(n.children)}
</li>
))}
</ol>
);
}
export default function TocBox({ htmlContent }: { htmlContent: string }) {
const { headingsTree, contentWithIds } = useMemo(() => {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const nodes = doc.querySelectorAll('h1,h2,h3,h4,h5,h6');
const flat: HeadingItem[] = Array.from(nodes).map((node) => {
const text = node.textContent || '';
const id = convertToSlug(text);
node.setAttribute('id', id);
return {
id,
text,
level: parseInt(node.tagName.substring(1)),
};
});
return {
headingsTree: buildTree(flat),
contentWithIds: doc.body.innerHTML,
};
}, [htmlContent]);
if (!headingsTree.length) return null;
return (
<>
<div className="archor-text-group">
<div className="toc_title flex items-center justify-between gap-2">
<b className="text-fint-toc flex items-center text-base font-bold">
<span>Nội dung chính</span>
</b>
</div>
<div id="js-outp">{renderTree(headingsTree)}</div>
</div>
<div
className="box-article-detail-ct nd js_find"
dangerouslySetInnerHTML={{ __html: contentWithIds }}
/>
</>
);
}

View File

@@ -0,0 +1,109 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import type { TypeArticleDetailPage } from '@/types/article/TypeArticleDetailPage';
import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
import { DataArticleCategory } from '@/data/article/ListCategory';
import { ErrorLink } from '@components/Common/error';
import { findDetailBySlug } from '@/lib/article/detail';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import TocBox from './TocBox';
interface DetailPageProps {
slug: string; // khai báo prop slug
}
const ArticleDetailPage: React.FC<DetailPageProps> = ({ slug }) => {
// Ép kiểu dữ liệu từ index.ts về CategoryData[]
const details = ArticleDetailPageData as TypeArticleDetailPage[];
const page = findDetailBySlug(slug, details);
const breadcrumbItems = [
{ name: 'Tin tức', url: '/tin-tuc' },
{ name: page?.article_detail.title, url: page?.article_detail.url },
];
// Trường hợp không tìm thấy danh mục
if (!page) {
return <ErrorLink />;
}
// lấy danh sách tin tức liên quan mới
const ListRelayNew = Object.values(page.article_other_same_category.new);
// lấy danh sách tin tức liên quan cũ
const ListRelayOld = Object.values(page.article_other_same_category.old);
const combinedList = [...ListRelayNew.slice(0, 6), ...ListRelayOld.slice(0, 6)];
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-article box-article-detail container">
<div className="tabs-category-article flex items-center">
{DataArticleCategory.map((item, index) => (
<Link
href={item.url}
key={index}
className={`item-tab-article ${page?.article_detail.categoryInfo[0].id === item.id ? 'active' : ''}`}
>
<h2 className="title-cate-article font-[400]">{item.title}</h2>
</Link>
))}
</div>
<div className="row article-detail-page mt-5">
<div className="col-md-8">
<div className="box-article-detail-title">
<h1 className="font-weight-700">{page.article_detail.title}</h1>
<div className="post__user border-bottom my-5 flex items-center gap-2">
<span className="author-name">{page.article_detail.author}</span>
<span className="post-time">{page.article_detail.createDate}</span>
</div>
{/* nội dung */}
<TocBox htmlContent={page.article_detail.content} />
</div>
</div>
{page.article_other_same_category && (
<div className="col-md-4">
<div className="box-article-relay">
<p className="title-ar">
Bài viết <span>liên quan</span>
</p>
<div className="article-list list-article-relative flex flex-wrap gap-3">
{combinedList.map((item, index) => (
<div className="item-article d-flex flex-column gap-12" key={index}>
<Link href={item.url} className="img-article boder-radius-10">
<Image
className="boder-radius-10"
src={item.image.original}
fill
alt={item.title}
/>
</Link>
<div className="content-article flex-1">
<a href={item.url} className="title-article">
<h3 className="font-weight-400 line-clamp-2">{item.title}</h3>
</a>
<p className="time-article d-flex align-items-center gap-4">
<i className="sprite sprite-clock-item-article"></i>
<span>{item.createDate}</span>
</p>
<p className="descreption-article line-clamp-2">{item.summary}</p>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</section>
</>
);
};
export default ArticleDetailPage;

View File

@@ -5,8 +5,8 @@ import Image from 'next/image';
export const BoxArticleMid = () => {
return (
<div className="box-article-home-middle flex justify-between gap-2">
<div className="box-article-tech col-left-article boder-radius-10 border-box-article">
<div className="box-article-home-middle grid grid-cols-3 gap-2">
<div className="box-article-tech col-left-article boder-radius-10 border-box-article col-span-2">
<p className="title-box-article font-[600]">Tin công nghệ</p>
<div className="list-article-tech">
{DataListArticleNews.slice(0, 9).map((item, index) => (

View File

@@ -4,11 +4,11 @@ import Link from 'next/link';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { DataArticleCategory } from '@/data/article/ListCategory';
import { ArticleTopLeft } from './HomeArticle/ArticleTopLeft';
import { ArticleTopRight } from './HomeArticle/ArticleTopRight';
import { BoxVideoArticle } from './HomeArticle/BoxVideoArticle';
import { BoxArticleMid } from './HomeArticle/BoxArticleMid';
import { BoxArticleReview } from './HomeArticle/BoxArticleReview';
import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from '../ArticleTopRight';
import { BoxVideoArticle } from './BoxVideoArticle';
import { BoxArticleMid } from './BoxArticleMid';
import { BoxArticleReview } from './BoxArticleReview';
const ArticleHome = () => {
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
@@ -26,11 +26,9 @@ const ArticleHome = () => {
))}
</div>
<div className="box-article-home-top flex gap-3">
<div className="col-left-article border-box-article box-new-article boder-radius-10">
<div className="flex gap-12">
<ArticleTopLeft />
</div>
<div className="box-article-home-top grid grid-cols-3 gap-3">
<div className="col-left-article border-box-article box-new-article boder-radius-10 col-span-2">
<ArticleTopLeft />
</div>
<ArticleTopRight />
</div>

View File

@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import { menuData } from '../../Other/Header/menuData';
import { menuData } from '../../../../components/Other/Header/menuData';
import ItemCategory from './ItemCategory';
import { InfoCategory } from '@/types';

View File

@@ -5,9 +5,9 @@ import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import { FaCaretRight } from 'react-icons/fa';
import { productDealData } from './productDealData';
import { ListDealData } from '@/data/deal';
import CounDown from '../../Common/CounDown';
import CounDown from '../../../../components/Common/CounDown';
import ProductItem from './ProductItem';
const BoxProductDeal: React.FC = () => {
@@ -34,7 +34,7 @@ const BoxProductDeal: React.FC = () => {
loop={true}
navigation={true}
>
{productDealData.map((Item, index) => (
{ListDealData.map((Item, index) => (
<SwiperSlide key={index}>
<ProductItem item={Item} />
</SwiperSlide>

View File

@@ -31,13 +31,13 @@ export const Breadcrumb = ({ items }: { items: BreadcrumbItem[] }) => {
itemProp="itemListElement"
itemScope
itemType="http://schema.org/ListItem"
className="flex items-center"
className="flex items-center gap-2"
>
<Link href={item.url ?? '/'} itemProp="item">
<span itemProp="name">{item?.name}</span>
</Link>
{idx < items.length - 1 && <FaAngleRight className="text-gray-700" />}
<meta itemProp="position" content={(idx + 1).toString()} />
{idx < items.length - 1 && <span className="mx-1">/</span>}
</li>
))}
</ol>

View File

@@ -1,11 +1,12 @@
'use client';
import React from 'react';
import { Article } from '@/types';
import { ArticleItem } from '@/types/article/TypeArticleCatePage';
import Link from 'next/link';
import Image from 'next/image';
type ItemArticleProps = {
item: Article;
item: Article | ArticleItem;
};
const ItemArticle: React.FC<ItemArticleProps> = ({ item }) => {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,187 +1,186 @@
import { TypeArticleCategory } from '@/types/article/ListCategoryArticle'
import { TypeArticleCategory } from '@/types/article/ListCategoryArticle';
export const DataArticleCategory: TypeArticleCategory[] = [
{
"id": "243",
"title": "C\u00f4ng ngh\u1ec7",
"summary": "",
"parentId": "0",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/tin-cong-nghe",
"item_count": "2787",
"children": []
},
{
"id": "2490",
"title": "Review",
"summary": "0",
"parentId": "0",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/tin-tuc-review",
"item_count": "1235",
"children": []
},
{
"id": "2491",
"title": "H\u01b0\u1edbng d\u1eabn",
"summary": "0",
"parentId": "0",
"isParent": "1",
"thumbnail": "0",
"type": "article",
"url": "\/tin-tuc-huong-dan",
"item_count": "1828",
"children": [
{
"id": "2493",
"title": "Ki\u1ebfn th\u1ee9c m\u00e1y t\u00ednh",
"summary": "0",
"parentId": "2491",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/kien-thuc-may-tinh",
"item_count": "1558",
"children": []
},
{
"id": "2494",
"title": "Ph\u1ea7n m\u1ec1m \u0111\u1ed3 h\u1ecda",
"summary": "0",
"parentId": "2491",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/phan-mem-do-hoa",
"item_count": "174",
"children": []
},
{
"id": "2495",
"title": "Ph\u1ea7n m\u1ec1m v\u0103n ph\u00f2ng",
"summary": "0",
"parentId": "2491",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/phan-mem-van-phong",
"item_count": "71",
"children": []
}
]
},
{
"id": "263",
"title": "Tuy\u1ec3n d\u1ee5ng",
"summary": "0",
"parentId": "0",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/tuyen-dung",
"item_count": "19",
"children": []
},
{
"id": "2488",
"title": "Tin t\u1ee9c khuy\u1ebfn m\u1ea1i",
"summary": "0",
"parentId": "0",
"isParent": "0",
"thumbnail": "0",
"type": "article",
"url": "\/tin-tuc-khuyen-mai",
"item_count": "90",
"children": []
},
{
"id": "2497",
"title": "Tin t\u1ee9c build PC",
"summary": "",
"parentId": "0",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/tin-tuc-build-pc",
"item_count": "36",
"children": []
},
{
"id": "2505",
"title": "Game",
"summary": "",
"parentId": "0",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/game",
"item_count": "16",
"children": []
},
{
"id": "2501",
"title": "S\u1ef1 ki\u1ec7n",
"summary": "",
"parentId": "0",
"isParent": "1",
"thumbnail": "",
"type": "article",
"url": "\/su-kien",
"item_count": "65",
"children": [
{
"id": "2504",
"title": "Chung",
"summary": "",
"parentId": "2501",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/chung",
"item_count": "30",
"children": []
},
{
"id": "2500",
"title": "COMPUTEX 2025",
"summary": "",
"parentId": "2501",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/computex-2025",
"item_count": "16",
"children": []
},
{
"id": "2502",
"title": "Ng\u00e0y h\u1ed9i tuy\u1ec3n sinh 2025",
"summary": "",
"parentId": "2501",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/ngay-hoi-tuyen-sinh-2025",
"item_count": "6",
"children": []
},
{
"id": "2503",
"title": "Ch\u00e0o t\u00e2n sinh vi\u00ean",
"summary": "",
"parentId": "2501",
"isParent": "0",
"thumbnail": "",
"type": "article",
"url": "\/chao-tan-sinh-vien",
"item_count": "15",
"children": []
}
]
}
]
{
id: '243',
title: 'C\u00f4ng ngh\u1ec7',
summary: '',
parentId: '0',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/tin-cong-nghe',
item_count: '2787',
children: [],
},
{
id: '2490',
title: 'Review',
summary: '0',
parentId: '0',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/tin-tuc-review',
item_count: '1235',
children: [],
},
{
id: '2491',
title: 'H\u01b0\u1edbng d\u1eabn',
summary: '0',
parentId: '0',
isParent: '1',
thumbnail: '0',
type: 'article',
url: '\/tin-tuc-huong-dan',
item_count: '1828',
children: [
{
id: '2493',
title: 'Ki\u1ebfn th\u1ee9c m\u00e1y t\u00ednh',
summary: '0',
parentId: '2491',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/kien-thuc-may-tinh',
item_count: '1558',
children: [],
},
{
id: '2494',
title: 'Ph\u1ea7n m\u1ec1m \u0111\u1ed3 h\u1ecda',
summary: '0',
parentId: '2491',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/phan-mem-do-hoa',
item_count: '174',
children: [],
},
{
id: '2495',
title: 'Ph\u1ea7n m\u1ec1m v\u0103n ph\u00f2ng',
summary: '0',
parentId: '2491',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/phan-mem-van-phong',
item_count: '71',
children: [],
},
],
},
{
id: '263',
title: 'Tuy\u1ec3n d\u1ee5ng',
summary: '0',
parentId: '0',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/tuyen-dung',
item_count: '19',
children: [],
},
{
id: '2488',
title: 'Tin t\u1ee9c khuy\u1ebfn m\u1ea1i',
summary: '0',
parentId: '0',
isParent: '0',
thumbnail: '0',
type: 'article',
url: '\/tin-tuc-khuyen-mai',
item_count: '90',
children: [],
},
{
id: '2497',
title: 'Tin t\u1ee9c build PC',
summary: '',
parentId: '0',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/tin-tuc-build-pc',
item_count: '36',
children: [],
},
{
id: '2505',
title: 'Game',
summary: '',
parentId: '0',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/game',
item_count: '16',
children: [],
},
{
id: '2501',
title: 'S\u1ef1 ki\u1ec7n',
summary: '',
parentId: '0',
isParent: '1',
thumbnail: '',
type: 'article',
url: '\/su-kien',
item_count: '65',
children: [
{
id: '2504',
title: 'Chung',
summary: '',
parentId: '2501',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/chung',
item_count: '30',
children: [],
},
{
id: '2500',
title: 'COMPUTEX 2025',
summary: '',
parentId: '2501',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/computex-2025',
item_count: '16',
children: [],
},
{
id: '2502',
title: 'Ng\u00e0y h\u1ed9i tuy\u1ec3n sinh 2025',
summary: '',
parentId: '2501',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/ngay-hoi-tuyen-sinh-2025',
item_count: '6',
children: [],
},
{
id: '2503',
title: 'Ch\u00e0o t\u00e2n sinh vi\u00ean',
summary: '',
parentId: '2501',
isParent: '0',
thumbnail: '',
type: 'article',
url: '\/chao-tan-sinh-vien',
item_count: '15',
children: [],
},
],
},
];

View File

@@ -1,6 +1,6 @@
import { TypeListProductDeal } from '@/types';
export const productDealData: TypeListProductDeal = [
export const ListDealData: TypeListProductDeal = [
{
id: '565',
pro_id: '25404',

View File

@@ -0,0 +1,18 @@
import { TypeArticleCatePage } from '@/types/article/TypeArticleCatePage';
// Hàm helper để lấy URL an toàn từ các cấu trúc dữ liệu khác nhau
function getSlug(url: string): string {
const parts = url.split('/').filter(Boolean);
return parts[parts.length - 1];
}
/**
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
*/
export function findCategoryBySlug(
slug: string,
categories: TypeArticleCatePage[],
): TypeArticleCatePage | null {
const found = categories.find((item) => item.category_info.request_path == slug);
return found ?? null;
}

View File

@@ -0,0 +1,18 @@
import { TypeArticleDetailPage } from '@/types/article/TypeArticleDetailPage';
// Hàm helper để lấy URL an toàn từ các cấu trúc dữ liệu khác nhau
function getSlug(url: string): string {
const parts = url.split('/').filter(Boolean);
return parts[parts.length - 1];
}
/**
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
*/
export function findDetailBySlug(
slug: string,
categories: TypeArticleDetailPage[],
): TypeArticleDetailPage | null {
const found = categories.find((item) => item.article_detail.url == slug);
return found ?? null;
}

View File

@@ -1,5 +1,7 @@
import { productCategoryData } from '@/data/product/category';
import { productDetailData } from '@/data/product/detail';
import { ArticleCateDetailPageData } from '@/data/article/ArticleCateDetailPageData';
import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
export function resolvePageType(slug: string) {
// kiểm tra danh mục
@@ -12,7 +14,17 @@ export function resolvePageType(slug: string) {
}
// tin tức
if ('/tin-tuc' == slug) {
return 'article-home'
return 'article-home';
}
// danh mục tin tức
if (ArticleCateDetailPageData.some((c) => c.category_info.request_path == slug)) {
return 'article-category';
}
// chi tiết tin tức
if (ArticleDetailPageData.some((c) => c.article_detail.url == slug)) {
return 'article-detail';
}
return '404';
}

View File

@@ -2730,6 +2730,7 @@ textarea::placeholder {
left: 12px;
text-shadow: 1px 1px 1px #000;
z-index: 3;
padding-right: 10px;
}
.page-hompage .box-article-global .box-left .content-article-item .title {
font-size: 24px;
@@ -4380,7 +4381,6 @@ textarea::placeholder {
margin-top: 18px;
}
.page-deal .box-list-item-deal .product-item {
width: calc(100% / 4 - 12px);
}
.page-deal .box-list-item-deal .product-item .icon-cart-deal {
width: 32px;
@@ -4409,10 +4409,10 @@ textarea::placeholder {
}
.page-deal .box-list-item-deal .product-item:first-child,
.page-deal .box-list-item-deal .product-item:nth-child(2) {
width: calc(100% / 2 - 6px);
border-radius: 22px;
background: #0f5b9a;
padding: 10px 8px 28px;
grid-column: span 2 / span 2;
}
.page-deal .box-list-item-deal .product-item:first-child .item-deal,
.page-deal .box-list-item-deal .product-item:nth-child(2) .item-deal {
@@ -4881,9 +4881,6 @@ textarea::placeholder {
background-size: 155px 131.5px !important;
margin: 0 auto;
}
.col-left-article {
width: 66.5%;
}
.page-article .box-article-home-top .swiper-pagination {
position: relative;
bottom: 0;
@@ -5207,7 +5204,6 @@ textarea::placeholder {
}
.box-article-detail .box-article-detail-ct {
width: 100%;
float: left;
margin-bottom: 50px;
}
.box-article-detail img:hover {
@@ -5248,7 +5244,6 @@ textarea::placeholder {
}
.box-article-detail .title-ar {
width: 100%;
float: left;
border-bottom: solid 1px #e1e1e1;
position: relative;
font-size: 24px;
@@ -6050,3 +6045,52 @@ textarea::placeholder {
font-size: 14px;
}
}
.box-article-detail .archor-text-group {
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e1e4e6;
margin-bottom: 24px;
}
.box-article-detail .toc_title {
padding: 16px 14px 12px;
border-bottom: 1px solid #e1e4e6;
font-size: 20px;
font-weight: 500;
line-height: 28px;
color: #32373d;
text-transform: uppercase;
}
.box-article-detail #js-outp ol li {
list-style: none !important;
line-height: 27px;
}
.box-article-detail #js-outp ol {
padding-left: 20px !important;
margin: 10px 0;
}
.box-article-detail #js-outp ol ol {
padding-left: 18px !important;
margin: 3px 0;
font-style: italic;
}
.box-article-detail #js-outp a {
color: #444b52;
font-size: 16px;
text-decoration: none;
font-weight: 500;
}
.box-article-detail #js-outp a:hover {
color: #0664f9;
}
.box-article-detail .box-article-detail-ct {
width: 100%;
margin-bottom: 50px;
}
.box-article-detail .box-article-detail-cmt {
width: 100%;
float: left;
}
.box-article-detail .box-article-detail-cmt iframe {
width: 100% !important;
}

View File

@@ -0,0 +1,94 @@
// Kiểu cho từng bài viết
export interface ArticleItem {
id: string | number;
title: string;
extend: {
pixel_code: string;
};
summary: string;
createDate: string;
createBy: string;
lastUpdate: string;
lastUpdateBy: string;
visit: string;
is_featured: string;
article_time: string;
review_rate: string;
review_count: string;
video_code: string;
external_url: string;
author: string;
counter: string;
url: string;
image: {
thum: string;
original: string;
};
}
// Kiểu cho thông tin đường dẫn
interface PathItem {
id: string;
url: string;
name: string;
}
interface Path {
path: PathItem[];
path_url: string;
}
// Kiểu cho category_info
interface Info {
id: string;
type: string;
catPath: string;
childListId: string;
sellerId: string;
url: string;
url_hash: string;
name: string;
summary: string;
description: string;
isParent: string;
imgUrl: string;
parentId: string;
status: string;
ordering: string;
item_count: string;
display_option: string;
createDate: string;
createBy: string;
lastUpdate: string;
lastUpdateBy: string;
meta_title: string;
meta_keyword: string;
meta_description: string;
request_path: string;
relate_product: string;
visit: string;
path: Path;
related: [];
}
// Kiểu cho phân trang
interface PagingItem {
name: string;
url: string;
is_active: string;
}
// Kiểu tổng thể cho dữ liệu
export interface TypeArticleCatePage {
keywords: string;
description: string;
title: string;
favicon: string;
canonical: string;
image: string;
category_info: Info;
paging_collection: PagingItem[];
paging: string;
paging_count: string;
article_list: Record<string, ArticleItem>;
}

View File

@@ -0,0 +1,125 @@
// Kiểu cho hình ảnh
interface ImageInfo {
thum: string;
original: string;
}
// Kiểu cho category
interface CategoryInfo {
id: string;
type: string;
catPath: string;
childListId: string;
sellerId: string;
url: string;
url_hash: string;
name: string;
summary: string;
description: string;
isParent: string;
imgUrl: string;
parentId: string;
status: string;
ordering: string;
item_count: string;
display_option: string;
createDate: string;
createBy: string;
lastUpdate: string;
lastUpdateBy: string;
meta_title: string;
meta_keyword: string;
meta_description: string;
request_path: string;
relate_product: string;
visit: string;
}
// Kiểu cho đường dẫn bài viết
interface ArticlePath {
path: { id: string; url: string; name: string }[];
path_url: string;
}
interface ImageItem {
caption: string;
file_location: string;
}
// Chi tiết bài viết
interface ArticleDetail {
id: string;
title: string;
summary: string;
type: string;
content: string;
article_category: string[];
image_background: string;
extend: { pixel_code: string };
meta_title: string;
meta_keywords: string;
meta_description: string;
album_id: string;
url: string;
video_code: string;
external_url: string;
allow_se_index: string;
url_canonical: string;
tags: string;
review_rate: string;
review_count: string;
article_time: string;
relate_article: string;
visit: string;
createDate: string;
lastUpdate: string;
author: string;
image: ImageInfo;
image_list: ImageItem[];
categoryInfo: CategoryInfo[];
articlePath: ArticlePath[];
related: [];
tag_list: [];
}
// Bài viết khác cùng chuyên mục
interface OtherArticle {
id: string;
title: string;
extend: { pixel_code: string };
summary: string;
createDate: string;
createBy: string;
lastUpdate: string;
lastUpdateBy: string;
visit: string;
is_featured: string;
lastUpdateByUser: string;
article_time: string;
review_rate: string;
review_count: string;
video_code: string;
external_url: string;
author: string;
counter: string;
url: string;
image: ImageInfo;
}
interface ArticleOtherSameCategory {
new: Record<string, OtherArticle>;
old: Record<string, OtherArticle>;
}
// Root type
export interface TypeArticleDetailPage {
keywords: string;
description: string;
title: string;
canonical: string;
image: string;
article_detail: ArticleDetail;
article_other_same_category: ArticleOtherSameCategory;
comment_list: [];
article_album: string;
}