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": { "dependencies": {
"@fancyapps/ui": "^6.1.7", "@fancyapps/ui": "^6.1.7",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"lightgallery": "^2.9.0", "lightgallery": "^2.9.0",
"next": "16.0.10", "next": "16.0.10",
@@ -2749,6 +2750,16 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

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

View File

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

View File

@@ -6,8 +6,7 @@ import { Metadata } from 'next';
import { Breadcrumb } from '@components/Common/Breadcrumb'; import { Breadcrumb } from '@components/Common/Breadcrumb';
import { bannerData } from '@/data/banner'; import { bannerData } from '@/data/banner';
import { ListDealData } from '@/data/deal'; import { ListDealData } from '@/data/deal';
import { formatCurrency } from '@/lib/formatPrice'; import ItemDeal from '@components/Deal/ItemDeal';
import CounDown from '@/components/Common/CounDown';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Danh sách deal', title: 'Danh sách deal',
@@ -16,6 +15,7 @@ export const metadata: Metadata = {
export default function DealPage() { export default function DealPage() {
const breadcrumbItems = [{ name: 'Danh sách deal', url: '/deal' }]; const breadcrumbItems = [{ name: 'Danh sách deal', url: '/deal' }];
return ( return (
<> <>
<div className="container"> <div className="container">
@@ -39,85 +39,9 @@ export default function DealPage() {
</div> </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) => ( {ListDealData.map((Item, index) => (
<div className="product-item" key={index}> <ItemDeal key={index} Item={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">
<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>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { FaCaretRight } from 'react-icons/fa';
import { ListDealData } from '@/data/deal'; import { ListDealData } from '@/data/deal';
import CounDown from '../../../../components/Common/CounDown'; import CounDown from '@components/Common/CounDown';
import ProductItem from './ProductItem'; import ProductItem from './ProductItem';
const BoxProductDeal: React.FC = () => { 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> <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> <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"> <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>
</div> </div>
<Link href="/deal" className="button-deal color-white mb-10 flex items-center"> <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 { Breadcrumb } from '@/components/Common/Breadcrumb';
import BannerCategory from './BannerCategory'; import BannerCategory from './BannerCategory';
import ItemCategoryChild from './ItemCategoryChild'; import ItemCategoryChild from './ItemCategoryChild';
import BoxFilter from './BoxFilter'; import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from './BoxSort'; import BoxSort from '@components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct'; import ItemProduct from '@/components/Common/ItemProduct';
interface CategoryPageProps { interface CategoryPageProps {

View File

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

View File

@@ -70,7 +70,7 @@ export const ListReview = () => {
{/* reply list */} {/* reply list */}
<div className="reply-holder reply-list-container"> <div className="reply-holder reply-list-container">
{review.new_replies.map((reply) => ( {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="flex items-center justify-between">
<div className="comment-left-form item-center flex gap-2"> <div className="comment-left-form item-center flex gap-2">
<b className="avatar-user avatar-admin"> <b className="avatar-user avatar-admin">
@@ -86,7 +86,9 @@ export const ListReview = () => {
</div> </div>
</div> </div>
<div className="info_feeback comment-right-form"> <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> </div>
<div className="comment-content boder-radius-10">{reply.content}</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> <Link href={ItemPrice.url}>{ItemPrice.name}</Link>
<a href={ItemPrice.url}> <a href={ItemPrice.url}>
(${ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count}) ({ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count})
</a> </a>
</div> </div>
))} ))}

View File

@@ -1,8 +1,9 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { parse } from 'date-fns';
interface CountDownProps { interface CountDownProps {
deadline: Date | string; deadline: number | string;
} }
const CounDown: React.FC<CountDownProps> = ({ deadline }) => { const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
@@ -13,8 +14,10 @@ const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
const getTime = () => { const getTime = () => {
let time: number; 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 { } else {
time = Number(deadline) * 1000 - Date.now(); time = Number(deadline) * 1000 - Date.now();
} }

View File

@@ -11,7 +11,7 @@ export const ListDealData: TypeListProductDeal = [
min_purchase: '1', min_purchase: '1',
max_purchase: '0', max_purchase: '0',
from_time: '19-12-2025, 8:00 am', 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', is_featured: '0',
last_update: '1766109733', last_update: '1766109733',
last_update_by: '0', 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', id: '564',
pro_id: '28304', 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'; import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
export function resolvePageType(slug: string) { 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)) { if (productCategoryData.some((c) => c.current_category.url == slug)) {
return 'category'; 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)) { if (productDetailData.some((c) => c.product_info.productUrl == slug)) {
return 'product-detail'; return 'product-detail';
} }

View File

@@ -2909,6 +2909,8 @@ textarea::placeholder {
-ms-transform-origin: top left; -ms-transform-origin: top left;
transform-origin: top left; transform-origin: top left;
padding: 14px 10px; padding: 14px 10px;
max-height: 400px;
overflow: auto;
} }
.page-category .box-content-category .list-filter-category .item:hover ul { .page-category .box-content-category .list-filter-category .item:hover ul {
-webkit-transform: scale(1); -webkit-transform: scale(1);
@@ -4437,6 +4439,20 @@ textarea::placeholder {
.page-deal .box-list-item-deal .product-item:nth-child(2) .text-deal-item { .page-deal .box-list-item-deal .product-item:nth-child(2) .text-deal-item {
display: block !important; 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 { .page-cart {
background: #f0f0f0; background: #f0f0f0;
padding: 20px 0 40px; padding: 20px 0 40px;

View File

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

View File

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