update 31/01

This commit is contained in:
2026-01-31 12:00:43 +07:00
parent eb44cc2575
commit 12509e3a7b
11 changed files with 6705 additions and 6920 deletions

25
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"photoswipe": "^5.4.4", "photoswipe": "^5.4.4",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"sonner": "^2.0.7",
"swiper": "^12.0.3" "swiper": "^12.0.3"
}, },
"devDependencies": { "devDependencies": {
@@ -5628,6 +5629,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6346,6 +6356,21 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
} }
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.0.tgz",
"integrity": "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View File

@@ -15,6 +15,7 @@
"photoswipe": "^5.4.4", "photoswipe": "^5.4.4",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"sonner": "^2.0.7",
"swiper": "^12.0.3" "swiper": "^12.0.3"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Toaster } from 'sonner';
import Header from "@/components/other/header"; import Header from "@/components/other/header";
import Footer from "@/components/other/footer"; import Footer from "@/components/other/footer";
import TooltipProvider from "@/components/providers/TooltipProvider"; import TooltipProvider from "@/components/providers/TooltipProvider";
@@ -24,8 +25,14 @@ export default function RootLayout({
{children} {children}
</TooltipProvider> </TooltipProvider>
<Footer /> <Footer />
<Toaster
position="top-right"
closeButton
richColors
/>
</body> </body>
</html> </html>
); );

View File

@@ -1,4 +1,8 @@
export default function Buttons() { 'use client';
import { useCart } from '@/hooks/useCart';
export default function Buttons( {item}:any ) {
const { addToCart } = useCart();
return ( return (
<> <>
<div className="pd-button-group my-5 gap-2 grid grid-cols-2 text-18 font-500"> <div className="pd-button-group my-5 gap-2 grid grid-cols-2 text-18 font-500">
@@ -6,26 +10,27 @@ export default function Buttons() {
type="button" type="button"
className="col-span-2 uppercase rounded-[12px] text-white h-[52px] border border-[#FFD83E] bg-[linear-gradient(148.21deg,#FFD83E_-14.02%,#FF4E2A_70.14%)]" className="col-span-2 uppercase rounded-[12px] text-white h-[52px] border border-[#FFD83E] bg-[linear-gradient(148.21deg,#FFD83E_-14.02%,#FF4E2A_70.14%)]"
aria-label="Mua sản phẩm" aria-label="Mua sản phẩm"
onClick={() => addToCart(item, 1, '/cart')}
> >
{" "} Mua ngay
Mua ngay{" "}
</button> </button>
<button <button
type="button" type="button"
className="col-span-1 flex items-center justify-center uppercase rounded-[12px] text-white h-[52px] border border-[#259AFF] bg-[linear-gradient(165.29deg,#259AFF_8.53%,#114CDD_93.19%)]" className="col-span-1 flex items-center justify-center uppercase rounded-[12px] text-white h-[52px] border border-[#259AFF] bg-[linear-gradient(165.29deg,#259AFF_8.53%,#114CDD_93.19%)]"
aria-label="Mua sản phẩm" aria-label="Mua sản phẩm"
onClick={() => addToCart(item)}
> >
<i className="icons icon-cart !w-[22px] !h-[22px] mr-[6px]" /> <i className="icons icon-cart !w-[22px] !h-[22px] mr-[6px]" />
<span> Thêm vào giỏ </span> <span> Thêm vào giỏ </span>
</button> </button>
<button
type="button" <a href='https://hoanghapc.vn/huong-dan-mua-tra-gop' target='_blank'
className="col-span-1 flex items-center justify-center uppercase rounded-[12px] text-white h-[52px] border border-[#259AFF] bg-[linear-gradient(165.29deg,#259AFF_8.53%,#114CDD_93.19%)]" className="col-span-1 flex items-center justify-center uppercase rounded-[12px] text-white h-[52px] border border-[#259AFF] bg-[linear-gradient(165.29deg,#259AFF_8.53%,#114CDD_93.19%)]"
aria-label="Mua sản phẩm"
> >
<i className="icons icon-card w-[22px] h-[22px] mr-[6px]" /> <i className="icons icon-card w-[22px] h-[22px] mr-[6px]" />
<span> Mua trả góp </span> <span> Mua trả góp </span>
</button> </a>
</div> </div>
</> </>
) )

View File

@@ -1,30 +1,57 @@
export default function ProductDescription() { 'use client';
return ( import { useState, useEffect, useRef } from "react";
<div className="pd-box-group bg-white mb-6 p-8 pt-6 rounded-[24px]"> import parse from 'html-react-parser';
<p className="group-title border-b border-[#D0D8E3] leading-[31px] font-600 text-24 mb-4 pb-4">
Đánh giá HHPC CORE i7 14700 | 32G DDR5 | NVIDIA RTX 3060 12G{" "}
</p>
<div className="js-static-container static-container leading-[135%]">
<div className="js-static-content static-content text-16 leading-[22px] text-justify">
export default function ProductDescription( {name, description}:any ) {
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isOverflow, setIsOverflow] = useState(false);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
if (contentRef.current) {
if (contentRef.current.scrollHeight > 700) {
setIsOverflow(true);
}
}
}, [description]);
const handleCollapse = () => {
setExpanded(false);
containerRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
};
return (
<div className="pd-box-group bg-white mb-6 p-8 pt-6 rounded-[24px]" ref={containerRef}>
<p className="group-title border-b border-[#D0D8E3] leading-[31px] font-600 text-24 mb-4 pb-4">
Đánh giá {name}
</p>
<div className="js-static-container static-container leading-[135%]" ref={contentRef}>
<div className={`static-content text-16 leading-[22px] text-justify transition-all duration-300
${!expanded && isOverflow ? 'max-h-[700px] overflow-hidden' : ''}
`}>
{parse(description)}
</div> </div>
<div className="static-btn"> {isOverflow && (
<button <div className="static-btn mt-4 flex justify-center">
type="button" {!expanded ? (
aria-label="Xem thêm" <button onClick={() => setExpanded(true)}>
className="js-showmore-button"
>
Xem thêm <i className="bx bx-chevron-down" /> Xem thêm <i className="bx bx-chevron-down" />
</button> </button>
<button ) : (
type="button" <button onClick={handleCollapse}>
aria-label="Thu gọn"
className="js-showless-button"
>
Thu gọn <i className="bx bx-chevron-up" /> Thu gọn <i className="bx bx-chevron-up" />
</button> </button>
)}
</div> </div>
)}
</div> </div>
</div> </div>
) )

View File

@@ -14,6 +14,7 @@ import ProductSummary from "./summary";
export default async function ProductDetail({ slug }: any) { export default async function ProductDetail({ slug }: any) {
const { const {
productName, productName,
productId,
review, review,
visit, visit,
quantity, quantity,
@@ -21,12 +22,13 @@ export default async function ProductDetail({ slug }: any) {
productImage, imageCollection, productImage, imageCollection,
price, marketPrice, deal_list, price_off, sale_rules, price, marketPrice, deal_list, price_off, sale_rules,
hasVAT, warranty, hasVAT, warranty,
specialOffer specialOffer,
productDescription,
productSpec
} = slug } = slug
const image = { const image = {
productImage, productImage, imageCollection
imageCollection
} }
const priceData = { const priceData = {
@@ -85,7 +87,7 @@ export default async function ProductDetail({ slug }: any) {
<ProductOffer item={specialOffer}/> <ProductOffer item={specialOffer}/>
<Buttons /> <Buttons item={productId} />
<p className="m-0 flex items-center gap-3 text-16 leading-[22px]"> <p className="m-0 flex items-center gap-3 text-16 leading-[22px]">
<i className="icons icon-truck-2 !w-6" /> <i className="icons icon-truck-2 !w-6" />
@@ -101,9 +103,10 @@ export default async function ProductDetail({ slug }: any) {
<div className="pd-content-container flex flex-wrap items-baseline gap-6"> <div className="pd-content-container flex flex-wrap items-baseline gap-6">
<div className="col-left w-[784px]"> <div className="col-left w-[784px]">
<ProductDescription /> { productDescription &&
<ProductDescription name={productName} description={productDescription} />
}
{/* Đánh giá và bình luận */}
<div className="pd-comment-container bg-white mb-6 p-8 pt-6 rounded-[24px] text-16 leading-[22px]"> <div className="pd-comment-container bg-white mb-6 p-8 pt-6 rounded-[24px] text-16 leading-[22px]">
<Review /> <Review />
@@ -112,7 +115,9 @@ export default async function ProductDetail({ slug }: any) {
</div> </div>
<div className="col-right w-[440px]"> <div className="col-right w-[440px]">
<ProductSpec /> {productSpec &&
<ProductSpec item={productSpec} />
}
<Article /> <Article />
</div> </div>

View File

@@ -1,349 +1,30 @@
export default function ProductSpec() { import parse from 'html-react-parser';
export default function ProductSpec( {item} : any ) {
console.log(item)
return ( return (
<div className="pd-box-group bg-white mb-6 px-4 py-6 rounded-[24px]"> <div className="pd-box-group bg-white mb-6 px-4 py-6 rounded-[24px]">
<p className="group-title border-b border-[#D0D8E3] leading-[31px] font-600 text-24 mb-4 pb-4"> <p className="group-title border-b border-[#D0D8E3] leading-[31px] font-600 text-24 mb-4 pb-4">
{" "} Thông số kỹ thuật
Thông số kỹ thuật{" "}
</p> </p>
<div className="pd-spec-group"> <div className="pd-spec-group">
<table> {parse(item)}
<tbody>
<tr>
<td style={{ textAlign: "center" }}>
<strong>STT</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong> HÀNG</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>TÊN HÀNG</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>THỜI HẠN BẢO HÀNH</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>1</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>CPU</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/cpu-intel-core-ultra-7-265k">
INTEL CORE ULTRA 7 265K UP 5.5GHz | 20 CORE | 20 THREAD
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>2</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>MAIN</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/mainboard-colorful-battle-ax-z890m-plus-v20">
COLORFUL BATTLE-AX Z890M-PLUS V20 DDR5
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>3</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>TẢN NHIỆT</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/tan-nhiet-cpu-id-cooling-frozn-a620-pro-se-argb">
ID-COOLING FROZN A620 PRO SE ARGB
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>12 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>4</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>RAM</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/ram-ddr5-teamgroup-t-create-expert-32gb-6000mhz">
DDR5 TEAMGROUP T-CREATE EXPERT 32GB 6000MHz (2x16GB)
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>5</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>SSD</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/o-cung-ssd-teamgroup-g50-1tb">
TEAMGROUP G50 1TB PCIE Gen4x4 - RW 5000MB/s
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>60 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>6</strong>
</td>
<td style={{ textAlign: "center" }}>
<span style={{ color: "#000" }}>
<strong>VGA</strong>
</span>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/vga-colorful-rtx-3060-nb-duo-12g-v4-l-v">
COLORFUL RTX 3060 NB DUO 12G V4 L-V GDDR6
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>7</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>PSU</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/nguon-deepcool-pl750d-750w">
DEEPCOOL PL750D 750W 80 PLUS BRONZE | ATX 3.1 | PCIE 5.1
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>60 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>8</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>CASE</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/vo-case-xigmatek-gaming-x-ii-3f-3fan-rgb">
XIGMATEK GAMING X II 3F - 3FAN RGB
</a>
</strong>
</td>
<td style={{ textAlign: "center" }} />
</tr>
</tbody>
</table>
</div> </div>
<a <a
href="#fancybox-spec" href="#fancybox-spec"
data-fancybox="" data-fancybox=""
className="table m-auto mt-4 text-white leading-10 uppercase rounded-[40px] bg-btn font-500 text-16 px-6" className="table m-auto mt-4 text-white leading-10 uppercase rounded-[40px] bg-btn font-500 text-16 px-6"
> >
{" "}
Xem tất cả thông số{" "} Xem tất cả thông số
</a> </a>
<div <div
className="pd-spec-group p-3" className="pd-spec-group p-3"
id="fancybox-spec" id="fancybox-spec"
style={{ display: "none" }} style={{ display: "none" }}
> >
<table> {parse(item)}
<tbody>
<tr>
<td style={{ textAlign: "center" }}>
<strong>STT</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong> HÀNG</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>TÊN HÀNG</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>THỜI HẠN BẢO HÀNH</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>1</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>CPU</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/cpu-intel-core-ultra-7-265k">
INTEL CORE ULTRA 7 265K UP 5.5GHz | 20 CORE | 20 THREAD
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>2</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>MAIN</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/mainboard-colorful-battle-ax-z890m-plus-v20">
COLORFUL BATTLE-AX Z890M-PLUS V20 DDR5
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>3</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>TẢN NHIỆT</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/tan-nhiet-cpu-id-cooling-frozn-a620-pro-se-argb">
ID-COOLING FROZN A620 PRO SE ARGB
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>12 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>4</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>RAM</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/ram-ddr5-teamgroup-t-create-expert-32gb-6000mhz">
DDR5 TEAMGROUP T-CREATE EXPERT 32GB 6000MHz (2x16GB)
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>5</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>SSD</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/o-cung-ssd-teamgroup-g50-1tb">
TEAMGROUP G50 1TB PCIE Gen4x4 - RW 5000MB/s
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>60 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>6</strong>
</td>
<td style={{ textAlign: "center" }}>
<span style={{ color: "#000" }}>
<strong>VGA</strong>
</span>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/vga-colorful-rtx-3060-nb-duo-12g-v4-l-v">
COLORFUL RTX 3060 NB DUO 12G V4 L-V GDDR6
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>36 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>7</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>PSU</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/nguon-deepcool-pl750d-750w">
DEEPCOOL PL750D 750W 80 PLUS BRONZE | ATX 3.1 | PCIE 5.1
</a>
</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>60 THÁNG</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center" }}>
<strong>8</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>CASE</strong>
</td>
<td style={{ textAlign: "center" }}>
<strong>
<a href="https://hoanghapc.vn/vo-case-xigmatek-gaming-x-ii-3f-3fan-rgb">
XIGMATEK GAMING X II 3F - 3FAN RGB
</a>
</strong>
</td>
<td style={{ textAlign: "center" }} />
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
) )

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import { toast } from 'sonner';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { import {
getCartItems, getCartItems,
@@ -14,9 +14,12 @@ import {
CART_CHANGE_EVENT, CART_CHANGE_EVENT,
type CartItem, type CartItem,
} from '../services/cart'; } from '../services/cart';
import { useRouter } from 'next/navigation';
export function useCart() { export function useCart() {
const [cartItems, setCartItems] = useState<CartItem[] | null>(null); const [cartItems, setCartItems] = useState<CartItem[] | null>(null);
const router = useRouter();
// Load cart lần đầu // Load cart lần đầu
useEffect(() => { useEffect(() => {
@@ -47,9 +50,26 @@ export function useCart() {
setCartItems(getCartItems()); setCartItems(getCartItems());
}, []); }, []);
const addToCart = useCallback((productId: number, quantity: number = 1) => { const addToCart = useCallback((productId: number, quantity: number = 1, redirect = '') => {
const result = addProductToCart(productId, quantity); const result = addProductToCart(productId, quantity);
if (result?.success) {
toast.success('Đã thêm vào giỏ hàng', {
description: result.message,
duration: 2500,
});
if (redirect) {
router.push(redirect);
}
return result;
}
toast.error('Thêm sản phẩm thất bại', {
description: result?.message || 'Vui lòng thử lại',
});
return result; return result;
}, []); }, []);

View File

@@ -1,6 +1,7 @@
// hoanghapc/src/lib/productPage.ts // hoanghapc/src/lib/productPage.ts
import { categories } from "@/data/categories"; import { categories } from "@/data/categories";
import { productList } from "@/data/productList"; import { productList } from "@/data/productList";
import { productDetail } from "@/data/productDetail"
export type ProductResult = export type ProductResult =
| { type: "product_category"; data: any } | { type: "product_category"; data: any }
@@ -28,7 +29,16 @@ export function resolveProductPage(slug: string): ProductResult | null {
.find((p: any) => p.productUrl === url); .find((p: any) => p.productUrl === url);
if (product) { if (product) {
return { type: "product_detail", data: product }; const data = {
...product,
productDescription : productDetail.productDescription,
productSpec : productDetail.productSpec
}
return {
type: "product_detail",
data
};
} }
return null; return null;