update 30/01

This commit is contained in:
2026-01-30 17:09:41 +07:00
parent bf78d0583d
commit eb44cc2575
17 changed files with 6781 additions and 6473 deletions

12549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,9 @@
}, },
"dependencies": { "dependencies": {
"@fancyapps/ui": "5.0", "@fancyapps/ui": "5.0",
"html-react-parser": "^5.2.14",
"next": "16.1.0", "next": "16.1.0",
"photoswipe": "^5.4.4",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"swiper": "^12.0.3" "swiper": "^12.0.3"

View File

@@ -0,0 +1,53 @@
'use client'
import { useEffect, useRef } from 'react'
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import 'photoswipe/style.css'
export default function PhotoSwipeImage({
images,
activeIndex
}: {
images: string[]
activeIndex: number
}) {
const galleryRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!galleryRef.current) return
const lightbox = new PhotoSwipeLightbox({
gallery: '#pswp-gallery',
children: 'a',
pswpModule: () => import('photoswipe')
})
lightbox.init()
return () => lightbox.destroy()
}, [])
return (
<div
id="pswp-gallery"
ref={galleryRef}
className="w-full"
>
{images.map((img, index) => (
<a
key={index}
href={img}
data-pswp-width="1200"
data-pswp-height="1200"
className={index === activeIndex ? 'block' : 'hidden'}
>
<img
src={img}
alt=""
className="w-full object-contain cursor-zoom-in"
/>
</a>
))}
</div>
)
}

View File

@@ -1,84 +1,56 @@
export default function ProductImage() { 'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Navigation } from 'swiper/modules'
import Link from 'next/link'
const PhotoSwipeImage = dynamic(
() => import('./PhotoSwipeImage'),
{ ssr: false }
)
export default function ProductImage({ data }: any) {
const images: string[] = Array.from(
new Set([
data.productImage.large.replace('250_', ''),
...(data.imageCollection?.map((i: any) =>
i.image.large.replace('250_', '')
) || [])
])
)
const [activeIndex, setActiveIndex] = useState(0)
return ( return (
<> <>
<div className="pd-image-top mb-3"> <div className="pd-image-top mb-4">
<a <PhotoSwipeImage
className="MagicZoom" images={images}
id="Zoomer" activeIndex={activeIndex}
rel="selectors-effect-speed: 600"
href="images/product-1.jpg"
>
<img src="images/product-1.jpg" alt="" />
</a>
<a
href="javascript:void(0)"
className="pd-image-btn"
/>
<a
href="javascript:void(0)"
className="pd-image-btn btn-next"
/> />
</div> </div>
<div className="pd-gallery-list"> <div className="pd-gallery-list">
<div className="swiper w-full" id="js-pd-gallery"> <Swiper
<div className="swiper-wrapper"> modules={[Navigation]}
<div className="swiper-slide"> spaceBetween={12}
<a slidesPerView={5}
href="images/product-1.jpg" >
rel="zoom-id:Zoomer" {images.map((img, index) => (
rev="images/product-1.jpg" <SwiperSlide key={img}>
> <img
<img src="images/product-1.jpg" alt="" /> src={img}
</a> onClick={() => setActiveIndex(index)}
</div> className={`cursor-pointer border rounded-md
<div className="swiper-slide"> ${activeIndex === index
<a ? 'border-[#0678DB]'
href="images/product-2.jpg" : 'border-transparent'}`}
rel="zoom-id:Zoomer" />
rev="images/product-2.jpg" </SwiperSlide>
> ))}
<img src="images/product-2.jpg" alt="" /> </Swiper>
</a>
</div>
<div className="swiper-slide">
<a
href="images/product-3.jpg"
rel="zoom-id:Zoomer"
rev="images/product-3.jpg"
>
<img src="images/product-3.jpg" alt="" />
</a>
</div>
<div className="swiper-slide">
<a
href="images/product-4.jpg"
rel="zoom-id:Zoomer"
rev="images/product-4.jpg"
>
<img src="images/product-4.jpg" alt="" />
</a>
</div>
<div className="swiper-slide">
<a
href="images/product-5.jpg"
rel="zoom-id:Zoomer"
rev="images/product-5.jpg"
>
<img src="images/product-5.jpg" alt="" />
</a>
</div>
<div className="swiper-slide">
<a
href="images/product-6.jpg"
rel="zoom-id:Zoomer"
rev="images/product-6.jpg"
>
<img src="images/product-6.jpg" alt="" />
</a>
</div>
</div>
</div>
</div> </div>
</> </>
) )

View File

@@ -17,10 +17,22 @@ export default async function ProductDetail({ slug }: any) {
review, review,
visit, visit,
quantity, quantity,
productSummary productSummary,
productImage, imageCollection,
price, marketPrice, deal_list, price_off, sale_rules,
hasVAT, warranty,
specialOffer
} = slug } = slug
const image = {
productImage,
imageCollection
}
const priceData = {
price, marketPrice, deal_list, price_off, sale_rules, hasVAT, warranty, quantity
}
console.log(slug) console.log(slug)
return ( return (
@@ -33,7 +45,7 @@ export default async function ProductDetail({ slug }: any) {
<div className="gap-6 flex flex-wrap items-start leading-[18px]"> <div className="gap-6 flex flex-wrap items-start leading-[18px]">
<div className="col-left-group w-[424px] sticky top-[90px]"> <div className="col-left-group w-[424px] sticky top-[90px]">
<ProductImage /> <ProductImage data={image} />
</div> </div>
<div className="col-middle-group w-[464px]"> <div className="col-middle-group w-[464px]">
@@ -69,9 +81,9 @@ export default async function ProductDetail({ slug }: any) {
<ProductSummary item={productSummary} /> <ProductSummary item={productSummary} />
} }
<ProductPrice /> <ProductPrice item={priceData} />
<ProductOffer /> <ProductOffer item={specialOffer}/>
<Buttons /> <Buttons />

View File

@@ -1,43 +1,21 @@
export default function ProductOffer() { import { formatTextList } from "@/lib/utils"
import parse from 'html-react-parser';
export default function ProductOffer({ item }: any) {
return ( return (
<> <>
{/* Khuyến mại hấp dẫn */} {item.all.length > 0 &&
<div className="pd-offer-group mb-4 bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2 rounded-[8px]"> <div className="pd-offer-group mb-4 bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2 rounded-[8px]">
<div className="group-title font-600 text-white flex items-center leading-[22px] mb-2 px-2 text-16"> <div className="group-title font-600 text-white flex items-center leading-[22px] mb-2 px-2 text-16">
<i className="icons icon-discount mr-[6px] animation-tada" /> <i className="icons icon-gift mr-2 animation-tada -mt-1" />
<span> Khuyến mại hấp dẫn </span> <span> Quà tặng ưu đãi kèm theo </span>
</div>
<div className="rounded-[8px] bg-[#FEF2F2] px-2 py-4">
<div className="item">
{" "}
Giảm ngay 100.000đ khi mua thêm Màn Hình Máy Tính.
</div> </div>
<div className="item"> Giảm ngay 100.000đ khi mua thêm RAM </div>
<div className="item">
{" "}
Giảm ngay 100.000đ khi mua thêm Card màn hình{" "}
</div>
</div>
</div>
{/* Quà tặng và ưu đãi kèm theo */} <div className="rounded-[8px] bg-[#FEF2F2] px-2 py-4">
<div className="pd-offer-group mb-4 bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2 rounded-[8px]"> {parse(formatTextList(item.all[0].title))}
<div className="group-title font-600 text-white flex items-center leading-[22px] mb-2 px-2 text-16">
<i className="icons icon-gift mr-2 animation-tada -mt-1" />
<span> Quà tặng ưu đãi kèm theo </span>
</div>
<div className="rounded-[8px] bg-[#FEF2F2] px-2 py-4">
<div className="item">
{" "}
Giảm ngay 100.000đ khi mua thêm Màn Hình Máy Tính.
</div>
<div className="item"> Giảm ngay 100.000đ khi mua thêm RAM </div>
<div className="item">
{" "}
Giảm ngay 100.000đ khi mua thêm Card màn hình{" "}
</div> </div>
</div> </div>
</div> }
</> </>
) )
} }

View File

@@ -0,0 +1,92 @@
import { formatPrice } from "@/lib/utils";
import { DealCountdown } from "@/lib/times"
export default function DealPrice( {item} : any ) {
const price = item.sale_rules.price;
const normal_price = item.sale_rules.normal_price;
const discount = Math.ceil(((normal_price - price) / normal_price) * 100);
const sale_quantity = item.deal_list[0].sale_quantity;
const quantity = item.deal_list[0].quantity;
const saleRemainPercent = 100 - (sale_quantity / quantity) * 100;
return (
<>
<div className="pd-deal-group rounded-[12px] bg-[#FEF2F2] overflow-hidden border border-[#FA354A] my-3">
<div className="group-title p-2 bg-[linear-gradient(270.05deg,#CB0F23_0.04%,#FF3246_99.97%)] flex items-center flex-wrap justify-between gap-2">
<p className="m-0 leading-7 font-600 text-18 flex items-center gap-[6px] text-white">
<i className="icons icon-flame animation-beat" />
<span> BIG SALE </span>
</p>
<div className="relative bg-[#EBEBEB] rounded-[20px] text-center font-500 text-13 leading-[22px] px-[51px]">
<i className="w-[22px] h-[28px] absolute left-[-8px] top-[-4px] z-[1] bg-no-repeat bg-center bg-[length:100%_100%] animation-bounce lazy"
data-bg="url(images/deal-icon-bolt.png)"
/>
<i className="bg-[#FFE078] absolute inset-0 max-w-[100%] rounded-[20px]"
style={{ width: saleRemainPercent+"%" }}
/>
<span className="relative z-[1] block">
Còn: {sale_quantity}/{quantity} sản phẩm
</span>
</div>
</div>
<div className="p-4">
<div className="leading-8 flex items-baseline flex-wrap mb-1">
<p className="pd-price text-[#FF4E2A] font-bold mb-0 mr-3 text-24">
{formatPrice(price)} đ
</p>
{ discount > 0 &&
<>
<del className="mr-2 text-16">
{ formatPrice(normal_price) } đ
</del>
<span className="pd-discount bg-[#FA354A] text-white leading-4 rounded-[20px] px-[6px] font-500">
-{discount}%
</span>
</>
}
</div>
<div className="flex flex-wrap gap-1 font-500 text-[#2563EB] leading-[22px]">
{item.hasVAT == 1 &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Giá đã bao gồm VAT </p>
}
{item.hasVAT == 2 &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Giá chưa bao gồm VAT </p>
}
{item.warranty &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> {item.warranty} </p>
}
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{ item.quantity > 0 ? 'Còn hàng' : 'Liên hệ' }
</p>
</div>
<div className="my-2 flex items-center leading-6 gap-2">
<p className="m-0"> Kết thúc sau: </p>
<div className="deal-time-holder flex items-center gap-5 text-white text-14 font-600">
<DealCountdown endTime={item.deal_list[0].to_time} />
</div>
</div>
<p className="m-0 font-600 text-13 leading-4 text-[#FF4E2A] mt-3">
*KHÔNG ÁP DỤNG CỘNG DỒN CHƯƠNG TRÌNH KHUYẾN MẠI KHÁC
</p>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,40 @@
import { formatPrice } from "@/lib/utils";
export default function Price( {item} : any ) {
return (
<div className="my-3 border border-[#FA354A] rounded-[12px] leading-[22px] p-4 pd-price-group">
<div className="leading-8 flex items-baseline flex-wrap mb-1">
<p className="pd-price text-[#FF4E2A] font-bold mb-0 mr-3 text-24">
{item.price > 0 ? formatPrice(item.price) + 'đ' : ''}
</p>
{ item.price_off &&
<>
<del className="mr-2 text-16"> {formatPrice(item.marketPrice)} đ </del>
<span className="pd-discount bg-[#FA354A] text-white leading-4 rounded-[20px] px-[6px] font-500">
-{item.price_off}%
</span>
</>
}
</div>
<div className="flex flex-wrap gap-1 font-500 text-[#2563EB]">
{item.hasVAT == 1 &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Giá đã bao gồm VAT </p>
}
{item.hasVAT == 2 &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Giá chưa bao gồm VAT </p>
}
{item.warranty &&
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> {item.warranty} </p>
}
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{ item.quantity > 0 ? 'Còn hàng' : 'Liên hệ' }
</p>
</div>
</div>
)
}

View File

@@ -1,86 +1,18 @@
export default function ProductPrice() { import DealPrice from "./DealPrice";
import Price from "./Price";
export default function ProductPrice({ item }: any) {
if (!item) return null
const isDeal = item?.sale_rules?.type === 'deal';
return ( return (
<> <>
{/* Deal */} {isDeal ? (
<div className="pd-deal-group rounded-[12px] bg-[#FEF2F2] overflow-hidden border border-[#FA354A] "> <DealPrice item={item} />
<div className="group-title p-2 bg-[linear-gradient(270.05deg,#CB0F23_0.04%,#FF3246_99.97%)] flex items-center flex-wrap justify-between gap-2"> ) : (
<p className="m-0 leading-7 font-600 text-18 flex items-center gap-[6px] text-white"> <Price item={item} />
<i className="icons icon-flame animation-beat" /> )}
<span> BIG SALE </span>
</p>
<div className="relative bg-[#EBEBEB] rounded-[20px] text-center font-500 text-13 leading-[22px] px-[51px]">
<i
className="w-[22px] h-[28px] absolute left-[-8px] top-[-4px] z-[1] bg-no-repeat bg-center bg-[length:100%_100%] animation-bounce lazy"
data-bg="url(images/deal-icon-bolt.png)"
/>
<i
className="bg-[#FFE078] absolute inset-0 max-w-[100%] rounded-[20px]"
style={{ width: "50%" }}
/>
<span className="relative z-[1] block"> Còn: 3/5 sản phẩm </span>
</div>
</div>
<div className="p-4">
<div className="leading-8 flex items-baseline flex-wrap mb-1">
<p className="pd-price text-[#FF4E2A] font-bold mb-0 mr-3 text-24">
{" "}
48.990.000 đ{" "}
</p>
<del className="mr-2 text-16"> 52.000.000 đ </del>
<span className="pd-discount bg-[#FA354A] text-white leading-4 rounded-[20px] px-[6px] font-500">
{" "}
-6%{" "}
</span>
</div>
<div className="flex flex-wrap gap-1 font-500 text-[#2563EB] leading-[22px]">
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{" "}
Giá đã bao gồm VAT{" "}
</p>
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{" "}
Bảo hành theo từng linh kiện{" "}
</p>
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Liên hệ </p>
</div>
<div className="my-2 flex items-center leading-6 gap-2">
<p className="m-0"> Kết thúc sau: </p>
<div className="deal-time-holder flex items-center gap-5 text-white text-14 font-600">
<p> 00 </p> <p> 00 </p> <p> 00 </p> <p> 00 </p>
</div>
</div>
<p className="m-0 font-600 text-13 leading-4 text-[#FF4E2A] mt-3">
{" "}
*KHÔNG ÁP DỤNG CỘNG DỒN CHƯƠNG TRÌNH KHUYẾN MẠI KHÁC{" "}
</p>
</div>
</div>
{/* Price */}
<div className="my-3 border border-[#FA354A] rounded-[12px] leading-[22px] p-4 pd-price-group">
<div className="leading-8 flex items-baseline flex-wrap mb-1">
<p className="pd-price text-[#FF4E2A] font-bold mb-0 mr-3 text-24">
{" "}
48.990.000 đ{" "}
</p>
<del className="mr-2 text-16"> 52.000.000 đ </del>
<span className="pd-discount bg-[#FA354A] text-white leading-4 rounded-[20px] px-[6px] font-500">
{" "}
-6%{" "}
</span>
</div>
<div className="flex flex-wrap gap-1 font-500 text-[#2563EB]">
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{" "}
Giá đã bao gồm VAT{" "}
</p>
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2">
{" "}
Bảo hành theo từng linh kiện{" "}
</p>
<p className="m-0 bg-[#EFF6FF] rounded-[6px] px-2"> Liên hệ </p>
</div>
</div>
</> </>
) )
} }

View File

@@ -1,6 +1,11 @@
'use client';
import Link from "next/link"
import FancyboxWrapper from "@/components/shared/FancyboxWrapper"
export default function Static() { export default function Static() {
return ( return (
<> <FancyboxWrapper>
<div className="group relative border border-[#D6DAE1] leading-[38px] rounded-[8px] pl-3 pr-2 mb-3"> <div className="group relative border border-[#D6DAE1] leading-[38px] rounded-[8px] pl-3 pr-2 mb-3">
<p className="m-0 flex items-center justify-between cursor-pointer"> <p className="m-0 flex items-center justify-between cursor-pointer">
<span> Xem chi nhánh còn hàng </span> <span> Xem chi nhánh còn hàng </span>
@@ -11,7 +16,7 @@ export default function Static() {
<b className="block underline px-2 font-600 mb-2"> <b className="block underline px-2 font-600 mb-2">
Showroom Miền Bắc: Showroom Miền Bắc:
</b> </b>
<a <Link
href="https://goo.gl/maps/56ARHjWKoVhpWBCF6" href="https://goo.gl/maps/56ARHjWKoVhpWBCF6"
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
@@ -19,8 +24,8 @@ export default function Static() {
> >
<i className="icons icon-location" /> <i className="icons icon-location" />
<span> 41 Khúc Thừa Dụ, Phường Cầu Giấy, Nội </span> <span> 41 Khúc Thừa Dụ, Phường Cầu Giấy, Nội </span>
</a> </Link>
<a <Link
href="https://g.page/hoanghapc?share" href="https://g.page/hoanghapc?share"
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
@@ -28,13 +33,13 @@ export default function Static() {
> >
<i className="icons icon-location" /> <i className="icons icon-location" />
<span> 94E-94F Đưng Láng, Phường Đng Đa, Nội </span> <span> 94E-94F Đưng Láng, Phường Đng Đa, Nội </span>
</a> </Link>
</div> </div>
<div className="my-3"> <div className="my-3">
<b className="block underline px-2 font-600 mb-2"> <b className="block underline px-2 font-600 mb-2">
Showroom Miền Trung: Showroom Miền Trung:
</b> </b>
<a <Link
href="https://goo.gl/maps/1HQrD6mdf4VMYccs6" href="https://goo.gl/maps/1HQrD6mdf4VMYccs6"
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
@@ -42,13 +47,13 @@ export default function Static() {
> >
<i className="icons icon-location" /> <i className="icons icon-location" />
<span>72 Lợi, Thành Vinh, Nghệ An </span> <span>72 Lợi, Thành Vinh, Nghệ An </span>
</a> </Link>
</div> </div>
<div className="my-3"> <div className="my-3">
<b className="block underline px-2 font-600 mb-2"> <b className="block underline px-2 font-600 mb-2">
Showroom Miền Nam: Showroom Miền Nam:
</b> </b>
<a <Link
href="https://g.page/hoanghapchcm?share" href="https://g.page/hoanghapchcm?share"
target="_blank" target="_blank"
rel="nofollow" rel="nofollow"
@@ -56,102 +61,102 @@ export default function Static() {
> >
<i className="icons icon-location" /> <i className="icons icon-location" />
<span> <span>
{" "}
K8bis Bửu Long, Phường Hoà Hưng, Thành phố Hồ Chí Minh K8bis Bửu Long, Phường Hoà Hưng, Thành phố Hồ Chí Minh
</span> </span>
</a> </Link>
</div> </div>
<i className="block red my-3 px-2"> <i className="block red my-3 px-2">
Chú ý: Sản phẩm thể điều chuyển kho theo yêu cầu của quý khách. Chú ý: Sản phẩm thể điều chuyển kho theo yêu cầu của quý khách.
</i> </i>
</div> </div>
</div> </div>
{/* Yên Tâm Mua Sắm Tại HoangHaPC */} {/* Yên Tâm Mua Sắm Tại HoangHaPC */}
<div className="pd-static-group mb-3 rounded-[12px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,#114CDD_100.92%)] p-1 pt-2"> <div className="pd-static-group mb-3 rounded-[12px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,#114CDD_100.92%)] p-1 pt-2">
<p className="group-title text-white leading-[21px] text-16 font-600 mb-2 text-center"> <p className="group-title text-white leading-[21px] text-16 font-600 mb-2 text-center">
{" "} Yên Tâm Mua Sắm Tại HoangHaPC
Yên Tâm Mua Sắm Tại HoangHaPC{" "}
</p> </p>
<div className="pd-static-list bg-white p-[16px_8px] leading-[18px] font-500 rounded-[8px]"> <div className="pd-static-list bg-white p-[16px_8px] leading-[18px] font-500 rounded-[8px]">
<p className="last:mb-0 mb-2 item-circle"> <p className="last:mb-0 mb-2 item-circle">
{" "} Đi ngũ kỹ thuật vấn chuyên sâu
Đi ngũ kỹ thuật vấn chuyên sâu{" "}
</p> </p>
<p className="last:mb-0 mb-2 item-circle"> <p className="last:mb-0 mb-2 item-circle">
{" "} Thanh toán thuận tiện
Thanh toán thuận tiện{" "}
</p> </p>
<p className="last:mb-0 mb-2 item-circle"> <p className="last:mb-0 mb-2 item-circle">
{" "} Sản phẩm 100% chính hãng
Sản phẩm 100% chính hãng{" "}
</p> </p>
<p className="last:mb-0 mb-2 item-circle"> <p className="last:mb-0 mb-2 item-circle">
{" "} Bảo hành 1 đi 1 tại nơi sử dụng
Bảo hành 1 đi 1 tại nơi sử dụng{" "}
</p> </p>
<p className="last:mb-0 mb-2 item-circle"> <p className="last:mb-0 mb-2 item-circle">
{" "} Giá cạnh tranh nhất thị trường
Giá cạnh tranh nhất thị trường{" "}
</p> </p>
</div> </div>
</div> </div>
{/* Liên Hệ Với Kinh Doanh Online */}
<div className="pd-static-group mb-3 rounded-[12px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,#114CDD_100.92%)] p-1 pt-2"> <div className="pd-static-group mb-3 rounded-[12px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,#114CDD_100.92%)] p-1 pt-2">
<p className="group-title text-white leading-[21px] text-16 font-600 mb-2 text-center"> <p className="group-title text-white leading-[21px] text-16 font-600 mb-2 text-center">
{" "} Liên Hệ Với Kinh Doanh Online
Liên Hệ Với Kinh Doanh Online{" "}
</p> </p>
<div className="pd-static-list bg-white p-[16px_8px] leading-[18px] font-500 rounded-[8px]"> <div className="pd-static-list bg-white p-[16px_8px] leading-[18px] font-500 rounded-[8px]">
<div className="last:mb-0 mb-2 flex gap-2"> <div className="last:mb-0 mb-2 flex gap-2">
<i className="icons icon-phone" /> <i className="icons icon-phone" />
<p className="m-0"> <p className="m-0">
Hotline Nội:{" "} Hotline Nội:
<a href="tel:0969123666" className="red font-500"> <Link href="tel:0969123666" className="red font-500">
{" "} 0969123666
0969123666{" "} </Link>
</a>
</p> </p>
</div> </div>
<div className="last:mb-0 mb-2 flex gap-2"> <div className="last:mb-0 mb-2 flex gap-2">
<i className="icons icon-phone" /> <i className="icons icon-phone" />
<p className="m-0"> <p className="m-0">
Hotline Vinh, Nghệ An:{" "} Hotline Vinh, Nghệ An:
<a href="tel:0988.163.666" className="red font-500"> <Link href="tel:0988.163.666" className="red font-500">
{" "} 0988.163.666
0988.163.666{" "} </Link>
</a>
</p> </p>
</div> </div>
<div className="last:mb-0 mb-2 flex gap-2"> <div className="last:mb-0 mb-2 flex gap-2">
<i className="icons icon-phone" /> <i className="icons icon-phone" />
<p className="m-0"> <p className="m-0">
Hotline Hồ Chí Minh:{" "} Hotline Hồ Chí Minh:
<a href="tel:0968.123.666" className="red font-500"> <Link href="tel:0968.123.666" className="red font-500">
{" "} 0968.123.666
0968.123.666{" "} </Link>
</a>
</p> </p>
</div> </div>
<div className="last:mb-0 mb-2 flex gap-2"> <div className="last:mb-0 mb-2 flex gap-2">
<i className="icons icon-phone" /> <i className="icons icon-phone" />
<p className="m-0"> <p className="m-0">
Hotline Bảo Hành:{" "} Hotline Bảo Hành:
<a href="tel:1900.6100" className="red font-500"> <Link href="tel:1900.6100" className="red font-500">
{" "} 1900.6100
1900.6100{" "} </Link>
</a>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div className="border border-[#0678DB] rounded-[12px] px-3 py-4 gap-[6px] flex flex-wrap items-center"> <div className="border border-[#0678DB] rounded-[12px] px-3 py-4 gap-[6px] flex flex-wrap items-center">
<a <a
href="https://hoanghapc.vn/media/lib/17-10-2022/qr-hoang-ha-pc-nhom.png" href="https://hoanghapc.vn/media/lib/17-10-2022/qr-hoang-ha-pc-nhom.png"
data-fancybox="" data-fancybox="gallery"
className="w-[110px]" className="w-[110px]"
> >
<img <img
data-src="https://hoanghapc.vn/media/lib/17-10-2022/qr-hoang-ha-pc-nhom.png" src="https://hoanghapc.vn/media/lib/17-10-2022/qr-hoang-ha-pc-nhom.png"
alt="QR code" alt="QR code"
width={110} width={110}
height={110} height={110}
@@ -159,11 +164,11 @@ export default function Static() {
/> />
</a> </a>
<p className="m-0 font-500 w-[calc(100%-116px)]"> <p className="m-0 font-500 w-[calc(100%-116px)]">
{" "}
Tham gia Cộng đng "Cẩm Nang Build PC - Đồ Họa, Render, Giả Lập" đ Tham gia Cộng đng "Cẩm Nang Build PC - Đồ Họa, Render, Giả Lập" đ
theo dõi các ưu đãi dành riêng cho thành viên{" "} theo dõi các ưu đãi dành riêng cho thành viên
</p> </p>
</div> </div>
</> </FancyboxWrapper>
) )
} }

View File

@@ -1,7 +1,17 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'
export default function ProductSummary({ item }: any) { export default function ProductSummary({ item }: any) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return ( return (
<div className="mb-3 pd-summary-group"> <div className="mb-3 pd-summary-group">
<p className="leading-6 mb-2 text-16 font-600"> Thông số sản phẩm </p> <p className="leading-6 mb-2 text-16 font-600"> Thông số sản phẩm </p>
@@ -12,33 +22,33 @@ export default function ProductSummary({ item }: any) {
} }
function renderSummary(data:any) { function renderSummary(data: any) {
if (!data) return null; if (!data) return null;
if (typeof data === 'string' && data.includes('<')) { if (typeof data === 'string' && data.includes('<')) {
if (typeof window === 'undefined') return null; const parser = new DOMParser()
const doc = parser.parseFromString(data, 'text/html')
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
return Array.from(doc.body.childNodes) return Array.from(doc.body.childNodes)
.filter( .filter(
node => node =>
node.nodeType === 1 && node.textContent !== null && node.textContent.trim() !== '' node.nodeType === 1 &&
node.textContent &&
node.textContent.trim() !== ''
) )
.map((node, index) => ( .map((node, index) => (
<div key={index} className="item-circle"> <div key={index} className="item-circle">
{node.textContent?.trim()} {node.textContent!.trim()}
</div> </div>
)); ))
} }
return data return data
.split(/\r?\n/) .split(/\r?\n/)
.filter((line:any) => line.trim() !== '') .filter((line: string) => line.trim() !== '')
.map((line:any, index:any) => ( .map((line: string, index: number) => (
<div key={index} className="item-circle"> <div key={index} className="item-circle">
{line.trim()} {line.trim()}
</div> </div>
)); ))
} }

View File

@@ -0,0 +1,19 @@
'use client';
import { useEffect } from 'react';
import { Fancybox as NativeFancybox } from '@fancyapps/ui';
export default function FancyboxWrapper({ children }: { children: React.ReactNode }) {
useEffect(() => {
NativeFancybox.bind('[data-fancybox]', {
// Options
});
return () => {
NativeFancybox.destroy();
};
}, []);
return <>{children}</>;
}

View File

@@ -1,9 +1,10 @@
'use client'; 'use client';
import Link from "next/link"; import Link from "next/link";
import { formatPrice } from "@/lib/utils"; import { formatPrice,formatTextList } from "@/lib/utils";
import { useProductItem } from "@/hooks/useProductItem" import { useProductItem } from "@/hooks/useProductItem"
import { useCart } from '@/hooks/useCart'; import { useCart } from '@/hooks/useCart';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import parse from 'html-react-parser';
export default function ProductItem({item}:any){ export default function ProductItem({item}:any){
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -131,8 +132,7 @@ export default function ProductItem({item}:any){
</p> </p>
<div className="tooltip-spec"> <div className="tooltip-spec">
<div suppressHydrationWarning {parse(formatTextList(displaySummary, 5))}
dangerouslySetInnerHTML={{ __html: displaySummary }}/>
</div> </div>
</div> </div>
} }
@@ -145,8 +145,7 @@ export default function ProductItem({item}:any){
</p> </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 suppressHydrationWarning {parse(formatTextList(displayOffer, 5))}
dangerouslySetInnerHTML={{ __html: displayOffer }}/>
</div> </div>
</div> </div>
} }

55
src/lib/times.tsx Normal file
View File

@@ -0,0 +1,55 @@
'use client';
import { useEffect, useState } from "react";
export function DealCountdown({ endTime }: { endTime: number }) {
const [mounted, setMounted] = useState(false);
const [timeLeft, setTimeLeft] = useState(getTimeLeft(endTime));
useEffect(() => {
setMounted(true);
const timer = setInterval(() => {
setTimeLeft(getTimeLeft(endTime));
}, 1000);
return () => clearInterval(timer);
}, [endTime]);
if (!mounted) return null;
if (timeLeft.total <= 0) {
return <span className="text-red-500">Deal đã kết thúc</span>;
}
return (
<>
<p>{timeLeft.days}</p>
<p>{timeLeft.hours}</p>
<p>{timeLeft.minutes}</p>
<p>{timeLeft.seconds}</p>
</>
);
}
export function getTimeLeft(endTime: number) {
const now = Math.floor(Date.now() / 1000);
const distance = endTime - now;
if (distance <= 0) {
return {
total : 0,
days : '00',
hours : '00',
minutes : '00',
seconds : '00'
};
}
return {
total : distance,
days : String(Math.floor(distance / 86400)).padStart(2, '0'),
hours : String(Math.floor((distance % 86400) / 3600)).padStart(2, '0'),
minutes : String(Math.floor((distance % 3600) / 60)).padStart(2, '0'),
seconds : String(distance % 60).padStart(2, '0'),
};
}

View File

@@ -8,7 +8,7 @@ export function getAllProducts() {
export function formatTextList( export function formatTextList(
text?: string | any[], text?: string | any[],
limit = 5 limit?: number | undefined
) { ) {
if (!text) return ''; if (!text) return '';

View File

@@ -1,8 +1,6 @@
@import url('https://cdn.boxicons.com/fonts/basic/boxicons.min.css'); @import url('https://cdn.boxicons.com/fonts/basic/boxicons.min.css');
@import url('https://fonts.cdnfonts.com/css/sf-pro-display'); @import url('https://fonts.cdnfonts.com/css/sf-pro-display');
@import "@fancyapps/ui/dist/fancybox/fancybox.css"; @import "@fancyapps/ui/dist/fancybox/fancybox.css";
@import "tailwindcss"; @import "tailwindcss";
@import 'swiper/css'; @import 'swiper/css';
@import 'swiper/css/navigation'; @import 'swiper/css/navigation';
@@ -11,5 +9,5 @@
@import "../../public/styles/tailwind.css"; @import "../../public/styles/tailwind.css";
@import './pc_style.css'; @import './pc_style.css';
.fancybox__dialog .fancybox__container.is-ready {opacity: 1;} .fancybox__dialog .fancybox__container.is-ready {opacity: 1;}
.mz-thumb:not(.mz-thumb-selected):hover img{border: 0 !important}

View File

@@ -273,7 +273,7 @@ body{min-width:1248px;background:#E8ECF6}
.item-circle::after{content:"";background:#fff;width:3.5px;height:3.5px;border-radius:50%;position:absolute;left:6.5px;top:6px} .item-circle::after{content:"";background:#fff;width:3.5px;height:3.5px;border-radius:50%;position:absolute;left:6.5px;top:6px}
.item-circle * {margin: 0;} .item-circle * {margin: 0;}
.pd-gallery-list a{border:2px solid transparent;overflow:hidden;display:block;pointer-events:auto!important} .pd-gallery-list a{border:2px solid transparent;overflow:hidden;display:block;pointer-events:auto!important}
.pd-gallery-list img{filter:unset!important;display:block;margin:auto;max-width:100%;max-height:100%} .pd-gallery-list img{filter:unset!important;display:block;margin:auto;max-width:100%;max-height:100%;border-width: 2px}
.pd-gallery-list .mz-thumb-selected{border-color:#0678DB} .pd-gallery-list .mz-thumb-selected{border-color:#0678DB}
.pd-offer-group .icon-discount{background-position:-76px -194px} .pd-offer-group .icon-discount{background-position:-76px -194px}
.pd-offer-group .item{margin:0 0 8px} .pd-offer-group .item{margin:0 0 8px}