Compare commits

..

16 Commits

Author SHA1 Message Date
a8e30f32a0 update 2026-01-06 13:53:48 +07:00
9486dabdb0 update 2026-01-06 11:02:01 +07:00
28a252f7d7 update 2026-01-05 13:50:16 +07:00
aae8e26135 update 2025-12-30 17:03:47 +07:00
15240ff81f update 2025-12-30 16:47:24 +07:00
9fa4b50b68 update 2025-12-29 23:46:30 +07:00
bf063f244c update 2025-12-29 17:29:51 +07:00
1bb5ad52ed update 2025-12-28 21:43:14 +07:00
71089d1eef update 2025-12-27 12:04:51 +07:00
7606157d26 update 2025-12-27 12:01:54 +07:00
e2063bce4c update 2025-12-27 10:03:53 +07:00
1805ff8674 Merge branch 'master' of https://repo.hurasoft.com/tieptk/nguyencongpc_nextjs 2025-12-26 10:27:12 +07:00
7fc0be90b8 update 2025-12-26 10:27:02 +07:00
a3ac2e27d6 Merge branch 'master' of https://repo.hurasoft.com/tieptk/nguyencongpc_nextjs 2025-12-25 19:03:33 +07:00
a6549f46d5 Merge branch 'master' of https://repo.hurasoft.com/tieptk/nguyencongpc_nextjs 2025-12-23 20:39:25 +07:00
9adb0f7918 com 2025-12-23 20:36:46 +07:00
124 changed files with 66765 additions and 516 deletions

71
package-lock.json generated
View File

@@ -8,7 +8,11 @@
"name": "nguyencongpc",
"version": "0.1.0",
"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",
"postcss": "^8.5.6",
"react": "19.2.1",
@@ -462,6 +466,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@fancyapps/ui": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/@fancyapps/ui/-/ui-6.1.7.tgz",
"integrity": "sha512-KHOvuy90JBFDgbNa2V1N9Jg7PE/lSQMXN9VbhR+WQSIxIEi4PV7kndeao7ezir5WeJ8OZRyDelNKJVLicXfBIg==",
"license": "SEE LICENSE IN LICENSE.md"
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2740,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",
@@ -3650,6 +3670,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4591,6 +4638,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lightgallery": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/lightgallery/-/lightgallery-2.9.0.tgz",
"integrity": "sha512-58Ud1DyhD2ao58t+kPEqSZrjFxg23tGd5ZKr75erm7q31g5xhUtWUJH3sTUkhHzlyJAKHj5eTrJ37HQRXG4Wbg==",
"license": "GPLv3",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -4965,6 +5021,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -9,7 +9,11 @@
"lint": "eslint"
},
"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",
"postcss": "^8.5.6",
"react": "19.2.1",

View File

@@ -1,17 +1,38 @@
'use client';
import { useParams } from 'next/navigation';
import { productCategoryData } from '@/data/product/category';
import NotFound from '../pages/404';
import { resolvePageType } from '@/lib/resolvePageType';
// import component
import CategoryPage from '@components/layout/product/Category';
import CategoryPage from '@/app/pages/Product/Category';
import ProductSearchPage from '@/app/pages/Product/ProductSearch';
import ProductDetailPage from '@/app/pages/Product/ProductDetail';
import ProductHotPage from '@/app/pages/Product/ProductHot';
import ArticlePage from '@/app/pages/Article/HomeArticlePage';
import ArticleCategoryPage from '@/app/pages/Article/CategoryPage';
import ArticleDetailPage from '@/app/pages/Article/DetailPage';
export default function DynamicPage() {
const params = useParams();
const slug = ('/' + params?.slug) as string;
const { slug } = useParams();
const fullSlug = '/' + slug;
if (productCategoryData.find((c) => c.current_category.url == slug)) {
return <CategoryPage slug={slug} />;
const pageType = resolvePageType(fullSlug);
switch (pageType) {
case 'category':
return <CategoryPage slug={fullSlug} />;
case 'product-search':
return <ProductSearchPage slug={fullSlug} />;
case 'product-detail':
return <ProductDetailPage slug={fullSlug} />;
case 'product-hot':
return <ProductHotPage slug={fullSlug} />;
case 'article-home':
return <ArticlePage />;
case 'article-category':
return <ArticleCategoryPage slug={fullSlug} />;
case 'article-detail':
return <ArticleDetailPage slug={fullSlug} />;
default:
return <NotFound />;
}
return <div>404 Không tìm thấy</div>;
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { category_config } from '@/data/buildpc/category';
import { FaPlus } from 'react-icons/fa';
export const BoxListAccessory = () => {
return (
<div className="list-drive" id="js-buildpc-layout" style={{ border: 'solid 1px #e1e1e1' }}>
{category_config.map((category, index) => (
<div key={category.id} className="item-drive flex">
<div className="name-item-drive">
<h3
className="d-name d-name-{{id}}"
style={{ fontSize: '15px', borderBottom: 'none', marginBottom: 10 }}
>
{index + 1}. {category.name}
</h3>
</div>
<div className="drive-checked flex-1" style={{ marginLeft: 0 }}>
<span
className="show-popup_select span-last open-selection"
id="js-category-info-{{id}}"
data-info="{{info}}"
>
+ {''}
<span> Chọn {category.name}</span>
</span>
<div id="js-selected-item-{{id}}" data-id="{{id}}" className="js-item-row"></div>
</div>
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,49 @@
import { FaImage, FaFileExcel, FaPrint, FaShoppingCart } from 'react-icons/fa';
const BtnAction = () => {
return (
<>
<div className="clear"></div>
<p style={{ float: 'right', fontSize: '18px', color: '#d00', marginTop: '10px' }}>
Chi phí dự tính: <span className="js-config-summary"></span>
</p>
<div className="clear"></div>
<ul className="list-btn-action" id="js-buildpc-action">
<li>
<span data-action="create-image">
<div className="flex items-center justify-center gap-3">
<p> tải nh cấu hình</p>
<FaImage />
</div>
</span>
</li>
<li>
<span data-action="download-excel">
<div className="flex items-center justify-center gap-3">
<p>tải file excel cấu hình </p>
<FaFileExcel />
</div>
</span>
</li>
<li>
<span data-action="view">
<div className="flex items-center justify-center gap-3">
<p>Xem & In </p>
<FaPrint />
</div>
</span>
</li>
<li>
<span data-action="add-cart">
<div className="flex items-center justify-center gap-3">
<p>Thêm vào giỏ hàng </p>
<FaShoppingCart />
</div>
</span>
</li>
</ul>
</>
);
};
export default BtnAction;

View File

@@ -0,0 +1,38 @@
'use client';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import Image from 'next/image';
import Link from 'next/link';
import { bannerData } from '@/data/banner';
const Slider = () => {
const dataSlider = bannerData[0].header;
return (
<div className="banner-buildpc" style={{ marginBottom: '40px' }}>
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={12}
slidesPerView={1}
loop={true}
>
{dataSlider?.banner_buildpc?.map((banner, index) => (
<SwiperSlide key={index}>
<Link href={banner.desUrl} className="item-banner boder-radius-10">
<Image
src={banner.fileUrl}
width={1909}
height={57}
alt={banner.title}
priority={true}
className="boder-radius-10"
/>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default Slider;

117
src/app/buildpc/page.tsx Normal file
View File

@@ -0,0 +1,117 @@
import React from 'react';
import { Metadata } from 'next';
import '@styles/buildpc.css';
import { FaUndo } from 'react-icons/fa';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import Slider from '@/app/buildpc/Slider';
import { BoxListAccessory } from './BoxListAccessory';
import BtnAction from './BtnAction';
export const metadata: Metadata = {
title: 'Build PC - Xây dựng cấu hình máy tính PC giá rẻ chuẩn nhất',
description:
'Build pc gaming giá rẻ nhất 2025 - Tự build PC đơn giản nhất - Xây dựng cấu hình máy tính PC với mọi nhu cầu gaming, đồ họa, văn phòng phù hợp với ngân sách',
};
export default function BuildPcPage() {
const breadcrumbItems = [{ name: 'Build PC', url: '/buildpc' }];
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<div className="build-pc pc">
<div className="content container">
<div
className="build-pc_content"
style={{ background: '#fff', padding: '20px', marginTop: '0px' }}
>
<Slider />
<h1
style={{
fontSize: '30px',
lineHeight: '36px',
marginBottom: '10px',
textAlign: 'center',
fontWeight: 500,
}}
>
Build PC - Xây dựng cấu hình máy tính PC giá rẻ chuẩn nhất
</h1>
<h2
style={{
fontSize: '26px',
lineHeight: '30px',
marginBottom: '10px',
fontWeight: 500,
}}
>
Chọn linh kiện xây dựng cấu hình - Tự build PC
</h2>
{/* tab */}
<ul
className="list-btn-action list-btn-action-new"
style={{
marginTop: 10,
float: 'left',
border: 'none',
width: '100%',
marginBottom: 20,
}}
>
<li style={{ width: 'auto' }} className="active">
<span style={{ padding: '0 20px' }}>Cấu hình 1</span>
</li>
<li style={{ width: 'auto' }}>
<span style={{ padding: '0 20px' }}>Cấu hình 2</span>
</li>
<li style={{ width: 'auto' }}>
<span style={{ padding: '0 20px' }}>Cấu hình 3</span>
</li>
<li style={{ width: 'auto' }}>
<span style={{ padding: '0 20px' }}>Cấu hình 4</span>
</li>
<li style={{ width: 'auto' }}>
<span style={{ padding: '0 20px' }}>Cấu hình 5</span>
</li>
</ul>
<div className="clear"></div>
<ul
className="list-btn-action"
style={{ margin: '0 0 0 0', float: 'left', border: 'none' }}
>
<li style={{ width: 'auto' }}>
<div
className="flex cursor-pointer items-center gap-2 text-sm"
style={{ padding: '10px 20px', background: 'var(--color-primary)' }}
>
<p>Làm mới</p>
<FaUndo />
</div>
</li>
</ul>
<div>
<p style={{ float: 'right', fontSize: '18px', color: '#d00', marginTop: '10px' }}>
Chi phí dự tính: <span className="js-config-summary"></span>{' '}
</p>
<div className="js-buildpc-promotion-content" style={{ marginBottom: '0px' }}></div>
</div>
<div className="clear"></div>
{/* Hiển thị dữ liệu tai đây */}
<BoxListAccessory />
{/* btn */}
<BtnAction />
</div>
</div>
</div>
</>
);
}

16
src/app/cart/page.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react';
import HomeCart from '@/components/Cart/Home';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Thông tin giỏ hàng',
description: 'Xem các sản phẩm đã thêm vào trong giỏ hàng',
};
export default function CartPage() {
return (
<>
<HomeCart />
</>
);
}

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

@@ -0,0 +1,51 @@
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 ItemDeal from '@components/Deal/ItemDeal';
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 pb-10" id="js-deal-page">
{ListDealData.map((Item, index) => (
<ItemDeal key={index} Item={Item} />
))}
</div>
</div>
</section>
</>
);
}

View File

@@ -5,10 +5,10 @@ import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import '@styles/globals.css';
import Header from '@/components/layout/other/Header';
import Footer from '@/components/layout/other/Footer';
import Header from '@/components/Other/Header';
import Footer from '@/components/Other/Footer';
import PreLoader from '@components/common/PreLoader';
import PreLoader from '@/components/Common/PreLoader';
export default function RootLayout({
children,

View File

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

View File

@@ -0,0 +1,55 @@
'use client';
import Link from 'next/link';
import { motion } from 'framer-motion';
const NotFound = () => {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 via-white to-blue-100 px-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md rounded-3xl bg-white p-8 text-center shadow-xl"
>
{/* Icon lỗi link */}
<motion.div
animate={{ y: [0, -4, 0] }}
transition={{ repeat: Infinity, duration: 1.8 }}
className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-orange-100"
>
<svg
className="h-10 w-10 text-orange-500"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.828 10.172a4 4 0 00-5.656 5.656M7 7a7 7 0 019.9 9.9M12 12l.01.01"
/>
</svg>
</motion.div>
<h1 className="text-2xl font-bold text-gray-800">Đưng dẫn không hợp lệ</h1>
<p className="mt-3 text-sm text-gray-600">
Bạn truy cập không tồn tại hoặc đưng dẫn đã bị thay đi.
</p>
{/* CTA */}
<div className="mt-8 flex flex-col gap-3">
<Link
href="/"
className="rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-blue-700"
>
Về trang chủ
</Link>
</div>
</motion.div>
</div>
);
};
export default NotFound;

View File

@@ -0,0 +1,19 @@
import ItemArticle from '@/components/Common/ItemArticle';
import { DataListArticleNews } from '@/data/article/ListArticleNews';
export const ArticleTopLeft = () => {
return (
<div className="flex gap-3">
<div className="box-left">
{DataListArticleNews.slice(0, 1).map((item, index) => (
<ItemArticle item={item} key={index} />
))}
</div>
<div className="box-right flex flex-1 flex-col gap-3">
{DataListArticleNews.slice(0, 4).map((item, index) => (
<ItemArticle item={item} key={index} />
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { DataListArticleNews } from '@/data/article/ListArticleNews';
import Link from 'next/link';
export const ArticleTopRight = () => {
return (
<div className="col-right-article box-view-article flex-1">
<form
method="get"
action="/tim-bai"
name="search"
className="boder-radius-10 border-box-article article-search-container"
>
<input type="text" name="q" placeholder="Tìm kiếm bài viết" defaultValue="" />
<button type="submit" className="fas fa-search"></button>
</form>
<div className="boder-radius-10 border-box-article">
<div className="title-box-article font-bold">Xem nhiều</div>
<ul className="list-most-view-article flex flex-col gap-4">
{DataListArticleNews.slice(0, 6).map((item, index) => (
<li className="item-most-view-article flex items-center gap-2" key={index}>
<span className="number flex items-center justify-center font-[600]"></span>
<Link href={item.url} className="line-clamp-2 flex-1">
{item.title}
</Link>
</li>
))}
</ul>
</div>
</div>
);
};

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

@@ -0,0 +1,57 @@
import ItemArticle from '@/components/Common/ItemArticle';
import { DataListArticleNews } from '@/data/article/ListArticleNews';
import Link from 'next/link';
import Image from 'next/image';
export const BoxArticleMid = () => {
return (
<div className="box-article-home-middle grid grid-cols-3 gap-2">
<div className="box-article-tech col-left-article boder-radius-10 border-box-article col-span-2">
<p className="title-box-article font-[600]">Tin công nghệ</p>
<div className="list-article-tech">
{DataListArticleNews.slice(0, 9).map((item, index) => (
<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 flex-1">
<div className="box-article-hot border-box-article boder-radius-10">
<p className="title-box-article font-bold">Tin nổi bật</p>
<div className="list-article-hot">
{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>
);
};

View File

@@ -0,0 +1,45 @@
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
import Image from 'next/image';
import Link from 'next/link';
import { DataListArticleNews } from '@/data/article/ListArticleNews';
export const BoxArticleReview = () => {
return (
<div className="box-article-category page-hompage">
<div className="box-article-global box-artice-review">
<div className="title-box-product-home mb-5 flex flex-col items-center">
<p className="title font-[500]">Review sản phẩm</p>
<p className="border-title"></p>
</div>
<div className="list-article-global">
<Swiper
modules={[Autoplay, Navigation, Pagination, Thumbs]}
spaceBetween={15}
slidesPerView={3}
loop={true}
>
{DataListArticleNews.map((item, index) => (
<SwiperSlide key={index}>
<div className="item-article">
<Link href={item.url} className="img-article">
<Image src={item.image.original} fill alt={item.title} />
</Link>
<div className="content-article-item">
<Link href={item.url} className="title font-weight-500 line-clamp-2">
{item.title}
</Link>
<div className="time-aricle-item flex items-center">
<i className="sprite sprite-clock-item-article"></i>
<span>{item.createDate}</span>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,123 @@
import Link from 'next/link';
import { FaYoutube } from 'react-icons/fa6';
import { DataListArticleVideo } from '@/data/article/ListAricleVideo';
import Image from 'next/image';
import useFancybox from '@/hooks/useFancybox';
export const BoxVideoArticle = () => {
const getYoutubeEmbedUrl = (url: string): string => {
try {
const urlObj = new URL(url);
// nếu là link youtube dạng watch?v=...
if (urlObj.hostname.includes('youtube.com')) {
const videoId = urlObj.searchParams.get('v');
if (videoId) {
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
}
}
// nếu là link youtu.be/xxxx
if (urlObj.hostname.includes('youtu.be')) {
const videoId = urlObj.pathname.replace('/', '');
if (videoId) {
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
}
}
// fallback: trả về chính url
return url;
} catch {
return url;
}
};
const [fancyboxRef] = useFancybox({
closeButton: 'auto',
dragToClose: true,
});
return (
<div className="box-video-article boder-radius-10">
<div className="title-video-article flex justify-between">
<p className="title font-bold">Youtube channel</p>
<Link
href="https://www.youtube.com/c/NGUYENCONGPC"
className="follow-youtube flex items-center gap-2"
>
<FaYoutube />
<span className="font-bold">Theo dõi trên YouTube</span>
</Link>
</div>
<div className="list-video-article flex justify-between gap-2">
<div className="box-left" ref={fancyboxRef}>
{DataListArticleVideo.slice(0, 1).map((item, index) => (
<div className="item-article-video d-flex w-50 gap-10" key={index}>
<Link
href={getYoutubeEmbedUrl(item.external_url)}
className="img-article img-article-video boder-radius-10 relative"
data-fancybox
>
<Image
src={item.image.original}
width={430}
height={310}
className="boder-radius-10"
alt={item.title}
/>
<i className="sprite sprite-big-play-video-article icon-play"></i>
<Image
className="icon-play-small"
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
alt="play"
width={58}
height={41}
/>
</Link>
<Link
href={getYoutubeEmbedUrl(item.external_url)}
className="title-article-video flex-1"
data-fancybox
>
{item.title}
</Link>
</div>
))}
</div>
<div className="box-right grid grid-cols-2 gap-2">
{DataListArticleVideo.slice(1, 7).map((item, index) => (
<div className="item-article-video flex w-50 gap-2" key={index}>
<Link
href={getYoutubeEmbedUrl(item.external_url)}
className="img-article img-article-video boder-radius-10 relative"
data-fancybox
>
<Image
src={item.image.original}
width={430}
height={310}
className="boder-radius-10"
alt={item.title}
/>
<i className="sprite sprite-big-play-video-article icon-play"></i>
<Image
className="icon-play-small"
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
alt="play"
width={58}
height={41}
/>
</Link>
<Link
href={getYoutubeEmbedUrl(item.external_url)}
className="title-article-video flex-1"
data-fancybox
>
{item.title}
</Link>
</div>
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,49 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { DataArticleCategory } from '@/data/article/ListCategory';
import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from '../ArticleTopRight';
import { BoxVideoArticle } from './BoxVideoArticle';
import { BoxArticleMid } from './BoxArticleMid';
import { BoxArticleReview } from './BoxArticleReview';
const ArticleHome = () => {
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
return (
<section className="page-article pb-10">
<div className="container">
<Breadcrumb items={breadcrumbItems} />
<div className="tabs-category-article flex items-center">
{DataArticleCategory.map((item, index) => (
<Link href={item.url} key={index} className="item-tab-article">
<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>
{/* box video */}
<BoxVideoArticle />
{/* box mid */}
<BoxArticleMid />
{/* review */}
<BoxArticleReview />
</div>
</section>
);
};
export default ArticleHome;

View File

@@ -4,11 +4,11 @@ import Link from 'next/link';
import { FaCaretDown } from 'react-icons/fa';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import ItemProduct from '@/components/common/ItemProduct';
import ItemProduct from '@/components/Common/ItemProduct';
import { InfoCategory } from '@/types';
import { menuData } from '@/components/layout/other/Header/menuData';
import { menuData } from '@/components/Other/Header/menuData';
import { productData } from './productData';
const BoxListCategory: React.FC = () => {

View File

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

View File

@@ -1,17 +1,12 @@
import React from 'react';
import Tippy from '@tippyjs/react';
import 'tippy.js/dist/tippy.css';
import { DealType } from '@/types';
import { formatCurrency } from '@/lib/formatPrice';
import Image from 'next/image';
type ProductItemProps = {
item: DealType;
};
const formatCurrency = (value: number | string) => {
const num = typeof value === 'string' ? parseInt(value) : value;
return num.toLocaleString('vi-VN');
};
const ProductItem: React.FC<ProductItemProps> = ({ item }) => {
const { product_info } = item;
const offers = product_info.specialOffer?.all ?? [];
@@ -20,7 +15,7 @@ const ProductItem: React.FC<ProductItemProps> = ({ item }) => {
<div className="product-item">
<a href={product_info.productUrl} className="product-image relative">
{product_info.productImage.large ? (
<img
<Image
src={product_info.productImage.large}
width="164"
height="164"
@@ -28,7 +23,7 @@ const ProductItem: React.FC<ProductItemProps> = ({ item }) => {
className="lazy"
/>
) : (
<img
<Image
src="/static/assets/nguyencong_2023/images/not-image.png"
width="164"
height="164"

View File

@@ -1,11 +1,14 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import { FaCaretRight } from 'react-icons/fa';
import CounDown from './CounDown';
import { ListDealData } from '@/data/deal';
import CounDown from '@components/Common/CounDown';
import ProductItem from './ProductItem';
import { productDealData } from './productDealData';
const BoxProductDeal: React.FC = () => {
return (
@@ -16,12 +19,12 @@ 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 />
<CounDown deadline={'31-01-2026, 9:30 am'} />
</div>
</div>
<a 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">
Xem thêm khuyến mãi <FaCaretRight size={16} />
</a>
</Link>
</div>
<div className="box-list-item-deal swiper-box-deal">
<Swiper
@@ -31,9 +34,9 @@ const BoxProductDeal: React.FC = () => {
loop={true}
navigation={true}
>
{productDealData.map((item, index) => (
{ListDealData.map((Item, index) => (
<SwiperSlide key={index}>
<ProductItem item={item} />
<ProductItem item={Item} />
</SwiperSlide>
))}
</Swiper>

View File

@@ -1,11 +1,11 @@
import React from 'react';
import SliderHome from './SliderHome';
import BoxProductDeal from './BoxDeal';
import BoxProductDeal from './Deal';
import CategoryFeature from './CategoryFeature';
import BoxListCategory from './BoxCategory';
import BoxListCategory from './Category';
import BoxArticle from './BoxArticle';
import BoxArticleVideo from './BoxArticleVideo';
import BoxReviewCustomer from './BoxReviewCustomer';
import BoxArticleVideo from './ArticleVideo';
import BoxReviewCustomer from './ReviewCustomer';
const Home = () => {
return (

View File

@@ -13,7 +13,7 @@ const ItemCategoryChild: React.FC<BoxCategoryChildProps> = ({ item }) => {
? item.big_image
: item.thumnail
? item.thumnail
: '/static/assets/nguyencong_2023/images/favicon.png';
: 'https://nguyencongpc.vn/static/assets/nguyencong_2023/images/favicon.png';
return (
<li>

View File

@@ -3,22 +3,22 @@ import React from 'react';
import Link from 'next/link';
import type { CategoryData } from '@/types';
import { productCategoryData } from '@/data/product/category';
import { findCategoryBySlug } from '@/lib/category';
import { findCategoryBySlug } from '@/lib/product/category';
// box
import { Breadcrumb } from '@components/common/Breadcrumb';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BannerCategory from './BannerCategory';
import ItemCategoryChild from './ItemCategoryChild';
import BoxFilter from './BoxFilter';
import BoxSort from './BoxSort';
import ItemProduct from '@/components/common/ItemProduct';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
interface CategoryPageProps {
slug: string; // khai báo prop slug
}
const CategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
// Ép kiểu dữ liệu từ index.ts về CategoryData[] nếu cần
// Ép kiểu dữ liệu từ index.ts về CategoryData[]
const categories = productCategoryData as unknown as CategoryData[];
const currentCategory = findCategoryBySlug(slug, categories);

View File

@@ -0,0 +1,86 @@
import { FaCheckSquare } from 'react-icons/fa';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
export const BoxBought = () => {
return (
<div className="pro-customer-bought">
<svg
className="pcb-icon"
viewBox="0 0 438.533 438.533"
width={16}
height={16}
fill="red"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M409.133,109.203c-19.608-33.592-46.205-60.189-79.798-79.796C295.736,9.801,259.058,0,219.273,0
c-39.781,0-76.47,9.801-110.063,29.407c-33.595,19.604-60.192,46.201-79.8,79.796C9.801,142.8,0,179.489,0,219.267
c0,39.78,9.804,76.463,29.407,110.062c19.607,33.592,46.204,60.189,79.799,79.798c33.597,19.605,70.283,29.407,110.063,29.407
s76.47-9.802,110.065-29.407c33.593-19.602,60.189-46.206,79.795-79.798c19.603-33.596,29.403-70.284,29.403-110.062
C438.533,179.485,428.732,142.795,409.133,109.203z M334.332,232.111L204.71,361.736c-3.617,3.613-7.896,5.428-12.847,5.428
c-4.952,0-9.235-1.814-12.85-5.428l-29.121-29.13c-3.617-3.613-5.426-7.898-5.426-12.847c0-4.941,1.809-9.232,5.426-12.847
l87.653-87.646l-87.657-87.65c-3.617-3.612-5.426-7.898-5.426-12.845c0-4.949,1.809-9.231,5.426-12.847l29.121-29.13
c3.619-3.615,7.898-5.424,12.85-5.424c4.95,0,9.233,1.809,12.85,5.424l129.622,129.621c3.613,3.614,5.42,7.898,5.42,12.847
C339.752,224.213,337.945,228.498,334.332,232.111z"
/>
</g>
</svg>
<div className="pcb-slider swiper-customer-bought">
<Swiper
modules={[Autoplay, Navigation, Pagination, Thumbs]}
spaceBetween={12}
slidesPerView={1}
loop={true}
autoplay={{
delay: 3000,
disableOnInteraction: false,
}}
>
<SwiperSlide>
<div>
<p>
<b>Khách hàng Anh Tuấn (036 856 xxxx)</b>
</p>
<p>Đã mua hàng 2 giờ trước</p>
</div>
</SwiperSlide>
<SwiperSlide>
<div>
<p>
<b>Khách hàng Quốc Trung (035 348 xxxx)</b>
</p>
<p>Đã mua hàng 1 giờ trước</p>
</div>
</SwiperSlide>
<SwiperSlide>
<div>
<p>
<b>Khách hàng Quang Ngọc (097 478 xxxx)</b>
</p>
<p>Đã mua hàng 30 phút trước</p>
</div>
</SwiperSlide>
<SwiperSlide>
<div>
<p>
<b>Khách hàng Mạnh Lực (037 204 xxxx)</b>
</p>
<p>Đã mua hàng 25 phút trước</p>
</div>
</SwiperSlide>
<SwiperSlide>
<div>
<p>
<b>Khách hàng Hiếu (096 859 xxxx)</b>
</p>
<p>Đã mua hàng 20 phút trước</p>
</div>
</SwiperSlide>
</Swiper>
</div>
</div>
);
};

View File

@@ -0,0 +1,91 @@
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' &&
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;
return (
<>
<p
className="js-line-deal-left"
style={{ width: `${percentRemaining}%` }}
></p>
</>
);
})()}
</div>
</div>
</div>
</div>
)}
{/* giá */}
{item.product_info.marketPrice > '0' && item.product_info.sale_rules.type == 'deal' && (
<div
className="box-price-detail boder-radius-10 flex flex-wrap items-center"
style={{ rowGap: '8px' }}
>
<p className="price-detail font-bold">
{item.product_info.price !== '0'
? `${formatCurrency(item.product_info.price)}đ`
: 'Liên hệ'}
</p>
{item.product_info.marketPrice > '0' && (
<>
<span className="market-price-detail font-weight-500">
{formatCurrency(item.product_info.marketPrice)}
</span>
<div className="save-price-detail flex items-center gap-1">
<span>Tiết kiệm</span>
{(() => {
return formatCurrency(
Number(item.product_info.marketPrice) - Number(item.product_info.price),
);
})()}
<span>đ</span>
</div>
</>
)}
</div>
)}
</>
);
};

View File

@@ -0,0 +1,156 @@
'use client';
import { useRouter } from 'next/navigation';
import type { ProductDetailData } from '@/types';
import Link from 'next/link';
import { BoxPrice } from './BoxPrice';
import { BoxBought } from './BoxBought';
// thêm giỏ hàng
import { addToCart } from '@/lib/ButtonCart';
export const BoxInfoRight = (item: ProductDetailData) => {
const router = useRouter();
const handleBuyNow = () => {
router.push('/cart');
addToCart(item.product_info.productId);
};
return (
<>
<h1 className="product-name color-black line-clamp-3 font-bold">
{item.product_info.productName}
</h1>
<div className="list-basic-product-info flex flex-wrap items-center">
<div className="item-basic">
SP: <span className="color-primary">{item.product_info.productSKU}</span>
</div>
<div className="item-basic">
Đánh giá: <span className="color-primary">{item.product_info.review.summary.total}</span>
</div>
<div className="item-basic">
Bình luận:{' '}
<span className="color-primary">{item.product_info.comment.summary.total}</span>
</div>
<div className="item-basic">
Lượt xem: <span className="color-primary">{item.product_info.visit}</span>
</div>
{item.product_info.extend.buy_count?.length > 0 && (
<div className="item-basic last-item-basic position-relative">
Đã bán: <span className="color-primary">{item.product_info.extend.buy_count}</span>
</div>
)}
</div>
{/* tình trạng */}
<div className="list-basic-product-info flex flex-wrap items-center gap-6">
<div className="item-basic">
Bảo hành: <span className="color-red">{item.product_info.warranty}</span>
</div>
{item.product_info.quantity > '0' && (
<div className="item-basic last-item-basic position-relative">
Tình trạng: <span className="color-green">Còn hàng</span>
</div>
)}
</div>
{/* giá */}
<BoxPrice {...item} />
{item.product_info.specialOffer.all.length > 0 && (
<div className="box-offer-detail border-radius-10">
<div className="title-offer-detail flex items-center">
<i className="sprite sprite-gift-detail"></i>
<p className="font-weight-600">Khuyến mãi</p>
</div>
<div className="list-info-offter">
{item.product_info.specialOffer.all.map((_item, idx) => (
<div key={idx} className="item-offer">
<i className="icon"></i>
<div dangerouslySetInnerHTML={{ __html: _item.title }} />
</div>
))}
</div>
</div>
)}
{/* mua hàng */}
{(item.product_info.quantity > '0' || item.product_info.price > '0') && (
<>
<div className="product-buy-quantity flex items-center">
<p className="title-quantity">Số lượng:</p>
<div className="cart-quantity-select flex items-center justify-center">
<p className="js-quantity-change" data-value="-1">
{' '}
{' '}
</p>
<input
type="text"
className="js-buy-quantity js-quantity-change bk-product-qty font-bold"
defaultValue={1}
/>
<p className="js-quantity-change" data-value="1">
{' '}
+{' '}
</p>
</div>
<button
className="addCart flex cursor-pointer flex-wrap items-center justify-center gap-3"
onClick={() => {
addToCart(item.product_info.productId);
alert('Sản phẩm đã được thêm vào giỏ hàng!');
}}
>
<i className="sprite sprite-cart-detail"></i>
<p className="title-cart">Thêm vào giỏ hàng</p>
</button>
<input type="hidden" className="js-buy-quantity-temp" value="1" />
</div>
<div
id="detail-buy-ads"
className="detail-buy grid grid-cols-2 gap-2"
onClick={() => handleBuyNow()}
>
<button className="detail-buy-now col-span-2 cursor-pointer">
<span>ĐT MUA NGAY</span>
Giao hàng tận nơi nhanh chóng
</button>
<button className="detail-add-cart">
<span>TRẢ GÓP QUA HỒ </span>
Chỉ từ 2.665.000/ tháng
</button>
<button className="detail-add-cart">
<span>TRẢ GÓP QUA THẺ</span>
Chỉ từ 1.332.500/ tháng
</button>
</div>
</>
)}
{/* yên tâm mua hàng */}
<div className="box-product-policy-detal boder-radius-10" style={{ marginTop: '24px' }}>
<h2 className="title font-[600]">Yên tâm mua hàng</h2>
<div className="list-showroom-detail flex flex-wrap justify-between">
<div className="item flex items-center gap-2">
<i className="sprite sprite-camket-detail"></i>
<p>Cam kết giá tốt nhất thị trường.</p>
</div>
<div className="item flex items-center gap-2">
<i className="sprite sprite-sanphammoi-detail"></i>
<p>Sản phẩm mới 100%.</p>
</div>
<div className="item flex items-center gap-2">
<i className="sprite sprite-1doi1-detail"></i>
<p>Lỗi 1 đi 1 ngay lập tức.</p>
</div>
<div className="item flex items-center gap-2">
<i className="sprite sprite-hotrotragop-detail"></i>
<p>Hỗ trợ trả góp - Thủ tục nhanh gọn.</p>
</div>
</div>
</div>
<BoxBought />
</>
);
};

View File

@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { ComboProduct } from '@/types';
import { formatCurrency } from '@/lib/formatPrice';
interface ChangePopupProps {
titleGroup: string;
products: ComboProduct[];
open: boolean; // nhận trạng thái mở
onClose: () => void; // hàm đóng popup
onSelect: (product: ComboProduct) => void;
}
export const ChangeProductPopup: React.FC<ChangePopupProps> = ({
titleGroup,
products,
open,
onClose,
onSelect,
}) => {
if (!open) return null; // chỉ render khi open = true
return (
<dialog open className="modal">
<div className="modal-box max-w-5xl bg-white">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-bold">Chọn {titleGroup} khác</h3>
<button className="btn btn-sm btn-circle btn-ghost" onClick={onClose}>
</button>
</div>
{/* Danh sách sản phẩm */}
<div className="grid grid-cols-4 gap-3">
{products.map((p) => (
<div key={p.id} className="product-item c-pro-item">
<Link href={p.url} className="product-image">
<Image
src={p.images.large || '/static/assets/not-image.png'}
alt={p.title}
className="mb-2 object-cover"
height={170}
width={170}
/>
</Link>
<div className="product-info">
<Link href={p.url}>
<h3 className="product-title line-clamp-2">{p.title}</h3>
</Link>
<div className="product-price-main flex items-center justify-between">
<div className='class="product-price"'>
<b className="price font-[600]">
{Number(p.price) > 0 ? `${formatCurrency(p.price)} đ` : 'Liên hệ'}
</b>
</div>
</div>
<span
className="c-btn js-c-btn"
onClick={() => {
onSelect(p);
}}
>
Chọn mua
</span>
</div>
</div>
))}
</div>
</div>
{/* Overlay */}
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog>
);
};

View File

@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { ComboProduct } from '@/types';
import { formatCurrency } from '@/lib/formatPrice';
interface ItemComboProps {
item: ComboProduct;
keyGroup: string;
titleGroup: string;
setId: string;
products: ComboProduct[];
onOpenPopup: (titleGroup: string, products: ComboProduct[], item: ComboProduct) => void;
}
export const ItemComboSet: React.FC<ItemComboProps> = ({
item,
keyGroup,
titleGroup,
setId,
products,
onOpenPopup,
}) => {
const hasDiscount = Number(item.normal_price) > Number(item.price) && Number(item.price) > 0;
return (
<>
<div
className={`product-item c-pro-item ${
item.is_free === 'yes' ? 'w-select' : ''
} js-pro-${item.id}`}
>
<Link href={item.url} className="product-image">
{item.images?.large ? (
<Image src={item.images.large} alt={item.title} width={175} height={175} />
) : (
<Image
src="/static/assets/nguyencong_2023/images/not-image.png"
width={175}
height={175}
alt={item.title}
/>
)}
</Link>
<div className="product-info">
<Link href={item.url}>
<h3 className="product-title line-clamp-2">{item.title}</h3>
</Link>
<div className="product-price-main d-flex align-items-center justify-content-between">
<div className="product-price">
<b className="price font-weight-600">
{Number(item.price) > 0 ? `${formatCurrency(item.price)} đ` : 'Liên hệ'}
</b>
</div>
</div>
{hasDiscount ? (
<div className="product-martket-main d-flex align-items-center flex-wrap gap-4">
<p className="product-market-price">{item.normal_price} đ</p>
{item.discount.includes('%') ? (
<div
className="product-percent-price"
style={{ fontSize: '10px', padding: '0 8px' }}
>
-{item.discount}
</div>
) : (
<p style={{ fontSize: '10px', color: '#BE1F2D' }}>(-{item.discount} đ)</p>
)}
</div>
) : (
<div className="product-martket-main d-flex align-items-center"></div>
)}
<p
className="c-pro-change js-chagne-pro"
data-id={item.id}
onClick={() => onOpenPopup(titleGroup, products, item)}
>
Chọn {titleGroup} khác
</p>
<div className="check-box-comboset">
<input
type="checkbox"
className={`position-relative js-price js-check-select js-combo-set js-combo-set-select-product cursor-pointer ${
item.is_free === 'yes' ? 'product_free' : ''
}`}
data-price={item.price}
data-unprice={item.normal_price}
data-idpk={item.id}
data-set-id={setId}
data-group-key={keyGroup}
data-product-id={item.id}
/>
</div>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import 'swiper/css';
import { ComboSet, ComboProduct, ComboGroup } from '@/types';
import { ItemComboSet } from './ItemComboset';
import { ChangeProductPopup } from './ChangeProductPopup';
interface ComboProps {
combo_set: ComboSet[];
}
interface PopupGroup {
title: string;
products: ComboProduct[];
}
export const ComboSetBox: React.FC<ComboProps> = ({ combo_set }) => {
const [openPopup, setOpenPopup] = useState(false);
const [popupGroup, setPopupGroup] = useState<PopupGroup>({
title: '',
products: [],
});
const [selectedProduct, setSelectedProduct] = useState<ComboProduct | null>(null);
const handleOpenPopup = (
titleGroup: string,
products: ComboProduct[],
currentItem: ComboProduct,
) => {
setPopupGroup({ title: titleGroup, products });
setSelectedProduct(currentItem); // lưu sản phẩm đang hiển thị
setOpenPopup(true);
};
const handleReplaceProduct = (newProduct: ComboProduct) => {
// cập nhật selectedProduct bằng sản phẩm mới
setSelectedProduct(newProduct);
setOpenPopup(false);
};
const getDisplayedProduct = (group: ComboGroup) => {
// Nếu selectedProduct thuộc group này thì hiển thị nó
if (selectedProduct && group.product_list.some((p) => p.id === selectedProduct.id)) {
return selectedProduct;
}
// Ngược lại lấy sản phẩm mặc định
return group.product_list.find((p) => p.is_first === 'yes') || group.product_list[0];
};
if (!combo_set || combo_set.length === 0) return null;
const setInfo = combo_set[0];
return (
<div className="box-comboset mb-8">
<p className="title-comboset font-weight-600">Mua theo combo</p>
<div id="comboset">
<Swiper
className="list-product-comboset swiper-comboset"
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={16}
slidesPerView={3}
navigation
>
{setInfo.group_list.map((group, index) => {
// lấy sản phẩm đầu tiên theo logic "is_first" hoặc mặc định
const firstProduct =
group.product_list.find((p) => p.is_first === 'yes') || group.product_list[0];
return (
<SwiperSlide key={index}>
<ItemComboSet
item={getDisplayedProduct(group)}
keyGroup={group.key}
titleGroup={group.title}
setId={setInfo.id}
products={group.product_list}
onOpenPopup={handleOpenPopup}
/>
</SwiperSlide>
);
})}
</Swiper>
<div className="comboset-info mt-4 flex justify-between">
<div className="box-left">
<div className="total-comboset flex items-center gap-2">
<p>Tạm tính:</p>
<p className="js-pass-price price text-red font-weight-600">
{/* giả sử lấy giá từ product_info */}
3.050.000 đ
</p>
</div>
<p className="save-price-combo">
Tiết kiệm thêm <span className="save-price">215.000đ</span>
</p>
</div>
<div className="box-right flex items-center justify-end gap-2">
<p className="js-combo-set js-combo-set-checkout buy_combo" data-set-id={setInfo.id}>
Mua thêm <span id="count-pro-selected">0</span> sản phẩm
</p>
</div>
</div>
</div>
<ChangeProductPopup
titleGroup={popupGroup.title}
products={popupGroup.products}
open={openPopup}
onClose={() => setOpenPopup(false)}
onSelect={handleReplaceProduct}
/>
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { useState, useEffect } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
import Image from 'next/image';
import Link from 'next/link';
import { ProductImageGallery } from '@/types';
import type { Swiper as SwiperType } from 'swiper';
import useFancybox from '@/hooks/useFancybox';
interface ImageProps {
ItemImage: ProductImageGallery[];
}
export const ImageProduct: React.FC<ImageProps> = ({ ItemImage }) => {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
const [fancyboxRef] = useFancybox({
closeButton: 'auto',
dragToClose: true,
});
return (
<div className="product-images-show">
<div className="gallery-top product-info-image" ref={fancyboxRef}>
<Swiper
modules={[Autoplay, Navigation, Pagination, Thumbs]}
spaceBetween={12}
slidesPerView={1}
loop={true}
thumbs={{ swiper: thumbsSwiper }}
>
{ItemImage?.map((item, index) => (
<SwiperSlide key={index}>
<Link href={item.size.original} className="bigImage" data-fancybox>
<Image src={item.size.original} alt={''} width="595" height="595" />
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
<div className="gallery-thumbs product-images-slider mt-2">
<Swiper
modules={[Autoplay, Navigation, Pagination, Thumbs]}
spaceBetween={12}
slidesPerView={6}
loop={true}
onSwiper={setThumbsSwiper}
>
{ItemImage?.map((item, index) => (
<SwiperSlide key={index}>
<div className="smallImage">
<Image src={item.size.original} alt={''} width="90" height="60" />
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
};

View File

@@ -0,0 +1,40 @@
import React from 'react';
interface FormCommentProps {
open: boolean;
onClose: () => void;
}
export const FormComment: React.FC<FormCommentProps> = ({ open, onClose }) => {
return (
<dialog id="commentDialog" className={`modal ${open ? 'modal-open' : ''}`}>
{' '}
<div className="modal-box">
<h3 className="mb-4 font-semibold">Nhập thông tin</h3>
<div className="space-y-4">
<div className="flex gap-4">
<label className="label cursor-pointer">
<input type="radio" name="sex" value="Anh" className="radio radio-xs" />
<span className="ml-2">Anh</span>
</label>
<label className="label cursor-pointer">
<input type="radio" name="sex" value="Chị" className="radio radio-xs" />
<span className="ml-2">Chị</span>
</label>
</div>
<input type="text" className="input input-md w-[95%]" placeholder="Họ tên (bắt buộc)" />
<input
type="email"
className="input input-md w-[95%]"
placeholder="Email (để nhận phản hồi qua mail)"
/>
<button className="btn btn-active w-[93.5%] bg-red-500 text-white">Cập nhật</button>
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>Đóng</button>
</form>
</dialog>
);
};

View File

@@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { ListCommentData } from '@/data/ListComment';
import Image from 'next/image';
export const ListComment = () => {
return (
<div className="comment-list">
{ListCommentData.slice(0.3).map((item, index) => (
<div className="item-comment" id={`comment_${item.id}`} key={index}>
<div className="form-reply-comment">
{/* header */}
<div className="comment-name flex justify-between">
<div className="comment-form-left flex items-center gap-2">
{item.user_avatar ? (
<b className="avatar-user">
<img src={item.user_avatar} alt={item.user_name} />
</b>
) : (
<b className="avatar-user flex items-center justify-center">
{' '}
{item.user_name.charAt(0)}{' '}
</b>
)}
<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>{new Date(Number(item.post_time) * 1000).toLocaleDateString('vi-VN')}</span>
</div>
</div>{' '}
{/* content */}
<div className="comment-content relative mt-3 rounded p-2">
<p>{item.content}</p>
<div className="info_feeback mt-2 flex items-center gap-2">
<i className="sprite sprite-icon-reply-detail"></i>
<button className="btn-reply font-medium"> Trả lời </button>{' '}
</div>{' '}
</div>{' '}
{/* reply list */}
<div className="reply-list-container mt-4">
{item.new_replies.map((reply) => (
<div key={reply.id} className="item_reply mt-3">
<div className="flex justify-between">
<div className="flex items-center gap-2">
{reply.user_avatar !== '0' ? (
<b className="avatar-user flex items-center justify-center">
{' '}
<img src={reply.user_avatar} alt={reply.user_name} />{' '}
</b>
) : (
<b className="avatar-user flex items-center justify-center">
{reply.user_name.charAt(0)}
</b>
)}
<div className="comment-name">
<b className="user-name">{reply.user_name}</b>
{reply.is_user_admin === '1' && <i className="note font-medium">QTV</i>}
</div>{' '}
</div>
<div className="text-sm text-gray-500">
{new Date(Number(reply.post_time) * 1000).toLocaleDateString('vi-VN')}
</div>{' '}
</div>
<div className="comment-content mt-2 rounded p-2">{reply.content} </div>{' '}
</div>
))}
</div>
</div>
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { FormComment } from './FormComment';
import { ListComment } from './ListComment';
export const ProductComment: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<div className="box-comment">
<p className="title-comment font-[600]">Hỏi đáp</p>
<div className="comment-detail">
<div className="form-comment flex justify-between gap-2">
<textarea
className="comment_reply_content boder-radius-10"
id="content0"
placeholder="Xin mời để lại câu hỏi, Nguyencong sẽ trả lời ngay trong 1h, các câu hỏi sau 22h - 8h sẽ được trả lời vào sáng hôm sau."
name="user_post[content]"
></textarea>
<button
className="btn-send-form-comment send-comment-pc flex items-center justify-center gap-2"
onClick={() => setOpen(true)}
>
<i className="sprite sprite-icon-send-detail"></i>Gửi
</button>
</div>
<FormComment open={open} onClose={() => setOpen(false)} />
</div>
{/* list comment */}
<ListComment />
</div>
);
};

View File

@@ -0,0 +1,29 @@
import { useState } from 'react';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';
import type { ProductDetailData } from '@/types';
import Link from 'next/link';
export const ProductDescription = (item: ProductDetailData) => {
const [expanded, setExpanded] = useState(false);
if (!item.product_info.productDescription) return null;
return (
<div className="box-descreption-detail">
<h2 className="titlle-descreption font-[500]">Giới thiệu {item.product_info.productName}</h2>
<div
className={`content-descreption-detail static-html relative ${
expanded ? 'max-h-none' : 'max-h-[467px] overflow-hidden'
}`}
dangerouslySetInnerHTML={{ __html: item.product_info.productDescription }}
/>
<div
onClick={() => setExpanded(!expanded)}
className="btn-article-col js-viewmore-content flex items-center justify-center gap-2 font-[500]"
>
<span>{expanded ? 'Thu gọn' : 'Xem tất cả'}</span>
{expanded ? <FaAngleUp /> : <FaAngleDown />}
</div>
</div>
);
};

View File

@@ -0,0 +1,137 @@
import React from 'react';
export const FormReview: React.FC = () => {
return (
<div className="box-form-review" id="js-box-review">
<textarea
className="review_reply_content"
id="rating-content"
placeholder="Mời bạn để lại đánh giá..."
name="user_post[content]"
></textarea>
<div className="actions-comment">
<div className="infomation-customer">
<table>
<tbody>
<tr className="flex items-center">
<td>
<label>Đánh giá:</label>
</td>
<td>
<div className="rating" id="select-rate-pro">
<div className="rating-selection" id="rating-review0">
<input
type="radio"
className="rating-input"
id="rating-input-review-0-5"
value="5"
name="user_post[rate]"
defaultChecked
/>
<label
htmlFor="rating-input-review-0-5"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-4"
value="4"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-4"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-3"
value="3"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-3"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-2"
value="2"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-2"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-1"
value="1"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-1"
className="sprite-1star rating-star"
></label>
</div>
</div>
</td>
</tr>
<tr className="flex items-center">
<td>Tên bạn</td>
<td>
<input
type="text"
id="rating-name"
name="user_post[user_name]"
className="form-control"
defaultValue=""
/>
</td>
</tr>
<tr className="flex items-center">
<td>Email</td>
<td>
<input
type="text"
id="rating-email"
name="user_post[user_email]"
className="form-control"
defaultValue=""
/>
</td>
</tr>
</tbody>
</table>
</div>
<p
id="js-review-note"
className="font-weight-700 flex"
style={{ color: 'red', maxWidth: '100%' }}
></p>
<button
type="button"
className="btn-review send_form mt-12 mb-10"
onClick={() => {
// TODO: viết hàm send_vote() trong React
console.log('Send vote clicked');
}}
>
Gửi đánh giá
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { ListReviewData } from '@/data/ListReview';
import Image from 'next/image';
export const ListReview = () => {
const [showAll, setShowAll] = useState(false);
const visibleReviews = showAll ? ListReviewData : ListReviewData.slice(0, 3);
return (
<div className="list-review">
{visibleReviews.map((review) => {
const avatarLetter = review.user_name.charAt(0).toUpperCase();
const date = new Date(Number(review.post_time) * 1000).toLocaleDateString('vi-VN');
return (
<div key={review.id} className="item-comment">
<div className="form-reply-comment">
{/* header */}
<div className="comment-name flex items-center justify-between">
<div className="comment-form-left flex items-center gap-2">
{review.user_avatar ? (
<b className="avatar-user js-avatar-user flex items-center justify-center">
<Image src={review.user_avatar} alt={review.user_name} />
</b>
) : (
<b className="avatar-user js-avatar-user flex items-center justify-center">
{avatarLetter}
</b>
)}
<b className="user-name flex items-center gap-2">{review.user_name}</b>
</div>
<div className="comment-form-right flex items-center gap-2">
<i className="fa-regular fa-clock"></i>
<span style={{ color: '#787878', fontSize: 12, marginRight: 4 }}>{date}</span>
</div>
</div>
{/* content */}
<div className="comment-content boder-radius-10 relative mt-3">
<div className="text-review flex flex-col gap-2">
<p className="flex items-center">
<b>Đánh giá:</b> <i className={`sprite-star-5 star${review.rate}`}></i>
</p>
<p className="flex items-center">
<b>Nhận xét:</b>
<span style={{ width: '80%' }}>{review.content}</span>
</p>
</div>
{/* feedback actions */}
<div className="info_feeback flex items-center gap-2">
<i className="sprite sprite-icon-reply-detail"></i>
<button className="write_reply btn-reply font-weight-500">Trả lời</button>
</div>
{/* images nếu có */}
<div className="jd-img-review flex flex-col gap-2">
{review.files.map((file) => (
<Image
key={file.id}
src={file.file_path}
alt={file.title}
width={100}
height={60}
/>
))}
</div>
</div>
{/* reply list */}
<div className="reply-holder reply-list-container">
{review.new_replies.map((reply) => (
<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">
{reply.user_avatar !== '0' ? (
<img src={reply.user_avatar} alt={reply.user_name} />
) : (
reply.user_name.charAt(0)
)}
</b>
<div className="comment-name mb-10">
<b className="user-name">{reply.user_name}</b>
{reply.is_user_admin === '1' && <i className="note font-[500]">QTV</i>}
</div>
</div>
<div className="info_feeback comment-right-form">
<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>
</div>
))}
</div>
</div>
</div>
);
})}
{!showAll && ListReviewData.length > 3 && (
<button
id="first-review"
className="btn-more cursor-pointer"
onClick={() => setShowAll(true)}
>
Xem thêm đánh giá
</button>
)}
{showAll && (
<button
id="hide-review"
className="btn-more cursor-pointer"
onClick={() => setShowAll(false)}
>
Thu gọn
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { Review } from '@/types';
import { FaStar } from 'react-icons/fa6';
import { FormReview } from './FormReview';
import { ListReview } from './ListReview';
interface Props {
ItemReview: Review;
}
export const ProductReview: React.FC<Props> = ({ ItemReview }) => {
const [showForm, setShowForm] = useState(false);
const { summary } = ItemReview;
const totalRate = summary.list_rate.reduce((acc, item) => acc + Number(item.total), 0);
// Tạo object chứa số lượng và phần trăm cho từng sao const
const rates = {
rate1: Number(summary.list_rate.find((r) => r.rate === '1')?.total || 0),
rate2: Number(summary.list_rate.find((r) => r.rate === '2')?.total || 0),
rate3: Number(summary.list_rate.find((r) => r.rate === '3')?.total || 0),
rate4: Number(summary.list_rate.find((r) => r.rate === '4')?.total || 0),
rate5: Number(summary.list_rate.find((r) => r.rate === '5')?.total || 0),
};
const percents = {
percent1: totalRate > 0 ? (rates.rate1 / totalRate) * 100 : 0,
percent2: totalRate > 0 ? (rates.rate2 / totalRate) * 100 : 0,
percent3: totalRate > 0 ? (rates.rate3 / totalRate) * 100 : 0,
percent4: totalRate > 0 ? (rates.rate4 / totalRate) * 100 : 0,
percent5: totalRate > 0 ? (rates.rate5 / totalRate) * 100 : 0,
};
return (
<div className="box-review">
<p className="title-review font-[600]">Bình luận đánh giá</p>
<div className="review-customer-detail">
<form
action="/ajax/post_comment.php"
method="post"
encType="multipart/form-data"
className="form-post"
>
<div className="review-info boder-radius-10 flex">
<div className="avgRate flex flex-col items-center justify-center">
<span className="font-bold">{summary.avgRate}/5</span>
<i className={`sprite-star-5 star${summary.avgRate} icon-star-detail`}></i>
<p className="mt-3">{summary.total} đánh giá nhận xét</p>
</div>
<div className="box-avg-rate-count">
<div className="avg-rate-count">
{[5, 4, 3, 2, 1].map((rate) => {
const percent = percents[`percent${rate}` as keyof typeof percents];
const total = rates[`rate${rate}` as keyof typeof rates];
return (
<div key={rate} className="avg-rate-item mt-2 flex items-center justify-center">
<span className="rate-number flex items-center gap-1">
{rate} <FaStar className="text-yellow-500" />
</span>
<div className="nhan-xet-bar">
<div
className={`percent percent${rate}`}
style={{ width: `${percent}%` }}
></div>
</div>
<span className="total-avg-rate">
<strong>{total}</strong> đánh giá
</span>
</div>
);
})}
</div>
</div>
</div>
<p className="text-danh-gia mb-5">Bạn đánh giá sao sản phẩm này</p>
{!showForm ? (
<div
className="button-review mx-auto flex cursor-pointer items-center justify-center"
onClick={() => setShowForm(true)}
>
{' '}
Đánh giá ngay{' '}
</div>
) : (
<div
className="button-review mx-auto flex cursor-pointer items-center justify-center"
onClick={() => setShowForm(false)}
>
{' '}
Đóng lại{' '}
</div>
)}
{/* form */}
{showForm && <FormReview />}
</form>
</div>
<ListReview />
</div>
);
};

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';
import useFancybox from '@/hooks/useFancybox';
interface Props {
ItemSpec: string;
}
export const ProductSpec: React.FC<Props> = ({ ItemSpec }) => {
const [fancyboxRef] = useFancybox({
closeButton: 'auto',
dragToClose: true,
});
return (
<div className="box-spec">
<h2 className="title font-[600]">Thông số kỹ thuật</h2>
<div className="content-spec relative" dangerouslySetInnerHTML={{ __html: ItemSpec }} />
<div id="product-spec" style={{ display: 'none' }} ref={fancyboxRef}>
<div className="box-top-centent-spec d-flex justify-content-between hide">
<h2 className="font-weight-600">Thông số kỹ thuật</h2>
<p
className="delelte-content-spec d-flex justify-content-center align-items-center"
data-fancybox-close
>
<i className="fa-solid fa-xmark"></i>
</p>
</div>
<div className="content-spec">
{/* thay vì {{ page.product_info.productSpec }} bạn truyền từ props */}
{ItemSpec}
</div>
</div>
<a
data-fancybox
data-options='{"src": "#product-spec", "touch": false, "smallBtn": false}'
href="javascript:;"
className="btn-article-col font-weight-500 flex items-center justify-center gap-2"
>
Xem đy đ thông số kỹ thuật
<FaAngleDown />
</a>
</div>
);
};

View File

@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';
interface SummaryProps {
ItemSummary: string;
}
export const ProductSummary: React.FC<SummaryProps> = ({ ItemSummary }) => {
const summaryArray = ItemSummary.split('\r\n');
const [expanded, setExpanded] = useState(false);
// Nếu chưa expanded thì chỉ hiển thị 3 dòng đầu
const visibleItems = expanded ? summaryArray : summaryArray.slice(0, 3);
return (
<div className="box-product-summary boder-radius-10">
<p className="title font-weight-600">Thông số sản phẩm</p>
<ul className="list-product-summary">
{visibleItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{summaryArray.length > 3 && (
<div
className="button-showmore flex cursor-pointer items-center gap-2 font-[500]"
onClick={() => setExpanded(!expanded)}
>
<span>{expanded ? 'Thu gọn' : 'Xem thêm'}</span>
{expanded ? <FaAngleUp /> : <FaAngleDown />}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,131 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
// type
import type { ProductDetailData } from '@/types';
// data
import { productDetailData } from '@/data/product/detail';
import { productData } from '@/data/ListProduct';
import { findProductDetailBySlug } from '@/lib/product/productdetail';
import { ErrorLink } from '@/components/Common/error';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import { ImageProduct } from './ImageProduct';
import { ProductSummary } from './ProductSummary';
import { ComboSetBox } from './ComboSet';
import { BoxInfoRight } from './BoxInfoRight';
import ItemProduct from '@/components/Common/ItemProduct';
import { ProductDescription } from './ProductDescription';
import { ProductSpec } from './ProductSpec';
import { ProductReview } from './ProductReview';
import { ProductComment } from './ProductComment';
interface ProductDetailPageProps {
slug: string;
}
const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ slug }) => {
const productDetails = productDetailData as unknown as ProductDetailData[];
const Products = findProductDetailBySlug(slug, productDetails);
const breadcrumbItems = Products?.product_info?.productPath?.[0]?.path.map((item) => ({
name: item.name,
url: item.url,
})) ?? [{ name: 'Trang chủ', url: '/' }];
// Trường hợp không tìm thấy chi tiết sản phẩm
// Không tìm thấy chi tiết sản phẩm
if (!Products) {
return <ErrorLink />;
}
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-product-detail mt-2 bg-white">
<div className="container">
<div className="box-content-product-detail flex justify-between gap-5">
<div className="box-left">
{/* image product */}
<ImageProduct ItemImage={Products.product_info.productImageGallery} />
<ProductSummary ItemSummary={Products.product_info.productSummary} />
<ComboSetBox combo_set={Products.combo_set} />
</div>
<div className="box-right">
<BoxInfoRight {...Products} />
</div>
</div>
{/* sản phẩm tương tự */}
<div className="box-relative-product box-history-product page-hompage">
<div className="box-product-category">
<div className="title-box">
<h2 className="title title-box font-[600]">Sản phẩm tương tự</h2>
</div>
<div className="box-list-history-product">
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={12}
slidesPerView={5}
loop={true}
>
{productData.map((item, index) => (
<SwiperSlide key={index}>
<ItemProduct item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
{/* nội dung chi tiết sản phẩm */}
<div className="box-read-product-detail flex justify-between gap-3">
<div className="box-left">
{/* mô tả chi tiết sản phẩm */}
<ProductDescription {...Products} />
{/* đánh giá sản phẩm */}
<ProductReview ItemReview={Products.product_info.review} />
{/* bình luận sản phẩm */}
<ProductComment />
</div>
<div className="box-right">
<ProductSpec ItemSpec={Products.product_info.productSpec} />
</div>
</div>
{/* sản phẩm đã xem */}
<div className="box-history-product page-hompage mt-5">
<div className="box-product-category">
<div className="title-box">
<h2 className="title title-box font-[600]">Sản phẩm đã xem</h2>
</div>
<div className="box-list-history-product">
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={12}
slidesPerView={5}
loop={true}
>
{productData.map((item, index) => (
<SwiperSlide key={index}>
<ItemProduct item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</section>
</>
);
};
export default ProductDetailPage;

View File

@@ -0,0 +1,80 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { ErrorLink } from '@/components/Common/error';
import type { TypeProductHot } from '@/types/producthot';
import { ProductHotPageData } from '@/data/producthot';
import { findProductHotBySlug } from '@/lib/product/producthot';
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 ProductHotPageProps {
slug: string; // khai báo prop slug
}
const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
const ProductHot = ProductHotPageData as unknown as TypeProductHot[];
const Pages = findProductHotBySlug(slug, ProductHot);
const breadcrumbItems = [
{ name: 'Trang chủ', url: '/' },
{ name: Pages?.title, url: Pages?.url },
];
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">
<div className="mt-5 flex items-center gap-2">
<h1 className="current-cate-text font-bold"> {Pages.title} </h1>
<span className="current-cate-total">(Tổng {Pages.product_count} sản phẩm)</span>
</div>
</div>
<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>
</section>
</>
);
};
export default ProductHotPage;

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,40 @@
import React from 'react';
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Gửi đơn hàng',
description: 'Gửi đơn hàng',
};
export default function SendCartPage() {
return (
<section className="box-send-cart py-[100px]">
<div className="container">
<div className="send-cart-success">
<div className="send-cart-title">
<p className="send-cart-title-name pb-3">
<i className="sprite-sub sprite-icon-check-cart"></i>
ĐƠN HÀNG ĐÃ ĐƯC TIẾP NHẬN
</p>
<div className="send-cart-title-descreption leading-[150%]">
Cảm ơn quý khách đã đt hàng tại Đơn hàng đã đưc tiếp nhận. Đ kiểm tra đơn hàng hoặc
thay đi thông tin, vui lòng
<Link href="/dang-nhap" className="red-text px-2">
Đăng nhập
</Link>
vào website. Nếu khách hàng yêu cầu đc biệt, vui lòng liên hệ nhân viên vấn tại
<Link href="https://www.facebook.com/MAY.TINH.NGUYEN.CONG" className="red-text px-1">
Facebook
</Link>
hoặc Mua hàng trực tuyến: Hotline:
<b className="red-text">
<Link href="tel:0989336366">Điện thoại: 0989336366</Link>
</b>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { useState, forwardRef, useImperativeHandle } from 'react';
export interface FormCartRef {
validateForm: () => boolean;
}
export const FormCart = forwardRef<FormCartRef, object>((props, ref) => {
const [showTax, setShowTax] = useState(false);
const validateForm = () => {
const name = (document.getElementById('buyer_name') as HTMLInputElement)?.value.trim();
const tel = (document.getElementById('buyer_tel') as HTMLInputElement)?.value.trim();
const address = (document.getElementById('buyer_address') as HTMLInputElement)?.value.trim();
// Regex kiểm tra ký tự đặc biệt (chỉ cho phép chữ cái, số, khoảng trắng)
const regexNoSpecial = /^[\p{L}\p{N}\s]+$/u;
// Regex số điện thoại Việt Nam (10 số, bắt đầu bằng 0)
const regexPhone = /^0\d{9}$/;
// Kiểm tra tên
if (!name || name.length <= 4 || !regexNoSpecial.test(name)) {
alert('Bạn nhập tên chưa đúng định dạng!');
return false;
}
// Kiểm tra số điện thoại
if (!tel || !regexPhone.test(tel)) {
alert('Số điện thoại không hợp lệ! (Ví dụ: 0912345678)');
return false;
}
// Kiểm tra địa chỉ
if (!address || address.length <= 4 || !regexNoSpecial.test(address)) {
alert('Địa chỉ chưa hợp lệ!');
return false;
}
// Nếu hợp lệ thì xử lý đặt hàng
return true;
};
useImperativeHandle(ref, () => ({ validateForm }));
return (
<>
<div className="box-cart-info-customer">
<p className="title-section-cart font-[600]">Thông tin khách hàng</p>
<div className="list-info-customer">
<input type="text" placeholder="Họ tên*" name="user_info[name]" id="buyer_name" />
<div className="flex justify-between gap-2">
<input type="text" placeholder="Số điện thoại*" name="user_info[tel]" id="buyer_tel" />
<input type="text" name="user_info[email]" id="buyer_email" placeholder="Email" />
</div>
<input type="text" placeholder="Địa chỉ*" id="buyer_address" name="user_info[address]" />
<div className="flex justify-between gap-2">
<select name="user_info[province]" className="text-black" id="buyer_province">
<option value="0">Tỉnh/Thành phố</option>
<option value=""> Nội</option>
</select>
<select name="user_info[district]" id="js-district-holder">
<option value="0">Quận/Huyện</option>
</select>
</div>
<textarea placeholder="Ghi chú" name="user_info[note]" id="buyer_note"></textarea>
<div className="form-group-taxt">
<label className="tax-title label flex items-center gap-2">
<input
type="checkbox"
className="w-[20px]"
checked={showTax}
onChange={(e) => setShowTax(e.target.checked)}
/>
Yêu cầu xuất hóa đơn công ty
</label>
</div>
{showTax && (
<div className="js-tax-group">
<div className="form-group row">
<div className="input-taxt">
<input
type="text"
id="txtTaxName"
placeholder="Tên công ty"
className="form-control"
name="user_info[tax_company]"
/>
</div>
</div>
<div className="form-group row">
<div className="input-taxt">
<input
type="text"
id="txtTaxAddress"
placeholder="Địa chỉ công ty"
className="form-control"
name="user_info[tax_address]"
/>
</div>
</div>
<div className="form-group row">
<div className="input-taxt">
<input
type="text"
id="txtTaxCode"
placeholder="Mã số thuế"
className="form-control"
name="user_info[tax_code]"
/>
</div>
</div>
</div>
)}
</div>
</div>
</>
);
});
FormCart.displayName = 'FormCart';

View File

@@ -0,0 +1,110 @@
import Image from 'next/image';
import Link from 'next/link';
import { TypeCartItem } from '@/types/cart';
import { FaSortDown, FaTrashCan } from 'react-icons/fa6';
import { formatCurrency } from '@/lib/formatPrice';
interface PropsCart {
item: TypeCartItem;
onUpdate: (id: string, quantity: number) => void;
onDelete: (id: string) => void;
}
export const ItemCart: React.FC<PropsCart> = ({ item, onUpdate, onDelete }) => {
const handleChangeQuantity = (delta: number) => {
const newQuantity = Math.max(1, parseInt(item.in_cart.quantity) + delta);
onUpdate(item._id, newQuantity);
};
return (
<div className="cart-item-info js-item-row flex justify-between">
<div className="cart-item-left flex">
<Link className="cart-item-img relative" href={item.item_info.productUrl}>
<Image
src={item.item_info.productImage.large}
alt="model"
className="bk-product-image lazy"
width={100}
height={100}
/>
{item.item_info.sale_rules?.type == 'deal' && (
<Image
className="icon-deal-cart lazy"
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/static-icon-cart-deal.png"
width={100}
height={100}
alt="deal"
/>
)}
</Link>
<div className="cart-info-item flex-1">
<Link
href={item.item_info.productUrl}
className="cart-item-name bk-product-name line-clamp-2"
>
{item.item_info.productName}
</Link>
{item.item_info.specialOffer?.all && (
<div className="item-offer relative mt-3">
<p className="title flex items-center pl-0">
Khuyến mại{' '}
<span className="flex gap-2">
{' '}
(Chi tiết)
<FaSortDown />
</span>
</p>
<div className="item-offer-content">
{item.item_info.specialOffer.all.map((_item, idx) => (
<div key={idx} dangerouslySetInnerHTML={{ __html: _item.title }} />
))}
</div>
</div>
)}
</div>
<div className="box-change-quantity flex items-center">
<button
onClick={() => handleChangeQuantity(-1)}
className="js-quantity-change quantity-change flex items-center"
data-value="-1"
>
-
</button>
<input
type="text"
className="js-buy-quantity js-quantity-change bk-product-qty font-bold"
value={item.in_cart.quantity}
onChange={() => handleChangeQuantity(1)}
/>
<button
onClick={() => handleChangeQuantity(1)}
className="js-quantity-change quantity-change flex items-center"
data-value="1"
>
+
</button>
</div>
</div>
<div className="box-item-right flex flex-col items-end justify-between">
<div className="price-cart-item">
{item.in_cart.price == '0' ? (
<p className="price cart-item-price item-cart-price js-total-item-price font-bold">
0 đ
</p>
) : (
<p className="price cart-item-price item-cart-price js-total-item-price font-bold">
{formatCurrency(item.in_cart.total_price)} đ
</p>
)}
</div>
<button
onClick={() => onDelete(item._id)}
className="delete-item-cart item-cart-icon js-delete-item flex cursor-pointer items-center justify-center"
>
<FaTrashCan />
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,189 @@
'use client';
import { useState, useRef } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { FaChevronLeft } from 'react-icons/fa6';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import { TypeCartItem } from '@/types/cart';
import { ItemCart } from './ItemCart';
import { FormCart, FormCartRef } from './FormCart';
import { formatCurrency } from '@/lib/formatPrice';
const HomeCart = () => {
const router = useRouter();
const breadcrumbItems = [{ name: 'Giỏ hàng', url: '/cart' }];
const [cart, setCart] = useState<TypeCartItem[]>(() => {
const storedCart = localStorage.getItem('cart');
return storedCart ? JSON.parse(storedCart) : [];
});
const [payMethod, setPayMethod] = useState('2');
const formRef = useRef<FormCartRef>(null);
const updateCartItem = (id: string, quantity: number) => {
const newCart = cart.map((item) =>
item._id === id
? {
...item,
in_cart: {
...item.in_cart,
quantity: quantity.toString(),
total_price: quantity * Number(item.in_cart.price),
},
}
: item,
);
setCart(newCart);
localStorage.setItem('cart', JSON.stringify(newCart));
};
const deleteCartItem = (id: string) => {
const isConfirm = confirm('Bạn có chắc chắn xóa sản phẩm này không ?');
if (isConfirm) {
const newCart = cart.filter((item) => item._id !== id);
setCart(newCart);
localStorage.setItem('cart', JSON.stringify(newCart));
}
};
const deleteCart = () => {
const isConfirm = confirm('Bạn có chắc chắn xóa sản phẩm này không ?');
if (isConfirm) {
setCart([]);
localStorage.removeItem('cart');
}
};
// tính tổng tiền
const getTotalPrice = () => {
return formatCurrency(cart.reduce((sum, item) => sum + Number(item.in_cart.total_price), 0));
};
const handleClickOrder = () => {
if (formRef.current?.validateForm()) {
router.push('/send-cart');
}
};
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-cart">
{cart.length === 0 ? (
<div className="not-cart pt-5">
<Image
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/cart-home-min.png"
className="lazy"
width={130}
height={130}
alt="icon-cart"
/>
<p>Không sản phẩm nào trong giỏ hàng của bạn.</p>
<Link href="/" className="back-cart">
Tiết tục mua sắm
</Link>
</div>
) : (
<>
<div className="container-cart cart-header-title flex items-center justify-between">
<p>Giỏ hàng của bạn</p>
<Link className="back-homepage flex items-center gap-2" href="/">
<FaChevronLeft />
Mua thêm sản phẩm khác
</Link>
</div>
<div className="box-info-cart container-cart">
<div className="box-delete-all flex justify-end">
<button className="delete-cart-all" onClick={() => deleteCart()}>
{' '}
Xóa giỏ hàng{' '}
</button>
</div>
<div className="box-cart-item-list">
{cart.map((item, index) => (
<ItemCart
item={item}
key={index}
onUpdate={updateCartItem}
onDelete={deleteCartItem}
/>
))}
</div>
{/* form mua hàng */}
<FormCart ref={formRef} />
<div className="box-payment">
<p className="title-section-cart font-bold">Phương thức thanh toán</p>
<div className="list-method-payment">
<label className="label">
<input
type="radio"
name="pay_method"
id="pay2"
value="2"
checked={payMethod === '2'}
onChange={(e) => setPayMethod(e.target.value)}
/>
Thanh toán khi nhận hàng
</label>
</div>
<p className="title-section-cart font-bold">Tổng tiền</p>
<div className="list-price">
<p className="price-total1 flex items-center justify-between">
<b className="txt">Tổng cộng</b>
<b className="price js-total-before-fee-cart-price" id="total-cart-price">
{getTotalPrice()}
</b>
</p>
<p className="price-total2 flex items-center justify-between">
<b className="txt">Thành tiền</b>
<b className="price color-red js-total-cart-price font-bold">
{getTotalPrice()} đ
</b>
</p>
<span className="has-vat">(Giá đã bao gồm VAT)</span>
</div>
</div>
<div className="list-btn-cart">
<button type="submit" onClick={handleClickOrder} className="js-send-cart font-bold">
Đt hàng
</button>
<div className="list-print-cart flex justify-between gap-2">
<Link
href="/export_file.php?file_type=xls&content_type=shopping-cart"
className="down-excel font-bold"
target="_blank"
>
Tải file excel
</Link>
<Link
href="javascript:void(0)"
rel="nofollow"
className="down-img-cart font-bold"
>
Tải nh báo giá
</Link>
<Link
href="/print/user.php?view=cart"
target="_blank"
className="print-cart font-bold"
>
in báo giá
</Link>
</div>
</div>
</div>
</>
)}
</section>
</>
);
};
export default HomeCart;

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,27 @@ 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>
))}
</div>
</div>
)}
{/* Thương hiệu */}
{brand_filter_list && (
<div className="info-filter-category flex gap-10">
<p className="title">Thương hiệu:</p>
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-2">
{brand_filter_list.map((ItemBrand, index) => (
<div
key={index}
className={`item item-cetner flex gap-4 ${ItemBrand.is_selected == '1' ? 'current' : ''}`}
>
<Link href={ItemBrand.url}>{ItemBrand.name}</Link>
<a href={ItemBrand.url}>
({ItemBrand.is_selected == '1' ? 'Xóa' : ItemBrand.count})
</a>
</div>
))}
@@ -46,7 +66,7 @@ const BoxFilter: React.FC<BoxFilterProps> = ({ filters }) => {
<p className="title">Chọn theo tiêu chí:</p>
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-3">
{/* thương hiệu */}
{brand_filter_list && brand_filter_list.length > 0 && (
{brand_filter_list && (
<div className={`item ${brand_filter_list[0].is_selected === '1' ? 'current' : ''}`}>
<div className="flex items-center">
{brand_filter_list[0].is_selected === '1' ? (

View File

@@ -2,42 +2,41 @@ const BoxShowroom: React.FC = () => {
return (
<>
<dialog id="boxShowroom" className="modal">
<div className="modal-box">
<div className="modal-box max-w-[1000px] bg-white">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"></button>
</form>
<div className="popup-showrom-container d-block">
<p className="group-title">HỆ THỐNG SHOWROOM</p>
<div className="flex flex-wrap justify-between">
<div className="mt-5 grid grid-cols-2 gap-5">
<div className="item">
<p className="item-title">1. Nội</p>
<p>17 Kế Tấn, Phường Phương Liệt, Nội.</p>
<p>
Giờ làm việc: <b>08:30 - 20:30</b>
</p>
<div
className="map-holder js-map-holder"
data-src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3132.222076725264!2d105.83522224518104!3d20.998217116862435!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3135ac7b37915991%3A0xe20876d091ded6bc!2zMTcgUC4gSMOgIEvhur8gVOG6pW4sIFBoxrDGoW5nIExp4buHdCwgVGhhbmggWHXDom4sIEjDoCBO4buZaSwgVmnhu4d0IE5hbQ!5e0!3m2!1svi!2s!4v1720509407173!5m2!1svi!2s"
></div>
<iframe
width={'100%'}
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3132.222076725264!2d105.83522224518104!3d20.998217116862435!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3135ac7b37915991%3A0xe20876d091ded6bc!2zMTcgUC4gSMOgIEvhur8gVOG6pW4sIFBoxrDGoW5nIExp4buHdCwgVGhhbmggWHXDom4sIEjDoCBO4buZaSwgVmnhu4d0IE5hbQ!5e0!3m2!1svi!2s!4v1720509407173!5m2!1svi!2s"
></iframe>
</div>
<div className="item">
<p className="item-title">2. Hồ Chí Minh</p>
<p>249 Thường Kiệt, Phường Phú Thọ, TP. Hồ Chí Minh</p>
<p>
Giờ làm việc: <b>08:30 - 20:30</b>
</p>
<div
className="map-holder js-map-holder"
data-src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d15678.56730501209!2d106.66439700000001!3d10.762063!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x85a7fc3a74bcd7fd!2zTcOheSBUw61uaCBOZ3V54buFbiBDw7RuZyAxNzYgVMOibiBQaMaw4bubYw!5e0!3m2!1svi!2sus!4v1658936898247!5m2!1svi!2sus"
></div>
<iframe
width={'100%'}
src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d15678.56730501209!2d106.66439700000001!3d10.762063!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x85a7fc3a74bcd7fd!2zTcOheSBUw61uaCBOZ3V54buFbiBDw7RuZyAxNzYgVMOibiBQaMaw4bubYw!5e0!3m2!1svi!2sus!4v1658936898247!5m2!1svi!2sus"
></iframe>
</div>
</div>
</div>
</div>
<label className="modal-backdrop" htmlFor="my_modal_7">
Close
</label>
<form method="dialog" className="modal-backdrop">
<button>Close</button>
</form>
</dialog>
</>
);

View File

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

View File

@@ -1,16 +1,26 @@
'use client';
import React, { useState, useEffect } from 'react';
import { parse } from 'date-fns';
const CounDown: React.FC = () => {
interface CountDownProps {
deadline: number | string;
}
const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
const [days, setDays] = useState(0);
const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0);
const deadline: Date = new globalThis.Date('2025-12-31');
const getTime = () => {
const time = deadline.getTime() - Date.now();
let time: number;
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();
}
setDays(Math.floor(time / (1000 * 60 * 60 * 24)));
setHours(Math.floor((time / (1000 * 60 * 60)) % 24));

View File

@@ -0,0 +1,57 @@
'use client';
import React from 'react';
import { Article } from '@/types';
import { ArticleItem } from '@/types/article/TypeArticleCatePage';
import Link from 'next/link';
import Image from 'next/image';
type ItemArticleProps = {
item: Article | ArticleItem;
};
const ItemArticle: React.FC<ItemArticleProps> = ({ item }) => {
// chọn link: nếu có external_url thì dùng, ngược lại dùng url
const linkHref = item.external_url && item.external_url !== '' ? item.external_url : item.url;
// chọn ảnh: nếu có original thì dùng, ngược lại ảnh mặc định
const imageSrc =
item.image?.original && item.image.original !== ''
? item.image.original
: '/static/assets/nguyencong_2023/images/not-image.png';
// chọn thời gian: ưu tiên article_time, fallback createDate
const timeDisplay =
item.article_time && item.article_time !== '' ? item.article_time : item.createDate;
return (
<div className="item-article flex gap-3">
<Link href={linkHref} className="img-article boder-radius-10 position-relative">
<Image
className="boder-radius-10"
src={imageSrc}
fill
alt={item.title}
sizes="(max-width: 768px) 100vw, 265px"
/>
{/* icon video nếu cần */}
<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={linkHref} className="title-article">
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
</Link>
<p className="time-article flex items-center gap-1">
<i className="sprite sprite-clock-item-article"></i>
<span>{timeDisplay}</span>
</p>
<p className="descreption-article line-clamp-2">{item.summary}</p>
</div>
</div>
);
};
export default ItemArticle;

View File

@@ -4,16 +4,12 @@ import 'tippy.js/dist/tippy.css';
import { Product } from '@/types';
import Image from 'next/image';
import Link from 'next/link';
import { formatCurrency } from '@/lib/formatPrice';
type ProductItemProps = {
item: Product;
};
const formatCurrency = (value: number | string) => {
const num = typeof value === 'string' ? parseInt(value) : value;
return num.toLocaleString('vi-VN');
};
const ItemProduct: React.FC<ProductItemProps> = ({ item }) => {
const offers = item.specialOffer?.all ?? [];
@@ -48,7 +44,7 @@ const ItemProduct: React.FC<ProductItemProps> = ({ item }) => {
{item.marketPrice.toLocaleString()}
<u>đ</u>
</p>
<div className="product-percent-price">-{Math.round(item.price_off)} %</div>
<div className="product-percent-price">-{Math.round(Number(item.price_off))} %</div>
</div>
) : (
<div className="product-martket-main flex items-center"></div>

View File

@@ -0,0 +1,59 @@
import Link from 'next/link';
import { motion } from 'framer-motion';
export const ErrorLink = () => {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 via-white to-blue-100 px-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md rounded-3xl bg-white p-8 text-center shadow-xl"
>
{/* Icon lỗi link */}
<motion.div
animate={{ y: [0, -4, 0] }}
transition={{ repeat: Infinity, duration: 1.8 }}
className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-orange-100"
>
<svg
className="h-10 w-10 text-orange-500"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.828 10.172a4 4 0 00-5.656 5.656M7 7a7 7 0 019.9 9.9M12 12l.01.01"
/>
</svg>
</motion.div>
<h1 className="text-2xl font-bold text-gray-800">Đưng dẫn không hợp lệ</h1>
<p className="mt-3 text-sm text-gray-600">
Bạn truy cập không tồn tại hoặc đưng dẫn đã bị thay đi.
</p>
{/* CTA */}
<div className="mt-8 flex flex-col gap-3">
<Link
href="/"
className="rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-blue-700"
>
Về trang chủ
</Link>
<Link
href="/products"
className="rounded-xl border border-gray-300 px-6 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-100"
>
Xem tất cả sản phẩm
</Link>
</div>
</motion.div>
</div>
);
};

View File

@@ -0,0 +1,163 @@
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { TypeListProductDeal } from '@types/TypeListProductDeal';
const formatCurrency = (price: number | string) => {
return Number(price).toLocaleString('vi-VN');
};
const DealProductItem = ({ item }: { item: TypeListProductDeal }) => {
const product = item.product_info;
const quantityLeft = item.quantity - item.sale_quantity;
return (
<div
className="product-item"
data-id={product.id}
data-time={item.deal_time_left}
data-type={product.sale_rules.type}
>
<Link href={product.productUrl || '#'} className="product-image relative">
<Image
src={product.productImage?.large || '/static/assets/nguyencong_2023/images/not-image.png'}
width={164}
height={164}
alt={product.productName}
className="lazy"
unoptimized // Thêm nếu dùng ảnh từ domain bên ngoài chưa config
/>
<span className="p-type-holder">
{product.productType?.isHot === 1 && <i className="p-icon-type p-icon-hot"></i>}
{product.productType?.isNew === 1 && <i className="p-icon-type p-icon-new"></i>}
</span>
</Link>
<div className="product-info">
<Link href={product.productUrl || '#'}>
<h3 className="product-title line-clamp-3">{product.productName}</h3>
</Link>
<div className="product-martket-main flex items-center">
{product.marketPrice > 0 ? (
<>
<p className="product-market-price">{product.marketPrice.toLocaleString()} </p>
<div className="product-percent-price">-{parseInt(product.price_off || '0')}%</div>
</>
) : product.sale_rules?.type === 'deal' ? (
<>
<p className="product-market-price">
{product.sale_rules.normal_price.toLocaleString()}
</p>
<div className="product-percent-price">0%</div>
</>
) : null}
</div>
<div className="product-price-main font-semibold">
{Number(item.price) > 0 ? `${formatCurrency(item.price)}đ` : 'Liên hệ'}
</div>
<div
className="p-quantity-sale"
data-quantity-left={quantityLeft}
data-quantity-sale-total={item.quantity}
>
<i className="sprite sprite-fire-deal"></i>
<div className="bg-gradient"></div>
<p className="js-line-deal-left"></p>
<span>
Còn {quantityLeft}/ {item.quantity} sản phẩm
</span>
</div>
{product.specialOffer?.all?.length > 0 ? (
<div
className="product-offer line-clamp-2"
dangerouslySetInnerHTML={{ __html: product.specialOffer.all[0].title }}
/>
) : (
<div className="product-offer line-clamp-2"></div>
)}
</div>
{/* TOOLTIP */}
<div className="tooltip p-tooltip tippy-box">
<div className="tooltip-name">{product.productName}</div>
<div className="tooltip-descreption">
<div className="tooltip-descreption-price">
{product.marketPrice > 0 ? (
<p>Giá niêm yết</p>
) : (
product.sale_rules?.type === 'deal' && <p>Giá gốc</p>
)}
<p>Giá bán</p>
{product.warranty !== '' && <p>Bảo hành</p>}
<p>Tình trạng</p>
</div>
<div className="tooltip-descreption-info">
{product.marketPrice > 0 ? (
<div className="d-flex align-items-center">
<p className="card-price-origin color-black" style={{ position: 'relative' }}>
{product.marketPrice.toLocaleString()}
<span className="card-price-origin-line-through"></span>
</p>
<span className="color-red" style={{ marginLeft: '4px' }}>
-{product.price_off}%
</span>
</div>
) : product.sale_rules?.type === 'deal' ? (
<div className="d-flex align-items-center">
<p className="card-price-origin color-black" style={{ position: 'relative' }}>
{product.sale_rules.normal_price.toLocaleString()}
<span className="card-price-origin-line-through"></span>
</p>
<span className="color-red" style={{ marginLeft: '4px' }}>
-
{Math.floor(
100 -
(Number(product.sale_rules.price) / product.sale_rules.normal_price) * 100,
)}
%
</span>
</div>
) : null}
<p>{Number(product.price) > 0 ? `${formatCurrency(product.price)}đ` : 'Liên hệ'}</p>
<p className="color-primary">{product.warranty}</p>
<p className="color-secondary">{quantityLeft > 0 ? 'Còn DEAL' : 'Hết DEAL'}</p>
</div>
</div>
{product.productSummary && (
<>
<div className="tooltip-input">
<i className="fa-solid fa-database icon-database"></i>
<span>Thông số sản phẩm</span>
</div>
<div className="tooltip-list">
<span dangerouslySetInnerHTML={{ __html: product.productSummary }} />
</div>
</>
)}
{product.specialOffer?.all?.length > 0 && (
<div className="box-tooltip-gift">
<div className="tooltip-input tooltip-gift">
<p className="icon-gift">
<i className="fa-solid fa-gift"></i> Khuyến mãi
</p>
</div>
<div className="tooltip-list tooltip-list-gift">
<ul dangerouslySetInnerHTML={{ __html: product.specialOffer.all[0].title }} />
</div>
</div>
)}
</div>
</div>
);
};
export default DealProductItem;

View File

@@ -0,0 +1,134 @@
const BoxHotLine = () => {
return (
<dialog id="boxHotline" className="modal">
<div className="modal-box max-w-[750px] bg-white">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"></button>
</form>
<div id="popup-hotline">
<div className="flex justify-between gap-5">
<div className="content-pop khach-ca-nhan khach-hang-ca-nhan">
<p className="title-content-pop">Khách nhân</p>
<div className="item-pop">
<div className="title-item-pop"> Vấn - Bán Hàng Online:</div>
<div className="item-people">
<p className="phone">0828.333.363</p>
<span>Mr Ngọc</span>
</div>
<div className="item-people">
<p className="phone">0989.336.366</p>
<span>Mr Hùng</span>
</div>
<div className="item-people">
<p className="phone">0707.08.6666</p>
<span>Mr Hoàng</span>
</div>
<div className="item-people">
<p className="phone">089.9999.191</p>
<span>Mr Lộc</span>
</div>
<div className="item-people">
<p className="phone">0812.666.665</p>
<span>Mr Tuấn Anh</span>
</div>
<div className="item-people">
<p className="phone">09.8888.2838</p>
<span>Mr. Minh</span>
</div>
</div>
<div className="item-pop">
<div className="title-item-pop">HOTLINE:</div>
<div className="item-people">
<p className="phone">098.33333.88</p>
<span>Showroom TP. Hồ Chí Minh</span>
</div>
<div className="item-people">
<p className="phone">097.9999.191</p>
<span>Showroom TP. Nội</span>
</div>
<div className="item-people">
<p className="phone">0765.666.668</p>
<span>Showroom TP. Nội</span>
</div>
</div>
<div className="item-pop">
<div className="title-item-pop">Bảo hành - Hỗ trợ kỹ thuật</div>
<div className="item-people">
<p className="phone">0705.666.668</p>
<span>17 Kế Tấn, Phường Phương Liệt, Nội</span>
</div>
<div className="item-people">
<p className="phone">079.9999.191</p>
<span>249 Thường Kiệt, phường Phú Thọ, TP. Hồ Chí Minh </span>
</div>
</div>
<div className="item-pop">
<div className="title-item-pop">Kế toán:</div>
<div className="item-people">
<p className="phone">0332.101.130</p>
<span></span>
</div>
</div>
<div className="item-pop">
<div className="title-item-pop">Kế toán công nợ:</div>
<div className="item-people">
<p className="phone">0968.929.992</p> <span></span>
</div>
</div>
<div className="item-cskh">
<b>GÓP Ý</b>:{' '}
<a
href="javascript:void(0)"
style={{ color: '#FFB233', fontWeight: 'bold', fontSize: '16px' }}
>
097.9999.191 -{' '}
</a>{' '}
<a
href="javascript:void(0)"
style={{ color: '#FFB233', fontWeight: 'bold', fontSize: '16px' }}
>
098.33333.88
</a>
</div>
</div>
<div className="content-pop khach-doanh-nghiep">
<p className="title-content-pop">Khách doanh nghiệp</p>
<div className="item-pop">
<div className="title-item-pop"> Vấn - Bán Hàng Online:</div>
<div className="item-people">
<p className="phone">097.9999.191</p>
<span>Mr Lực</span>
</div>
<div className="item-people">
<p className="phone">0828.333.363</p>
<span>Mr Ngọc</span>
</div>
<div className="item-people">
<p className="phone">0707.08.6666</p>
<span>Mr Hoàng</span>
</div>
</div>
<div className="item-pop">
<div className="title-item-pop">Khách hàng đi - MUA, BÁN BUÔN</div>
<div className="item-people">
<a href="tel:0981226969">098.122.6969</a>
<span>Ms Tuyết</span>
</div>
<div className="item-people">
<a href="tel:0987414899">098.741.4899</a>
<span>Ms Trang</span>
</div>
</div>
</div>
</div>
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button>Close</button>
</form>
</dialog>
);
};
export default BoxHotLine;

View File

@@ -1,18 +1,60 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { FaMapMarkerAlt, FaBars } from 'react-icons/fa';
import BoxShowroom from '@components/common/BoxShowroom';
import BoxShowroom from '@/components/Common/BoxShowroom';
import BoxHotLine from '../../BoxHotline';
import { TypeCartItem } from '@/types/cart';
import { formatCurrency } from '@/lib/formatPrice';
const HeaderMid: React.FC = () => {
const [cartCount, setCartCount] = useState(() => {
const storedCart = localStorage.getItem('cart');
return storedCart ? JSON.parse(storedCart).length : 0;
});
const [cart, setCart] = useState<TypeCartItem[]>(() => {
const storedCart = localStorage.getItem('cart');
return storedCart ? JSON.parse(storedCart) : [];
});
const [cartQuantity, setCartQuantity] = useState(() => {
return cart.reduce((sum: number, item) => sum + Number(item.in_cart.quantity), 0);
});
const [cartTotal, setCartTotal] = useState(() => {
return cart.reduce((sum: number, item) => sum + Number(item.in_cart.total_price), 0);
});
const [isFixed, setIsFixed] = useState(false);
useEffect(() => {
const handleScroll = () => {
const distanceFromTop = window.scrollY;
if (distanceFromTop > 680) {
setIsFixed(true);
} else {
setIsFixed(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const PopupAddress = () => {
const modal = document.getElementById('boxShowroom') as HTMLDialogElement;
modal?.showModal();
};
const PopupHotLine = () => {
const modal = document.getElementById('boxHotline') as HTMLDialogElement;
modal?.showModal();
};
return (
<div className="header-middle">
<div className={`header-middle ${isFixed ? 'header-fixed' : ''}`}>
<div className="container flex items-center justify-between">
<div className="header-middle-left flex items-center">
<Link href="/">
@@ -63,68 +105,106 @@ const HeaderMid: React.FC = () => {
</div>
<div className="box-tabs-header flex items-center">
<Link href="/buildpc" className="item-tab-header flex-column flex items-center gap-4">
<Link href="/buildpc" className="item-tab-header flex flex-col items-center gap-4">
<p className="icon-item-tab flex items-center justify-center">
<i className="sprite sprite-buildpc-header"></i>
</p>
<span className="font-500">Xây dựng cấu hình</span>
</Link>
<Link
href="javascript:void(0)"
className="item-tab-header flex-column flex items-center gap-4"
<button
onClick={PopupHotLine}
className="item-tab-header flex flex-col items-center gap-4"
>
<p className="icon-item-tab flex items-center justify-center">
<i className="sprite sprite-lienhe-header"></i>
</p>
<span className="font-500">Khách hàng liên hệ</span>
</Link>
</button>
<Link href="/tin-tuc" className="item-tab-header flex-column flex items-center gap-4">
<Link href="/tin-tuc" className="item-tab-header flex flex-col items-center gap-4">
<p className="icon-item-tab flex items-center justify-center">
<i className="sprite sprite-article-header"></i>
</p>
<span className="font-weight-500">Tin tức công nghệ</span>
</Link>
<div id="js-header-cart" className="position-relative">
<Link href="/cart" className="item-tab-header flex-column flex items-center gap-4">
<div id="js-header-cart" className="relative">
<Link href="/cart" className="item-tab-header flex flex-col items-center gap-4">
<p className="icon-item-tab icon-cart-header flex items-center justify-center">
<i className="sprite sprite-cart-header"></i>
<u className="cart-count header-features-cart-amount">1</u>
<u className="cart-count header-features-cart-amount">{cartCount}</u>
</p>
<span className="font-weight-500">Giỏ hàng</span>
</Link>
<div className="cau-noi"></div>
<div className="cart-ttip" id="js-cart-tooltip">
<div className="cart-ttip-item-container"></div>
<div className="cart-ttip-price justify-content-end flex items-center gap-6">
<div className="cart-ttip-item-container">
{cart.map((item, index) => (
<div
className="compare-item js-compare-item flex items-center gap-2"
key={index}
>
<Link className="img-compare" href={item.item_info.productUrl}>
<Image
src={item.item_info.productImage.large}
width={80}
height={80}
alt={item.item_info.productName}
/>
</Link>
<div className="compare-item-text flex-1">
<Link
href={item.item_info.productUrl}
className="name-compare-item mb-10 line-clamp-2"
>
{item.item_info.productName}
</Link>
<div className="header-cart-item-price flex justify-between">
<b>x {item.in_cart.quantity}</b>
<b className="price-compare">
{item.in_cart.price == '0'
? 'Liên Hệ'
: `${formatCurrency(item.in_cart.total_price)} đ`}
</b>
</div>
</div>
</div>
))}
{/* end item */}
</div>
<div className="cart-ttip-price flex items-center justify-end gap-2">
<p>Tổng tiền hàng</p>
<p id="js-header-cart-quantity" className="font-weight-500"></p>
<p id="js-header-cart-total-price" className="font-weight-700"></p>
<p id="js-header-cart-quantity" className="font-[500]">
({cartQuantity} sản phẩm)
</p>
<p id="js-header-cart-total-price" className="font-bold">
{formatCurrency(cartTotal)}đ
</p>
</div>
<Link
href="/cart"
className="cart-ttip-price-button flex items-center justify-center"
>
<p className="font-weight-700">THANH TOÁN NGAY </p>
<p className="font-bold">THANH TOÁN NGAY </p>
</Link>
</div>
</div>
<Link
href="/taikhoan"
className="user-header item-tab-header flex-column flex items-center gap-4"
className="user-header item-tab-header flex flex-col items-center gap-4"
>
<p className="icon-item-tab flex items-center justify-center">
<i className="sprite sprite-account-header"></i>
</p>
<span className="font-weight-500">Tài khoản</span>
<span className="font-[500]">Tài khoản</span>
</Link>
</div>
</div>
</div>
<BoxShowroom />
<BoxHotLine />
</div>
);
};

View File

@@ -0,0 +1,647 @@
import { ProductCommentData } from '@/types/Comment';
export const ListCommentData: ProductCommentData[] = [
{
id: '2434',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyễn Thanh Tùng',
rate: '5',
title: 'Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA 4GB [TẶNG MÀN HÌNH]',
content: 'Cây này treo được bao nhiêu tab giả lập vậy ạ',
files: [],
approved: '1',
post_time: '1766807320',
counter: 1,
new_replies: [
{
id: '1616',
comment_id: '2434',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1766824192',
},
],
},
{
id: '2389',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Lưu Gia Dân ',
rate: '5',
title: 'Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB [TẶNG MÀN HÌNH]',
content: 'Bộ pc tặng màn hình này có ở sài gòn 0? Cụ thể là quận 10 cũ. ',
files: [],
approved: '1',
post_time: '1764736998',
counter: 2,
new_replies: [
{
id: '1579',
comment_id: '2389',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Dạ bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB sẵn hàng tại HCM ạ',
post_time: '1764745059',
},
],
},
{
id: '2377',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Phan Van Manh',
rate: '5',
title: 'Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB [TẶNG MÀN HÌNH]',
content: 'mình có như cầu mua, cần tư vấn thêm',
files: [],
approved: '1',
post_time: '1764322841',
counter: 3,
new_replies: [
{
id: '1571',
comment_id: '2377',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1764383633',
},
],
},
{
id: '2368',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyen Van Phung',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Bộ này em có sẵn màn LG UltraGear 24GS50F-B 24 inch Gắn dc kh ạ',
files: [],
approved: '1',
post_time: '1764167165',
counter: 4,
new_replies: [
{
id: '1563',
comment_id: '2368',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ bộ Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB gắn được ạ',
post_time: '1764211206',
},
],
},
{
id: '2362',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Xuân hữu',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Bộ này chơi TFT FPS CÓ BỊ TỤT KHÔNG SHOP',
files: [],
approved: '1',
post_time: '1763897716',
counter: 5,
new_replies: [
{
id: '1557',
comment_id: '2362',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Dạ bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB chơi TFT thoải mái ạ',
post_time: '1763950132',
},
],
},
{
id: '2341',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'dat phan',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Bộ này dùng để code được kh ạ\n',
files: [],
approved: '1',
post_time: '1763463359',
counter: 6,
new_replies: [
{
id: '1546',
comment_id: '2341',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Dạ code được ạ. Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1763536700',
},
],
},
{
id: '2291',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Tú nè nè',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Có giảm gì không anh em còn đúng 10tr',
files: [],
approved: '1',
post_time: '1762067763',
counter: 7,
new_replies: [
{
id: '1506',
comment_id: '2291',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ bộ PC có chương trình tặng màn hình ạ.',
post_time: '1762136212',
},
],
},
{
id: '2282',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyễn Văn Sáu',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content:
'Shop ơi mình chưa lắp pc bao giờ shop có hỗ trợ lắp đặt ở Phú Thọ k ạ, mình ở Phú Thọ',
files: [],
approved: '1',
post_time: '1761828423',
counter: 8,
new_replies: [
{
id: '1499',
comment_id: '2282',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1761875528',
},
],
},
{
id: '2260',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '0',
is_user_admin: '0',
user_avatar: '',
user_name: 'Trần Huy',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content:
'Mình đặt hàng từ lúc giá là 10tr9 mà không được liên hệ tư vấn, đến nay lên 11tr4 rồi thì giá lúc đặt hàng vẫn áp dụng chứ ?',
files: [],
approved: '0',
post_time: '1761133383',
counter: 9,
new_replies: [],
},
{
id: '2253',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Minh Hải Nguyễn',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Shop ơi bộ này kèm cả case và màn đúng không ạ?\n',
files: [],
approved: '1',
post_time: '1760971409',
counter: 10,
new_replies: [
{
id: '1473',
comment_id: '2253',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ bộ PC đã kèm vỏ ạ và được tặng màn hình ạ',
post_time: '1761011806',
},
],
},
{
id: '2222',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'thái 0507',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'bộ này có màn k ạ\n',
files: [],
approved: '1',
post_time: '1759637210',
counter: 11,
new_replies: [
{
id: '1449',
comment_id: '2222',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ bộ này có tặng màn hình ạ',
post_time: '1759715368',
},
],
},
{
id: '2161',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Hải Đăng',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Shop có quyẹt thẻ tín dụng tốn phí ko nhỉ',
files: [],
approved: '1',
post_time: '1757032318',
counter: 12,
new_replies: [
{
id: '1393',
comment_id: '2161',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1757038972',
},
],
},
{
id: '2096',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Bui Van Giang',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'shop co mua dc phu kien roi ko',
files: [],
approved: '1',
post_time: '1755095868',
counter: 13,
new_replies: [
{
id: '1336',
comment_id: '2096',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1755135208',
},
],
},
{
id: '1991',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'phạm long',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content:
'bên shop miễn phí lắp đặt nhà ở hà nội ko shop hay tính bao nhiêu phí nữa như là vânhj chuyển ',
files: [],
approved: '1',
post_time: '1751287702',
counter: 14,
new_replies: [
{
id: '1247',
comment_id: '1991',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Chào bạn, shop miễn phí lắp đặt bán kính 20km nội thành ạ',
post_time: '1751334482',
},
],
},
{
id: '1978',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyễn Xuân Hùng ',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'Giá 10tr590 là kèm màn hình luôn hả shop ',
files: [],
approved: '1',
post_time: '1751003230',
counter: 15,
new_replies: [
{
id: '1237',
comment_id: '1978',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Dạ đúng rồi ạ. Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1751006270',
},
],
},
{
id: '1977',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyễn Xuân Hùng ',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content:
'Mình mua kèm card wifi thì shop gắn vào pc luôn hay là mình nhận được hàng rồi tự gắn vậy shop',
files: [],
approved: '1',
post_time: '1750987766',
counter: 16,
new_replies: [
{
id: '1236',
comment_id: '1977',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Chào bạn, bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1750988487',
},
],
},
{
id: '1974',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Nguyễn hải Dăng ',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'vga chuyển từ rx 6500 xt sang 1660ti thì bộ này nhiêu v shop ',
files: [],
approved: '1',
post_time: '1750897125',
counter: 17,
new_replies: [
{
id: '1231',
comment_id: '1974',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1750902650',
},
],
},
{
id: '1972',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'La Thanh Ki ',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB',
content: 'bộ này chuyển từ rx 6500xt sang 1660super thì nhiêu shop ',
files: [],
approved: '1',
post_time: '1750862925',
counter: 18,
new_replies: [
{
id: '1232',
comment_id: '1972',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content:
'Bạn vui lòng liên hệ 0828.333.363, nhân viên kinh doanh bên mình sẽ tư vấn cụ thể nhé',
post_time: '1750902659',
},
],
},
{
id: '1941',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Minh Tú',
rate: '5',
title: '[TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA 4GB',
content: 'Màn tặng hả shop',
files: [],
approved: '1',
post_time: '1749125191',
counter: 19,
new_replies: [
{
id: '1205',
comment_id: '1941',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ, mua PC được tặng màn hình ạ',
post_time: '1749184735',
},
],
},
{
id: '1757',
item_type: 'product',
item_id: '25404',
people_like_count: '0',
people_dislike_count: '0',
reply_count: '1',
is_user_admin: '0',
user_avatar: '',
user_name: 'Hoang ky',
rate: '5',
title: 'BỘ PC GAMING AMD Ryzen 5 5500/ RAM 16GB/ VGA 4G ',
content: 'ổ có cài win sẵn không ạ',
files: [],
approved: '1',
post_time: '1741874131',
counter: 20,
new_replies: [
{
id: '1113',
comment_id: '1757',
user_avatar: '0',
user_name: 'Trần Mạnh',
is_user_admin: '1',
people_like_count: '0',
approved: '0',
people_dislike_count: '0',
content: 'Dạ có ạ',
post_time: '1748338266',
},
],
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,277 @@
import { ProductReviewData } from '@/types/Review'
export const ListReviewData: ProductReviewData[] = [
{
"id": "758",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Đỗ Văn Bính",
"rate": "5",
"title": "Đánh giá sản phẩm Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA 4GB [TẶNG MÀN HÌNH]",
"content": "đến tận showroom ở HCM mua cho yên tâm, được unbox đồ nhé mọi người. Cảm quan ban đầu thấy showroom rất to , sạch, nhân viên nhiệt tình, giá tốt, quà tặng màn hình là tặng thật,",
"files": [
{
"id": "375",
"title": "",
"file_path": "https://nguyencongpc.vn/media/user_upload/19-12-2025/MzK0dsw3o8EaSfOUnuhn/2114970452708823840.jpg",
"width": "600",
"height": "338",
"approved": "0",
"create_time": "1766118957"
}
],
"approved": "1",
"post_time": "1766119040",
"counter": 1,
"new_replies": []
},
{
"id": "741",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "1",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Duy trường",
"rate": "5",
"title": "Đánh giá sản phẩm Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB [TẶNG MÀN HÌNH]",
"content": "Ai mua rồi cho mình hỏi valorant dc bao nhiêu fps ạ",
"files": [],
"approved": "1",
"post_time": "1765170972",
"counter": 2,
"new_replies": [
{
"id": "51",
"comment_id": "741",
"user_avatar": "0",
"user_name": "Trần Mạnh",
"is_user_admin": "1",
"people_like_count": "0",
"approved": "0",
"people_dislike_count": "0",
"content": "Dạ khoảng 170-180fps ở 1080P ạ",
"post_time": "1765277614"
}
]
},
{
"id": "707",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Phong Nguyễn",
"rate": "5",
"title": "Đánh giá sản phẩm Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB [TẶNG MÀN HÌNH]",
"content": "giá tốt, con màn trên bàn là con được tặng luôn nha",
"files": [
{
"id": "328",
"title": "",
"file_path": "https://nguyencongpc.vn/media/user_upload/01-12-2025/FC5YOYSvCSQtAD6ukdTy/f9a2f40c6fb2e3ecbaa3.jpg",
"width": "500",
"height": "646",
"approved": "0",
"create_time": "1764586266"
}
],
"approved": "1",
"post_time": "1764586309",
"counter": 3,
"new_replies": []
},
{
"id": "690",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Quang Bình",
"rate": "5",
"title": "Đánh giá sản phẩm Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB [TẶNG MÀN HÌNH]",
"content": "Ryzen 5 5500 chơi game làm việc ổn, giá tốt và có tặng màn hình như thông báo nhé , 5 SAO",
"files": [
{
"id": "311",
"title": "",
"file_path": "https://nguyencongpc.vn/media/user_upload/29-11-2025/PiKzIDmPTSvb0ngJbVAV/e1b569fd265aaa04f34b-2.jpg",
"width": "500",
"height": "375",
"approved": "0",
"create_time": "1764387131"
}
],
"approved": "1",
"post_time": "1764387192",
"counter": 4,
"new_replies": []
},
{
"id": "674",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Đình Ấn",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "Pc có sử dụng wifi được không shop, hay cần phải gắn dây mạng",
"files": [],
"approved": "1",
"post_time": "1764170309",
"counter": 5,
"new_replies": []
},
{
"id": "673",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Nguyen Van Thinh",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "Bỏ màn giá sao ạ",
"files": [],
"approved": "1",
"post_time": "1764164501",
"counter": 6,
"new_replies": []
},
{
"id": "672",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "0",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Nguyen Van Thinh",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "Bỏ màn giá sao ạ",
"files": [],
"approved": "1",
"post_time": "1764164500",
"counter": 7,
"new_replies": []
},
{
"id": "653",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "1",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Phụng ",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "Dùng loại tai nghe kh chụp tai dc kh ạ ",
"files": [],
"approved": "1",
"post_time": "1763484666",
"counter": 8,
"new_replies": [
{
"id": "46",
"comment_id": "653",
"user_avatar": "0",
"user_name": "Trần Mạnh",
"is_user_admin": "1",
"people_like_count": "0",
"approved": "0",
"people_dislike_count": "0",
"content": "dạ được ạ",
"post_time": "1763541896"
}
]
},
{
"id": "626",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "1",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "Văn đạt",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "Bộ này chơi dc gta5 k shop",
"files": [],
"approved": "1",
"post_time": "1759751921",
"counter": 9,
"new_replies": [
{
"id": "44",
"comment_id": "626",
"user_avatar": "0",
"user_name": "Trần Mạnh",
"is_user_admin": "1",
"people_like_count": "0",
"approved": "0",
"people_dislike_count": "0",
"content": "dạ được ạ, nhưng setting thấp ạ",
"post_time": "1759974716"
}
]
},
{
"id": "625",
"item_type": "product",
"item_id": "25404",
"people_like_count": "0",
"people_dislike_count": "0",
"reply_count": "1",
"is_user_admin": "0",
"user_avatar": "",
"user_name": "thái 0507",
"rate": "5",
"title": "Đánh giá sản phẩm [TẶNG MÀN HÌNH] Bộ PC Gaming AMD Ryzen 5 5500, RAM 16GB, VGA RX 6500 XT 4GB",
"content": "shop ơi bộ này có kèm màn k ạ\n",
"files": [],
"approved": "1",
"post_time": "1759626317",
"counter": 10,
"new_replies": [
{
"id": "43",
"comment_id": "625",
"user_avatar": "0",
"user_name": "Trần Mạnh",
"is_user_admin": "1",
"people_like_count": "0",
"approved": "0",
"people_dislike_count": "0",
"content": "Dạ bộ này được tặng màn hình ạ",
"post_time": "1759736954"
}
]
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,264 @@
import { ListArticle } from '@/types/article/TypeListArticle'
export const DataListArticleVideo: ListArticle = [
{
"id": 4185,
"title": "Chuyện RAM ĐẮT - Góc nhìn mà anh em chưa thấy",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "28-11-2025, 11:51 am",
"createBy": "53",
"lastUpdate": "28-11-2025, 11:51 am",
"lastUpdateBy": "53",
"visit": 8,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=rH9Aq_2yZEc",
"author": "Trần Mạnh",
"counter": 1,
"url": "/chuyen-ram-dat-goc-nhin-ma-anh-em-chua-thay",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4185---efwegweg.jpg",
"original": "https://nguyencongpc.vn/media/news/4185---efwegweg.jpg"
}
},
{
"id": 4184,
"title": "Build PC GAMING tầm giá 20 Triệu trong mùa BÃO RAM - Cũng KHOAI phết",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "28-11-2025, 11:49 am",
"createBy": "53",
"lastUpdate": "28-11-2025, 11:49 am",
"lastUpdateBy": "53",
"visit": 32,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=c-JQPclPXmg",
"author": "Trần Mạnh",
"counter": 2,
"url": "/build-pc-gaming-tam-gia-20-trieu-trong-mua-bao-ram-cung-khoai-phet",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4184-maxresdefault.jpg",
"original": "https://nguyencongpc.vn/media/news/4184-maxresdefault.jpg"
}
},
{
"id": 4171,
"title": "Điểm dừng cho PC GAMING - Nhiều tiền thì cũng PHÍ",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "10-11-2025, 2:41 pm",
"createBy": "53",
"lastUpdate": "10-11-2025, 2:41 pm",
"lastUpdateBy": "53",
"visit": 11,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=xUpMSpaa_H0",
"author": "Trần Mạnh",
"counter": 3,
"url": "/diem-dung-cho-pc-gaming-nhieu-tien-thi-cung-phi",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4171-dvsdfgrsdf.jpg",
"original": "https://nguyencongpc.vn/media/news/4171-dvsdfgrsdf.jpg"
}
},
{
"id": 3683,
"title": "Bộ PC KHỦNG BỐ tới đâu mà đích thân Chủ Tịch MaxHome phải tự đi build ???",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "12-03-2025, 9:59 am",
"createBy": "53",
"lastUpdate": "12-03-2025, 9:59 am",
"lastUpdateBy": "53",
"visit": 64,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=Ir9zlznA9ms",
"author": "Trần Mạnh",
"counter": 4,
"url": "/bo-pc-khung-bo-toi-dau-ma-dich-than-chu-tich-maxhome-phai-tu-di-build",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-3683-tymyumyj.jpg",
"original": "https://nguyencongpc.vn/media/news/3683-tymyumyj.jpg"
}
},
{
"id": 4107,
"title": "Intel ĐẮT quá nên BUILD PC với AMD chỉ 17 TRIỆU mà chiến ALL GAME",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "04-10-2025, 5:39 pm",
"createBy": "53",
"lastUpdate": "04-10-2025, 5:40 pm",
"lastUpdateBy": "53",
"visit": 8,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=DBuud_Lwt6w",
"author": "Trần Mạnh",
"counter": 5,
"url": "/intel-dat-qua-nen-build-pc-voi-amd-chi-17-trieu-ma-chien-all-game",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4107---gherthert.jpg",
"original": "https://nguyencongpc.vn/media/news/4107---gherthert.jpg"
}
},
{
"id": 4079,
"title": "Tôi thấy chán PC HIỆU NĂNG/GIÁ THÀNH sau khi thấy bộ PC này",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "20-09-2025, 10:42 am",
"createBy": "53",
"lastUpdate": "20-09-2025, 10:42 am",
"lastUpdateBy": "53",
"visit": 29,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=ceT_nSB1JCA",
"author": "Trần Mạnh",
"counter": 6,
"url": "/toi-thay-chan-pc-hieu-nang-gia-thanh-sau-khi-thay-bo-pc-nay",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4079-ewgergherth.jpg",
"original": "https://nguyencongpc.vn/media/news/4079-ewgergherth.jpg"
}
},
{
"id": 4004,
"title": "Sinh Viên ĐỒ HOẠ lên cấu hình PC nào dưới 20 TRIỆU trong 2025",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "15-08-2025, 2:04 pm",
"createBy": "53",
"lastUpdate": "15-08-2025, 2:04 pm",
"lastUpdateBy": "53",
"visit": 90,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=k6rIzVmU9bA",
"author": "Trần Mạnh",
"counter": 7,
"url": "/sinh-vien-do-hoa-len-cau-hinh-pc-nao-duoi-20-trieu-trong-2025",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4004-dhtrhj.jpg",
"original": "https://nguyencongpc.vn/media/news/4004-dhtrhj.jpg"
}
},
{
"id": 3951,
"title": "Cấu hình PC 10 Triệu cả Màn hình - Test GAME AAA vẫn OK",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "19-07-2025, 4:57 pm",
"createBy": "53",
"lastUpdate": "19-07-2025, 4:57 pm",
"lastUpdateBy": "53",
"visit": 47,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=QCQwdLcosQc",
"author": "Trần Mạnh",
"counter": 8,
"url": "/cau-hinh-pc-10-trieu-ca-man-hinh-test-game-aaa-van-ok",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-3951-dfbeadbeat.jpg",
"original": "https://nguyencongpc.vn/media/news/3951-dfbeadbeat.jpg"
}
},
{
"id": 3950,
"title": "Tại sao mình ít làm video CORE ULTRA - Có đáng không 40 Triệu cho Ultra 7 265K + RTX 5070",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "19-07-2025, 4:56 pm",
"createBy": "53",
"lastUpdate": "19-07-2025, 5:00 pm",
"lastUpdateBy": "53",
"visit": 52,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=Y6PBwYe5My0",
"author": "Trần Mạnh",
"counter": 9,
"url": "/tai-sao-minh-it-lam-video-core-ultra-co-dang-khong-40-trieu-cho-ultra-7-265k-rtx-5070",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-3950-maxresdefault.jpg",
"original": "https://nguyencongpc.vn/media/news/3950-maxresdefault.jpg"
}
},
{
"id": 3949,
"title": "Cấu hình PC PHỔ BIẾN nhất THẾ GIỚI gaming - Cũng rẻ phết",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "19-07-2025, 4:54 pm",
"createBy": "53",
"lastUpdate": "19-07-2025, 4:54 pm",
"lastUpdateBy": "53",
"visit": 28,
"is_featured": 0,
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "https://www.youtube.com/watch?v=KXfA10koGDk",
"author": "Trần Mạnh",
"counter": 10,
"url": "/cau-hinh-pc-pho-bien-nhat-the-gioi-gaming-cung-re-phet",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-3949----herthrtn.jpg",
"original": "https://nguyencongpc.vn/media/news/3949----herthrtn.jpg"
}
}
]

View File

@@ -0,0 +1,274 @@
import { ListArticle } from '@/types/article/TypeListArticle'
export const DataListArticleNews: ListArticle = [
{
"id": 4200,
"title": "Top PC 15 triệu tối ưu hiệu năng nhất trong mùa bão giá RAM",
"extend": {
"pixel_code": ""
},
"summary": "Chỉ với 15 triệu đồng, người dùng đã có thể sở hữu một bộ máy tính tối ưu hiệu năng cho nhu cầu học tập, làm việc và giải trí. Nguyễn Công PC mang đến nhiều cấu hình cân bằng giữa sức mạnh và giá trị, đảm bảo hoạt động mượt mà trong mọi tác vụ. Đây là lựa chọn lý tưởng cho những ai muốn đầu tư một hệ thống mạnh mẽ với chi phí hợp lý.",
"createDate": "10-12-2025, 5:44 pm",
"createBy": "75",
"lastUpdate": "24-12-2025, 11:11 am",
"lastUpdateBy": "75",
"visit": 198,
"is_featured": 0,
"lastUpdateByUser": "Diệu Linh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Diệu Linh",
"counter": 7,
"url": "/top-pc-15-trieu-toi-uu-hieu-nang-cho-gaming-va-lam-viec",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4200-chi-voi-15-trieu-ban-da-co-ngay-mot-bo-pc-chat-luong-dam-bao-hieu-nang1.jpg",
"original": "https://nguyencongpc.vn/media/news/4200-chi-voi-15-trieu-ban-da-co-ngay-mot-bo-pc-chat-luong-dam-bao-hieu-nang1.jpg"
}
},
{
"id": 4195,
"title": "Cách nhận chứng chỉ Google Gemini Educator làm đẹp CV của bạn ngay hôm nay!",
"extend": {
"pixel_code": ""
},
"summary": "Chứng chỉ Google Gemini Educator giúp bạn khẳng định kỹ năng sử dụng AI trong giáo dục và công nghệ. Việc sở hữu chứng chỉ này không chỉ tăng tính chuyên nghiệp cho CV mà còn mở ra nhiều cơ hội nghề nghiệp mới. Bài viết sẽ hướng dẫn bạn cách đăng ký, học và nhận chứng chỉ nhanh chóng nhất.",
"createDate": "08-12-2025, 11:26 am",
"createBy": "75",
"lastUpdate": "08-12-2025, 12:07 pm",
"lastUpdateBy": "75",
"visit": 3440,
"is_featured": 0,
"lastUpdateByUser": "Diệu Linh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Diệu Linh",
"counter": 4,
"url": "/cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-2025",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4195-cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-20251.jpg",
"original": "https://nguyencongpc.vn/media/news/4195-cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-20251.jpg"
}
},
{
"id": 2722,
"title": "Top 100+ cấu hình PC Gaming giá tốt nhất năm 2025",
"extend": {
"pixel_code": ""
},
"summary": "Trong bài viết, Nguyễn Công PC đã tổng hợp hơn 100 cấu hình PC gaming tối ưu nhất năm 2025, phù hợp với nhiều mức ngân sách từ phổ thông đến cao cấp. Mỗi cấu hình cân bằng giữa hiệu năng và giá thành, đáp ứng nhu cầu chơi game mượt mà, đồ họa sắc nét và khả năng nâng cấp linh hoạt trong tương lai.\r\n\r\n\r\n",
"createDate": "16-01-2024, 10:52 am",
"createBy": "50",
"lastUpdate": "06-12-2025, 4:30 pm",
"lastUpdateBy": "53",
"visit": 37078,
"is_featured": 0,
"lastUpdateByUser": "Trần Mạnh",
"article_time": "07-11-2025, 9:00 am",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Trần Mạnh",
"counter": 2,
"url": "/top-100-cau-hinh-pc-gaming-gia-tot-nhat",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-2722-pc-gaming.jpg",
"original": "https://nguyencongpc.vn/media/news/2722-pc-gaming.jpg"
}
},
{
"id": 2718,
"title": "Top 50 cấu hình PC đồ họa giá tốt nhất hiện nay",
"extend": {
"pixel_code": ""
},
"summary": "Với đà phát triển của truyền thông, công nghệ số, kỹ thuật số,... Cần rất nhiều công cụ để hỗ trợ cho công việc, làm việc của bạn. Sức mạnh ngành chuyền thông nói riêng cũng như công nghệ nói chung càng ngày càng phát triển mạnh mẽ, vượt trội, chính vì để hỗ trợ cho việc xây dựng các bộ (PC Render) làm việc cũng như giải trí đang là nhu cầu lơn trên thị trường hiện nay.\r\n\r\n",
"createDate": "15-01-2024, 1:39 pm",
"createBy": "50",
"lastUpdate": "24-11-2025, 10:23 am",
"lastUpdateBy": "74",
"visit": 24523,
"is_featured": 0,
"lastUpdateByUser": "Anh Tuấn",
"article_time": "05-11-2025, 10:00 am",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Anh Tuấn",
"counter": 1,
"url": "/top-cau-hinh-do-hoa-gia-tot-nhat",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-2718-pc-do-hoa.jpg",
"original": "https://nguyencongpc.vn/media/news/2718-pc-do-hoa.jpg"
}
},
{
"id": 4203,
"title": "NGUYỄN CÔNG PC - NHÀ TÀI TRỢ KIM CƯƠNG CHÀO TÂN SINH VIÊN ĐẠI HỌC KIẾN TRÚC HÀ NỘI 2025",
"extend": {
"pixel_code": ""
},
"summary": "Máy tính Nguyễn Công tiếp tục khẳng định vị thế là đối tác tin cậy hàng đầu khi vinh dự trở thành Nhà Tài Trợ Kim Cương liên tục trong 7 năm (2019 2025) cho chương trình Chào Tân Sinh Viên của Đại học Kiến Trúc Hà Nội (HAU).",
"createDate": "15-12-2025, 10:09 am",
"createBy": "74",
"lastUpdate": "15-12-2025, 4:00 pm",
"lastUpdateBy": "53",
"visit": 63,
"is_featured": 0,
"lastUpdateByUser": "Trần Mạnh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Trần Mạnh",
"counter": 8,
"url": "/nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4203-nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025-06.jpg",
"original": "https://nguyencongpc.vn/media/news/4203-nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025-06.jpg"
}
},
{
"id": 4199,
"title": "Đại chiến đồ họa: Canva và Photoshop: Ai là \"Vua\" thiết kế hiện nay",
"extend": {
"pixel_code": ""
},
"summary": "Canva và Photoshop đang là hai nền tảng thiết kế phổ biến nhất, mỗi công cụ sở hữu những ưu nhược điểm riêng. Trong khi Canva mang đến sự tiện lợi và tốc độ, Photoshop lại vượt trội về sức mạnh xử lý và khả năng sáng tạo chuyên sâu. Cuộc đối đầu này giúp người dùng lựa chọn đúng công cụ phù hợp với nhu cầu thiết kế của mình.",
"createDate": "09-12-2025, 6:56 pm",
"createBy": "75",
"lastUpdate": "11-12-2025, 2:21 pm",
"lastUpdateBy": "75",
"visit": 83,
"is_featured": 0,
"lastUpdateByUser": "Diệu Linh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Diệu Linh",
"counter": 6,
"url": "/dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4199-dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay5.jpg",
"original": "https://nguyencongpc.vn/media/news/4199-dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay5.jpg"
}
},
{
"id": 4197,
"title": "Người dùng nên nâng cấp Windows 11 hiện đại hay tiếp tục sử dụng Windows 10 ổn định? ",
"extend": {
"pixel_code": ""
},
"summary": "",
"createDate": "09-12-2025, 11:03 am",
"createBy": "75",
"lastUpdate": "09-12-2025, 5:23 pm",
"lastUpdateBy": "75",
"visit": 108,
"is_featured": 0,
"lastUpdateByUser": "Diệu Linh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Diệu Linh",
"counter": 5,
"url": "/nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4197-nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh2.jpg",
"original": "https://nguyencongpc.vn/media/news/4197-nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh2.jpg"
}
},
{
"id": 3954,
"title": "Hướng Dẫn Các Bước Cài Đặt Plugin Sketch Up Nhanh Chóng, Đơn Giản Nhất",
"extend": {
"pixel_code": ""
},
"summary": "Theo dõi các hướng dẫn chi tiết cách cài đặt plugin cho phần mềm SketchUp cùng Nguyễn Công PC để giúp người dùng mở rộng chức năng, tiết kiệm thời gian thiết kế và nâng cao hiệu suất làm việc. ",
"createDate": "21-07-2025, 10:39 am",
"createBy": "75",
"lastUpdate": "22-07-2025, 9:06 am",
"lastUpdateBy": "75",
"visit": 6972,
"is_featured": 0,
"lastUpdateByUser": "Diệu Linh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Diệu Linh",
"counter": 3,
"url": "/huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-3954-huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat10.jpg",
"original": "https://nguyencongpc.vn/media/news/3954-huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat10.jpg"
}
},
{
"id": 4212,
"title": "Trải nghiệm Photoshop 2026: Tính năng AI đỉnh cao và Cách cài đặt nhanh chóng",
"extend": {
"pixel_code": ""
},
"summary": "Phiên bản Photoshop 2026 tập trung vào việc ứng dụng AI để rút ngắn thời gian chỉnh sửa và nâng cao độ chính xác. Giao diện được tối ưu giúp người dùng thao tác nhanh hơn trên nhiều thiết bị cấu hình khác nhau. Nhờ đó, cả designer chuyên nghiệp lẫn người mới đều có thể khai thác tối đa sức mạnh phần mềm.",
"createDate": "18-12-2025, 3:20 pm",
"createBy": "75",
"lastUpdate": "24-12-2025, 4:31 pm",
"lastUpdateBy": "53",
"visit": 250,
"is_featured": 0,
"lastUpdateByUser": "Trần Mạnh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Trần Mạnh",
"counter": 9,
"url": "/trai-nghiem-photoshop-2026-tinh-nang-ai-dinh-cao-va-cach-cai-dat-nhanh-chong",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4212-z7359296182053_5ab9b88e01d2b87a466f12e021064a29.jpg",
"original": "https://nguyencongpc.vn/media/news/4212-z7359296182053_5ab9b88e01d2b87a466f12e021064a29.jpg"
}
},
{
"id": 4213,
"title": "Top 30+ Hình Nền 2K 4K Tết Nguyên Đán Bính Ngọ 2026 Cực Hot ",
"extend": {
"pixel_code": ""
},
"summary": "Bài viết giới thiệu bộ sưu tập hơn 30 hình nền Tết Nguyên Đán Bính Ngọ 2026 với độ phân giải 2K và 4K sắc nét. Nội dung tập trung vào các chủ đề truyền thống như hoa mai, hoa đào, linh vật Ngọ và không khí xuân rộn ràng. Đây là lựa chọn lý tưởng để trang trí màn hình PC, laptop và điện thoại dịp đầu năm mới.",
"createDate": "19-12-2025, 11:46 am",
"createBy": "75",
"lastUpdate": "24-12-2025, 4:30 pm",
"lastUpdateBy": "53",
"visit": 169,
"is_featured": 0,
"lastUpdateByUser": "Trần Mạnh",
"article_time": "",
"review_rate": 0,
"review_count": 0,
"video_code": "",
"external_url": "",
"author": "Trần Mạnh",
"counter": 10,
"url": "/top-30-hinh-nen-tet-2026-cuc-hot-cho-nguoi-dung",
"image": {
"thum": "https://nguyencongpc.vn/media/news/120-4213-top-30-hinh-nen-tet-2026-cuc-hot-cho-nguoi-dung18.jpg",
"original": "https://nguyencongpc.vn/media/news/4213-top-30-hinh-nen-tet-2026-cuc-hot-cho-nguoi-dung18.jpg"
}
}
]

View File

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

View File

@@ -0,0 +1,114 @@
export const category_config = [
{
id: 277,
name: 'CPU - B\u1ed9 Vi X\u1eed L\u00fd',
},
{
id: 278,
name: 'Main - Bo M\u1ea1ch Ch\u1ee7',
},
{
id: 283,
name: 'RAM - B\u1ed9 Nh\u1edb Trong',
},
{
id: 3274,
name: '\u1ed4 C\u1ee9ng SSD 1',
},
{
id: 3644,
name: '\u1ed4 C\u1ee9ng SSD 2',
},
{
id: 3273,
name: '\u1ed4 C\u1ee9ng HDD',
},
{
id: 279,
name: 'VGA - Card M\u00e0n H\u00ecnh',
},
{
id: 282,
name: 'PSU - Ngu\u1ed3n M\u00e1y T\u00ednh',
},
{
id: 280,
name: 'Case - V\u1ecf M\u00e1y T\u00ednh',
},
{
id: 3270,
name: 'T\u1ea3n Nhi\u1ec7t Kh\u00ed',
},
{
id: 3269,
name: 'T\u1ea3n Nhi\u1ec7t N\u01b0\u1edbc AIO',
},
{
id: 3630,
name: 'T\u1ea3n Nhi\u1ec7t N\u01b0\u1edbc Custom',
},
{
id: 3271,
name: 'Fan T\u1ea3n Nhi\u1ec7t',
},
{
id: 281,
name: 'Monitor - M\u00e0n H\u00ecnh',
},
{
id: 3705,
name: 'Monitor - M\u00e0n H\u00ecnh 2',
},
{
id: 1235,
name: 'B\u00e0n Ph\u00edm',
},
{
id: 1147,
name: 'Mouse - Chu\u1ed9t',
},
{
id: 1118,
name: 'Pad - B\u00e0n Di Chu\u1ed9t',
},
{
id: 3309,
name: 'Tai Nghe',
},
{
id: 3308,
name: 'Loa',
},
{
id: 3307,
name: 'Gh\u1ebf Gaming',
},
{
id: 3411,
name: 'B\u00e0n Gaming',
},
{
id: 3287,
name: 'Webcam',
},
{
id: 3341,
name: 'Microphones',
},
{
id: 3413,
name: 'Thi\u1ebft B\u1ecb Studio, Stream',
},
{
id: 1751,
name: 'Thi\u1ebft B\u1ecb M\u1ea1ng',
},
{
id: 3598,
name: 'Gi\u00e1 treo m\u00e0n h\u00ecnh',
},
{
id: 3437,
name: 'Ph\u1ea7n m\u1ec1m',
},
];

View File

View File

@@ -1,6 +1,6 @@
import { TypeListProductDeal } from '@/types';
export const productDealData: TypeListProductDeal = [
export const ListDealData: TypeListProductDeal = [
{
id: '565',
pro_id: '25404',
@@ -11,7 +11,7 @@ export const productDealData: 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 productDealData: 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 productDealData: 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 one or more lines are too long

File diff suppressed because it is too large Load Diff

29269
src/data/producthot/index.ts Normal file

File diff suppressed because it is too large Load Diff

17
src/hooks/useFancybox.ts Normal file
View File

@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';
import { type FancyboxOptions, Fancybox } from '@fancyapps/ui/dist/fancybox/';
import '@fancyapps/ui/dist/fancybox/fancybox.css';
export default function useFancybox(options: Partial<FancyboxOptions> = {}) {
const [root, setRoot] = useState<HTMLElement | null>(null);
useEffect(() => {
if (root) {
Fancybox.bind(root, '[data-fancybox]', options);
return () => Fancybox.unbind(root);
}
}, [root, options]);
return [setRoot];
}

View File

@@ -0,0 +1,69 @@
import { TypeCartItem } from '@/types/cart';
// data
import { productData } from '@/data/ListProduct';
export const addToCart = (productId: string | number) => {
// Lấy giỏ hàng hiện tại từ localStorage
const cart: TypeCartItem[] = JSON.parse(localStorage.getItem('cart') || '[]');
console.log('chay tiếp');
const product = productData.find((p) => p.productId == productId);
if (!product) return;
// Kiểm tra sản phẩm đã có trong giỏ chưa
const existing = cart.find((item) => item.item_info.id == productId);
if (existing) {
// Nếu có rồi thì tăng số lượng
existing.in_cart.quantity = (parseInt(existing.in_cart.quantity) + 1).toString();
existing.in_cart.total_price =
Number(existing.in_cart.quantity) * Number(existing.in_cart.price);
} else {
// Nếu chưa có thì thêm mới
const cartItem = {
_id: `product-${product.id}-0`,
item_type: 'product',
item_id: `${product.id}-0`,
item_info: {
id: product.productId,
priceUnit: product.priceUnit,
marketPrice: product.marketPrice,
hasVAT: product.hasVAT,
weight: product.weight,
price: product.price,
currency: product.currency,
bulk_price: product.bulk_price,
configurable: product.configurable,
productName: product.productName,
productImage: product.productImage,
productUrl: product.productUrl,
brand: product.brand,
productSKU: product.productSKU,
quantity: product.quantity,
addon: product.addon,
warranty: product.warranty,
variants: product.variants,
variant_option: product.variant_option,
extend: product.extend,
categories: product.categories,
specialOffer: product.specialOffer,
specialOfferGroup: product.specialOfferGroup,
sale_rules: product.sale_rules,
},
in_cart: {
quantity: '1',
buyer_note: '',
price: product.price,
total_price: Number(product.quantity) * Number(product.price),
weight: '0',
total_weight: '0',
},
};
cart.push(cartItem);
}
// Lưu lại vào localStorage
localStorage.setItem('cart', JSON.stringify(cart));
};

Some files were not shown because too many files have changed in this diff Show More