This commit is contained in:
2026-01-06 11:02:01 +07:00
parent 28a252f7d7
commit 9486dabdb0
15 changed files with 31638 additions and 46 deletions

View File

@@ -6,6 +6,7 @@ import { resolvePageType } from '@/lib/resolvePageType';
import CategoryPage from '@/app/pages/Product/Category';
import ProductSearchPage from '@/app/pages/Product/ProductSearch';
import ProductDetailPage from '@/app/pages/Product/ProductDetail';
import 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';
@@ -23,6 +24,8 @@ export default function DynamicPage() {
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':

View File

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

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

@@ -0,0 +1,85 @@
import React from 'react';
import '@styles/buildpc.css';
import { Metadata } from 'next';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import Slider from '@/app/buildpc/Slider';
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>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,80 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { ErrorLink } from '@/components/Common/error';
import type { TypeProductHot } from '@/types/producthot';
import { ProductHotPageData } from '@/data/producthot';
import { findProductHotBySlug } from '@/lib/product/producthot';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
interface ProductHotPageProps {
slug: string; // khai báo prop slug
}
const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
const ProductHot = ProductHotPageData as unknown as TypeProductHot[];
const Pages = findProductHotBySlug(slug, ProductHot);
const breadcrumbItems = [
{ name: 'Trang chủ', url: '/' },
{ name: Pages?.title, url: Pages?.url },
];
if (!Pages) {
return <ErrorLink />;
}
// lấy sản phẩm
const products = Object.values(Pages.product_list);
return (
<>
<div className="container">
<Breadcrumb items={breadcrumbItems} />
</div>
<section className="page-category page-search container">
<div className="current-cate-title">
<div className="mt-5 flex items-center gap-2">
<h1 className="current-cate-text font-bold"> {Pages.title} </h1>
<span className="current-cate-total">(Tổng {Pages.product_count} sản phẩm)</span>
</div>
</div>
<div className="box-content-category">
{/* filter */}
<BoxFilter filters={Pages} />
<div className="box-list-product-category boder-radius-10">
{/* filter sort */}
<BoxSort sort_by_collection={Pages.sort_by_collection} product_display_type="grid" />
</div>
{/* list product */}
<div className="list-product-category grid grid-cols-5 gap-3">
{products.map((item, index) => (
<ItemProduct key={index} item={item} />
))}
</div>
<div className="paging flex items-center justify-center">
{Pages.paging_collection.map((item, index) => (
<Link
key={index}
href={item.url}
className={`item ${item.is_active === '1' ? 'current' : ''}`}
>
{item.name}
</Link>
))}
</div>
</div>
</section>
</>
);
};
export default ProductHotPage;

View File

@@ -40,13 +40,33 @@ const BoxFilter: React.FC<BoxFilterProps> = ({ filters }) => {
</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>
))}
</div>
</div>
)}
{/* chọn thiêu tiêu trí */}
{attribute_filter_list && (
<div className="info-filter-category flex gap-10">
<p className="title">Chọn theo tiêu chí:</p>
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-3">
{/* thương hiệu */}
{brand_filter_list && brand_filter_list.length > 0 && (
{brand_filter_list && (
<div className={`item ${brand_filter_list[0].is_selected === '1' ? 'current' : ''}`}>
<div className="flex items-center">
{brand_filter_list[0].is_selected === '1' ? (

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ import Image from 'next/image';
import Link from 'next/link';
import { FaMapMarkerAlt, FaBars } from 'react-icons/fa';
import BoxShowroom from '@/components/Common/BoxShowroom';
import BoxHotLine from '../../BoxHotline';
import { TypeCartItem } from '@/types/cart';
import { formatCurrency } from '@/lib/formatPrice';
@@ -30,6 +32,11 @@ const HeaderMid: React.FC = () => {
modal?.showModal();
};
const PopupHotLine = () => {
const modal = document.getElementById('boxHotline') as HTMLDialogElement;
modal?.showModal();
};
return (
<div className="header-middle">
<div className="container flex items-center justify-between">
@@ -89,15 +96,15 @@ const HeaderMid: React.FC = () => {
<span className="font-500">Xây dựng cấu hình</span>
</Link>
<Link
href="javascript:void(0)"
<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 flex-col items-center gap-4">
<p className="icon-item-tab flex items-center justify-center">
@@ -181,6 +188,7 @@ const HeaderMid: React.FC = () => {
</div>
</div>
<BoxShowroom />
<BoxHotLine />
</div>
);
};

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
import { TypeProductHot } from '@/types/producthot';
// 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];
}
export function findProductHotBySlug(
slug: string,
ProductHot: TypeProductHot[],
): TypeProductHot | null {
const found = ProductHot.find((item) => item.url === slug);
return found ?? null;
}

View File

@@ -1,5 +1,6 @@
import { productCategoryData } from '@/data/product/category';
import { productDetailData } from '@/data/product/detail';
import { ProductHotPageData } from '@/data/producthot';
import { ArticleCateDetailPageData } from '@/data/article/ArticleCateDetailPageData';
import { ArticleDetailPageData } from '@/data/article/ArticleDetailPageData';
@@ -8,16 +9,19 @@ export function resolvePageType(slug: string) {
if (productCategoryData.some((c) => c.current_category.url == slug)) {
return 'category';
}
// hiển thị trang tìm kiếm
if ('/tim' == slug) {
return 'product-search';
}
// hiển thị trang chi tiết sản phẩm
if (productDetailData.some((c) => c.product_info.productUrl == slug)) {
return 'product-detail';
}
// hiển thị trang sản phẩm hot, mới, bán chạy
if (ProductHotPageData.some((c) => c.url == slug)) {
return 'product-hot';
}
// tin tức
if ('/tin-tuc' == slug) {
return 'article-home';

1909
src/styles/buildpc.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1726,23 +1726,7 @@ textarea::placeholder {
.header .header-bottom .list-category-child .box-right .item-product-bestsale:hover .product-name {
color: var(--color-primary);
}
#popup-hotline {
max-width: 750px;
padding: 0;
max-height: 650px;
display: none;
border-radius: 10px;
}
#popup-hotline.active {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: #fff;
overflow: auto;
z-index: 9999;
display: block;
}
#popup-hotline .title-content-pop {
font-size: 20px;
text-transform: uppercase;
@@ -5747,11 +5731,6 @@ textarea::placeholder {
max-width: 1000px;
}
.popup-showrom-container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.popup-showrom-container .group-title {
border-bottom: 1px solid;
@@ -5765,7 +5744,6 @@ textarea::placeholder {
padding: 15px;
font-size: 16px;
line-height: 1.6;
width: calc(50% - 10px);
margin-bottom: 20px;
border-radius: 12px;
background: -webkit-gradient(linear, left top, left bottom, from(#3343da), to(#85b6e3));
@@ -6110,3 +6088,11 @@ textarea::placeholder {
.box-article-detail .box-article-detail-cmt iframe {
width: 100% !important;
}
.build-pc {
background: #f4f4f4;
padding: 20px 0;
}
.build-pc .title-page-all {
font-size: 32px;
line-height: 36px;
}

View File

@@ -36,9 +36,12 @@ export interface CategoryCollection {
id: string;
name: string;
url: string;
count: string;
count?: string;
is_selected: string;
category_url?: string;
urlSearch?: string;
productCount?: string;
url_back?: string;
}
export interface BrandFilter {
id: string;
@@ -46,15 +49,20 @@ export interface BrandFilter {
brand_index: string;
image: string;
count: string;
is_selected: string;
is_selected?: string;
url: string;
isCurrent?: string;
brand_url?: string;
}
export interface PriceFilter {
count: string;
is_selected: string;
is_selected?: string;
name: string;
url: string;
isCurrent?: string;
max?: string;
min?: string;
}
export interface CurrentCategory {

View File

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