Compare commits
13 Commits
a3ac2e27d6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a8e30f32a0 | |||
| 9486dabdb0 | |||
| 28a252f7d7 | |||
| aae8e26135 | |||
| 15240ff81f | |||
| 9fa4b50b68 | |||
| bf063f244c | |||
| 1bb5ad52ed | |||
| 71089d1eef | |||
| 7606157d26 | |||
| e2063bce4c | |||
| 1805ff8674 | |||
| 7fc0be90b8 |
71
package-lock.json
generated
71
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
33
src/app/buildpc/BoxListAccessory/index.tsx
Normal file
33
src/app/buildpc/BoxListAccessory/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
49
src/app/buildpc/BtnAction/index.tsx
Normal file
49
src/app/buildpc/BtnAction/index.tsx
Normal 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;
|
||||
38
src/app/buildpc/Slider/index.tsx
Normal file
38
src/app/buildpc/Slider/index.tsx
Normal 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
117
src/app/buildpc/page.tsx
Normal 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
16
src/app/cart/page.tsx
Normal 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
51
src/app/deal/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
55
src/app/pages/404/index.tsx
Normal file
55
src/app/pages/404/index.tsx
Normal 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;
|
||||
19
src/app/pages/Article/ArticleTopLeft/index.tsx
Normal file
19
src/app/pages/Article/ArticleTopLeft/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
32
src/app/pages/Article/ArticleTopRight/index.tsx
Normal file
32
src/app/pages/Article/ArticleTopRight/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
116
src/app/pages/Article/CategoryPage/index.tsx
Normal file
116
src/app/pages/Article/CategoryPage/index.tsx
Normal 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;
|
||||
113
src/app/pages/Article/DetailPage/TocBox/index.tsx
Normal file
113
src/app/pages/Article/DetailPage/TocBox/index.tsx
Normal 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 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
109
src/app/pages/Article/DetailPage/index.tsx
Normal file
109
src/app/pages/Article/DetailPage/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
123
src/app/pages/Article/HomeArticlePage/BoxVideoArticle/index.tsx
Normal file
123
src/app/pages/Article/HomeArticlePage/BoxVideoArticle/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
49
src/app/pages/Article/HomeArticlePage/index.tsx
Normal file
49
src/app/pages/Article/HomeArticlePage/index.tsx
Normal 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;
|
||||
@@ -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 = () => {
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
@@ -1,10 +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';
|
||||
|
||||
const BoxProductDeal: React.FC = () => {
|
||||
return (
|
||||
@@ -15,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
|
||||
@@ -29,7 +33,13 @@ const BoxProductDeal: React.FC = () => {
|
||||
slidesPerView={6}
|
||||
loop={true}
|
||||
navigation={true}
|
||||
></Swiper>
|
||||
>
|
||||
{ListDealData.map((Item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<ProductItem item={Item} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -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 (
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
156
src/app/pages/Product/ProductDetail/BoxInfoRight/index.tsx
Normal file
156
src/app/pages/Product/ProductDetail/BoxInfoRight/index.tsx
Normal 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">
|
||||
Mã 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Ồ SƠ</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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
114
src/app/pages/Product/ProductDetail/ComboSet/index.tsx
Normal file
114
src/app/pages/Product/ProductDetail/ComboSet/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
60
src/app/pages/Product/ProductDetail/ImageProduct/index.tsx
Normal file
60
src/app/pages/Product/ProductDetail/ImageProduct/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
32
src/app/pages/Product/ProductDetail/ProductComment/index.tsx
Normal file
32
src/app/pages/Product/ProductDetail/ProductComment/index.tsx
Normal 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 và đá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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
100
src/app/pages/Product/ProductDetail/ProductReview/index.tsx
Normal file
100
src/app/pages/Product/ProductDetail/ProductReview/index.tsx
Normal 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 và đá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á và 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>
|
||||
);
|
||||
};
|
||||
45
src/app/pages/Product/ProductDetail/ProductSpec/index.tsx
Normal file
45
src/app/pages/Product/ProductDetail/ProductSpec/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
33
src/app/pages/Product/ProductDetail/ProductSummary/index.tsx
Normal file
33
src/app/pages/Product/ProductDetail/ProductSummary/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
131
src/app/pages/Product/ProductDetail/index.tsx
Normal file
131
src/app/pages/Product/ProductDetail/index.tsx
Normal 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;
|
||||
80
src/app/pages/Product/ProductHot/index.tsx
Normal file
80
src/app/pages/Product/ProductHot/index.tsx
Normal 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;
|
||||
113
src/app/pages/Product/ProductSearch/index.tsx
Normal file
113
src/app/pages/Product/ProductSearch/index.tsx
Normal 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;
|
||||
40
src/app/send-cart/page.tsx
Normal file
40
src/app/send-cart/page.tsx
Normal 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 có yêu cầu đặc biệt, vui lòng liên hệ nhân viên tư 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>
|
||||
);
|
||||
}
|
||||
122
src/components/Cart/Home/FormCart/index.tsx
Normal file
122
src/components/Cart/Home/FormCart/index.tsx
Normal 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="">Hà 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';
|
||||
110
src/components/Cart/Home/ItemCart/index.tsx
Normal file
110
src/components/Cart/Home/ItemCart/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
189
src/components/Cart/Home/index.tsx
Normal file
189
src/components/Cart/Home/index.tsx
Normal 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 có 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;
|
||||
95
src/components/Deal/ItemDeal.tsx
Normal file
95
src/components/Deal/ItemDeal.tsx
Normal 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;
|
||||
@@ -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' ? (
|
||||
@@ -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. Hà Nội</p>
|
||||
<p>17 Hà Kế Tấn, Phường Phương Liệt, Hà 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 Lý 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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
57
src/components/common/ItemArticle/index.tsx
Normal file
57
src/components/common/ItemArticle/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
59
src/components/common/error/index.tsx
Normal file
59
src/components/common/error/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
134
src/components/other/BoxHotline/index.tsx
Normal file
134
src/components/other/BoxHotline/index.tsx
Normal 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 cá nhân</p>
|
||||
<div className="item-pop">
|
||||
<div className="title-item-pop">Tư 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. Hà Nội</span>
|
||||
</div>
|
||||
<div className="item-people">
|
||||
<p className="phone">0765.666.668</p>
|
||||
<span>Showroom TP. Hà 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 Hà Kế Tấn, Phường Phương Liệt, Hà Nội</span>
|
||||
</div>
|
||||
<div className="item-people">
|
||||
<p className="phone">079.9999.191</p>
|
||||
<span>249 Lý 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">Tư 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 lý - 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
647
src/data/ListComment/index.tsx
Normal file
647
src/data/ListComment/index.tsx
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
4468
src/data/ListProduct/index.tsx
Normal file
4468
src/data/ListProduct/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
277
src/data/ListReview/index.ts
Normal file
277
src/data/ListReview/index.ts
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2124
src/data/article/ArticleCateDetailPageData.ts
Normal file
2124
src/data/article/ArticleCateDetailPageData.ts
Normal file
File diff suppressed because it is too large
Load Diff
3429
src/data/article/ArticleDetailPageData.ts
Normal file
3429
src/data/article/ArticleDetailPageData.ts
Normal file
File diff suppressed because one or more lines are too long
264
src/data/article/ListAricleVideo.ts
Normal file
264
src/data/article/ListAricleVideo.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
274
src/data/article/ListArticleNews.ts
Normal file
274
src/data/article/ListArticleNews.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
186
src/data/article/ListCategory.ts
Normal file
186
src/data/article/ListCategory.ts
Normal 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: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
114
src/data/buildpc/category/index.ts
Normal file
114
src/data/buildpc/category/index.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
0
src/data/buildpc/index.ts
Normal file
0
src/data/buildpc/index.ts
Normal 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ÀN HÌNH : Màn hì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á PC khi không lấy quà 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ÀN HÌNH : Màn hì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á PC khi không lấy quà 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ÀN HÌNH : Màn hì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á PC khi không lấy quà 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ÀN HÌNH : Màn hì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á PC khi không lấy quà 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
5268
src/data/product/detail/index.ts
Normal file
5268
src/data/product/detail/index.ts
Normal file
File diff suppressed because one or more lines are too long
13214
src/data/product/search/index.ts
Normal file
13214
src/data/product/search/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
29269
src/data/producthot/index.ts
Normal file
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
17
src/hooks/useFancybox.ts
Normal 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];
|
||||
}
|
||||
69
src/lib/ButtonCart/index.ts
Normal file
69
src/lib/ButtonCart/index.ts
Normal 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));
|
||||
};
|
||||
18
src/lib/article/category/index.ts
Normal file
18
src/lib/article/category/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TypeArticleCatePage } from '@/types/article/TypeArticleCatePage';
|
||||
|
||||
// Hàm helper để lấy URL an toàn từ các cấu trúc dữ liệu khác nhau
|
||||
function getSlug(url: string): string {
|
||||
const parts = url.split('/').filter(Boolean);
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
|
||||
*/
|
||||
export function findCategoryBySlug(
|
||||
slug: string,
|
||||
categories: TypeArticleCatePage[],
|
||||
): TypeArticleCatePage | null {
|
||||
const found = categories.find((item) => item.category_info.request_path == slug);
|
||||
return found ?? null;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user