update
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
|
||||
export const ArticleTopLeft = () => {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="box-left">
|
||||
{DataListArticleNews.slice(0, 1).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<div className="box-right flex flex-1 flex-col gap-3">
|
||||
{DataListArticleNews.slice(0, 4).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const ArticleTopRight = () => {
|
||||
return (
|
||||
<div className="col-right-article box-view-article flex-1">
|
||||
<form
|
||||
method="get"
|
||||
action="/tim-bai"
|
||||
name="search"
|
||||
className="boder-radius-10 border-box-article article-search-container"
|
||||
>
|
||||
<input type="text" name="q" placeholder="Tìm kiếm bài viết" defaultValue="" />
|
||||
<button type="submit" className="fas fa-search"></button>
|
||||
</form>
|
||||
|
||||
<div className="boder-radius-10 border-box-article">
|
||||
<div className="title-box-article font-bold">Xem nhiều</div>
|
||||
<ul className="list-most-view-article flex flex-col gap-4">
|
||||
{DataListArticleNews.slice(0, 6).map((item, index) => (
|
||||
<li className="item-most-view-article flex items-center gap-2" key={index}>
|
||||
<span className="number flex items-center justify-center font-[600]"></span>
|
||||
<Link href={item.url} className="line-clamp-2 flex-1">
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
export const BoxArticleMid = () => {
|
||||
return (
|
||||
<div className="box-article-home-middle flex justify-between gap-2">
|
||||
<div className="box-article-tech col-left-article boder-radius-10 border-box-article">
|
||||
<p className="title-box-article font-[600]">Tin công nghệ</p>
|
||||
<div className="list-article-tech">
|
||||
{DataListArticleNews.slice(0, 9).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href="/tin-cong-nghe"
|
||||
className="btn-article-col flex items-center justify-center gap-2 font-[500]"
|
||||
>
|
||||
Xem tất cả
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-right-article flex-1">
|
||||
<div className="box-article-hot border-box-article boder-radius-10">
|
||||
<p className="title-box-article font-bold">Tin nổi bật</p>
|
||||
<div className="list-article-hot">
|
||||
{DataListArticleNews.slice(0, 5).map((item, index) => (
|
||||
<div className="item-article flex gap-4" key={index}>
|
||||
<Link href={item.url} className="img-article boder-radius-10 relative">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={item.image.original}
|
||||
fill
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
|
||||
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
|
||||
</Link>
|
||||
<div className="content-article content-article-item flex flex-1 flex-col">
|
||||
<Link href="/tuyen-dung-nhan-vien-ky-thuat-1-2" className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
<p className="time-article flex items-center gap-2">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</p>
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
|
||||
export const BoxArticleReview = () => {
|
||||
return (
|
||||
<div className="box-article-category page-hompage">
|
||||
<div className="box-article-global box-artice-review">
|
||||
<div className="title-box-product-home mb-5 flex flex-col items-center">
|
||||
<p className="title font-[500]">Review sản phẩm</p>
|
||||
<p className="border-title"></p>
|
||||
</div>
|
||||
<div className="list-article-global">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={15}
|
||||
slidesPerView={3}
|
||||
loop={true}
|
||||
>
|
||||
{DataListArticleNews.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div className="item-article">
|
||||
<Link href={item.url} className="img-article">
|
||||
<Image src={item.image.original} fill alt={item.title} />
|
||||
</Link>
|
||||
<div className="content-article-item">
|
||||
<Link href={item.url} className="title font-weight-500 line-clamp-2">
|
||||
{item.title}
|
||||
</Link>
|
||||
<div className="time-aricle-item flex items-center">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { FaYoutube } from 'react-icons/fa6';
|
||||
import { DataListArticleVideo } from '@/data/article/ListAricleVideo';
|
||||
import Image from 'next/image';
|
||||
import useFancybox from '@/hooks/useFancybox';
|
||||
|
||||
export const BoxVideoArticle = () => {
|
||||
const getYoutubeEmbedUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
// nếu là link youtube dạng watch?v=...
|
||||
if (urlObj.hostname.includes('youtube.com')) {
|
||||
const videoId = urlObj.searchParams.get('v');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// nếu là link youtu.be/xxxx
|
||||
if (urlObj.hostname.includes('youtu.be')) {
|
||||
const videoId = urlObj.pathname.replace('/', '');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// fallback: trả về chính url
|
||||
return url;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
const [fancyboxRef] = useFancybox({
|
||||
closeButton: 'auto',
|
||||
dragToClose: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="box-video-article boder-radius-10">
|
||||
<div className="title-video-article flex justify-between">
|
||||
<p className="title font-bold">Youtube channel</p>
|
||||
<Link
|
||||
href="https://www.youtube.com/c/NGUYENCONGPC"
|
||||
className="follow-youtube flex items-center gap-2"
|
||||
>
|
||||
<FaYoutube />
|
||||
<span className="font-bold">Theo dõi trên YouTube</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="list-video-article flex justify-between gap-2">
|
||||
<div className="box-left" ref={fancyboxRef}>
|
||||
{DataListArticleVideo.slice(0, 1).map((item, index) => (
|
||||
<div className="item-article-video d-flex w-50 gap-10" key={index}>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="img-article img-article-video boder-radius-10 relative"
|
||||
data-fancybox
|
||||
>
|
||||
<Image
|
||||
src={item.image.original}
|
||||
width={430}
|
||||
height={310}
|
||||
className="boder-radius-10"
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-big-play-video-article icon-play"></i>
|
||||
<Image
|
||||
className="icon-play-small"
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
|
||||
alt="play"
|
||||
width={58}
|
||||
height={41}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="title-article-video flex-1"
|
||||
data-fancybox
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="box-right grid grid-cols-2 gap-2">
|
||||
{DataListArticleVideo.slice(1, 7).map((item, index) => (
|
||||
<div className="item-article-video flex w-50 gap-2" key={index}>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="img-article img-article-video boder-radius-10 relative"
|
||||
data-fancybox
|
||||
>
|
||||
<Image
|
||||
src={item.image.original}
|
||||
width={430}
|
||||
height={310}
|
||||
className="boder-radius-10"
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-big-play-video-article icon-play"></i>
|
||||
<Image
|
||||
className="icon-play-small"
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
|
||||
alt="play"
|
||||
width={58}
|
||||
height={41}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="title-article-video flex-1"
|
||||
data-fancybox
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Breadcrumb } from '@components/Common/Breadcrumb';
|
||||
import { DataArticleCategory } from '@/data/article/ListCategory';
|
||||
|
||||
import { ArticleTopLeft } from './HomeArticle/ArticleTopLeft';
|
||||
import { ArticleTopRight } from './HomeArticle/ArticleTopRight';
|
||||
import { BoxVideoArticle } from './HomeArticle/BoxVideoArticle';
|
||||
import { BoxArticleMid } from './HomeArticle/BoxArticleMid';
|
||||
import { BoxArticleReview } from './HomeArticle/BoxArticleReview';
|
||||
|
||||
const ArticleHome = () => {
|
||||
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
|
||||
|
||||
return (
|
||||
<section className="page-article pb-10">
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="tabs-category-article flex items-center">
|
||||
{DataArticleCategory.map((item, index) => (
|
||||
<Link href={item.url} key={index} className="item-tab-article">
|
||||
<h2 className="title-cate-article font-[400]">{item.title}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="box-article-home-top flex gap-3">
|
||||
<div className="col-left-article border-box-article box-new-article boder-radius-10">
|
||||
<div className="flex gap-12">
|
||||
<ArticleTopLeft />
|
||||
</div>
|
||||
</div>
|
||||
<ArticleTopRight />
|
||||
</div>
|
||||
|
||||
{/* box video */}
|
||||
<BoxVideoArticle />
|
||||
|
||||
{/* box mid */}
|
||||
<BoxArticleMid />
|
||||
|
||||
{/* review */}
|
||||
<BoxArticleReview />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleHome;
|
||||
@@ -31,13 +31,13 @@ export const Breadcrumb = ({ items }: { items: BreadcrumbItem[] }) => {
|
||||
itemProp="itemListElement"
|
||||
itemScope
|
||||
itemType="http://schema.org/ListItem"
|
||||
className="flex items-center"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link href={item.url ?? '/'} itemProp="item">
|
||||
<span itemProp="name">{item?.name}</span>
|
||||
</Link>
|
||||
{idx < items.length - 1 && <FaAngleRight className="text-gray-700" />}
|
||||
<meta itemProp="position" content={(idx + 1).toString()} />
|
||||
{idx < items.length - 1 && <span className="mx-1">/</span>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Article } from '@/types';
|
||||
import { ArticleItem } from '@/types/article/TypeArticleCatePage';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
type ItemArticleProps = {
|
||||
item: Article;
|
||||
item: Article | ArticleItem;
|
||||
};
|
||||
|
||||
const ItemArticle: React.FC<ItemArticleProps> = ({ item }) => {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Article } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
type ItemArticleProps = {
|
||||
item: Article;
|
||||
};
|
||||
|
||||
const ItemArticle: React.FC<ItemArticleProps> = ({ item }) => {
|
||||
// chọn link: nếu có external_url thì dùng, ngược lại dùng url
|
||||
const linkHref = item.external_url && item.external_url !== '' ? item.external_url : item.url;
|
||||
|
||||
// chọn ảnh: nếu có original thì dùng, ngược lại ảnh mặc định
|
||||
const imageSrc =
|
||||
item.image?.original && item.image.original !== ''
|
||||
? item.image.original
|
||||
: '/static/assets/nguyencong_2023/images/not-image.png';
|
||||
|
||||
// chọn thời gian: ưu tiên article_time, fallback createDate
|
||||
const timeDisplay =
|
||||
item.article_time && item.article_time !== '' ? item.article_time : item.createDate;
|
||||
|
||||
return (
|
||||
<div className="item-article flex gap-3">
|
||||
<Link href={linkHref} className="img-article boder-radius-10 position-relative">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={imageSrc}
|
||||
width={265}
|
||||
height={180}
|
||||
alt={item.title}
|
||||
/>
|
||||
{/* icon video nếu cần */}
|
||||
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
|
||||
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
|
||||
</Link>
|
||||
|
||||
<div className="content-article content-article-item flex flex-1 flex-col">
|
||||
<Link href={linkHref} className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
|
||||
<p className="time-article flex items-center gap-4">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{timeDisplay}</span>
|
||||
</p>
|
||||
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemArticle;
|
||||
@@ -1,282 +0,0 @@
|
||||
import { ListArticle } from '@/types';
|
||||
|
||||
export const dataArticle: ListArticle = [
|
||||
{
|
||||
id: 4200,
|
||||
title: 'Top PC 15 triệu tối ưu hiệu năng nhất trong mùa bão giá RAM',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Chỉ với 15 triệu đồng, người dùng đã có thể sở hữu một bộ máy tính tối ưu hiệu năng cho nhu cầu học tập, làm việc và giải trí. Nguyễn Công PC mang đến nhiều cấu hình cân bằng giữa sức mạnh và giá trị, đảm bảo hoạt động mượt mà trong mọi tác vụ. Đây là lựa chọn lý tưởng cho những ai muốn đầu tư một hệ thống mạnh mẽ với chi phí hợp lý.',
|
||||
createDate: '10-12-2025, 5:44 pm',
|
||||
createBy: '75',
|
||||
lastUpdate: '22-12-2025, 5:03 pm',
|
||||
lastUpdateBy: '75',
|
||||
visit: 157,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 1,
|
||||
url: '/top-pc-15-trieu-toi-uu-hieu-nang-cho-gaming-va-lam-viec',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4200-chi-voi-15-trieu-ban-da-co-ngay-mot-bo-pc-chat-luong-dam-bao-hieu-nang1.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4200-chi-voi-15-trieu-ban-da-co-ngay-mot-bo-pc-chat-luong-dam-bao-hieu-nang1.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4195,
|
||||
title: 'Cách nhận chứng chỉ Google Gemini Educator làm đẹp CV của bạn ngay hôm nay!',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Chứng chỉ Google Gemini Educator giúp bạn khẳng định kỹ năng sử dụng AI trong giáo dục và công nghệ. Việc sở hữu chứng chỉ này không chỉ tăng tính chuyên nghiệp cho CV mà còn mở ra nhiều cơ hội nghề nghiệp mới. Bài viết sẽ hướng dẫn bạn cách đăng ký, học và nhận chứng chỉ nhanh chóng nhất.',
|
||||
createDate: '08-12-2025, 11:26 am',
|
||||
createBy: '75',
|
||||
lastUpdate: '08-12-2025, 12:07 pm',
|
||||
lastUpdateBy: '75',
|
||||
visit: 3067,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 2,
|
||||
url: '/cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-2025',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4195-cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-20251.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4195-cach-nhan-chung-chi-google-gemini-educator-mien-phi-nam-20251.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2722,
|
||||
title: 'Top 100+ cấu hình PC Gaming giá tốt nhất năm 2025',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Trong bài viết, Nguyễn Công PC đã tổng hợp hơn 100 cấu hình PC gaming tối ưu nhất năm 2025, phù hợp với nhiều mức ngân sách từ phổ thông đến cao cấp. Mỗi cấu hình cân bằng giữa hiệu năng và giá thành, đáp ứng nhu cầu chơi game mượt mà, đồ họa sắc nét và khả năng nâng cấp linh hoạt trong tương lai.\r\n\r\n\r\n',
|
||||
createDate: '16-01-2024, 10:52 am',
|
||||
createBy: '50',
|
||||
lastUpdate: '06-12-2025, 4:30 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 36705,
|
||||
is_featured: 0,
|
||||
article_time: '07-11-2025, 9:00 am',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 3,
|
||||
url: '/top-100-cau-hinh-pc-gaming-gia-tot-nhat',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-2722-pc-gaming.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/2722-pc-gaming.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2718,
|
||||
title: 'Top 50 cấu hình PC đồ họa giá tốt nhất hiện nay',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Với đà phát triển của truyền thông, công nghệ số, kỹ thuật số,... Cần rất nhiều công cụ để hỗ trợ cho công việc, làm việc của bạn. Sức mạnh ngành chuyền thông nói riêng cũng như công nghệ nói chung càng ngày càng phát triển mạnh mẽ, vượt trội, chính vì để hỗ trợ cho việc xây dựng các bộ (PC Render) làm việc cũng như giải trí đang là nhu cầu lơn trên thị trường hiện nay.\r\n\r\n',
|
||||
createDate: '15-01-2024, 1:39 pm',
|
||||
createBy: '50',
|
||||
lastUpdate: '24-11-2025, 10:23 am',
|
||||
lastUpdateBy: '74',
|
||||
visit: 24390,
|
||||
is_featured: 0,
|
||||
article_time: '05-11-2025, 10:00 am',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Anh Tuấn',
|
||||
counter: 4,
|
||||
url: '/top-cau-hinh-do-hoa-gia-tot-nhat',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-2718-pc-do-hoa.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/2718-pc-do-hoa.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4203,
|
||||
title:
|
||||
'NGUYỄN CÔNG PC - NHÀ TÀI TRỢ KIM CƯƠNG CHÀO TÂN SINH VIÊN ĐẠI HỌC KIẾN TRÚC HÀ NỘI 2025',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Máy tính Nguyễn Công tiếp tục khẳng định vị thế là đối tác tin cậy hàng đầu khi vinh dự trở thành Nhà Tài Trợ Kim Cương liên tục trong 7 năm (2019 – 2025) cho chương trình Chào Tân Sinh Viên của Đại học Kiến Trúc Hà Nội (HAU).',
|
||||
createDate: '15-12-2025, 10:09 am',
|
||||
createBy: '74',
|
||||
lastUpdate: '15-12-2025, 4:00 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 51,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 5,
|
||||
url: '/nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4203-nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025-06.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4203-nguyen-cong-pc-nha-tai-tro-kim-cuong-chao-tan-sinh-vien-dai-hoc-kien-truc-ha-noi-2025-06.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4199,
|
||||
title: 'Đại chiến đồ họa: Canva và Photoshop: Ai là "Vua" thiết kế hiện nay',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Canva và Photoshop đang là hai nền tảng thiết kế phổ biến nhất, mỗi công cụ sở hữu những ưu – nhược điểm riêng. Trong khi Canva mang đến sự tiện lợi và tốc độ, Photoshop lại vượt trội về sức mạnh xử lý và khả năng sáng tạo chuyên sâu. Cuộc đối đầu này giúp người dùng lựa chọn đúng công cụ phù hợp với nhu cầu thiết kế của mình.',
|
||||
createDate: '09-12-2025, 6:56 pm',
|
||||
createBy: '75',
|
||||
lastUpdate: '11-12-2025, 2:21 pm',
|
||||
lastUpdateBy: '75',
|
||||
visit: 72,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 6,
|
||||
url: '/dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4199-dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay5.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4199-dai-chien-do-hoa-canva-va-photoshop-ai-la-vua-thiet-ke-hien-nay5.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4197,
|
||||
title: 'Người dùng nên nâng cấp Windows 11 hiện đại hay tiếp tục sử dụng Windows 10 ổn định? ',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '09-12-2025, 11:03 am',
|
||||
createBy: '75',
|
||||
lastUpdate: '09-12-2025, 5:23 pm',
|
||||
lastUpdateBy: '75',
|
||||
visit: 92,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 7,
|
||||
url: '/nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4197-nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh2.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4197-nguoi-dung-nen-nang-cap-windows-11-hien-dai-hay-tiep-tuc-su-dung-windows-10-on-dinh2.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3954,
|
||||
title: 'Hướng Dẫn Các Bước Cài Đặt Plugin Sketch Up Nhanh Chóng, Đơn Giản Nhất',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Theo dõi các hướng dẫn chi tiết cách cài đặt plugin cho phần mềm SketchUp cùng Nguyễn Công PC để giúp người dùng mở rộng chức năng, tiết kiệm thời gian thiết kế và nâng cao hiệu suất làm việc. ',
|
||||
createDate: '21-07-2025, 10:39 am',
|
||||
createBy: '75',
|
||||
lastUpdate: '22-07-2025, 9:06 am',
|
||||
lastUpdateBy: '75',
|
||||
visit: 6532,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 8,
|
||||
url: '/huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-3954-huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat10.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/3954-huong-dan-cac-buoc-cai-dat-plugin-sketch-up-nhanh-chong-don-gian-nhat10.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4198,
|
||||
title: 'Bạn đã biết cách tạo ảnh AI cực hot với công cụ Nano Banana từ Gemini chưa?',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Nano Banana là công cụ AI mới giúp người dùng tạo ảnh nhanh, đẹp và chuẩn ý tưởng chỉ từ vài dòng mô tả. Tại Nguyễn Công PC, bạn có thể dễ dàng trải nghiệm Nano Banana với giao diện đơn giản, tốc độ xử lý mạnh mẽ. Công cụ này phù hợp cho designer, marketer, người làm nội dung và bất kỳ ai muốn tạo hình ảnh chuyên nghiệp.',
|
||||
createDate: '09-12-2025, 4:59 pm',
|
||||
createBy: '75',
|
||||
lastUpdate: '09-12-2025, 6:48 pm',
|
||||
lastUpdateBy: '75',
|
||||
visit: 137,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 9,
|
||||
url: '/ban-da-biet-cach-tao-anh-ai-cuc-hot-voi-cong-cu-nano-banana-tu-gemini-chua',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4198-ban-da-biet-cach-tao-anh-ai-cuc-hot-voi-cong-cu-nano-banana-tu-gemini-chua7.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4198-ban-da-biet-cach-tao-anh-ai-cuc-hot-voi-cong-cu-nano-banana-tu-gemini-chua7.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4118,
|
||||
title: 'Windows 10 chính thức ngừng hoạt động, người dùng cần làm gì để giữ máy tính an toàn?',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary:
|
||||
'Microsoft đã chính thức ngừng hỗ trợ Windows 10, đồng nghĩa với việc hệ điều hành này không còn nhận được các bản cập nhật bảo mật. Người dùng tiếp tục sử dụng có nguy cơ cao bị tấn công mạng hoặc gặp lỗi nghiêm trọng. Vì vậy, việc nâng cấp hoặc áp dụng các biện pháp bảo vệ bổ sung là điều cấp thiết để giữ máy tính an toàn.',
|
||||
createDate: '14-10-2025, 5:37 pm',
|
||||
createBy: '75',
|
||||
lastUpdate: '15-10-2025, 10:14 am',
|
||||
lastUpdateBy: '75',
|
||||
visit: 325,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: '',
|
||||
author: 'Diệu Linh',
|
||||
counter: 10,
|
||||
url: '/windows-10-chinh-thuc-ngung-hoat-dong-nguoi-dung-can-lam-gi-de-giu-may-tinh-an-toan',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4118-huong-dan-reset-windows-10-ve-trang-thai-moi-cai-dat14.jpg',
|
||||
original:
|
||||
'https://nguyencongpc.vn/media/news/4118-huong-dan-reset-windows-10-ve-trang-thai-moi-cai-dat14.jpg',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
import { FaCaretRight } from 'react-icons/fa';
|
||||
import { dataArticle } from './dataArticle';
|
||||
import ItemArticle from './ItemArticle';
|
||||
|
||||
const BoxArticle: React.FC = () => {
|
||||
return (
|
||||
<div className="box-article-group boder-radius-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title-box">
|
||||
<h2 className="title-box font-[600]">Tin tức công nghệ</h2>
|
||||
</div>
|
||||
<a href="/tin-cong-nghe" className="btn-article-group flex items-center gap-1">
|
||||
<span>Xem tất cả</span>
|
||||
<FaCaretRight size={16} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="list-article-group flex items-center gap-10">
|
||||
{dataArticle.slice(0, 4).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxArticle;
|
||||
@@ -1,123 +0,0 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { Article } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
type ItemArticleProps = {
|
||||
item: Article;
|
||||
};
|
||||
|
||||
const ItemArticleVideo: React.FC<ItemArticleProps> = ({ item }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// chọn link: nếu có external_url thì dùng, ngược lại dùng url
|
||||
const linkHref = item.external_url && item.external_url !== '' ? item.external_url : item.url;
|
||||
|
||||
// chọn ảnh: nếu có original thì dùng, ngược lại ảnh mặc định
|
||||
const imageSrc =
|
||||
item.image?.original && item.image.original !== ''
|
||||
? item.image.original
|
||||
: '/static/assets/nguyencong_2023/images/not-image.png';
|
||||
|
||||
// chọn thời gian: ưu tiên article_time, fallback createDate
|
||||
const timeDisplay =
|
||||
item.article_time && item.article_time !== '' ? item.article_time : item.createDate;
|
||||
|
||||
const getYoutubeEmbedUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
// nếu là link youtube dạng watch?v=...
|
||||
if (urlObj.hostname.includes('youtube.com')) {
|
||||
const videoId = urlObj.searchParams.get('v');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// nếu là link youtu.be/xxxx
|
||||
if (urlObj.hostname.includes('youtu.be')) {
|
||||
const videoId = urlObj.pathname.replace('/', '');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// fallback: trả về chính url
|
||||
return url;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="item-article flex gap-3">
|
||||
<Link
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}}
|
||||
href="javascript:void(0)"
|
||||
className="img-article boder-radius-10 relative"
|
||||
>
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={imageSrc}
|
||||
width={265}
|
||||
height={180}
|
||||
alt={item.title}
|
||||
/>
|
||||
{/* icon video nếu cần */}
|
||||
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
|
||||
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
|
||||
</Link>
|
||||
|
||||
<div className="content-article content-article-item flex flex-1 flex-col">
|
||||
<Link href={linkHref} className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
|
||||
<p className="time-article flex items-center gap-4">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{timeDisplay}</span>
|
||||
</p>
|
||||
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<dialog id="video_modal" className="modal modal-open">
|
||||
<div className="modal-box w-11/12 max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold">{item.title}</h3>
|
||||
<form method="dialog">
|
||||
<button
|
||||
className="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<FaTimes size={16} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="video-wrapper mt-4">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="400"
|
||||
src={getYoutubeEmbedUrl(item.external_url)}
|
||||
title={item.title}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
<button onClick={() => setOpen(false)}>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemArticleVideo;
|
||||
@@ -1,265 +0,0 @@
|
||||
import { ListArticle } from '@/types';
|
||||
|
||||
export const dataArticle: ListArticle = [
|
||||
{
|
||||
id: 4185,
|
||||
title: 'Chuyện RAM ĐẮT - Góc nhìn mà anh em chưa thấy',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '28-11-2025, 11:51 am',
|
||||
createBy: '53',
|
||||
lastUpdate: '28-11-2025, 11:51 am',
|
||||
lastUpdateBy: '53',
|
||||
visit: 8,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=rH9Aq_2yZEc',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 1,
|
||||
url: '/chuyen-ram-dat-goc-nhin-ma-anh-em-chua-thay',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4185---efwegweg.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4185---efwegweg.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4184,
|
||||
title: 'Build PC GAMING tầm giá 20 Triệu trong mùa BÃO RAM - Cũng KHOAI phết',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '28-11-2025, 11:49 am',
|
||||
createBy: '53',
|
||||
lastUpdate: '28-11-2025, 11:49 am',
|
||||
lastUpdateBy: '53',
|
||||
visit: 7,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=c-JQPclPXmg',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 2,
|
||||
url: '/build-pc-gaming-tam-gia-20-trieu-trong-mua-bao-ram-cung-khoai-phet',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4184-maxresdefault.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4184-maxresdefault.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4171,
|
||||
title: 'Điểm dừng cho PC GAMING - Nhiều tiền thì cũng PHÍ',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '10-11-2025, 2:41 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '10-11-2025, 2:41 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 8,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=xUpMSpaa_H0',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 3,
|
||||
url: '/diem-dung-cho-pc-gaming-nhieu-tien-thi-cung-phi',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4171-dvsdfgrsdf.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4171-dvsdfgrsdf.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3683,
|
||||
title: 'Bộ PC KHỦNG BỐ tới đâu mà đích thân Chủ Tịch MaxHome phải tự đi build ???',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '12-03-2025, 9:59 am',
|
||||
createBy: '53',
|
||||
lastUpdate: '12-03-2025, 9:59 am',
|
||||
lastUpdateBy: '53',
|
||||
visit: 64,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=Ir9zlznA9ms',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 4,
|
||||
url: '/bo-pc-khung-bo-toi-dau-ma-dich-than-chu-tich-maxhome-phai-tu-di-build',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-3683-tymyumyj.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/3683-tymyumyj.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4107,
|
||||
title: 'Intel ĐẮT quá nên BUILD PC với AMD chỉ 17 TRIỆU mà chiến ALL GAME',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '04-10-2025, 5:39 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '04-10-2025, 5:40 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 7,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=DBuud_Lwt6w',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 5,
|
||||
url: '/intel-dat-qua-nen-build-pc-voi-amd-chi-17-trieu-ma-chien-all-game',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4107---gherthert.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4107---gherthert.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4079,
|
||||
title: 'Tôi thấy chán PC HIỆU NĂNG/GIÁ THÀNH sau khi thấy bộ PC này',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '20-09-2025, 10:42 am',
|
||||
createBy: '53',
|
||||
lastUpdate: '20-09-2025, 10:42 am',
|
||||
lastUpdateBy: '53',
|
||||
visit: 28,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=ceT_nSB1JCA',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 6,
|
||||
url: '/toi-thay-chan-pc-hieu-nang-gia-thanh-sau-khi-thay-bo-pc-nay',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4079-ewgergherth.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4079-ewgergherth.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4004,
|
||||
title: 'Sinh Viên ĐỒ HOẠ lên cấu hình PC nào dưới 20 TRIỆU trong 2025',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '15-08-2025, 2:04 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '15-08-2025, 2:04 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 44,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=k6rIzVmU9bA',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 7,
|
||||
url: '/sinh-vien-do-hoa-len-cau-hinh-pc-nao-duoi-20-trieu-trong-2025',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-4004-dhtrhj.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/4004-dhtrhj.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3951,
|
||||
title: 'Cấu hình PC 10 Triệu cả Màn hình - Test GAME AAA vẫn OK',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '19-07-2025, 4:57 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '19-07-2025, 4:57 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 43,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=QCQwdLcosQc',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 8,
|
||||
url: '/cau-hinh-pc-10-trieu-ca-man-hinh-test-game-aaa-van-ok',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-3951-dfbeadbeat.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/3951-dfbeadbeat.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3950,
|
||||
title:
|
||||
'Tại sao mình ít làm video CORE ULTRA - Có đáng không 40 Triệu cho Ultra 7 265K + RTX 5070',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '19-07-2025, 4:56 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '19-07-2025, 5:00 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 51,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=Y6PBwYe5My0',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 9,
|
||||
url: '/tai-sao-minh-it-lam-video-core-ultra-co-dang-khong-40-trieu-cho-ultra-7-265k-rtx-5070',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-3950-maxresdefault.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/3950-maxresdefault.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3949,
|
||||
title: 'Cấu hình PC PHỔ BIẾN nhất THẾ GIỚI gaming - Cũng rẻ phết',
|
||||
extend: {
|
||||
pixel_code: '',
|
||||
},
|
||||
summary: '',
|
||||
createDate: '19-07-2025, 4:54 pm',
|
||||
createBy: '53',
|
||||
lastUpdate: '19-07-2025, 4:54 pm',
|
||||
lastUpdateBy: '53',
|
||||
visit: 28,
|
||||
is_featured: 0,
|
||||
article_time: '',
|
||||
review_rate: 0,
|
||||
review_count: 0,
|
||||
video_code: '',
|
||||
external_url: 'https://www.youtube.com/watch?v=KXfA10koGDk',
|
||||
author: 'Trần Mạnh',
|
||||
counter: 10,
|
||||
url: '/cau-hinh-pc-pho-bien-nhat-the-gioi-gaming-cung-re-phet',
|
||||
image: {
|
||||
thum: 'https://nguyencongpc.vn/media/news/120-3949----herthrtn.jpg',
|
||||
original: 'https://nguyencongpc.vn/media/news/3949----herthrtn.jpg',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,31 +0,0 @@
|
||||
import { FaCaretRight } from 'react-icons/fa';
|
||||
import Link from 'next/link';
|
||||
import { dataArticle } from './dataArticle';
|
||||
import ItemArticleVideo from './ItemArticleVideo';
|
||||
const BoxArticleVideo: React.FC = () => {
|
||||
return (
|
||||
<div className="box-videos-group box-article-group boder-radius-10 relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title-box">
|
||||
<h2 className="title-box font-[600]">Youtube Channel</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="https://www.youtube.com/NguyenCongPC"
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
className="btn-article-group flex items-center gap-2"
|
||||
>
|
||||
<span>Xem tất cả </span>
|
||||
<FaCaretRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="list-videos-group list-article-group flex items-center gap-10">
|
||||
{dataArticle.slice(0, 4).map((item, index) => (
|
||||
<ItemArticleVideo item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxArticleVideo;
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { FaCaretDown } from 'react-icons/fa';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
|
||||
import ItemProduct from '@/components/Common/ItemProduct';
|
||||
|
||||
import { InfoCategory } from '@/types';
|
||||
|
||||
import { menuData } from '@/components/Other/Header/menuData';
|
||||
import { productData } from './productData';
|
||||
|
||||
const BoxListCategory: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{menuData[0].product.all_category.map((item, index) => (
|
||||
<div className="box-product-category boder-radius-10" key={index}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title">
|
||||
<h2 className="title-box font-[600]">{item.title}</h2>
|
||||
</div>
|
||||
<div className="list-category-child flex flex-1 items-center justify-end">
|
||||
{item.children.slice(0, 4).map((item2, index2) => (
|
||||
<Link key={index2} href={item2.url} className="title-category">
|
||||
{item2.title}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<Link href={item.url} className="title-all-category flex items-center gap-2">
|
||||
<span>Xem tất cả </span>
|
||||
<FaCaretDown size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="box-list-item-category swiper-product-category">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={5}
|
||||
loop={true}
|
||||
navigation={true}
|
||||
>
|
||||
{productData.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<ItemProduct item={item} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxListCategory;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Category } from '@/types/global/Menu';
|
||||
|
||||
const ItemCategory: React.FC<{ item: Category }> = ({ item }) => {
|
||||
return (
|
||||
<Link href={item.url} className="item-category flex flex-col items-center">
|
||||
<p className="item-category-img">
|
||||
{item.thumnail ? (
|
||||
<Image
|
||||
src={item.thumnail}
|
||||
width={50}
|
||||
height={50}
|
||||
alt={item.title}
|
||||
className="swiper-lazy lazy"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/not-image.png"
|
||||
width={50}
|
||||
height={50}
|
||||
alt={item.title}
|
||||
className="border-radius-10"
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p className="title line-clamp-2">{item.title}</p>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemCategory;
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { menuData } from '../../Other/Header/menuData';
|
||||
import ItemCategory from './ItemCategory';
|
||||
import { InfoCategory } from '@/types';
|
||||
|
||||
const renderFeaturedCategories = (categories: InfoCategory[]) => {
|
||||
return categories.map((cat, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{cat.is_featured == '1' && <ItemCategory item={cat} />}
|
||||
{cat.children && renderFeaturedCategories(cat.children)}
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
const CategoryFeature = () => {
|
||||
return (
|
||||
<div className="box-category-outstanding boder-radius-10">
|
||||
<div className="title-box">
|
||||
<h2 className="title title-box font-[600]">Danh mục nổi bật</h2>
|
||||
</div>
|
||||
<div className="list-category-outstanding grid grid-cols-10 gap-3">
|
||||
{renderFeaturedCategories(menuData[0].product.all_category)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryFeature;
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DealType } from '@/types';
|
||||
import { formatCurrency } from '@/lib/formatPrice';
|
||||
import Image from 'next/image';
|
||||
|
||||
type ProductItemProps = {
|
||||
item: DealType;
|
||||
};
|
||||
|
||||
const ProductItem: React.FC<ProductItemProps> = ({ item }) => {
|
||||
const { product_info } = item;
|
||||
const offers = product_info.specialOffer?.all ?? [];
|
||||
|
||||
return (
|
||||
<div className="product-item">
|
||||
<a href={product_info.productUrl} className="product-image relative">
|
||||
{product_info.productImage.large ? (
|
||||
<Image
|
||||
src={product_info.productImage.large}
|
||||
width="164"
|
||||
height="164"
|
||||
alt={product_info.productName}
|
||||
className="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src="/static/assets/nguyencong_2023/images/not-image.png"
|
||||
width="164"
|
||||
height="164"
|
||||
alt={product_info.productName}
|
||||
className="lazy"
|
||||
/>
|
||||
)}
|
||||
<span className="p-type-holder">
|
||||
{product_info.productType.isHot === 1 && <i className="p-icon-type p-icon-hot"></i>}
|
||||
{product_info.productType.isNew === 1 && <i className="p-icon-type p-icon-new"></i>}
|
||||
</span>
|
||||
</a>
|
||||
<div className="product-info">
|
||||
<a href={product_info.productUrl}>
|
||||
<h3 className="product-title line-clamp-3">{product_info.productName}</h3>
|
||||
</a>
|
||||
<div className="product-martket-main flex items-center">
|
||||
{product_info.marketPrice > 0 ? (
|
||||
<p className="product-market-price">{product_info.marketPrice.toLocaleString()} ₫</p>
|
||||
) : (
|
||||
<p className="product-market-price">
|
||||
{product_info.sale_rules.normal_price.toLocaleString()} ₫
|
||||
</p>
|
||||
)}
|
||||
<div className="product-percent-price">-{product_info.price_off || 0}%</div>
|
||||
</div>
|
||||
<div className="product-price-main font-bold">
|
||||
{item.price > '0' ? `${formatCurrency(product_info.price)}đ` : 'Liên hệ'}
|
||||
</div>
|
||||
<div className="p-quantity-sale">
|
||||
<i className="sprite sprite-fire-deal"></i>
|
||||
<div className="bg-gradient"></div>
|
||||
<p className="js-line-deal-left"></p>
|
||||
<span>
|
||||
Còn {Number(item.quantity) - Number(item.sale_quantity)}/{Number(item.quantity)} sản
|
||||
phẩm
|
||||
</span>
|
||||
</div>
|
||||
{offers.length > 0 && (
|
||||
<div
|
||||
className="product-offer line-clamp-2"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: product_info.specialOffer!.all![0].title,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductItem;
|
||||
@@ -1,48 +0,0 @@
|
||||
'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 '../../Common/CounDown';
|
||||
import ProductItem from './ProductItem';
|
||||
|
||||
const BoxProductDeal: React.FC = () => {
|
||||
return (
|
||||
<div className="box-product-deal boder-radius-10">
|
||||
<div className="box-title-deal flex items-center justify-between">
|
||||
<div className="title-deal flex items-center justify-center gap-10">
|
||||
<i className="sprite sprite-icon-deal-home"></i>
|
||||
<h2 className="title font-bold">Giá tốt mỗi ngày</h2>
|
||||
<span className="text-time-deal-home color-white fz-16 font-bold">Kết thúc sau</span>
|
||||
<div className="global-time-deal flex items-center gap-2">
|
||||
<CounDown deadline={new Date('2025-12-31T23:59:59')} />
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/deal" className="button-deal color-white mb-10 flex items-center">
|
||||
Xem thêm khuyến mãi <FaCaretRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="box-list-item-deal swiper-box-deal">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={6}
|
||||
loop={true}
|
||||
navigation={true}
|
||||
>
|
||||
{productDealData.map((Item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<ProductItem item={Item} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxProductDeal;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface TypeReview {
|
||||
avatar: string;
|
||||
text: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
type ItemReviewProps = {
|
||||
item: TypeReview;
|
||||
};
|
||||
|
||||
const ItemReview: React.FC<ItemReviewProps> = ({ item }) => {
|
||||
return (
|
||||
<div className="item-review-customer-hompage relative flex items-center justify-between">
|
||||
<div className="left-review relative">
|
||||
<Image src={item.avatar} width={88} height={88} className="lazy" alt="avatar" />
|
||||
</div>
|
||||
<div className="right-reivew">
|
||||
<p className="text-reivew line-clamp-2">{item.text}</p>
|
||||
<b className="author-review font-[500]">{item.author}</b>
|
||||
<Image
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/5star-customer.png"
|
||||
width={80}
|
||||
height={15}
|
||||
alt="rating"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ItemReview;
|
||||
@@ -1,43 +0,0 @@
|
||||
interface Review {
|
||||
avatar: string;
|
||||
text: string;
|
||||
author: string;
|
||||
}
|
||||
export const dataReview: Review[] = [
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/static/assets/nguyencong_2023/images/avatar_nc.png',
|
||||
text: 'Cảm thấy hài lòng khi sử dụng sản phẩm và dịch vụ của Nguyễn Công PC',
|
||||
author: '- Cầu thủ ĐTQG Hoàng Đức -',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/media/news/0709_khoapub.jpg',
|
||||
text: 'Đến Nguyễn Công PC build là yên tâm rồi, tiền bạc không thành vấn đề. Anh chốt bộ PC 300 củ.',
|
||||
author: '- Youtuber Khoa Pug -',
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
'https://nguyencongpc.vn/media/lib/02-06-2025/z6664231683539_3ee948af405f481eaf36691798831c43saochp.jpg',
|
||||
text: 'Hệ thống đào tạo đồ họa và thiết kế - thi công của anh tất cả bộ máy đều của Nguyễn Công PC',
|
||||
author: '- Mr. Tuấn VietCG -',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/media/news/0709_tranngoc.jpg',
|
||||
text: 'Không cần lo về giá vì đã có Nguyễn Công PC.',
|
||||
author: '- MC Trần Ngọc -',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/media/news/0709_tienlinh.jpg',
|
||||
text: 'Thằng em Hoàng Đức của mình rất hài lòng vì bộ PC mình sắm cho nó tại đây',
|
||||
author: '- Cầu thủ ĐTQG Tiến Linh -',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/media/lib/02-06-2025/chimse.jpg',
|
||||
text: 'Từ lúc sắm PC tại Nguyễn Công PC vẩy E không trượt phát nào',
|
||||
author: '- Chim sẻ đi nắng -',
|
||||
},
|
||||
{
|
||||
avatar: 'https://nguyencongpc.vn/media/lib/02-06-2025/sep-tuan-maxhome.jpg',
|
||||
text: '12 chi nhánh Maxhome trên toàn quốc đều sử dụng máy tính tại NC. Mình build PC ở đây hơn 10 năm rồi vẫn rất hài lòng.',
|
||||
author: '- Mr. Tuấn CEO Maxhome -',
|
||||
},
|
||||
];
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client';
|
||||
import { dataReview } from './dataReview';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
|
||||
import ItemReview from './ItemReview';
|
||||
const BoxReviewCustomer: React.FC = () => {
|
||||
return (
|
||||
<div className="box-review-from-customer boder-radius-10">
|
||||
<div className="title-box">
|
||||
<h2 className="title-box font-[600]">Đánh giá từ khách hàng về Nguyễn Công PC</h2>
|
||||
</div>
|
||||
<div className="list-review-customer-homepage">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={15}
|
||||
slidesPerView={3}
|
||||
loop={true}
|
||||
pagination={{ clickable: true }}
|
||||
>
|
||||
{dataReview.map((item, index) => (
|
||||
<SwiperSlide key={index} className="item">
|
||||
<ItemReview item={item} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BoxReviewCustomer;
|
||||
@@ -1,65 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { bannerData } from '@/data/banner';
|
||||
|
||||
const SliderHome: React.FC = () => {
|
||||
// data banner slider
|
||||
const dataSlider = bannerData[0].homepage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="box-banner-homepage relative">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
>
|
||||
{dataSlider?.banner_slider_homepage_main?.map((banner, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<Link href={banner.desUrl} className="item-banner boder-radius-10">
|
||||
<Image
|
||||
src={banner.fileUrl}
|
||||
width={1909}
|
||||
height={57}
|
||||
alt={banner.title}
|
||||
priority={true}
|
||||
className="boder-radius-10"
|
||||
/>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
<div className="box-banner-under-slider">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={2}
|
||||
loop={true}
|
||||
navigation={true}
|
||||
>
|
||||
{dataSlider?.banner_under_slider_trangchu?.map((banner, index) => (
|
||||
<SwiperSlide key={index} className="item">
|
||||
<Link href={banner.desUrl} className="item-banner boder-radius-10">
|
||||
<Image
|
||||
src={banner.fileUrl}
|
||||
width={banner.width}
|
||||
height={banner.height}
|
||||
alt={banner.title}
|
||||
className="lazy boder-radius-10"
|
||||
/>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SliderHome;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import SliderHome from './SliderHome';
|
||||
import BoxProductDeal from './Deal';
|
||||
import CategoryFeature from './CategoryFeature';
|
||||
import BoxListCategory from './Category';
|
||||
import BoxArticle from './Article';
|
||||
import BoxArticleVideo from './ArticleVideo';
|
||||
import BoxReviewCustomer from './ReviewCustomer';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="page-hompage mt-4">
|
||||
<div className="container">
|
||||
{/* slider */}
|
||||
<SliderHome />
|
||||
{/* deal */}
|
||||
<BoxProductDeal />
|
||||
{/* box danh mục nổi bật */}
|
||||
<CategoryFeature />
|
||||
|
||||
{/* DANH SÁCH DANH MỤC */}
|
||||
<BoxListCategory />
|
||||
|
||||
{/* tin tức công nghệ */}
|
||||
<BoxArticle />
|
||||
|
||||
{/* tin tức video */}
|
||||
<BoxArticleVideo />
|
||||
|
||||
{/* đánh giá từa khách hàng */}
|
||||
<BoxReviewCustomer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { bannerData } from '@/data/banner';
|
||||
|
||||
const BannerCategory = () => {
|
||||
const dataSlider = bannerData[0].product_list;
|
||||
|
||||
return (
|
||||
<div className="box-banner-category">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
>
|
||||
{dataSlider?.banner_category_2023?.map((banner, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<Link href={banner.desUrl} className="item-banner boder-radius-10">
|
||||
<Image
|
||||
src={banner.fileUrl}
|
||||
width={1909}
|
||||
height={57}
|
||||
alt={banner.title}
|
||||
priority={true}
|
||||
className="boder-radius-10"
|
||||
/>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BannerCategory;
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { PriceFilter, AttributeFilterList, BrandFilter } from '@/types';
|
||||
import { FaXmark } from 'react-icons/fa6';
|
||||
|
||||
interface Filters {
|
||||
price_filter_list?: PriceFilter[];
|
||||
attribute_filter_list?: AttributeFilterList[];
|
||||
brand_filter_list?: BrandFilter[];
|
||||
current_category?: { url: string };
|
||||
}
|
||||
|
||||
const ActiveFilters: React.FC<{ filters: Filters }> = ({ filters }) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const fullUrl = `${pathname}?${searchParams.toString()}`;
|
||||
|
||||
const selectedPrice = filters.price_filter_list?.filter((f) => fullUrl.includes(f.url)) ?? [];
|
||||
const selectedBrand = filters.brand_filter_list?.filter((f) => fullUrl.includes(f.url)) ?? [];
|
||||
const selectedAttr =
|
||||
filters.attribute_filter_list?.flatMap((attr) =>
|
||||
attr.value_list.filter((v) => pathname.includes(v.url)),
|
||||
) ?? [];
|
||||
|
||||
const allSelected = [...selectedPrice, ...selectedBrand, ...selectedAttr];
|
||||
const isFiltered = allSelected.length;
|
||||
|
||||
console.log(isFiltered);
|
||||
|
||||
if (isFiltered === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="info-filter-category flex gap-3">
|
||||
<p className="title">Lọc theo:</p>
|
||||
<div className="list-filter-category list-filter-active list-filter-last flex flex-wrap items-center gap-3">
|
||||
{selectedPrice.map((item) => (
|
||||
<Link key={item.url} href={item.url} className="item flex items-center gap-2 bg-white">
|
||||
{item.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
{selectedBrand.map((item) => (
|
||||
<Link key={item.url} href={item.url} className="item flex items-center gap-2 bg-white">
|
||||
{item.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
{selectedAttr.map((val) => (
|
||||
<Link key={val.url} href={val.url} className="item flex items-center gap-2 bg-white">
|
||||
{val.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{isFiltered >= 1 && (
|
||||
<Link
|
||||
href={filters.current_category?.url ?? '#'}
|
||||
className="item delete-filter-all flex items-center gap-2 bg-white"
|
||||
>
|
||||
Xóa tất cả <FaXmark />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveFilters;
|
||||
@@ -1,106 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PriceFilter, AttributeFilterList, BrandFilter } from '@/types';
|
||||
import { FaSortDown } from 'react-icons/fa6';
|
||||
import ActiveFilters from './ActiveFilters';
|
||||
|
||||
interface Filters {
|
||||
price_filter_list?: PriceFilter[];
|
||||
attribute_filter_list?: AttributeFilterList[];
|
||||
brand_filter_list?: BrandFilter[];
|
||||
}
|
||||
|
||||
interface BoxFilterProps {
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
const BoxFilter: React.FC<BoxFilterProps> = ({ filters }) => {
|
||||
const { price_filter_list, attribute_filter_list, brand_filter_list } = filters;
|
||||
|
||||
return (
|
||||
<div className="box-filter-category boder-radius-10">
|
||||
{/* khoảng giá */}
|
||||
{price_filter_list && (
|
||||
<div className="info-filter-category flex gap-10">
|
||||
<p className="title">Khoảng giá:</p>
|
||||
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-2">
|
||||
{price_filter_list.map((ItemPrice, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`item item-cetner flex gap-4 ${ItemPrice.is_selected == '1' ? 'current' : ''}`}
|
||||
>
|
||||
<Link href={ItemPrice.url}>{ItemPrice.name}</Link>
|
||||
<a href={ItemPrice.url}>
|
||||
(${ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count})
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* chọn thiêu tiêu trí */}
|
||||
{attribute_filter_list && (
|
||||
<div className="info-filter-category flex gap-10">
|
||||
<p className="title">Chọn theo tiêu chí:</p>
|
||||
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-3">
|
||||
{/* thương hiệu */}
|
||||
{brand_filter_list && brand_filter_list.length > 0 && (
|
||||
<div className={`item ${brand_filter_list[0].is_selected === '1' ? 'current' : ''}`}>
|
||||
<div className="flex items-center">
|
||||
{brand_filter_list[0].is_selected === '1' ? (
|
||||
<span>{brand_filter_list[0].name}</span>
|
||||
) : (
|
||||
<span>Thương hiệu</span>
|
||||
)}
|
||||
<FaSortDown size={16} style={{ marginBottom: 8 }} />
|
||||
</div>
|
||||
<ul>
|
||||
{brand_filter_list.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center gap-3">
|
||||
<Link href={item.url}>{item.name}</Link>
|
||||
<Link href={item.url}>({item.is_selected === '1' ? 'Xóa' : item.count})</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attribute filter */}
|
||||
{attribute_filter_list && attribute_filter_list.length > 0 && (
|
||||
<>
|
||||
{attribute_filter_list.map((attr, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`item ${attr.value_list[0]?.is_selected === '1' ? 'current' : ''}`}
|
||||
>
|
||||
<a href="javascript:void(0)" className="flex items-center">
|
||||
{attr.value_list[0]?.is_selected === '1' ? (
|
||||
<span>{attr.value_list[0].name}</span>
|
||||
) : (
|
||||
<span>{attr.name}</span>
|
||||
)}
|
||||
<FaSortDown size={16} style={{ marginBottom: 8 }} />
|
||||
</a>
|
||||
<ul>
|
||||
{attr.value_list.map((val) => (
|
||||
<li key={val.id} className="flex items-center gap-3">
|
||||
<Link href={val.url}>{val.name}</Link>
|
||||
<Link href={val.url}>{val.is_selected === '1' ? 'Xóa' : val.count}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActiveFilters filters={filters} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BoxFilter;
|
||||
@@ -1,93 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { FaGrip, FaList } from 'react-icons/fa6';
|
||||
|
||||
interface SortItem {
|
||||
key: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SortProps {
|
||||
sort_by_collection: SortItem[];
|
||||
product_display_type?: 'grid' | 'list';
|
||||
}
|
||||
|
||||
const BoxSort: React.FC<SortProps> = ({ sort_by_collection, product_display_type }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="box-sort-category flex items-center justify-between">
|
||||
<div className="sort-option flex items-center gap-3">
|
||||
{sort_by_collection
|
||||
.filter((item) =>
|
||||
['price-desc', 'price-asc', 'comment', 'rating', 'name'].includes(item.key),
|
||||
)
|
||||
.map((item) => {
|
||||
let label: string | null = null;
|
||||
let iconClass: string | null = null;
|
||||
|
||||
switch (item.key) {
|
||||
case 'price-desc':
|
||||
label = 'Giá giảm dần';
|
||||
iconClass = 'sprite-more sprite-gia-giam-category';
|
||||
break;
|
||||
case 'price-asc':
|
||||
label = 'Giá tăng dần';
|
||||
iconClass = 'sprite-more sprite-gia-tang-cateogry';
|
||||
break;
|
||||
case 'comment':
|
||||
label = 'Trao đổi';
|
||||
iconClass = 'sprite-more sprite-trao-doi-category';
|
||||
break;
|
||||
case 'rating':
|
||||
label = 'Đánh giá';
|
||||
iconClass = 'sprite-more sprite-danh-gia-category';
|
||||
break;
|
||||
case 'name':
|
||||
label = 'Tên A->Z';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.url}
|
||||
className={`item flex items-center ${
|
||||
pathname.includes(item.key) ? 'selected' : ''
|
||||
}`}
|
||||
>
|
||||
{iconClass && <i className={iconClass}></i>}
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="sort-bar-select-category flex items-center gap-3">
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={`item-sort-bar d-flex align-items-center ${
|
||||
product_display_type === 'grid' ? 'active' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<FaGrip />
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={`item-sort-bar ${product_display_type === 'list' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
console.log('Set display to list');
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<FaList />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxSort;
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { ChildCategory } from '@/types';
|
||||
|
||||
interface BoxCategoryChildProps {
|
||||
item: ChildCategory;
|
||||
}
|
||||
|
||||
const ItemCategoryChild: React.FC<BoxCategoryChildProps> = ({ item }) => {
|
||||
const ItemImage = item.big_image
|
||||
? item.big_image
|
||||
: item.thumnail
|
||||
? item.thumnail
|
||||
: 'https://nguyencongpc.vn/static/assets/nguyencong_2023/images/favicon.png';
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link href={item.url}>
|
||||
<div className="border-img lazy flex items-center justify-center">
|
||||
<Image src={ItemImage} width={60} height={60} alt={item.title} />
|
||||
</div>
|
||||
<p className="txt font-weight-500">{item.title}</p>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemCategoryChild;
|
||||
@@ -1,124 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import type { CategoryData } from '@/types';
|
||||
import { productCategoryData } from '@/data/product/category';
|
||||
import { findCategoryBySlug } from '@/lib/product/category';
|
||||
|
||||
// box
|
||||
import { Breadcrumb } from '@/components/Common/Breadcrumb';
|
||||
import BannerCategory from './BannerCategory';
|
||||
import ItemCategoryChild from './ItemCategoryChild';
|
||||
import BoxFilter from './BoxFilter';
|
||||
import BoxSort from './BoxSort';
|
||||
import ItemProduct from '@/components/Common/ItemProduct';
|
||||
|
||||
interface CategoryPageProps {
|
||||
slug: string; // khai báo prop slug
|
||||
}
|
||||
|
||||
const CategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
|
||||
// Ép kiểu dữ liệu từ index.ts về CategoryData[]
|
||||
const categories = productCategoryData as unknown as CategoryData[];
|
||||
const currentCategory = findCategoryBySlug(slug, categories);
|
||||
|
||||
const breadcrumbItems = currentCategory?.current_category?.path?.path?.map((p) => ({
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
})) ?? [
|
||||
{ name: 'Trang chủ', url: '/' },
|
||||
{ name: currentCategory?.current_category.name, url: currentCategory?.current_category.url },
|
||||
];
|
||||
// Trường hợp không tìm thấy danh mục
|
||||
if (!currentCategory) {
|
||||
return (
|
||||
<div className="flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 py-50">
|
||||
<div className="max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.172 9.172a4 4 0 015.656 5.656M6.343 6.343a8 8 0 0111.314 11.314M12 12l.01.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold text-gray-800">Không tìm thấy danh mục</h1>
|
||||
|
||||
<p className="mt-3 text-gray-600">
|
||||
Đường dẫn <code className="rounded bg-gray-100 px-2 py-0.5 text-sm">{slug}</code> không
|
||||
tồn tại hoặc đã bị xoá.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-6 inline-flex items-center justify-center rounded-lg bg-blue-600 px-6 py-2.5 font-medium text-white transition hover:bg-blue-700 focus:ring-2 focus:ring-blue-400 focus:outline-none"
|
||||
>
|
||||
← Về trang chủ
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// lấy sản phẩm
|
||||
const products = Object.values(currentCategory.product_list);
|
||||
|
||||
return (
|
||||
<div className="page-category">
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<BannerCategory />
|
||||
|
||||
<h1 className="name-category font-bold">{currentCategory.current_category.name}</h1>
|
||||
<div className="box-content-category">
|
||||
<ul className="category-child boder-radius-10 flex flex-wrap justify-center">
|
||||
{currentCategory.current_category.children?.map((item, index) => (
|
||||
<ItemCategoryChild item={item} key={index} />
|
||||
))}
|
||||
</ul>
|
||||
{/* filter */}
|
||||
<BoxFilter filters={currentCategory} />
|
||||
|
||||
<div className="box-list-product-category boder-radius-10">
|
||||
{/* filter sort */}
|
||||
<BoxSort
|
||||
sort_by_collection={currentCategory.sort_by_collection}
|
||||
product_display_type="grid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* list product */}
|
||||
|
||||
<div className="list-product-category grid grid-cols-5 gap-3">
|
||||
{products.map((item, index) => (
|
||||
<ItemProduct key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="paging flex items-center justify-center">
|
||||
{currentCategory.paging_collection.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.url}
|
||||
className={`item ${item.is_active === '1' ? 'current' : ''}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
@@ -1,86 +0,0 @@
|
||||
import { FaCheckSquare } from 'react-icons/fa';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
|
||||
|
||||
export const BoxBought = () => {
|
||||
return (
|
||||
<div className="pro-customer-bought">
|
||||
<svg
|
||||
className="pcb-icon"
|
||||
viewBox="0 0 438.533 438.533"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="red"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M409.133,109.203c-19.608-33.592-46.205-60.189-79.798-79.796C295.736,9.801,259.058,0,219.273,0
|
||||
c-39.781,0-76.47,9.801-110.063,29.407c-33.595,19.604-60.192,46.201-79.8,79.796C9.801,142.8,0,179.489,0,219.267
|
||||
c0,39.78,9.804,76.463,29.407,110.062c19.607,33.592,46.204,60.189,79.799,79.798c33.597,19.605,70.283,29.407,110.063,29.407
|
||||
s76.47-9.802,110.065-29.407c33.593-19.602,60.189-46.206,79.795-79.798c19.603-33.596,29.403-70.284,29.403-110.062
|
||||
C438.533,179.485,428.732,142.795,409.133,109.203z M334.332,232.111L204.71,361.736c-3.617,3.613-7.896,5.428-12.847,5.428
|
||||
c-4.952,0-9.235-1.814-12.85-5.428l-29.121-29.13c-3.617-3.613-5.426-7.898-5.426-12.847c0-4.941,1.809-9.232,5.426-12.847
|
||||
l87.653-87.646l-87.657-87.65c-3.617-3.612-5.426-7.898-5.426-12.845c0-4.949,1.809-9.231,5.426-12.847l29.121-29.13
|
||||
c3.619-3.615,7.898-5.424,12.85-5.424c4.95,0,9.233,1.809,12.85,5.424l129.622,129.621c3.613,3.614,5.42,7.898,5.42,12.847
|
||||
C339.752,224.213,337.945,228.498,334.332,232.111z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="pcb-slider swiper-customer-bought">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
autoplay={{
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div>
|
||||
<p>
|
||||
<b>Khách hàng Anh Tuấn (036 856 xxxx)</b>
|
||||
</p>
|
||||
<p>Đã mua hàng 2 giờ trước</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div>
|
||||
<p>
|
||||
<b>Khách hàng Quốc Trung (035 348 xxxx)</b>
|
||||
</p>
|
||||
<p>Đã mua hàng 1 giờ trước</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div>
|
||||
<p>
|
||||
<b>Khách hàng Quang Ngọc (097 478 xxxx)</b>
|
||||
</p>
|
||||
<p>Đã mua hàng 30 phút trước</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div>
|
||||
<p>
|
||||
<b>Khách hàng Mạnh Lực (037 204 xxxx)</b>
|
||||
</p>
|
||||
<p>Đã mua hàng 25 phút trước</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div>
|
||||
<p>
|
||||
<b>Khách hàng Hiếu (096 859 xxxx)</b>
|
||||
</p>
|
||||
<p>Đã mua hàng 20 phút trước</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { ProductDetailData } from '@/types';
|
||||
import CounDown from '@/components/Common/CounDown';
|
||||
import { formatCurrency } from '@/lib/formatPrice';
|
||||
|
||||
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">
|
||||
<CounDown deadline={item.product_info.sale_rules.to_time} />
|
||||
</div>
|
||||
<div className="box-right">
|
||||
<div className="box-product-deal">
|
||||
<p className="text-deal-detail">
|
||||
Còn{' '}
|
||||
{(() => {
|
||||
const deal = item.product_info.deal_list[0];
|
||||
return Number(deal.quantity) - deal.sale_order;
|
||||
})()}
|
||||
/{item.product_info.deal_list[0].quantity} sản phẩm
|
||||
</p>
|
||||
|
||||
<div className="p-quantity-sale" data-quantity-left="3" data-quantity-sale-total="5">
|
||||
<i className="sprite sprite-fire-deal"></i>
|
||||
<div className="bg-gradient"></div>
|
||||
{(() => {
|
||||
const deal = item.product_info.deal_list[0];
|
||||
const percentRemaining =
|
||||
((Number(deal.quantity) - deal.sale_order) / Number(deal.quantity)) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p
|
||||
className="js-line-deal-left"
|
||||
style={{ width: `${percentRemaining}%` }}
|
||||
></p>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* giá */}
|
||||
|
||||
{item.product_info.marketPrice > '0' && item.product_info.sale_rules.type == 'deal' && (
|
||||
<div
|
||||
className="box-price-detail boder-radius-10 flex flex-wrap items-center"
|
||||
style={{ rowGap: '8px' }}
|
||||
>
|
||||
<p className="price-detail font-bold">
|
||||
{item.product_info.price !== '0'
|
||||
? `${formatCurrency(item.product_info.price)}đ`
|
||||
: 'Liên hệ'}
|
||||
</p>
|
||||
{item.product_info.marketPrice > '0' && (
|
||||
<>
|
||||
<span className="market-price-detail font-weight-500">
|
||||
{formatCurrency(item.product_info.marketPrice)}₫
|
||||
</span>
|
||||
<div className="save-price-detail flex items-center gap-1">
|
||||
<span>Tiết kiệm</span>
|
||||
{(() => {
|
||||
return formatCurrency(
|
||||
Number(item.product_info.marketPrice) - Number(item.product_info.price),
|
||||
);
|
||||
})()}
|
||||
<span>đ</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,156 +0,0 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { ProductDetailData } from '@/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { BoxPrice } from './BoxPrice';
|
||||
import { BoxBought } from './BoxBought';
|
||||
|
||||
// thêm giỏ hàng
|
||||
import { addToCart } from '@/lib/ButtonCart';
|
||||
|
||||
export const BoxInfoRight = (item: ProductDetailData) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBuyNow = () => {
|
||||
router.push('/cart');
|
||||
addToCart(item.product_info.productId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
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>
|
||||
<button
|
||||
className="addCart flex cursor-pointer flex-wrap items-center justify-center gap-3"
|
||||
onClick={() => {
|
||||
addToCart(item.product_info.productId);
|
||||
alert('Sản phẩm đã được thêm vào giỏ hàng!');
|
||||
}}
|
||||
>
|
||||
<i className="sprite sprite-cart-detail"></i>
|
||||
<p className="title-cart">Thêm vào giỏ hàng</p>
|
||||
</button>
|
||||
<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"
|
||||
onClick={() => handleBuyNow()}
|
||||
>
|
||||
<button className="detail-buy-now col-span-2 cursor-pointer">
|
||||
<span>ĐẶT MUA NGAY</span>
|
||||
Giao hàng tận nơi nhanh chóng
|
||||
</button>
|
||||
|
||||
<button className="detail-add-cart">
|
||||
<span>TRẢ GÓP QUA HỒ SƠ</span>
|
||||
Chỉ từ 2.665.000₫/ tháng
|
||||
</button>
|
||||
|
||||
<button className="detail-add-cart">
|
||||
<span>TRẢ GÓP QUA THẺ</span>
|
||||
Chỉ từ 1.332.500₫/ tháng
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* 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">
|
||||
<div className="item flex items-center gap-2">
|
||||
<i className="sprite sprite-camket-detail"></i>
|
||||
<p>Cam kết giá tốt nhất thị trường.</p>
|
||||
</div>
|
||||
<div className="item flex items-center gap-2">
|
||||
<i className="sprite sprite-sanphammoi-detail"></i>
|
||||
<p>Sản phẩm mới 100%.</p>
|
||||
</div>
|
||||
<div className="item flex items-center gap-2">
|
||||
<i className="sprite sprite-1doi1-detail"></i>
|
||||
<p>Lỗi 1 đổi 1 ngay lập tức.</p>
|
||||
</div>
|
||||
<div className="item flex items-center gap-2">
|
||||
<i className="sprite sprite-hotrotragop-detail"></i>
|
||||
<p>Hỗ trợ trả góp - Thủ tục nhanh gọn.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BoxBought />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ProductImageGallery } from '@/types';
|
||||
import type { Swiper as SwiperType } from 'swiper';
|
||||
import useFancybox from '@/hooks/useFancybox';
|
||||
|
||||
interface ImageProps {
|
||||
ItemImage: ProductImageGallery[];
|
||||
}
|
||||
|
||||
export const ImageProduct: React.FC<ImageProps> = ({ ItemImage }) => {
|
||||
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
|
||||
|
||||
const [fancyboxRef] = useFancybox({
|
||||
closeButton: 'auto',
|
||||
dragToClose: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="product-images-show">
|
||||
<div className="gallery-top product-info-image" ref={fancyboxRef}>
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
thumbs={{ swiper: thumbsSwiper }}
|
||||
>
|
||||
{ItemImage?.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<Link href={item.size.original} className="bigImage" data-fancybox>
|
||||
<Image src={item.size.original} alt={''} width="595" height="595" />
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
<div className="gallery-thumbs product-images-slider mt-2">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={12}
|
||||
slidesPerView={6}
|
||||
loop={true}
|
||||
onSwiper={setThumbsSwiper}
|
||||
>
|
||||
{ItemImage?.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div className="smallImage">
|
||||
<Image src={item.size.original} alt={''} width="90" height="60" />
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
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 relative 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>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
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 và đá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>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,137 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
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 và đá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á và 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>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
'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;
|
||||
}
|
||||
|
||||
const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ slug }) => {
|
||||
const productDetails = productDetailData as unknown as ProductDetailData[];
|
||||
const Products = findProductDetailBySlug(slug, productDetails);
|
||||
|
||||
const breadcrumbItems = Products?.product_info?.productPath?.[0]?.path.map((item) => ({
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
})) ?? [{ name: 'Trang chủ', url: '/' }];
|
||||
|
||||
// Trường hợp không tìm thấy chi tiết sản phẩm
|
||||
// Không tìm thấy chi tiết sản phẩm
|
||||
if (!Products) {
|
||||
return <ErrorLink />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</div>
|
||||
<section className="page-product-detail mt-2 bg-white">
|
||||
<div className="container">
|
||||
<div className="box-content-product-detail flex justify-between gap-5">
|
||||
<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>
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetailPage;
|
||||
Reference in New Issue
Block a user