update
This commit is contained in:
@@ -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 ?? [];
|
||||
|
||||
|
||||
@@ -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 { productDealData } from './productDealData';
|
||||
|
||||
import CounDown from './CounDown';
|
||||
import ProductItem from './ProductItem';
|
||||
|
||||
const BoxProductDeal: React.FC = () => {
|
||||
return (
|
||||
@@ -18,9 +22,9 @@ const BoxProductDeal: React.FC = () => {
|
||||
<CounDown />
|
||||
</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>
|
||||
>
|
||||
{productDealData.map((Item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<ProductItem item={Item} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ProductDetailData } from '@/types';
|
||||
|
||||
export const BoxPrice = (item: ProductDetailData) => {
|
||||
return (
|
||||
<>
|
||||
{item.product_info.sale_rules.type == 'deal' && (
|
||||
<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"></div>
|
||||
<div className="box-right">
|
||||
<div className="box-product-deal"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
130
src/components/product/ProductDetail/BoxInfoRight/index.tsx
Normal file
130
src/components/product/ProductDetail/BoxInfoRight/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { ProductDetailData } from '@/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { BoxPrice } from './BoxPrice';
|
||||
|
||||
export const BoxInfoRight = (item: ProductDetailData) => {
|
||||
return (
|
||||
<>
|
||||
<h1 className="product-name color-black line-clamp-3">{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>
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => addProductToCart(item.product_info.id, 0, '')}
|
||||
className="addCart flex flex-wrap items-center justify-center gap-3"
|
||||
>
|
||||
<i className="sprite sprite-cart-detail"></i>
|
||||
<p className="title-cart">Thêm vào giỏ hàng</p>
|
||||
</Link>
|
||||
<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">
|
||||
<Link
|
||||
href="#"
|
||||
className="detail-buy-now col-span-2"
|
||||
onClick={() => buyNow(item.product_info.id, 0, '')}
|
||||
>
|
||||
<span>ĐẶT MUA NGAY</span>
|
||||
Giao hàng tận nơi nhanh chóng
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="#"
|
||||
className="detail-add-cart"
|
||||
onClick={() => buyPayInstall(item.product_info.id, 0, '')}
|
||||
>
|
||||
<span>TRẢ GÓP QUA HỒ SƠ</span>
|
||||
Chỉ từ 2.665.000₫/ tháng
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="#"
|
||||
className="detail-add-cart"
|
||||
onClick={() => buyAlepay(item.product_info.id, 0, '')}
|
||||
>
|
||||
<span>TRẢ GÓP QUA THẺ</span>
|
||||
Chỉ từ 1.332.500₫/ tháng
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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/components/product/ProductDetail/ComboSet/index.tsx
Normal file
114
src/components/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>
|
||||
);
|
||||
};
|
||||
@@ -5,8 +5,7 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ProductImageGallery } from '@/types';
|
||||
import type { Swiper as SwiperType } from 'swiper';
|
||||
import '@fancyapps/ui/dist/fancybox/fancybox.css';
|
||||
import { Fancybox, type FancyboxOptions } from '@fancyapps/ui';
|
||||
import useFancybox from '@/hooks/useFancybox';
|
||||
|
||||
interface ImageProps {
|
||||
ItemImage: ProductImageGallery[];
|
||||
@@ -15,30 +14,14 @@ interface ImageProps {
|
||||
export const ImageProduct: React.FC<ImageProps> = ({ ItemImage }) => {
|
||||
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
Fancybox.bind("[data-fancybox='gallery']", {
|
||||
dragToClose: true,
|
||||
Toolbar: {
|
||||
display: {
|
||||
left: [],
|
||||
middle: ['counter'],
|
||||
right: ['zoom', 'close'],
|
||||
},
|
||||
},
|
||||
Thumbs: { autoStart: false },
|
||||
} as any);
|
||||
|
||||
return () => {
|
||||
Fancybox.unbind("[data-fancybox='gallery']");
|
||||
Fancybox.close();
|
||||
};
|
||||
}, []);
|
||||
const [fancyboxRef] = useFancybox({
|
||||
closeButton: 'auto',
|
||||
dragToClose: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="product-images-show">
|
||||
<div className="gallery-top product-info-image">
|
||||
<div className="gallery-top product-info-image" ref={fancyboxRef}>
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={12}
|
||||
@@ -48,7 +31,7 @@ export const ImageProduct: React.FC<ImageProps> = ({ ItemImage }) => {
|
||||
>
|
||||
{ItemImage?.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<Link href={item.size.original} data-fancybox="gallery" className="bigImage">
|
||||
<Link href={item.size.original} className="bigImage" data-fancybox>
|
||||
<Image src={item.size.original} alt={''} width="595" height="595" />
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,9 @@ 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';
|
||||
|
||||
interface ProductDetailPageProps {
|
||||
slug: string;
|
||||
@@ -39,8 +42,14 @@ const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ slug }) => {
|
||||
<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 className="box-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
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];
|
||||
}
|
||||
4
src/lib/formatPrice.ts
Normal file
4
src/lib/formatPrice.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const formatCurrency = (value: number | string) => {
|
||||
const num = typeof value === 'string' ? parseInt(value) : value;
|
||||
return num.toLocaleString('vi-VN');
|
||||
};
|
||||
@@ -3426,7 +3426,6 @@ textarea::placeholder {
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
.page-product-detail .box-content-product-detail .detail-buy a {
|
||||
width: calc(50% - 6px);
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
background: #0a76e4;
|
||||
@@ -3541,20 +3540,7 @@ textarea::placeholder {
|
||||
font-size: 13px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-product-detail
|
||||
.box-content-product-detail
|
||||
.box-product-summary
|
||||
.list-product-summary
|
||||
li:nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
.page-product-detail
|
||||
.box-content-product-detail
|
||||
.box-product-summary
|
||||
.list-product-summary.active
|
||||
li:nth-child(n + 4) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-product-detail .box-content-product-detail .list-product-comboset {
|
||||
padding: 2px;
|
||||
margin-bottom: 16px;
|
||||
@@ -4308,7 +4294,7 @@ textarea::placeholder {
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
.popup-change-pro .popup-main .c-btn {
|
||||
.c-pro-item .c-btn {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -199,7 +199,7 @@ export interface ProductInfo {
|
||||
}
|
||||
|
||||
// Combo Set
|
||||
interface ComboProduct {
|
||||
export interface ComboProduct {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
@@ -216,14 +216,14 @@ interface ComboProduct {
|
||||
is_recommended: string;
|
||||
}
|
||||
|
||||
interface ComboGroup {
|
||||
export interface ComboGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
product_list: ComboProduct[];
|
||||
product_count: string;
|
||||
}
|
||||
|
||||
interface ComboSet {
|
||||
export interface ComboSet {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
Reference in New Issue
Block a user