This commit is contained in:
2025-12-28 21:43:14 +07:00
parent 71089d1eef
commit 1bb5ad52ed
17 changed files with 3858 additions and 37 deletions

View File

@@ -10,7 +10,6 @@ export const BoxInfoRight = (item: ProductDetailData) => {
<h1 className="product-name color-black line-clamp-3 font-bold">
{item.product_info.productName}
</h1>
<div className="list-basic-product-info flex flex-wrap items-center">
<div className="item-basic">
SP: <span className="color-primary">{item.product_info.productSKU}</span>
@@ -32,7 +31,6 @@ export const BoxInfoRight = (item: ProductDetailData) => {
</div>
)}
</div>
{/* tình trạng */}
<div className="list-basic-product-info flex flex-wrap items-center gap-6">
<div className="item-basic">
@@ -45,11 +43,8 @@ export const BoxInfoRight = (item: ProductDetailData) => {
</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">
@@ -66,7 +61,6 @@ export const BoxInfoRight = (item: ProductDetailData) => {
</div>
</div>
)}
{/* mua hàng */}
{(item.product_info.quantity > '0' || item.product_info.price > '0') && (
<>
@@ -87,11 +81,7 @@ export const BoxInfoRight = (item: ProductDetailData) => {
+{' '}
</p>
</div>
<Link
href="#"
onClick={() => addProductToCart(item.product_info.id, 0, '')}
className="addCart flex flex-wrap items-center justify-center gap-3"
>
<Link href="#" 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>
@@ -99,37 +89,24 @@ export const BoxInfoRight = (item: ProductDetailData) => {
</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, '')}
>
<Link href="#" className="detail-buy-now col-span-2">
<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, '')}
>
<Link href="#" className="detail-add-cart">
<span>TRẢ GÓP QUA HỒ </span>
Chỉ từ 2.665.000/ tháng
</Link>
<Link
href="#"
className="detail-add-cart"
onClick={() => buyAlepay(item.product_info.id, 0, '')}
>
<Link href="#" className="detail-add-cart">
<span>TRẢ GÓP QUA THẺ</span>
Chỉ từ 1.332.500/ tháng
</Link>
</div>
</>
)}
{/* yên tâm mua hàng */}
n{/* yên tâm mua hàng */}
<div className="box-product-policy-detal boder-radius-10" style={{ marginTop: '24px' }}>
<h2 className="title font-[600]">Yên tâm mua hàng</h2>
<div className="list-showroom-detail flex flex-wrap justify-between">
@@ -151,7 +128,6 @@ export const BoxInfoRight = (item: ProductDetailData) => {
</div>
</div>
</div>
<BoxBought />
</>
);

View File

@@ -0,0 +1,40 @@
import React from 'react';
interface FormCommentProps {
open: boolean;
onClose: () => void;
}
export const FormComment: React.FC<FormCommentProps> = ({ open, onClose }) => {
return (
<dialog id="commentDialog" className={`modal ${open ? 'modal-open' : ''}`}>
{' '}
<div className="modal-box">
<h3 className="mb-4 font-semibold">Nhập thông tin</h3>
<div className="space-y-4">
<div className="flex gap-4">
<label className="label cursor-pointer">
<input type="radio" name="sex" value="Anh" className="radio radio-xs" />
<span className="ml-2">Anh</span>
</label>
<label className="label cursor-pointer">
<input type="radio" name="sex" value="Chị" className="radio radio-xs" />
<span className="ml-2">Chị</span>
</label>
</div>
<input type="text" className="input input-md w-[95%]" placeholder="Họ tên (bắt buộc)" />
<input
type="email"
className="input input-md w-[95%]"
placeholder="Email (để nhận phản hồi qua mail)"
/>
<button className="btn btn-active w-[93.5%] bg-red-500 text-white">Cập nhật</button>
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>Đóng</button>
</form>
</dialog>
);
};

View File

@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { ListCommentData } from '@/data/ListComment';
import Image from 'next/image';
export const ListComment = () => {
return (
<div className="comment-list">
{ListCommentData.slice(0.3).map((item, index) => (
<div className="item-comment" id={`comment_${item.id}`} key={index}>
<div className="form-reply-comment">
{/* header */}
<div className="comment-name flex justify-between">
<div className="comment-form-left flex items-center gap-2">
{item.user_avatar ? (
<b className="avatar-user">
<img src={item.user_avatar} alt={item.user_name} />
</b>
) : (
<b className="avatar-user flex items-center justify-center">
{' '}
{item.user_name.charAt(0)}{' '}
</b>
)}
<b className="user-name">{item.user_name}</b>
</div>
<div className="comment-form-right flex items-center gap-2 text-sm text-gray-500">
<i className="fa-regular fa-clock"></i> <span>{item.post_time}</span>
</div>
</div>{' '}
{/* content */}
<div className="comment-content mt-3 rounded p-2">
<p>{item.content}</p>
<div className="info_feeback mt-2 flex items-center gap-2">
<i className="sprite sprite-icon-reply-detail"></i>
<button className="btn-reply font-medium"> Trả lời </button>{' '}
</div>{' '}
</div>{' '}
{/* reply list */}
<div className="reply-list-container mt-4">
{item.new_replies.map((reply) => (
<div key={reply.id} className="item_reply mt-3">
<div className="flex justify-between">
<div className="flex items-center gap-2">
{reply.user_avatar !== '0' ? (
<b className="avatar-user flex items-center justify-center">
{' '}
<img src={reply.user_avatar} alt={reply.user_name} />{' '}
</b>
) : (
<b className="avatar-user flex items-center justify-center">
{reply.user_name.charAt(0)}
</b>
)}
<div className="comment-name">
<b className="user-name">{reply.user_name}</b>
{reply.is_user_admin === '1' && <i className="note font-medium">QTV</i>}
</div>{' '}
</div>
<div className="text-sm text-gray-500">
{new Date(Number(reply.post_time) * 1000).toLocaleDateString('vi-VN')}
</div>{' '}
</div>
<div className="comment-content mt-2 rounded p-2">{reply.content} </div>{' '}
</div>
))}
</div>
</div>
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { FormComment } from './FormComment';
import { ListComment } from './ListComment';
export const ProductComment: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<div className="box-comment">
<p className="title-comment font-[600]">Hỏi đáp</p>
<div className="comment-detail">
<div className="form-comment flex justify-between gap-2">
<textarea
className="comment_reply_content boder-radius-10"
id="content0"
placeholder="Xin mời để lại câu hỏi, Nguyencong sẽ trả lời ngay trong 1h, các câu hỏi sau 22h - 8h sẽ được trả lời vào sáng hôm sau."
name="user_post[content]"
></textarea>
<button
className="btn-send-form-comment send-comment-pc flex items-center justify-center gap-2"
onClick={() => setOpen(true)}
>
<i className="sprite sprite-icon-send-detail"></i>Gửi
</button>
</div>
<FormComment open={open} onClose={() => setOpen(false)} />
</div>
{/* list comment */}
<ListComment />
</div>
);
};

View File

@@ -0,0 +1,29 @@
import { useState } from 'react';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';
import type { ProductDetailData } from '@/types';
import Link from 'next/link';
export const ProductDescription = (item: ProductDetailData) => {
const [expanded, setExpanded] = useState(false);
if (!item.product_info.productDescription) return null;
return (
<div className="box-descreption-detail">
<h2 className="titlle-descreption font-[500]">Giới thiệu {item.product_info.productName}</h2>
<div
className={`content-descreption-detail static-html relative ${
expanded ? 'max-h-none' : 'max-h-[467px] overflow-hidden'
}`}
dangerouslySetInnerHTML={{ __html: item.product_info.productDescription }}
/>
<div
onClick={() => setExpanded(!expanded)}
className="btn-article-col js-viewmore-content flex items-center justify-center gap-2 font-[500]"
>
<span>{expanded ? 'Thu gọn' : 'Xem tất cả'}</span>
{expanded ? <FaAngleUp /> : <FaAngleDown />}
</div>
</div>
);
};

View File

@@ -0,0 +1,137 @@
import React from 'react';
export const FormReview: React.FC = () => {
return (
<div className="box-form-review" id="js-box-review">
<textarea
className="review_reply_content"
id="rating-content"
placeholder="Mời bạn để lại đánh giá..."
name="user_post[content]"
></textarea>
<div className="actions-comment">
<div className="infomation-customer">
<table>
<tbody>
<tr className="flex items-center">
<td>
<label>Đánh giá:</label>
</td>
<td>
<div className="rating" id="select-rate-pro">
<div className="rating-selection" id="rating-review0">
<input
type="radio"
className="rating-input"
id="rating-input-review-0-5"
value="5"
name="user_post[rate]"
defaultChecked
/>
<label
htmlFor="rating-input-review-0-5"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-4"
value="4"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-4"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-3"
value="3"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-3"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-2"
value="2"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-2"
className="sprite-1star rating-star"
></label>
<input
type="radio"
className="rating-input"
id="rating-input-review-0-1"
value="1"
name="user_post[rate]"
/>
<label
htmlFor="rating-input-review-0-1"
className="sprite-1star rating-star"
></label>
</div>
</div>
</td>
</tr>
<tr className="flex items-center">
<td>Tên bạn</td>
<td>
<input
type="text"
id="rating-name"
name="user_post[user_name]"
className="form-control"
defaultValue=""
/>
</td>
</tr>
<tr className="flex items-center">
<td>Email</td>
<td>
<input
type="text"
id="rating-email"
name="user_post[user_email]"
className="form-control"
defaultValue=""
/>
</td>
</tr>
</tbody>
</table>
</div>
<p
id="js-review-note"
className="font-weight-700 flex"
style={{ color: 'red', maxWidth: '100%' }}
></p>
<button
type="button"
className="btn-review send_form mt-12 mb-10"
onClick={() => {
// TODO: viết hàm send_vote() trong React
console.log('Send vote clicked');
}}
>
Gửi đánh giá
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { ListReviewData } from '@/data/ListReview';
import Image from 'next/image';
export const ListReview = () => {
const [showAll, setShowAll] = useState(false);
const visibleReviews = showAll ? ListReviewData : ListReviewData.slice(0, 3);
return (
<div className="list-review">
{visibleReviews.map((review) => {
const avatarLetter = review.user_name.charAt(0).toUpperCase();
const date = new Date(Number(review.post_time) * 1000).toLocaleDateString('vi-VN');
return (
<div key={review.id} className="item-comment">
<div className="form-reply-comment">
{/* header */}
<div className="comment-name flex items-center justify-between">
<div className="comment-form-left flex items-center gap-2">
{review.user_avatar ? (
<b className="avatar-user js-avatar-user flex items-center justify-center">
<Image src={review.user_avatar} alt={review.user_name} />
</b>
) : (
<b className="avatar-user js-avatar-user flex items-center justify-center">
{avatarLetter}
</b>
)}
<b className="user-name flex items-center gap-2">{review.user_name}</b>
</div>
<div className="comment-form-right flex items-center gap-2">
<i className="fa-regular fa-clock"></i>
<span style={{ color: '#787878', fontSize: 12, marginRight: 4 }}>{date}</span>
</div>
</div>
{/* content */}
<div className="comment-content boder-radius-10 relative mt-3">
<div className="text-review flex flex-col gap-2">
<p className="flex items-center">
<b>Đánh giá:</b> <i className={`sprite-star-5 star${review.rate}`}></i>
</p>
<p className="flex items-center">
<b>Nhận xét:</b>
<span style={{ width: '80%' }}>{review.content}</span>
</p>
</div>
{/* feedback actions */}
<div className="info_feeback flex items-center gap-2">
<i className="sprite sprite-icon-reply-detail"></i>
<button className="write_reply btn-reply font-weight-500">Trả lời</button>
</div>
{/* images nếu có */}
<div className="jd-img-review flex flex-col gap-2">
{review.files.map((file) => (
<Image
key={file.id}
src={file.file_path}
alt={file.title}
width={100}
height={60}
/>
))}
</div>
</div>
{/* reply list */}
<div className="reply-holder reply-list-container">
{review.new_replies.map((reply) => (
<div key={reply.id} className="item_reply mt-3">
<div className="flex items-center justify-between">
<div className="comment-left-form item-center flex gap-2">
<b className="avatar-user avatar-admin">
{reply.user_avatar !== '0' ? (
<img src={reply.user_avatar} alt={reply.user_name} />
) : (
reply.user_name.charAt(0)
)}
</b>
<div className="comment-name mb-10">
<b className="user-name">{reply.user_name}</b>
{reply.is_user_admin === '1' && <i className="note font-[500]">QTV</i>}
</div>
</div>
<div className="info_feeback comment-right-form">
<span style={{ color: '#787878', fontSize: 12 }}>({reply.post_time})</span>
</div>
</div>
<div className="comment-content boder-radius-10">{reply.content}</div>
</div>
))}
</div>
</div>
</div>
);
})}
{!showAll && ListReviewData.length > 3 && (
<button
id="first-review"
className="btn-more cursor-pointer"
onClick={() => setShowAll(true)}
>
Xem thêm đánh giá
</button>
)}
{showAll && (
<button
id="hide-review"
className="btn-more cursor-pointer"
onClick={() => setShowAll(false)}
>
Thu gọn
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { Review } from '@/types';
import { FaStar } from 'react-icons/fa6';
import { FormReview } from './FormReview';
import { ListReview } from './ListReview';
interface Props {
ItemReview: Review;
}
export const ProductReview: React.FC<Props> = ({ ItemReview }) => {
const [showForm, setShowForm] = useState(false);
const { summary } = ItemReview;
const totalRate = summary.list_rate.reduce((acc, item) => acc + Number(item.total), 0);
// Tạo object chứa số lượng và phần trăm cho từng sao const
const rates = {
rate1: Number(summary.list_rate.find((r) => r.rate === '1')?.total || 0),
rate2: Number(summary.list_rate.find((r) => r.rate === '2')?.total || 0),
rate3: Number(summary.list_rate.find((r) => r.rate === '3')?.total || 0),
rate4: Number(summary.list_rate.find((r) => r.rate === '4')?.total || 0),
rate5: Number(summary.list_rate.find((r) => r.rate === '5')?.total || 0),
};
const percents = {
percent1: totalRate > 0 ? (rates.rate1 / totalRate) * 100 : 0,
percent2: totalRate > 0 ? (rates.rate2 / totalRate) * 100 : 0,
percent3: totalRate > 0 ? (rates.rate3 / totalRate) * 100 : 0,
percent4: totalRate > 0 ? (rates.rate4 / totalRate) * 100 : 0,
percent5: totalRate > 0 ? (rates.rate5 / totalRate) * 100 : 0,
};
return (
<div className="box-review">
<p className="title-review font-[600]">Bình luận đánh giá</p>
<div className="review-customer-detail">
<form
action="/ajax/post_comment.php"
method="post"
encType="multipart/form-data"
className="form-post"
>
<div className="review-info boder-radius-10 flex">
<div className="avgRate flex flex-col items-center justify-center">
<span className="font-bold">{summary.avgRate}/5</span>
<i className={`sprite-star-5 star${summary.avgRate} icon-star-detail`}></i>
<p className="mt-3">{summary.total} đánh giá nhận xét</p>
</div>
<div className="box-avg-rate-count">
<div className="avg-rate-count">
{[5, 4, 3, 2, 1].map((rate) => {
const percent = percents[`percent${rate}` as keyof typeof percents];
const total = rates[`rate${rate}` as keyof typeof rates];
return (
<div key={rate} className="avg-rate-item mt-2 flex items-center justify-center">
<span className="rate-number flex items-center gap-1">
{rate} <FaStar className="text-yellow-500" />
</span>
<div className="nhan-xet-bar">
<div
className={`percent percent${rate}`}
style={{ width: `${percent}%` }}
></div>
</div>
<span className="total-avg-rate">
<strong>{total}</strong> đánh giá
</span>
</div>
);
})}
</div>
</div>
</div>
<p className="text-danh-gia mb-5">Bạn đánh giá sao sản phẩm này</p>
{!showForm ? (
<div
className="button-review mx-auto flex cursor-pointer items-center justify-center"
onClick={() => setShowForm(true)}
>
{' '}
Đánh giá ngay{' '}
</div>
) : (
<div
className="button-review mx-auto flex cursor-pointer items-center justify-center"
onClick={() => setShowForm(false)}
>
{' '}
Đóng lại{' '}
</div>
)}
{/* form */}
{showForm && <FormReview />}
</form>
</div>
<ListReview />
</div>
);
};

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa6';
import useFancybox from '@/hooks/useFancybox';
interface Props {
ItemSpec: string;
}
export const ProductSpec: React.FC<Props> = ({ ItemSpec }) => {
const [fancyboxRef] = useFancybox({
closeButton: 'auto',
dragToClose: true,
});
return (
<div className="box-spec">
<h2 className="title font-[600]">Thông số kỹ thuật</h2>
<div className="content-spec relative" dangerouslySetInnerHTML={{ __html: ItemSpec }} />
<div id="product-spec" style={{ display: 'none' }} ref={fancyboxRef}>
<div className="box-top-centent-spec d-flex justify-content-between hide">
<h2 className="font-weight-600">Thông số kỹ thuật</h2>
<p
className="delelte-content-spec d-flex justify-content-center align-items-center"
data-fancybox-close
>
<i className="fa-solid fa-xmark"></i>
</p>
</div>
<div className="content-spec">
{/* thay vì {{ page.product_info.productSpec }} bạn truyền từ props */}
{ItemSpec}
</div>
</div>
<a
data-fancybox
data-options='{"src": "#product-spec", "touch": false, "smallBtn": false}'
href="javascript:;"
className="btn-article-col font-weight-500 flex items-center justify-center gap-2"
>
Xem đy đ thông số kỹ thuật
<FaAngleDown />
</a>
</div>
);
};

View File

@@ -1,16 +1,28 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
// type
import type { ProductDetailData } from '@/types';
// data
import { productDetailData } from '@/data/product/detail';
import { productData } from '@/data/ListProduct';
import { findProductDetailBySlug } from '@/lib/product/productdetail';
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';
import ItemProduct from '@/components/Common/ItemProduct';
import { ProductDescription } from './ProductDescription';
import { ProductSpec } from './ProductSpec';
import { ProductReview } from './ProductReview';
import { ProductComment } from './ProductComment';
interface ProductDetailPageProps {
slug: string;
@@ -51,6 +63,65 @@ const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ slug }) => {
<BoxInfoRight {...Products} />
</div>
</div>
{/* sản phẩm tương tự */}
<div className="box-relative-product box-history-product page-hompage">
<div className="box-product-category">
<div className="title-box">
<h2 className="title title-box font-[600]">Sản phẩm tương tự</h2>
</div>
<div className="box-list-history-product">
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={12}
slidesPerView={5}
loop={true}
>
{productData.map((item, index) => (
<SwiperSlide key={index}>
<ItemProduct item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
{/* nội dung chi tiết sản phẩm */}
<div className="box-read-product-detail flex justify-between gap-3">
<div className="box-left">
{/* mô tả chi tiết sản phẩm */}
<ProductDescription {...Products} />
{/* đánh giá sản phẩm */}
<ProductReview ItemReview={Products.product_info.review} />
{/* bình luận sản phẩm */}
<ProductComment />
</div>
<div className="box-right">
<ProductSpec ItemSpec={Products.product_info.productSpec} />
</div>
</div>
{/* sản phẩm đã xem */}
<div className="box-history-product page-hompage mt-5">
<div className="box-product-category">
<div className="title-box">
<h2 className="title title-box font-[600]">Sản phẩm đã xem</h2>
</div>
<div className="box-list-history-product">
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={12}
slidesPerView={5}
loop={true}
>
{productData.map((item, index) => (
<SwiperSlide key={index}>
<ItemProduct item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</section>
</>