This commit is contained in:
2026-03-13 17:23:37 +07:00
parent 25111ff10e
commit 2d2bf85f83
43 changed files with 2094 additions and 448 deletions

View File

@@ -5,8 +5,8 @@ import Image from 'next/image';
import type { TypeArticleCatePage } from '@/types/article/TypeArticleCatePage';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { ErrorLink } from '@components/Common/Error';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import { ErrorLink } from '@/components/Common/Error';
import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from '../ArticleTopRight';
import ItemArticle from '@/components/Common/ItemArticle';
@@ -85,7 +85,7 @@ const ArticleCategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
))}
</div>
<Link
href="/tin-cong-nghe"
href={currentCategory.category_info.request_path}
className="btn-article-col flex items-center justify-center gap-2 font-[500]"
>
Xem tất cả

View File

@@ -92,18 +92,18 @@ export default function TocBox({ htmlContent }: { htmlContent: string }) {
};
}, [htmlContent]);
if (!headingsTree.length) return null;
return (
<>
<div className="archor-text-group">
<div className="toc_title flex items-center justify-between gap-2">
<b className="text-fint-toc flex items-center text-base font-bold">
<span>Nội dung chính</span>
</b>
{headingsTree.length > 0 && (
<div className="archor-text-group">
<div className="toc_title flex items-center justify-between gap-2">
<b className="text-fint-toc flex items-center text-base font-bold">
<span>Nội dung chính</span>
</b>
</div>
<div id="js-outp">{renderTree(headingsTree)}</div>
</div>
<div id="js-outp">{renderTree(headingsTree)}</div>
</div>
)}
<div
className="box-article-detail-ct nd js_find"
dangerouslySetInnerHTML={{ __html: contentWithIds }}

View File

@@ -4,9 +4,9 @@ import Link from 'next/link';
import Image from 'next/image';
import type { TypeArticleDetailPage } from '@/types/article/TypeArticleDetailPage';
import { ErrorLink } from '@components/Common/Error';
import { ErrorLink } from '@/components/Common/Error';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import TocBox from './TocBox';
import PreLoader from '@/components/Common/PreLoader';
import { getArticleCategories, getArticleDetail } from '@/lib/api/article';
@@ -93,9 +93,9 @@ const ArticleDetailPage: React.FC<DetailPageProps> = ({ slug }) => {
/>
</Link>
<div className="content-article flex-1">
<a href={item.url} className="title-article">
<Link href={item.url} className="title-article">
<h3 className="font-weight-400 line-clamp-2">{item.title}</h3>
</a>
</Link>
<p className="time-article d-flex align-items-center gap-4">
<i className="sprite sprite-clock-item-article"></i>
<span>{item.createDate}</span>

View File

@@ -2,7 +2,8 @@
import React from 'react';
import Link from 'next/link';
import { Breadcrumb } from '@components/Common/Breadcrumb';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import Skeleton from '@/components/Common/Skeleton';
import { ArticleTopLeft } from '../ArticleTopLeft';
import { ArticleTopRight } from '../ArticleTopRight';
@@ -13,14 +14,45 @@ import { getArticleCategories } from '@/lib/api/article';
import { useApiData } from '@/hooks/useApiData';
import type { TypeArticleCategory } from '@/types/article/ListCategoryArticle';
const ArticleHomeSkeleton = () => (
<section className="page-article pb-10">
<div className="container">
<Skeleton className="mb-4 h-5 w-48" />
<div className="tabs-category-article flex items-center gap-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-24" />
))}
</div>
<div className="box-article-home-top mt-4 grid grid-cols-3 gap-3">
<div className="col-span-2 flex gap-3">
<Skeleton className="aspect-video flex-1" />
<div className="flex flex-1 flex-col gap-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-20 w-full" />
))}
</div>
</div>
<div className="flex flex-col gap-3">
<Skeleton className="h-10 w-full" />
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</div>
</div>
</div>
</section>
);
const ArticleHome = () => {
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
const { data: categories } = useApiData(
const { data: categories, isLoading } = useApiData(
() => getArticleCategories(),
[],
{ initialData: [] as TypeArticleCategory[] },
);
if (isLoading) return <ArticleHomeSkeleton />;
return (
<section className="page-article pb-10">
<div className="container">

View File

@@ -6,9 +6,10 @@ import ItemArticleVideo from './ItemArticleVideo';
import { getArticleVideos } from '@/lib/api/article';
import { useApiData } from '@/hooks/useApiData';
import type { Article } from '@/types';
import Skeleton from '@/components/Common/Skeleton';
const BoxArticleVideo: React.FC = () => {
const { data: videos } = useApiData(
const { data: videos, isLoading } = useApiData(
() => getArticleVideos(),
[],
{ initialData: [] as Article[] },
@@ -31,9 +32,17 @@ const BoxArticleVideo: React.FC = () => {
</Link>
</div>
<div className="list-videos-group list-article-group flex items-center gap-10">
{videos.slice(0, 4).map((item) => (
<ItemArticleVideo item={item} key={item.id} />
))}
{isLoading
? Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex flex-1 flex-col gap-2">
<Skeleton className="aspect-video w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
))
: videos.slice(0, 4).map((item) => (
<ItemArticleVideo item={item} key={item.id} />
))}
</div>
</div>
);

View File

@@ -6,9 +6,10 @@ import ItemArticle from './ItemArticle';
import { getArticles } from '@/lib/api/article';
import { useApiData } from '@/hooks/useApiData';
import type { Article } from '@/types';
import Skeleton from '@/components/Common/Skeleton';
const BoxArticle: React.FC = () => {
const { data: articles } = useApiData(
const { data: articles, isLoading } = useApiData(
() => getArticles(),
[],
{ initialData: [] as Article[] },
@@ -26,9 +27,18 @@ const BoxArticle: React.FC = () => {
</Link>
</div>
<div className="list-article-group flex items-center gap-10">
{articles.slice(0, 4).map((item) => (
<ItemArticle item={item} key={item.id} />
))}
{isLoading
? Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex flex-1 flex-col gap-2">
<Skeleton className="aspect-video w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
))
: articles.slice(0, 4).map((item) => (
<ItemArticle item={item} key={item.id} />
))}
</div>
</div>
);

View File

@@ -6,17 +6,51 @@ 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 Skeleton from '@/components/Common/Skeleton';
import { menuData } from '@/components/Other/Header/menuData';
import { getProductHot } from '@/lib/api/product';
import { useApiData } from '@/hooks/useApiData';
import type { TypeListProduct } from '@/types/global/TypeListProduct';
const CategorySkeleton = () => (
<div className="box-product-category boder-radius-10">
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-40" />
<div className="flex gap-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-5 w-20" />
))}
</div>
</div>
<div className="mt-4 grid grid-cols-5 gap-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex flex-col gap-2">
<Skeleton className="aspect-square w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-5 w-1/2" />
</div>
))}
</div>
</div>
);
const BoxListCategory: React.FC = () => {
const { data: products } = useApiData(() => getProductHot(), [], {
const { data: products, isLoading } = useApiData(() => getProductHot(), [], {
initialData: [] as TypeListProduct,
});
if (isLoading) {
return (
<>
{menuData[0].product.all_category.map((_, index) => (
<CategorySkeleton key={index} />
))}
</>
);
}
return (
<>
{menuData[0].product.all_category.map((item, index) => (

View File

@@ -1,6 +1,7 @@
'use client';
import React from 'react';
import { menuData } from '@components/Other/Header/menuData';
import { menuData } from '@/components/Other/Header/menuData';
import ItemCategory from './ItemCategory';
import { InfoCategory } from '@/types';

View File

@@ -1,5 +1,5 @@
'use client';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules';
@@ -8,14 +8,35 @@ import { TypeListProductDeal } from '@/types';
import { getDeals } from '@/lib/api/deal';
import CountDown from '@/components/Common/CountDown';
import ProductItem from './ProductItem';
import Skeleton from '@/components/Common/Skeleton';
import { useApiData } from '@/hooks/useApiData';
const BoxProductDeal: React.FC = () => {
const [deals, setDeals] = useState<TypeListProductDeal>([]);
const [expired, setExpired] = useState(false);
const { data: deals, isLoading } = useApiData(() => getDeals(), [], {
initialData: [] as TypeListProductDeal,
});
useEffect(() => {
getDeals().then(setDeals).catch(console.error);
}, []);
if (isLoading) {
return (
<div className="box-product-deal boder-radius-10">
<div className="box-title-deal flex items-center justify-between">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-8 w-32" />
</div>
<div className="mt-4 grid grid-cols-6 gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex flex-col gap-2">
<Skeleton className="aspect-square w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-5 w-1/2" />
</div>
))}
</div>
</div>
);
}
if (expired) return null;

View File

@@ -6,9 +6,10 @@ import ItemReview from './ItemReview';
import { getHomeReviews } from '@/lib/api/home';
import { useApiData } from '@/hooks/useApiData';
import type { HomeReview } from '@/types';
import Skeleton from '@/components/Common/Skeleton';
const BoxReviewCustomer: React.FC = () => {
const { data: reviews } = useApiData(
const { data: reviews, isLoading } = useApiData(
() => getHomeReviews(),
[],
{ initialData: [] as HomeReview[] },
@@ -19,21 +20,40 @@ const BoxReviewCustomer: React.FC = () => {
<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 }}
>
{reviews.map((item, index) => (
<SwiperSlide key={`${item.author}-${index}`} className="item">
<ItemReview item={item} />
</SwiperSlide>
{isLoading ? (
<div className="mt-4 grid grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex flex-col gap-3 rounded-lg p-4">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex flex-col gap-1">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
))}
</Swiper>
</div>
</div>
) : (
<div className="list-review-customer-homepage">
<Swiper
modules={[Autoplay, Navigation, Pagination]}
spaceBetween={15}
slidesPerView={3}
loop={true}
pagination={{ clickable: true }}
>
{reviews.map((item, index) => (
<SwiperSlide key={`${item.author}-${index}`} className="item">
<ItemReview item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
)}
</div>
);
};

View File

@@ -1,18 +1,30 @@
'use client';
import React, { useEffect, useState } from 'react';
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 { getBanners } from '@/lib/api/banner';
import { TemplateBanner } from '@/types';
import Skeleton from '@/components/Common/Skeleton';
import { useApiData } from '@/hooks/useApiData';
const SliderHome: React.FC = () => {
const [banners, setBanners] = useState<TemplateBanner | null>(null);
const { data: banners, isLoading } = useApiData(() => getBanners(), [], {
initialData: null as TemplateBanner | null,
});
useEffect(() => {
getBanners().then(setBanners).catch(console.error);
}, []);
if (isLoading) {
return (
<>
<Skeleton className="h-100 w-full" />
<div className="mt-3 flex gap-3">
<Skeleton className="h-40 flex-1" />
<Skeleton className="h-40 flex-1" />
</div>
</>
);
}
const dataSlider = banners?.homepage;

View File

@@ -1,4 +1,5 @@
'use client';
import Link from 'next/link';
import { motion } from 'framer-motion';
@@ -11,7 +12,6 @@ const NotFound = () => {
transition={{ duration: 0.4 }}
className="w-full max-w-md rounded-3xl bg-white p-8 text-center shadow-xl"
>
{/* Icon lỗi link */}
<motion.div
animate={{ y: [0, -4, 0] }}
transition={{ repeat: Infinity, duration: 1.8 }}
@@ -38,13 +38,12 @@ const NotFound = () => {
Bạn truy cập không tồn tại hoặc đưng dẫn đã bị thay đi.
</p>
{/* CTA */}
<div className="mt-8 flex flex-col gap-3">
<Link
href="/"
className="rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-blue-700"
>
Về trang chủ
Về trang chủ
</Link>
</div>
</motion.div>

View File

@@ -8,8 +8,8 @@ import type { CategoryData } from '@/types';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BannerCategory from './BannerCategory';
import ItemCategoryChild from './ItemCategoryChild';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import BoxFilter from '@/components/Product/BoxFilter';
import BoxSort from '@/components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
import PreLoader from '@/components/Common/PreLoader';
import { getProductCategory } from '@/lib/api/product';

View File

@@ -1,5 +1,7 @@
'use client';
import React, { useState } from 'react';
import { submitProductReview } from '@/lib/api/product';
const STAR_OPTIONS = [5, 4, 3, 2, 1] as const;
@@ -8,36 +10,61 @@ interface ReviewFormError {
name?: string;
}
export const FormReview: React.FC = () => {
interface Props {
slug: string;
}
export const FormReview: React.FC<Props> = ({ slug }) => {
const [rate, setRate] = useState<number>(5);
const [content, setContent] = useState('');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<ReviewFormError>({});
const [submitted, setSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState('');
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: ReviewFormError = {};
if (!content.trim() || content.trim().length < 10) {
newErrors.content = 'Nội dung đánh giá tối thiểu 10 ký tự';
}
if (!name.trim()) {
newErrors.name = 'Vui lòng nhập tên của bạn';
}
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return;
// TODO: gọi API gửi đánh giá
console.log({ rate, content, name, email });
setSubmitted(true);
setErrors(newErrors);
setSubmitError('');
if (Object.keys(newErrors).length > 0) {
return;
}
try {
setIsSubmitting(true);
await submitProductReview(slug, {
content: content.trim(),
email: email.trim(),
name: name.trim(),
rate,
});
setSubmitted(true);
} catch {
setSubmitError('Không thể gửi đánh giá lúc này. Vui lòng thử lại sau.');
} finally {
setIsSubmitting(false);
}
};
if (submitted) {
return (
<div className="box-form-review">
<p className="font-medium text-green-600">
Cảm ơn bạn đã gửi đánh giá! Đánh giá của bạn đang chờ duyệt.
Cảm ơn bạn đã gửi đánh giá. Đánh giá của bạn đang chờ duyệt.
</p>
</div>
);
@@ -51,8 +78,8 @@ export const FormReview: React.FC = () => {
placeholder="Mời bạn để lại đánh giá..."
name="user_post[content]"
value={content}
onChange={(e) => {
setContent(e.target.value);
onChange={(event) => {
setContent(event.target.value);
setErrors((prev) => ({ ...prev, content: undefined }));
}}
/>
@@ -103,8 +130,8 @@ export const FormReview: React.FC = () => {
name="user_post[user_name]"
className="form-control"
value={name}
onChange={(e) => {
setName(e.target.value);
onChange={(event) => {
setName(event.target.value);
setErrors((prev) => ({ ...prev, name: undefined }));
}}
/>
@@ -123,7 +150,7 @@ export const FormReview: React.FC = () => {
name="user_post[user_email]"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(event) => setEmail(event.target.value)}
/>
</td>
</tr>
@@ -131,8 +158,10 @@ export const FormReview: React.FC = () => {
</table>
</div>
<button type="submit" className="btn-review send_form mb-10 mt-12">
Gửi đánh giá
{submitError && <p className="mb-3 text-sm text-red-500">{submitError}</p>}
<button type="submit" className="btn-review send_form mb-10 mt-12" disabled={isSubmitting}>
{isSubmitting ? 'Đang gửi...' : 'Gửi đánh giá'}
</button>
</div>
</form>

View File

@@ -1,8 +1,8 @@
'use client';
import React, { useState } from 'react';
import { Review } from '@/types';
import { FaStar } from 'react-icons/fa6';
import { Review } from '@/types';
import { FormReview } from './FormReview';
import { ListReview } from './ListReview';
@@ -15,14 +15,16 @@ export const ProductReview: React.FC<Props> = ({ review, slug }) => {
const [showForm, setShowForm] = useState(false);
const { summary } = review;
const totalRate = summary.list_rate.reduce((acc, item) => acc + Number(item.total), 0);
const totalRate = summary.list_rate.reduce((accumulator, item) => {
return accumulator + Number(item.total);
}, 0);
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),
rate1: Number(summary.list_rate.find((item) => item.rate === '1')?.total || 0),
rate2: Number(summary.list_rate.find((item) => item.rate === '2')?.total || 0),
rate3: Number(summary.list_rate.find((item) => item.rate === '3')?.total || 0),
rate4: Number(summary.list_rate.find((item) => item.rate === '4')?.total || 0),
rate5: Number(summary.list_rate.find((item) => item.rate === '5')?.total || 0),
};
const percents = {
percent1: totalRate > 0 ? (rates.rate1 / totalRate) * 100 : 0,
@@ -48,16 +50,14 @@ export const ProductReview: React.FC<Props> = ({ review, slug }) => {
{[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 className={`percent percent${rate}`} style={{ width: `${percent}%` }} />
</div>
<span className="total-avg-rate">
<strong>{total}</strong> đánh giá
@@ -68,24 +68,18 @@ export const ProductReview: React.FC<Props> = ({ review, slug }) => {
</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>
)}
{showForm && <FormReview />}
<p className="text-danh-gia mb-5">Bạn đánh giá sao sản phẩm này</p>
<button
type="button"
className="button-review mx-auto flex cursor-pointer items-center justify-center"
onClick={() => setShowForm((currentValue) => !currentValue)}
>
{showForm ? 'Đóng lại' : 'Đánh giá ngay'}
</button>
{showForm && <FormReview slug={slug} />}
</div>
</div>
<ListReview slug={slug} />

View File

@@ -81,10 +81,11 @@ const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ slug }) => {
return <ErrorLink />;
}
const breadcrumbItems = product.product_info.productPath?.[0]?.path.map((item) => ({
name: item.name,
url: item.url,
})) ?? [{ name: 'Trang chủ', url: '/' }];
const breadcrumbItems =
product.product_info.productPath?.[0]?.path.map((item) => ({
name: item.name,
url: item.url,
})) ?? [{ name: 'Trang chủ', url: '/' }];
return (
<>

View File

@@ -2,26 +2,28 @@
import React from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { ErrorLink } from '@/components/Common/Error';
import type { TypeProductHot } from '@/types/producthot';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import BoxFilter from '@/components/Product/BoxFilter';
import BoxSort from '@/components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
import PreLoader from '@/components/Common/PreLoader';
import { getProductHotPage } from '@/lib/api/product';
import { useApiData } from '@/hooks/useApiData';
import { getProductHotPage } from '@/lib/api/product';
import type { TypeProductHot } from '@/types/producthot';
interface ProductHotPageProps {
slug: string;
}
const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
const searchParams = useSearchParams();
const search = searchParams.toString();
const productDisplayType = searchParams.get('display') === 'list' ? 'list' : 'grid';
const { data: page, isLoading } = useApiData(
() => getProductHotPage(slug),
[slug],
() => getProductHotPage(slug, search),
[slug, search],
{ initialData: null as TypeProductHot | null },
);
@@ -37,7 +39,6 @@ const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
{ name: 'Trang chủ', url: '/' },
{ name: page.title, url: page.url },
];
const products = Object.values(page.product_list);
return (
@@ -52,11 +53,16 @@ const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
<span className="current-cate-total">(Tổng {page.product_count} sản phẩm)</span>
</div>
</div>
<div className="box-content-category">
<BoxFilter filters={page} />
<div className="box-list-product-category boder-radius-10">
<BoxSort sort_by_collection={page.sort_by_collection} product_display_type="grid" />
<BoxSort
sort_by_collection={page.sort_by_collection}
display_by_collection={page.display_by_collection}
product_display_type={productDisplayType}
/>
</div>
<div className="list-product-category grid grid-cols-5 gap-3">
@@ -64,10 +70,11 @@ const ProductHotPage: React.FC<ProductHotPageProps> = ({ slug }) => {
<ItemProduct key={item.id} item={item} />
))}
</div>
<div className="paging flex items-center justify-center">
{page.paging_collection.map((item, index) => (
{page.paging_collection.map((item) => (
<Link
key={index}
key={item.url}
href={item.url}
className={`item ${item.is_active === '1' ? 'current' : ''}`}
>

View File

@@ -3,25 +3,24 @@
import React from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { ErrorLink } from '@/components/Common/Error';
import type { TypeProductSearch } from '@/types/product/search';
import { Breadcrumb } from '@/components/Common/Breadcrumb';
import BoxFilter from '@components/Product/BoxFilter';
import BoxSort from '@components/Product/BoxSort';
import BoxFilter from '@/components/Product/BoxFilter';
import BoxSort from '@/components/Product/BoxSort';
import ItemProduct from '@/components/Common/ItemProduct';
import PreLoader from '@/components/Common/PreLoader';
import { getProductSearch } from '@/lib/api/product';
import { useApiData } from '@/hooks/useApiData';
import { getProductSearch } from '@/lib/api/product';
import type { TypeProductSearch } from '@/types/product/search';
const ProductSearchPage: React.FC = () => {
const searchParams = useSearchParams();
const keyword = searchParams.get('q') ?? '';
const search = searchParams.toString();
const productDisplayType = searchParams.get('display') === 'list' ? 'list' : 'grid';
const { data: page, isLoading } = useApiData(
() => getProductSearch(keyword),
[keyword],
() => getProductSearch(keyword, search),
[keyword, search],
{
initialData: null as TypeProductSearch | null,
enabled: keyword.length > 0,
@@ -40,7 +39,6 @@ const ProductSearchPage: React.FC = () => {
{ name: 'Trang chủ', url: '/' },
{ name: `Tìm kiếm "${keyword}"`, url: `/tim?q=${page.keywords}` },
];
const products = Object.values(page.product_list);
return (
@@ -57,8 +55,13 @@ const ProductSearchPage: React.FC = () => {
{products.length > 0 ? (
<div className="box-content-category">
<BoxFilter filters={page} />
<div className="box-list-product-category boder-radius-10">
<BoxSort sort_by_collection={page.sort_by_collection} product_display_type="grid" />
<BoxSort
sort_by_collection={page.sort_by_collection}
display_by_collection={page.display_by_collection}
product_display_type={productDisplayType}
/>
</div>
<div className="list-product-category grid grid-cols-5 gap-3">
@@ -66,10 +69,11 @@ const ProductSearchPage: React.FC = () => {
<ItemProduct key={item.id} item={item} />
))}
</div>
<div className="paging flex items-center justify-center">
{page.paging_collection.map((item, index) => (
{page.paging_collection.map((item) => (
<Link
key={index}
key={item.url}
href={item.url}
className={`item ${item.is_active === '1' ? 'current' : ''}`}
>
@@ -104,9 +108,7 @@ const ProductSearchPage: React.FC = () => {
<li>Thử lại bằng các từ khóa ngắn gọn hơn</li>
</ul>
</div>
<Link href="/">
<i className="fa fa-long-arrow-alt-left"></i> Quay lại trang chủ
</Link>
<Link href="/">Quay lại trang chủ</Link>
</div>
)}
</section>