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 NotFound from '../pages/404';
import { resolvePageType } from '@/lib/resolvePageType'; import { resolvePageType } from '@/lib/resolvePageType';
import CategoryPage from '@/components/Product/Category'; import CategoryPage from '@/app/pages/Product/Category';
import ProductDetailPage from '@/components/Product/ProductDetail'; import ProductDetailPage from '@/app/pages/Product/ProductDetail';
import ArticlePage from '@/components/Article'; 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() { export default function DynamicPage() {
const { slug } = useParams(); const { slug } = useParams();
@@ -20,6 +22,10 @@ export default function DynamicPage() {
return <ProductDetailPage slug={fullSlug} />; return <ProductDetailPage slug={fullSlug} />;
case 'article-home': case 'article-home':
return <ArticlePage />; return <ArticlePage />;
case 'article-category':
return <ArticleCategoryPage slug={fullSlug} />;
case 'article-detail':
return <ArticleDetailPage slug={fullSlug} />;
default: default:
return <NotFound />; 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 React from 'react';
import Home from '@/components/Home'; import Home from '@/app/pages/Home';
import { Metadata } from 'next'; import { Metadata } from 'next';
export const metadata: Metadata = { 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 = () => { export const BoxArticleMid = () => {
return ( return (
<div className="box-article-home-middle flex justify-between gap-2"> <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"> <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> <p className="title-box-article font-[600]">Tin công nghệ</p>
<div className="list-article-tech"> <div className="list-article-tech">
{DataListArticleNews.slice(0, 9).map((item, index) => ( {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 { Breadcrumb } from '@components/Common/Breadcrumb';
import { DataArticleCategory } from '@/data/article/ListCategory'; import { DataArticleCategory } from '@/data/article/ListCategory';
import { ArticleTopLeft } from './HomeArticle/ArticleTopLeft'; import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from './HomeArticle/ArticleTopRight'; import { ArticleTopRight } from '../ArticleTopRight';
import { BoxVideoArticle } from './HomeArticle/BoxVideoArticle'; import { BoxVideoArticle } from './BoxVideoArticle';
import { BoxArticleMid } from './HomeArticle/BoxArticleMid'; import { BoxArticleMid } from './BoxArticleMid';
import { BoxArticleReview } from './HomeArticle/BoxArticleReview'; import { BoxArticleReview } from './BoxArticleReview';
const ArticleHome = () => { const ArticleHome = () => {
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }]; const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
@@ -26,12 +26,10 @@ const ArticleHome = () => {
))} ))}
</div> </div>
<div className="box-article-home-top flex gap-3"> <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"> <div className="col-left-article border-box-article box-new-article boder-radius-10 col-span-2">
<div className="flex gap-12">
<ArticleTopLeft /> <ArticleTopLeft />
</div> </div>
</div>
<ArticleTopRight /> <ArticleTopRight />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { TypeListProductDeal } from '@/types'; import { TypeListProductDeal } from '@/types';
export const productDealData: TypeListProductDeal = [ export const ListDealData: TypeListProductDeal = [
{ {
id: '565', id: '565',
pro_id: '25404', 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 { productCategoryData } from '@/data/product/category';
import { productDetailData } from '@/data/product/detail'; import { productDetailData } from '@/data/product/detail';
import { ArticleCateDetailPageData } from '@/data/article/ArticleCateDetailPageData';
import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
export function resolvePageType(slug: string) { export function resolvePageType(slug: string) {
// kiểm tra danh mục // kiểm tra danh mục
@@ -12,7 +14,17 @@ export function resolvePageType(slug: string) {
} }
// tin tức // tin tức
if ('/tin-tuc' == slug) { 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'; return '404';
} }

View File

@@ -2730,6 +2730,7 @@ textarea::placeholder {
left: 12px; left: 12px;
text-shadow: 1px 1px 1px #000; text-shadow: 1px 1px 1px #000;
z-index: 3; z-index: 3;
padding-right: 10px;
} }
.page-hompage .box-article-global .box-left .content-article-item .title { .page-hompage .box-article-global .box-left .content-article-item .title {
font-size: 24px; font-size: 24px;
@@ -4380,7 +4381,6 @@ textarea::placeholder {
margin-top: 18px; margin-top: 18px;
} }
.page-deal .box-list-item-deal .product-item { .page-deal .box-list-item-deal .product-item {
width: calc(100% / 4 - 12px);
} }
.page-deal .box-list-item-deal .product-item .icon-cart-deal { .page-deal .box-list-item-deal .product-item .icon-cart-deal {
width: 32px; 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:first-child,
.page-deal .box-list-item-deal .product-item:nth-child(2) { .page-deal .box-list-item-deal .product-item:nth-child(2) {
width: calc(100% / 2 - 6px);
border-radius: 22px; border-radius: 22px;
background: #0f5b9a; background: #0f5b9a;
padding: 10px 8px 28px; 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:first-child .item-deal,
.page-deal .box-list-item-deal .product-item:nth-child(2) .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; background-size: 155px 131.5px !important;
margin: 0 auto; margin: 0 auto;
} }
.col-left-article {
width: 66.5%;
}
.page-article .box-article-home-top .swiper-pagination { .page-article .box-article-home-top .swiper-pagination {
position: relative; position: relative;
bottom: 0; bottom: 0;
@@ -5207,7 +5204,6 @@ textarea::placeholder {
} }
.box-article-detail .box-article-detail-ct { .box-article-detail .box-article-detail-ct {
width: 100%; width: 100%;
float: left;
margin-bottom: 50px; margin-bottom: 50px;
} }
.box-article-detail img:hover { .box-article-detail img:hover {
@@ -5248,7 +5244,6 @@ textarea::placeholder {
} }
.box-article-detail .title-ar { .box-article-detail .title-ar {
width: 100%; width: 100%;
float: left;
border-bottom: solid 1px #e1e1e1; border-bottom: solid 1px #e1e1e1;
position: relative; position: relative;
font-size: 24px; font-size: 24px;
@@ -6050,3 +6045,52 @@ textarea::placeholder {
font-size: 14px; 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;
}