This commit is contained in:
2026-01-05 13:50:16 +07:00
parent aae8e26135
commit 28a252f7d7
24 changed files with 13895 additions and 448 deletions

11
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@fancyapps/ui": "^6.1.7",
"@tippyjs/react": "^4.2.6",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.26",
"lightgallery": "^2.9.0",
"next": "16.0.10",
@@ -2749,6 +2750,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@fancyapps/ui": "^6.1.7",
"@tippyjs/react": "^4.2.6",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.26",
"lightgallery": "^2.9.0",
"next": "16.0.10",

View File

@@ -4,6 +4,7 @@ import NotFound from '../pages/404';
import { resolvePageType } from '@/lib/resolvePageType';
import CategoryPage from '@/app/pages/Product/Category';
import ProductSearchPage from '@/app/pages/Product/ProductSearch';
import ProductDetailPage from '@/app/pages/Product/ProductDetail';
import ArticlePage from '@/app/pages/Article/HomeArticlePage';
import ArticleCategoryPage from '@/app/pages/Article/CategoryPage';
@@ -18,6 +19,8 @@ export default function DynamicPage() {
switch (pageType) {
case 'category':
return <CategoryPage slug={fullSlug} />;
case 'product-search':
return <ProductSearchPage slug={fullSlug} />;
case 'product-detail':
return <ProductDetailPage slug={fullSlug} />;
case 'article-home':

View File

@@ -6,8 +6,7 @@ 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';
import ItemDeal from '@components/Deal/ItemDeal';
export const metadata: Metadata = {
title: 'Danh sách deal',
@@ -16,6 +15,7 @@ export const metadata: Metadata = {
export default function DealPage() {
const breadcrumbItems = [{ name: 'Danh sách deal', url: '/deal' }];
return (
<>
<div className="container">
@@ -39,85 +39,9 @@ export default function DealPage() {
</div>
)}
<div className="box-list-item-deal grid grid-cols-4 gap-3" id="js-deal-page">
<div className="box-list-item-deal grid grid-cols-4 gap-3 pb-10" 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>
<ItemDeal key={index} Item={Item} />
))}
</div>
</div>

View File

@@ -7,7 +7,7 @@ import { FaCaretRight } from 'react-icons/fa';
import { ListDealData } from '@/data/deal';
import CounDown from '../../../../components/Common/CounDown';
import CounDown from '@components/Common/CounDown';
import ProductItem from './ProductItem';
const BoxProductDeal: React.FC = () => {
@@ -19,7 +19,7 @@ const BoxProductDeal: React.FC = () => {
<h2 className="title font-bold">Giá tốt mỗi ngày</h2>
<span className="text-time-deal-home color-white fz-16 font-bold">Kết thúc sau</span>
<div className="global-time-deal flex items-center gap-2">
<CounDown deadline={new Date('2025-12-31T23:59:59')} />
<CounDown deadline={'31-01-2026, 9:30 am'} />
</div>
</div>
<Link href="/deal" className="button-deal color-white mb-10 flex items-center">

View File

@@ -9,8 +9,8 @@ import { findCategoryBySlug } from '@/lib/product/category';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BannerCategory from './BannerCategory';
import ItemCategoryChild from './ItemCategoryChild';
import BoxFilter from './BoxFilter';
import BoxSort from './BoxSort';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
interface CategoryPageProps {

View File

@@ -1,52 +1,61 @@
import { useEffect, useState } from 'react';
import type { ProductDetailData } from '@/types';
import CounDown from '@/components/Common/CounDown';
import { formatCurrency } from '@/lib/formatPrice';
export const BoxPrice = (item: ProductDetailData) => {
const [now, setNow] = useState(() => Date.now());
return (
<>
{item.product_info.sale_rules.type == 'deal' && (
<div className="box-flash-sale boder-radius-10 flex items-center">
<div className="box-left relative flex items-center">
<i className="sprite sprite-flashsale-detail"></i>
<p className="title-deal font-weight-800">flash sale</p>
</div>
<div className="box-middle product-time-holder global-time-deal flex gap-2">
<CounDown deadline={item.product_info.sale_rules.to_time} />
</div>
<div className="box-right">
<div className="box-product-deal">
<p className="text-deal-detail">
Còn{' '}
{(() => {
const deal = item.product_info.deal_list[0];
return Number(deal.quantity) - deal.sale_order;
})()}
/{item.product_info.deal_list[0].quantity} sản phẩm
</p>
{item.product_info.sale_rules.type == 'deal' &&
Number(item.product_info.sale_rules.to_time) > now && (
<div className="box-flash-sale boder-radius-10 flex items-center">
<div className="box-left relative flex items-center">
<i className="sprite sprite-flashsale-detail"></i>
<p className="title-deal font-weight-800">flash sale</p>
</div>
<div className="box-middle product-time-holder global-time-deal flex gap-2">
<CounDown deadline={Number(item.product_info.sale_rules.to_time)} />
</div>
<div className="box-right">
<div className="box-product-deal">
<p className="text-deal-detail">
Còn{' '}
{(() => {
const deal = item.product_info.deal_list[0];
return Number(deal.quantity) - deal.sale_order;
})()}
/{item.product_info.deal_list[0].quantity} sản phẩm
</p>
<div className="p-quantity-sale" data-quantity-left="3" data-quantity-sale-total="5">
<i className="sprite sprite-fire-deal"></i>
<div className="bg-gradient"></div>
{(() => {
const deal = item.product_info.deal_list[0];
const percentRemaining =
((Number(deal.quantity) - deal.sale_order) / Number(deal.quantity)) * 100;
<div
className="p-quantity-sale"
data-quantity-left="3"
data-quantity-sale-total="5"
>
<i className="sprite sprite-fire-deal"></i>
<div className="bg-gradient"></div>
{(() => {
const deal = item.product_info.deal_list[0];
const percentRemaining =
((Number(deal.quantity) - deal.sale_order) / Number(deal.quantity)) * 100;
return (
<>
<p
className="js-line-deal-left"
style={{ width: `${percentRemaining}%` }}
></p>
</>
);
})()}
return (
<>
<p
className="js-line-deal-left"
style={{ width: `${percentRemaining}%` }}
></p>
</>
);
})()}
</div>
</div>
</div>
</div>
</div>
)}
)}
{/* giá */}
{item.product_info.marketPrice > '0' && item.product_info.sale_rules.type == 'deal' && (

View File

@@ -24,7 +24,8 @@ export const ListComment = () => {
<b className="user-name">{item.user_name}</b>
</div>
<div className="comment-form-right flex items-center gap-2 text-sm text-gray-500">
<i className="fa-regular fa-clock"></i> <span>{item.post_time}</span>
<i className="fa-regular fa-clock"></i>{' '}
<span>{new Date(Number(item.post_time) * 1000).toLocaleDateString('vi-VN')}</span>
</div>
</div>{' '}
{/* content */}

View File

@@ -70,7 +70,7 @@ export const ListReview = () => {
{/* reply list */}
<div className="reply-holder reply-list-container">
{review.new_replies.map((reply) => (
<div key={reply.id} className="item_reply mt-3">
<div key={reply.id} className="item_reply relative mt-3">
<div className="flex items-center justify-between">
<div className="comment-left-form item-center flex gap-2">
<b className="avatar-user avatar-admin">
@@ -86,7 +86,9 @@ export const ListReview = () => {
</div>
</div>
<div className="info_feeback comment-right-form">
<span style={{ color: '#787878', fontSize: 12 }}>({reply.post_time})</span>
<span style={{ color: '#787878', fontSize: 12 }}>
({new Date(Number(reply.post_time) * 1000).toLocaleDateString()})
</span>
</div>
</div>
<div className="comment-content boder-radius-10">{reply.content}</div>

View File

@@ -0,0 +1,113 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { ErrorLink } from '@/components/Common/error';
import type { TypeProductSearch } from '@/types/product/search';
import { ProductSearchData } from '@/data/product/search';
import { findSearchBySlug } from '@/lib/product/search';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
interface ProductSearchPageProps {
slug: string; // khai báo prop slug
}
const ProductSearchPage: React.FC<ProductSearchPageProps> = ({ slug }) => {
const searchParams = useSearchParams();
const keys = searchParams.get('q');
const Searchs = ProductSearchData as unknown as TypeProductSearch[];
const Pages = findSearchBySlug(keys, Searchs);
const breadcrumbItems = [
{ name: 'Trang chủ', url: '/' },
{ name: `Tìm kiếm "${keys}"`, url: `/tim?q=${Pages?.keywords}` },
];
if (!Pages) {
return <ErrorLink />;
}
// lấy sản phẩm
const products = Object.values(Pages.product_list);
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-category page-search container">
<div className="current-cate-title flex items-center gap-2">
<h1 className="current-cate-text font-bold"> Tìm kiếm : {Pages.keywords} </h1>
<span className="current-cate-total">(Tổng {Pages.product_count} sản phẩm)</span>
</div>
{Pages.product_list ? (
<div className="box-content-category">
{/* filter */}
<BoxFilter filters={Pages} />
<div className="box-list-product-category boder-radius-10">
{/* filter sort */}
<BoxSort sort_by_collection={Pages.sort_by_collection} product_display_type="grid" />
</div>
{/* list product */}
<div className="list-product-category grid grid-cols-5 gap-3">
{products.map((item, index) => (
<ItemProduct key={index} item={item} />
))}
</div>
<div className="paging flex items-center justify-center">
{Pages.paging_collection.map((item, index) => (
<Link
key={index}
href={item.url}
className={`item ${item.is_active === '1' ? 'current' : ''}`}
>
{item.name}
</Link>
))}
</div>
</div>
) : (
<div className="text-center" style={{ padding: 20, fontSize: 15 }}>
<p style={{ fontSize: 24, margin: '15px 0 25px 0', fontWeight: 'bold' }}>
Rất tiếc, chúng tôi không tìm thấy kết quả của ${Pages.keywords}
</p>
<div
style={{
textAlign: 'left',
border: 'solid 1px #ccc',
maxWidth: '500px',
padding: '20px',
margin: '15px auto',
lineHeight: 2,
}}
>
<p style={{ textAlign: 'center', margin: '0 0 8px 0' }}>
<b>Đ tìm đưc kết quả chính xác hơn, xin vui lòng</b>
</p>
<ul>
<li>Kiểm tra lại chính tả của từ khóa đã nhập</li>
<li>Thử lại bằng từ khóa khác</li>
<li>Thử lại bằng các từ khóa tổng quát hơn</li>
<li>Thử lại bằng các từ khóa ngắn gọn hơn</li>
</ul>
</div>
<Link href="/">
<i className="fa fa-long-arrow-alt-left"></i> Quay lại trang chủ{' '}
</Link>
</div>
)}
</section>
</>
);
};
export default ProductSearchPage;

View File

@@ -0,0 +1,95 @@
'use client';
import React, { useState } from 'react';
import { parse } from 'date-fns';
import Link from 'next/link';
import Image from 'next/image';
import CounDown from '@/components/Common/CounDown';
import { DealType } from '@/types';
import { formatCurrency } from '@/lib/formatPrice';
type ItemDealProps = {
Item: DealType;
};
const ItemDeal: React.FC<ItemDealProps> = ({ Item }) => {
const [now] = useState(() => Date.now());
// ép kiểu to_time sang số (timestamp) hoặc Date
const deadline = parse(Item.to_time, 'dd-MM-yyyy, h:mm a', new Date()).getTime();
// chỉ hiển thị nếu deadline còn lớn hơn thời gian hiện tại
if (deadline <= now) {
return null;
}
return (
<div className="product-item">
<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 justify-center gap-2">
<div>Kết thúc sau: </div>
<CounDown deadline={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>
);
};
export default ItemDeal;

View File

@@ -32,7 +32,7 @@ const BoxFilter: React.FC<BoxFilterProps> = ({ filters }) => {
>
<Link href={ItemPrice.url}>{ItemPrice.name}</Link>
<a href={ItemPrice.url}>
(${ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count})
({ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count})
</a>
</div>
))}

View File

@@ -1,8 +1,9 @@
'use client';
import React, { useState, useEffect } from 'react';
import { parse } from 'date-fns';
interface CountDownProps {
deadline: Date | string;
deadline: number | string;
}
const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
@@ -13,8 +14,10 @@ const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
const getTime = () => {
let time: number;
if (deadline instanceof Date) {
time = deadline.getTime() - Date.now();
if (typeof deadline == 'string') {
const parsed = parse(deadline as string, 'dd-MM-yyyy, h:mm a', new Date());
time = parsed.getTime() - Date.now();
} else {
time = Number(deadline) * 1000 - Date.now();
}

View File

@@ -11,7 +11,7 @@ export const ListDealData: TypeListProductDeal = [
min_purchase: '1',
max_purchase: '0',
from_time: '19-12-2025, 8:00 am',
to_time: '22-12-2025, 9:30 am',
to_time: '31-01-2026, 9:30 am',
is_featured: '0',
last_update: '1766109733',
last_update_by: '0',
@@ -287,6 +287,291 @@ export const ListDealData: TypeListProductDeal = [
],
},
},
{
id: '560',
pro_id: '27720',
title: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
price: '24990000',
customer_group_price: '[]',
quantity: '5',
min_purchase: '1',
max_purchase: '0',
from_time: '08-12-2025, 8:00 am',
to_time: '31-01-2026, 9:30 am',
is_featured: '0',
last_update: '1766108851',
last_update_by: '0',
ordering: '0',
sale_order: '3',
sale_quantity: '3',
views: '1',
rating: '0',
review_count: '0',
auto_renew: '0',
auto_renew_history: null,
counter: 6,
request_path: '/deal/560',
deal_time_happen: 971685,
deal_time_left: 243315,
is_start: 1,
is_end: 0,
is_active: '1',
product_info: {
id: 27720,
productId: 27720,
priceUnit: 'chiếc',
marketPrice: 27690000,
price: '24990000',
price_off: 6,
currency: 'vnd',
sale_rules: {
price: '24990000',
normal_price: 25990000,
min_purchase: '1',
max_purchase: '0',
remain_quantity: 1,
from_time: '1765155600',
to_time: '1766370600',
type: 'deal',
type_id: '560',
},
lastUpdate: '2025-12-18 15:39:17',
warranty: 'Bảo hành dài theo từng linh kiện',
productName: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
productSummary:
'CPU Intel Core i5-13400F Tray New (Up To 4.60GHz, 10 Nhân 16 Luồng, 20 MB Cache, LGA 1700)\r\nMainboard BIOSTAR Z690MX2-E D4 (Intel Z690, Socket 1700, 2xDDR4, mATX)\r\nRAM Colorful Battle AX 16GB DDR4 3200MHz\r\nỔ Cứng SSD Acer FA100 512GB (NVMe PCIe/ Gen3x4 M2.2280/ 3200MB/s/ 2200MB/s)\r\nCard Màn Hình MSI RTX 5060 Ti 8GB SHADOW 2X OC Plus\r\nNguồn máy tính MIK C750B 750W PLUS BRONZE\r\nVỏ Case Xigmatek BLAST M (M-ATX) - Black\r\nTản Nhiệt Khí JONSBO CR-1000 EVO BLACK (Color RGB)\r\nFan Tản Nhiệt JUNGLE LEOPARD Prism 6Pro Black\r\n',
package_accessory: '',
productImage: {
small: 'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-25251326.jpg',
large: 'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-25251326.jpg',
original: '',
},
imageCollection: [
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-001.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-001.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-1.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-1.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-2.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-2.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-3.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-3.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-4.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-4.jpg',
original: '',
},
alt: '',
},
{
image: {
small: 'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-25251326.jpg',
large: 'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-25251326.jpg',
original: '',
},
alt: '',
},
],
productUrl: '/pc-gaming-ncpc-15',
brand: {
id: 124,
brand_index: 'ncpc',
name: 'NCPC',
image: '',
url: '/brand/ncpc',
},
visit: 39556,
rating: 5,
reviewCount: 1,
review: {
rate: 5,
total: 1,
},
comment: {
rate: 5,
total: 3,
},
quantity: 1,
productSKU: '',
productModel: '',
hasVAT: 0,
condition: 'Mới',
config_count: 0,
configurable: 0,
component_count: 0,
specialOffer: {
other: [
{
id: 0,
title:
'<p><span style="font-size: 10pt;"><strong><span style="color: #ff0000;">TẶNG M&Agrave;N H&Igrave;NH :&nbsp;&nbsp;M&agrave;n h&igrave;nh Gaming cong MSI MAG 276CF E20 27\' FHD VA 200Hz 0.5Ms</span></strong></span></p>\r\n<p><span style="font-size: 10pt;"><strong>Gi&aacute; PC khi kh&ocirc;ng lấy qu&agrave; tặng : 23.490.000đ</strong></span></p>\r\n<p><a href="https://khuyenmai.nguyencongpc.vn/build-pc"><img src="https://nguyencongpc.vn/media/lib/24-09-2025/z7044410660344_5550774fd1a8b1c78c2735d5f4aab705.jpg" alt="" width="100%" /></a></p>',
type: '',
thumbnail: '',
cash_value: 0,
quantity: 1,
from_time: '',
to_time: '',
url: '',
description: '',
status: 1,
},
],
all: [
{
id: 0,
title:
'<p><span style="font-size: 10pt;"><strong><span style="color: #ff0000;">TẶNG M&Agrave;N H&Igrave;NH :&nbsp;&nbsp;M&agrave;n h&igrave;nh Gaming cong MSI MAG 276CF E20 27\' FHD VA 200Hz 0.5Ms</span></strong></span></p>\r\n<p><span style="font-size: 10pt;"><strong>Gi&aacute; PC khi kh&ocirc;ng lấy qu&agrave; tặng : 23.490.000đ</strong></span></p>\r\n<p><a href="https://khuyenmai.nguyencongpc.vn/build-pc"><img src="https://nguyencongpc.vn/media/lib/24-09-2025/z7044410660344_5550774fd1a8b1c78c2735d5f4aab705.jpg" alt="" width="100%" /></a></p>',
type: '',
thumbnail: '',
cash_value: 0,
quantity: 1,
from_time: '',
to_time: '',
url: '',
description: '',
status: 1,
},
],
},
specialOfferGroup: [],
productType: {
isNew: 0,
isHot: 0,
isBestSale: 0,
isSaleOff: 0,
'online-only': 0,
},
bulk_price: [],
thum_poster: '0',
thum_poster_type: '',
addon: [],
variants: [],
variant_option: [],
extend: {
buy_count: '492',
pixel_code: '',
review_count: '43',
review_score: '4.3',
},
weight: 0,
promotion_price: null,
deal_list: [
{
id: '560',
pro_id: '27720',
title: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
price: '24990000',
quantity: '5',
min_purchase: '1',
max_purchase: '0',
is_featured: '0',
from_time: '1765155600',
to_time: '1766370600',
is_started: 1,
},
],
pricing_traces: [
{
price: '11690000',
type: 'deal',
type_id: '565',
},
{
price: '7600000',
type: 'deal',
type_id: '563',
},
{
price: '6990000',
type: 'deal',
type_id: '562',
},
{
price: '24990000',
type: 'deal',
type_id: '560',
},
],
categories: [
{
id: '1829',
catPath: ':1829:0',
name: 'PC GAMING',
url: '/pc-gaming',
},
{
id: '3468',
catPath: ':3468:1829:0',
name: 'CHỌN THEO NHU CẦU',
url: '/chon-theo-nhu-cau-1',
},
{
id: '3432',
catPath: ':3432:3468:1829:0',
name: 'PC ESPORT',
url: '/pc-esport',
},
{
id: '3433',
catPath: ':3433:3468:1829:0',
name: 'PC GAME AAA',
url: '/pc-game-aaa',
},
{
id: '3434',
catPath: ':3434:3468:1829:0',
name: 'PC STREAM GAME',
url: '/pc-stream-game',
},
{
id: '3469',
catPath: ':3469:1829:0',
name: 'CHỌN THEO KHOẢNG GIÁ',
url: '/chon-theo-khoang-gia-1',
},
{
id: '3472',
catPath: ':3472:3469:1829:0',
name: '20 Triệu - 30 Triệu',
url: '/20-trieu-30-trieu-1',
},
],
},
},
{
id: '564',
pro_id: '28304',
@@ -1376,289 +1661,4 @@ export const ListDealData: TypeListProductDeal = [
],
},
},
{
id: '560',
pro_id: '27720',
title: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
price: '24990000',
customer_group_price: '[]',
quantity: '5',
min_purchase: '1',
max_purchase: '0',
from_time: '08-12-2025, 8:00 am',
to_time: '22-12-2025, 9:30 am',
is_featured: '0',
last_update: '1766108851',
last_update_by: '0',
ordering: '0',
sale_order: '3',
sale_quantity: '3',
views: '1',
rating: '0',
review_count: '0',
auto_renew: '0',
auto_renew_history: null,
counter: 6,
request_path: '/deal/560',
deal_time_happen: 971685,
deal_time_left: 243315,
is_start: 1,
is_end: 0,
is_active: '1',
product_info: {
id: 27720,
productId: 27720,
priceUnit: 'chiếc',
marketPrice: 27690000,
price: '24990000',
price_off: 6,
currency: 'vnd',
sale_rules: {
price: '24990000',
normal_price: 25990000,
min_purchase: '1',
max_purchase: '0',
remain_quantity: 1,
from_time: '1765155600',
to_time: '1766370600',
type: 'deal',
type_id: '560',
},
lastUpdate: '2025-12-18 15:39:17',
warranty: 'Bảo hành dài theo từng linh kiện',
productName: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
productSummary:
'CPU Intel Core i5-13400F Tray New (Up To 4.60GHz, 10 Nhân 16 Luồng, 20 MB Cache, LGA 1700)\r\nMainboard BIOSTAR Z690MX2-E D4 (Intel Z690, Socket 1700, 2xDDR4, mATX)\r\nRAM Colorful Battle AX 16GB DDR4 3200MHz\r\nỔ Cứng SSD Acer FA100 512GB (NVMe PCIe/ Gen3x4 M2.2280/ 3200MB/s/ 2200MB/s)\r\nCard Màn Hình MSI RTX 5060 Ti 8GB SHADOW 2X OC Plus\r\nNguồn máy tính MIK C750B 750W PLUS BRONZE\r\nVỏ Case Xigmatek BLAST M (M-ATX) - Black\r\nTản Nhiệt Khí JONSBO CR-1000 EVO BLACK (Color RGB)\r\nFan Tản Nhiệt JUNGLE LEOPARD Prism 6Pro Black\r\n',
package_accessory: '',
productImage: {
small: 'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-25251326.jpg',
large: 'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-25251326.jpg',
original: '',
},
imageCollection: [
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-001.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-001.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-1.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-1.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-2.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-2.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-3.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-3.jpg',
original: '',
},
alt: '',
},
{
image: {
small:
'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-4.jpg',
large:
'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-intel-core-i5-13400f-ram-16g-vga-rtx-5060-ti-4.jpg',
original: '',
},
alt: '',
},
{
image: {
small: 'https://nguyencongpc.vn/media/product/75-27720-pc-gaming-25251326.jpg',
large: 'https://nguyencongpc.vn/media/product/250-27720-pc-gaming-25251326.jpg',
original: '',
},
alt: '',
},
],
productUrl: '/pc-gaming-ncpc-15',
brand: {
id: 124,
brand_index: 'ncpc',
name: 'NCPC',
image: '',
url: '/brand/ncpc',
},
visit: 39556,
rating: 5,
reviewCount: 1,
review: {
rate: 5,
total: 1,
},
comment: {
rate: 5,
total: 3,
},
quantity: 1,
productSKU: '',
productModel: '',
hasVAT: 0,
condition: 'Mới',
config_count: 0,
configurable: 0,
component_count: 0,
specialOffer: {
other: [
{
id: 0,
title:
'<p><span style="font-size: 10pt;"><strong><span style="color: #ff0000;">TẶNG M&Agrave;N H&Igrave;NH :&nbsp;&nbsp;M&agrave;n h&igrave;nh Gaming cong MSI MAG 276CF E20 27\' FHD VA 200Hz 0.5Ms</span></strong></span></p>\r\n<p><span style="font-size: 10pt;"><strong>Gi&aacute; PC khi kh&ocirc;ng lấy qu&agrave; tặng : 23.490.000đ</strong></span></p>\r\n<p><a href="https://khuyenmai.nguyencongpc.vn/build-pc"><img src="https://nguyencongpc.vn/media/lib/24-09-2025/z7044410660344_5550774fd1a8b1c78c2735d5f4aab705.jpg" alt="" width="100%" /></a></p>',
type: '',
thumbnail: '',
cash_value: 0,
quantity: 1,
from_time: '',
to_time: '',
url: '',
description: '',
status: 1,
},
],
all: [
{
id: 0,
title:
'<p><span style="font-size: 10pt;"><strong><span style="color: #ff0000;">TẶNG M&Agrave;N H&Igrave;NH :&nbsp;&nbsp;M&agrave;n h&igrave;nh Gaming cong MSI MAG 276CF E20 27\' FHD VA 200Hz 0.5Ms</span></strong></span></p>\r\n<p><span style="font-size: 10pt;"><strong>Gi&aacute; PC khi kh&ocirc;ng lấy qu&agrave; tặng : 23.490.000đ</strong></span></p>\r\n<p><a href="https://khuyenmai.nguyencongpc.vn/build-pc"><img src="https://nguyencongpc.vn/media/lib/24-09-2025/z7044410660344_5550774fd1a8b1c78c2735d5f4aab705.jpg" alt="" width="100%" /></a></p>',
type: '',
thumbnail: '',
cash_value: 0,
quantity: 1,
from_time: '',
to_time: '',
url: '',
description: '',
status: 1,
},
],
},
specialOfferGroup: [],
productType: {
isNew: 0,
isHot: 0,
isBestSale: 0,
isSaleOff: 0,
'online-only': 0,
},
bulk_price: [],
thum_poster: '0',
thum_poster_type: '',
addon: [],
variants: [],
variant_option: [],
extend: {
buy_count: '492',
pixel_code: '',
review_count: '43',
review_score: '4.3',
},
weight: 0,
promotion_price: null,
deal_list: [
{
id: '560',
pro_id: '27720',
title: 'Bộ PC Gaming Intel Core i5-13400F, RAM 16GB, RTX 5060 Ti [TẶNG MÀN HÌNH]',
price: '24990000',
quantity: '5',
min_purchase: '1',
max_purchase: '0',
is_featured: '0',
from_time: '1765155600',
to_time: '1766370600',
is_started: 1,
},
],
pricing_traces: [
{
price: '11690000',
type: 'deal',
type_id: '565',
},
{
price: '7600000',
type: 'deal',
type_id: '563',
},
{
price: '6990000',
type: 'deal',
type_id: '562',
},
{
price: '24990000',
type: 'deal',
type_id: '560',
},
],
categories: [
{
id: '1829',
catPath: ':1829:0',
name: 'PC GAMING',
url: '/pc-gaming',
},
{
id: '3468',
catPath: ':3468:1829:0',
name: 'CHỌN THEO NHU CẦU',
url: '/chon-theo-nhu-cau-1',
},
{
id: '3432',
catPath: ':3432:3468:1829:0',
name: 'PC ESPORT',
url: '/pc-esport',
},
{
id: '3433',
catPath: ':3433:3468:1829:0',
name: 'PC GAME AAA',
url: '/pc-game-aaa',
},
{
id: '3434',
catPath: ':3434:3468:1829:0',
name: 'PC STREAM GAME',
url: '/pc-stream-game',
},
{
id: '3469',
catPath: ':3469:1829:0',
name: 'CHỌN THEO KHOẢNG GIÁ',
url: '/chon-theo-khoang-gia-1',
},
{
id: '3472',
catPath: ':3472:3469:1829:0',
name: '20 Triệu - 30 Triệu',
url: '/20-trieu-30-trieu-1',
},
],
},
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
import { TypeProductSearch } from '@/types/product/search';
/**
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
*/
export function findSearchBySlug(
keys: string | null,
categories: TypeProductSearch[],
): TypeProductSearch | null {
console.log('Searching for keys:', keys);
const found = categories.find((item) => item.keywords === keys);
return found ?? null;
}

View File

@@ -4,11 +4,17 @@ import { ArticleCateDetailPageData } from '@/data/article/ArticleCateDetailPageD
import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
export function resolvePageType(slug: string) {
// kiểm tra danh mục
// hiển thị trang danh mục sản phẩm
if (productCategoryData.some((c) => c.current_category.url == slug)) {
return 'category';
}
// kiểm tra sản phẩm
// hiển thị trang tìm kiếm
if ('/tim' == slug) {
return 'product-search';
}
// hiển thị trang chi tiết sản phẩm
if (productDetailData.some((c) => c.product_info.productUrl == slug)) {
return 'product-detail';
}

View File

@@ -2909,6 +2909,8 @@ textarea::placeholder {
-ms-transform-origin: top left;
transform-origin: top left;
padding: 14px 10px;
max-height: 400px;
overflow: auto;
}
.page-category .box-content-category .list-filter-category .item:hover ul {
-webkit-transform: scale(1);
@@ -4437,6 +4439,20 @@ textarea::placeholder {
.page-deal .box-list-item-deal .product-item:nth-child(2) .text-deal-item {
display: block !important;
}
.page-deal .time-deal-page p {
background: #000;
color: #fff;
width: 25px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.page-deal .time-deal-page span {
text-align: left;
}
.page-cart {
background: #f0f0f0;
padding: 20px 0 40px;

View File

@@ -10,8 +10,8 @@ export interface ImageCollectionItem {
}
export interface Brand {
id: number;
brand_index: string;
id: number | string;
brand_index?: string;
name: string;
image: string;
url: string;
@@ -19,10 +19,10 @@ export interface Brand {
export interface SaleRules {
price: string | number;
normal_price: number;
normal_price: number | string;
min_purchase: string | number;
max_purchase: string | number;
remain_quantity: number;
remain_quantity: number | string;
from_time: string | number;
to_time: string | number;
type: string;
@@ -63,24 +63,24 @@ export interface Extend {
}
export interface SpecialOfferItem {
id: number;
id: number | string;
title: string;
type: string;
thumbnail: string;
cash_value: number;
quantity: number;
cash_value: number | string;
quantity: number | string;
from_time: string;
to_time: string;
url: string;
description: string;
status: number;
status: number | string;
}
export interface Product {
id: number;
productId: number;
id: number | string;
productId: number | string;
priceUnit: string;
marketPrice: number;
marketPrice: number | string;
price: string | number;
price_off: number | string;
currency: string;
@@ -94,27 +94,27 @@ export interface Product {
imageCollection: ImageCollectionItem[];
productUrl: string;
brand: Brand;
visit: number;
rating: number;
reviewCount: number;
review: { rate: number; total: number };
comment: { rate: number; total: number };
quantity: number;
visit: number | string;
rating: number | string;
reviewCount: number | string;
review: { rate: number | string; total: number | string };
comment: { rate: number | string; total: number | string };
quantity: number | string;
productSKU: string;
productModel: string;
hasVAT: number;
hasVAT: number | string;
condition: string;
config_count: number;
configurable: number;
component_count: number;
config_count: number | string;
configurable: number | string;
component_count: number | string;
specialOffer: { other?: SpecialOfferItem[]; all?: SpecialOfferItem[] };
specialOfferGroup: [];
productType: {
isNew: number;
isHot: number;
isBestSale: number;
isSaleOff: number;
'online-only': number;
isNew: number | string;
isHot: number | string;
isBestSale: number | string;
isSaleOff: number | string;
'online-only': number | string;
};
bulk_price: [];
thum_poster: string;
@@ -123,8 +123,8 @@ export interface Product {
variants: [];
variant_option: [];
extend: Extend;
weight: number;
promotion_price: number | null;
weight: number | string;
promotion_price: number | null | string;
deal_list: Deal[];
pricing_traces: PricingTrace[];
categories: Category[];

View File

@@ -11,7 +11,7 @@ export interface ChildCategory {
is_featured: string;
summary: string;
}
interface FilterCollection {
export interface FilterCollection {
url: string;
key: string;
name: string;
@@ -21,22 +21,24 @@ export interface SortCollection {
key: string;
name: string;
}
interface DisplayCollection {
export interface DisplayCollection {
url: string;
key: string;
name: string;
}
interface PagingCollection {
export interface PagingCollection {
name: string;
url: string;
is_active: string;
}
interface CategoryCollection {
export interface CategoryCollection {
id: string;
name: string;
url: string;
count: string;
is_selected: string;
category_url?: string;
}
export interface BrandFilter {
id: string;

View File

@@ -0,0 +1,33 @@
import {
DisplayCollection,
FilterCollection,
SortCollection,
PagingCollection,
Product,
BrandFilter,
AttributeFilterList,
PriceFilter,
CategoryCollection,
} from '@/types';
export interface TypeProductSearch {
keywords: string;
description: string;
title: string;
favicon: string;
canonical: string;
image: string;
search_query: string;
other_filter_collection: FilterCollection[];
sort_by_collection: SortCollection[];
display_by_collection: DisplayCollection[];
paging_collection: PagingCollection[];
paging: string;
paging_count: string;
product_count: string;
product_list: Record<string, Product>;
attribute_filter_list: AttributeFilterList[];
brand_filter_list: BrandFilter[];
price_filter_list: PriceFilter[];
category_collection: CategoryCollection[];
}