update 16/01

This commit is contained in:
2026-01-16 17:04:10 +07:00
parent b921d73f73
commit ddb60bd5f9
9 changed files with 269 additions and 89 deletions

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
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 '../styles/globals.css'; import '../styles/globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -19,7 +20,10 @@ export default function RootLayout({
<body> <body>
<Header /> <Header />
{children} <TooltipProvider>
{children}
</TooltipProvider>
<Footer /> <Footer />
</body> </body>

View File

@@ -53,11 +53,13 @@ export default function ProductCategories() {
modules={[Navigation, Pagination, Autoplay]} modules={[Navigation, Pagination, Autoplay]}
spaceBetween={24} spaceBetween={24}
slidesPerView={5} slidesPerView={5}
loop={categoryProducts[0].list?.length > 5} // loop={categoryProducts[0].list?.length > 5}
autoplay={{ loop={false}
delay: 3000, // autoplay={{
disableOnInteraction: false, // delay: 3000,
}} // disableOnInteraction: false,
// }}
autoplay={false}
navigation={{ navigation={{
prevEl: '.custom-nav .swiper-button-prev', prevEl: '.custom-nav .swiper-button-prev',
nextEl: '.custom-nav .swiper-button-next', nextEl: '.custom-nav .swiper-button-next',

View File

@@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { useCart } from "@/hooks/useCart"; import { useCart } from "@/hooks/useCart";
import { getAllProducts } from "@/lib/utils" import { formatPrice, getAllProducts } from "@/lib/utils"
import CartItem from "@/components/other/header/cart/index" import CartItem from "@/components/other/header/cart/index"
export default function Cart() { export default function Cart() {
@@ -66,7 +66,7 @@ export default function Cart() {
<p className="grey m-0 text-right"> <p className="grey m-0 text-right">
Tổng tiền hàng Tổng tiền hàng
(<span className="red"><span className="js-cart-count">{totalItems}</span> sản phẩm</span>): (<span className="red"><span className="js-cart-count">{totalItems}</span> sản phẩm</span>):
<span className="red text-18 font-600" style={{ verticalAlign: "top" }}> {totalPrice}đ </span> <span className="red text-18 font-600" style={{ verticalAlign: "top" }}> {formatPrice(totalPrice)}đ </span>
</p> </p>
<Link href="/cart" className="d-block text-center text-white btn-goCart"> THANH TOÁN NGAY </Link> <Link href="/cart" className="d-block text-center text-white btn-goCart"> THANH TOÁN NGAY </Link>
</div> </div>

View File

@@ -0,0 +1,18 @@
'use client';
import { ReactNode } from 'react';
import { useTooltip } from '../../hooks/useTooltip';
export default function TooltipProvider({
children,
}: {
children: ReactNode;
}) {
useTooltip();
return (
<>
{children}
</>
);
}

View File

@@ -1,56 +1,86 @@
'use client'; 'use client';
import Link from "next/link"; import Link from "next/link";
import { formatPrice } from "@/lib/utils"; import { formatPrice } from "@/lib/utils";
import { useProductItem } from "@/hooks/useProductItem"
import { useCart } from '@/hooks/useCart';
export default function ProductItem({item}:any){ export default function ProductItem({item}:any){
console.log('ProductItem: ', item) const product = useProductItem(item);
if (!product) return null;
const { addToCart, isInCart } = useCart();
const {
productId,
productUrl,
productImage,
productName,
quantity,
price,
marketPrice,
discount,
displayOffer,
warranty,
displaySummary
} = product;
const checkIncart = isInCart(productId);
const priceView = price > 0 ? formatPrice(price) + 'đ' : 'Liên hệ';
return ( return (
<div className="p-item"> <div className="p-item js-p-item">
<Link href={item.productUrl} className="p-img"> <Link href={productUrl} className="p-img">
<img <img
src={item.productImage.large} src={productImage}
alt={item.productName} alt={productName}
width={250} width={250} height={250}
height={250}
/> />
</Link> </Link>
<div className="p-text"> <div className="p-text">
<div className="p-price-group"> <div className="p-price-group">
<del>52.000.000 đ</del> {discount > 0 &&
<span className="p-discount">-10%</span> <>
<p className="p-price"> 22.000.000 đ </p> <del>{formatPrice(marketPrice)} đ</del>
<span className="p-discount">-{discount}%</span>
</>
}
<p className="p-price"> {priceView} </p>
</div> </div>
<Link href={item.productUrl} className="p-name"> <Link href={productUrl} className="p-name">
<h3>{item.productName}</h3> <h3>{productName}</h3>
</Link> </Link>
<div className="p-btn-group flex items-center justify-between text-16 font-500 leading-[23px]"> <div className="p-btn-group flex items-center justify-between text-16 font-500 leading-[23px]">
<div> <div>
<p className="m-0 text-[#00AD4F] flex items-center gap-1"> {quantity > 0 ?
<i className="bx bx-check-circle text-18 w-[18px]"></i> (
<span> Sẵn hàng </span> <p className="m-0 text-[#00AD4F] flex items-center gap-1">
</p> <i className="bx bx-check-circle text-18 w-[18px]"></i>
<span> Sẵn hàng </span>
</p>
) : (
<p className="m-0 red flex items-center gap-1">
<i className="bx bxs-phone text-18 w-[18px]"></i>
<span> Liên hệ </span>
</p>
)
}
{/* <p className="m-0 red flex items-center gap-1"> {displayOffer &&
<i className="bx bxs-phone text-18 w-[18px]"></i> <p className="m-0 text-[#E16B10] flex items-center gap-1">
<span> Liên hệ </span> <i className="bx bx-gift text-18 w-[18px]"></i>
</p> */} <span> Quà tặng </span>
</p>
<p className="m-0 text-[#E16B10] flex items-center gap-1"> }
<i className="bx bx-gift text-18 w-[18px]"></i>
<span> Quà tặng </span>
</p>
</div> </div>
<button type="button" className="" aria-label="Mua"></button>
<button className={`p-btn bx bg-btn text-white rounded-full w-9 h-9 text-20 ${checkIncart ? 'bx-check' : 'bx-plus' }`} <button className={`p-btn bx bg-btn text-white rounded-full w-9 h-9 text-20 ${checkIncart ? 'bx-check' : 'bx-plus' }`}
type="button" aria-label="Mua"
style={{ background: `${checkIncart ? '#ccc' : ''}` }} style={{ background: `${checkIncart ? '#ccc' : ''}` }}
disabled={checkIncart} disabled={checkIncart}
type="button" aria-label="Mua" onClick={() => addToCart(productId)}
onClick={() => addToCart(productInfo.id)}
></button> ></button>
</div> </div>
</div> </div>
@@ -58,48 +88,58 @@ export default function ProductItem({item}:any){
<div className="p-tooltip hidden"> <div className="p-tooltip hidden">
<div className="bg-white rounded-[20px] overflow-hidden border-2 border-[#EAECF0] shadow-[0px_6px_8px_-2px_#10182814]"> <div className="bg-white rounded-[20px] overflow-hidden border-2 border-[#EAECF0] shadow-[0px_6px_8px_-2px_#10182814]">
<p className="tooltip-name px-5 py-4 text-white font-600 text-16 leading-[21px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,_#114CDD_100.92%)] m-0"> <p className="tooltip-name px-5 py-4 text-white font-600 text-16 leading-[21px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,_#114CDD_100.92%)] m-0">
[Tặng bàn phím] HHPC ULTRA 7 265K | 32GB DDR5 | NVIDIA RTX 3060 12GB {productName}
</p> </p>
<div className="p-4 tooltip-content"> <div className="p-4 tooltip-content">
<div className="last:border-0 last:p-0 last:m-0 text-16 border-b border-[#DEE4EC] mb-4 pb-4"> <div className="last:border-0 last:p-0 last:m-0 text-16 border-b border-[#DEE4EC] mb-4 pb-4">
<p className="m-0 flex items-center flex-wrap gap-1"> <p className="m-0 flex items-center flex-wrap gap-1">
<b className="font-600"> Giá bán: </b> <b className="font-600"> Giá bán: </b>
<b className="font-600 text-[#FF4E2A] text-18"> 48.990.000 đ </b> <b className="font-600 text-[#FF4E2A] text-18"> {priceView} </b>
<del className="text-[#A0A5AC] font-300"> 52.000.000 đ </del>
<span className="bg-[#FA354A] text-white text-11 px-[6px] leading-[18px] rounded-[20px]"> -6% </span> {discount > 0 &&
<>
<del className="text-[#A0A5AC] font-300"> {formatPrice(marketPrice)} đ </del>
<span className="bg-[#FA354A] text-white text-11 px-[6px] leading-[18px] rounded-[20px]">
-{discount}%
</span>
</>
}
</p> </p>
<p className="m-0"> {warranty &&
<b className="font-600"> Bảo hành: </b> <p className="m-0">
<span> Theo từng linh kiện </span> <b className="font-600"> Bảo hành: </b>
</p> <span> {warranty} </span>
</p>
}
</div> </div>
<div className="last:mb-0 mb-6 px-1"> {displaySummary &&
<p className="text-16 font-600 flex items-center leading-6 mb-2"> <div className="last:mb-0 mb-6 px-1">
<i className="icons icon-screen"></i> <p className="text-16 font-600 flex items-center leading-6 mb-2">
<span> Thông số sản phẩm </span> <i className="icons icon-screen"></i>
</p> <span> Thông số sản phẩm </span>
</p>
<div className="tooltip-spec"> <div className="tooltip-spec">
<div className="item"> CPU: INTEL CORE i5 13400F up 4.6GHz | 10 CORE | 16 THREAD </div> <div dangerouslySetInnerHTML={{ __html: displaySummary }}/>
<div className="item"> RAM: DDR4 16GB (1x16G) 3200 MHz </div> </div>
<div className="item"> VGA: NVIDIA RTX 3060 12GB GDDR6 </div>
</div> </div>
</div> }
<div className="rounded-[12px] bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2"> { displayOffer &&
<p className="px-2 text-16 font-600 flex items-center leading-5 mb-2 text-white"> <div className="rounded-[12px] bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2">
<i className="icons icon-gift"></i> <p className="px-2 text-16 font-600 flex items-center leading-5 mb-2 text-white">
<span> Khuyến mại hấp dẫn </span> <i className="icons icon-gift"></i>
</p> <span> Khuyến mại hấp dẫn </span>
</p>
<div className="tooltip-offer rounded-[8px] bg-[#FEF2F2] px-2 py-4"> <div className="tooltip-offer rounded-[8px] bg-[#FEF2F2] px-2 py-4">
<div className="item"> <p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong> Bảo Hành Tại Nơi Sử Dụng (Áp Dụng Nội Thành Nội Hồ Chí Minh)</strong></span></p> </div> <div dangerouslySetInnerHTML={{ __html: displayOffer }}/>
<div className="item"><p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong> Bảo Hành Siêu Tốc 1 Đi 1 Trong 24h </strong></span></p></div> <div className="item"><p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong> Miễn Phí 100% Vận Chuyển Toàn Quốc </strong></span></p></div> </div>
</div> </div>
</div> }
</div> </div>
</div> </div>

View File

@@ -1,19 +1,31 @@
import { calculateDiscount } from "../lib/utils";
import type { ProductItemProps } from "../types/product" import type { ProductItemProps } from "../types/product"
export function useDealItem(item: ProductItemProps) { import {
const productUrl = item.productUrl; calculateDiscount,
const productImage = item.productImage.large; formatTextList
const productName = item.productName; } from "../lib/utils";
export function useProductItem(item: ProductItemProps) {
const productId = Number(item.productId);
const quantity = Number(item.quantity);
const price = Number(item.price); const price = Number(item.price);
const marketPrice = Number(item.marketPrice); const marketPrice = Number(item.marketPrice);
const discount = calculateDiscount(price, marketPrice); const discount = calculateDiscount(price, marketPrice);
const quantity = item.quantity; const productUrl = item.productUrl;
const specialOffer = item.specialOffer.all?.[0]?.title ?? ''; const productImage = item.productImage.large;
const productName = item.productName;
const specialOffer = item.specialOffer.all[0]?.title ?? null;
const displayOffer = specialOffer
? formatTextList(specialOffer)
: null;
const warranty = item.warranty; const warranty = item.warranty;
const productSummary= item.productSummary; const productSummary = item.productSummary;
const displaySummary = formatTextList(item.productSummary)
return { return {
productId,
productUrl, productUrl,
productImage, productImage,
productName, productName,
@@ -21,8 +33,8 @@ export function useDealItem(item: ProductItemProps) {
price, price,
marketPrice, marketPrice,
discount, discount,
specialOffer, specialOffer, displayOffer,
warranty, warranty,
productSummary productSummary, displaySummary,
}; };
} }

65
src/hooks/useTooltip.ts Normal file
View File

@@ -0,0 +1,65 @@
'use client';
import { useEffect } from 'react';
export function useTooltip() {
useEffect(() => {
const tooltip = document.getElementById('js-tooltip') as HTMLDivElement | null;
if (!tooltip) return;
const pad = 10;
function onMouseMove(e: MouseEvent) {
const img = e.currentTarget as HTMLElement | null;
if (!img) return;
const product = img.closest('.js-p-item');
const content = product?.querySelector<HTMLElement>('.p-tooltip');
if (!content) {
tooltip.style.display = 'none';
return;
}
tooltip.innerHTML = content.innerHTML;
tooltip.style.display = 'block';
const w = tooltip.offsetWidth;
const h = tooltip.offsetHeight;
const wrapRight = window.innerWidth;
const wrapTop = window.scrollY;
const left =
e.pageX + w > wrapRight
? e.pageX - w - pad
: e.pageX + pad;
const top =
e.pageY - h < wrapTop
? wrapTop
: e.pageY - h - pad;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
function onMouseOut() {
tooltip.style.display = 'none';
}
const images = document.querySelectorAll<HTMLImageElement>('.js-p-item img');
images.forEach(img => {
img.addEventListener('mousemove', onMouseMove);
img.addEventListener('mouseout', onMouseOut);
});
return () => {
images.forEach(img => {
img.removeEventListener('mousemove', onMouseMove);
img.removeEventListener('mouseout', onMouseOut);
});
};
}, []);
}

View File

@@ -1,3 +1,6 @@
import { ReactNode } from 'react';
// Add tất cả sp trong data product vào 1 mảng // Add tất cả sp trong data product vào 1 mảng
import { productList } from '@/data/products'; import { productList } from '@/data/products';
@@ -5,6 +8,41 @@ export function getAllProducts() {
return productList.flatMap((group:any) => group.list); return productList.flatMap((group:any) => group.list);
} }
export function formatTextList(
text?: string | any[],
limit = 5
) {
if (!text) return '';
if (typeof window === 'undefined') {
// server: trả raw HTML
return Array.isArray(text)
? String(text[1] ?? '')
: String(text);
}
const html = Array.isArray(text)
? String(text[1] ?? '')
: String(text);
if (!html) return '';
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const nodes = Array.from(
doc.body.querySelectorAll('p, div, li')
);
return nodes
.slice(0, limit)
.map(node => `<div class="item">${node.innerHTML}</div>`)
.join('');
}
// Format giá // Format giá
export function formatPrice(amount: number) { export function formatPrice(amount: number) {
return amount.toLocaleString('vi-VN'); return amount.toLocaleString('vi-VN');

View File

@@ -1,12 +1,13 @@
// src/types/deal.ts // src/types/deal.ts
export type ProductItemProps = { export type ProductItemProps = {
price: number; productId : number;
quantity: number; price : number;
productUrl: string; marketPrice : number;
productImage: string; quantity : number;
productName: string; productUrl : string;
marketPrice : number; productName : string;
specialOffer : string; warranty : string;
warranty: string; productSummary : string;
productSummary: string; productImage : any;
specialOffer : any;
}; };