This commit is contained in:
2025-12-25 13:54:37 +07:00
parent 9072f84681
commit fd9b5a3819
12 changed files with 3067 additions and 2461 deletions

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { FaCaretDown } from 'react-icons/fa'; import { FaCaretDown } from 'react-icons/fa';
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination } from 'swiper/modules'; import { Autoplay, Navigation, Pagination } from 'swiper/modules';
import ItemProduct from './ItemProduct'; import ItemProduct from '@/components/common/ItemProduct';
import { InfoCategory } from '@/types'; import { InfoCategory } from '@/types';

View File

@@ -47,7 +47,7 @@ const IconFixRight: React.FC = () => {
className="lazy" className="lazy"
/> />
<div className="contact-info"> <div className="contact-info">
<b className="d-block">Chat Facebook</b> <b className="block">Chat Facebook</b>
<span>(8h-22h30)</span> <span>(8h-22h30)</span>
</div> </div>
</Link> </Link>
@@ -57,14 +57,14 @@ const IconFixRight: React.FC = () => {
className="flex items-center" className="flex items-center"
style={{ style={{
background: '#fff', background: '#fff',
bottom: '135px', bottom: '145px',
width: '170px', width: '170px',
padding: '10px', padding: '10px',
borderRadius: '15px', borderRadius: '15px',
boxShadow: '0 1px 2px 1px #01010133', boxShadow: '0 1px 2px 1px #01010133',
color: '#007bff', color: '#007bff',
fontSize: '14px', fontSize: '14px',
height: '65px', height: '50px',
}} }}
> >
<Image <Image
@@ -76,7 +76,7 @@ const IconFixRight: React.FC = () => {
style={{ marginRight: '10px' }} style={{ marginRight: '10px' }}
/> />
<div className="contact-info"> <div className="contact-info">
<b className="d-block">Chat Zalo</b> <b className="block">Chat Zalo</b>
<span>(8h-22h30)</span> <span>(8h-22h30)</span>
</div> </div>
</Link> </Link>

View File

@@ -0,0 +1,67 @@
'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;

View File

@@ -0,0 +1,106 @@
'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;

View File

@@ -0,0 +1,93 @@
'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;

View File

@@ -0,0 +1,30 @@
'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
: '/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;

View File

@@ -4,8 +4,14 @@ import Link from 'next/link';
import type { CategoryData } from '@/types'; import type { CategoryData } from '@/types';
import { productCategoryData } from '@/data/product/category'; import { productCategoryData } from '@/data/product/category';
import { findCategoryBySlug } from '@/lib/category'; import { findCategoryBySlug } from '@/lib/category';
// box
import { Breadcrumb } from '@components/common/Breadcrumb'; import { Breadcrumb } from '@components/common/Breadcrumb';
import BannerCategory from './BannerCategory'; import BannerCategory from './BannerCategory';
import ItemCategoryChild from './ItemCategoryChild';
import BoxFilter from './BoxFilter';
import BoxSort from './BoxSort';
import ItemProduct from '@/components/common/ItemProduct';
interface CategoryPageProps { interface CategoryPageProps {
slug: string; // khai báo prop slug slug: string; // khai báo prop slug
@@ -14,20 +20,15 @@ interface CategoryPageProps {
const CategoryPage: React.FC<CategoryPageProps> = ({ slug }) => { const CategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
// Ép kiểu dữ liệu từ index.ts về CategoryData[] nếu cần // Ép kiểu dữ liệu từ index.ts về CategoryData[] nếu cần
const categories = productCategoryData as unknown as CategoryData[]; const categories = productCategoryData as unknown as CategoryData[];
console.log(slug);
// Tìm danh mục hiện tại
const currentCategory = findCategoryBySlug(slug, categories); const currentCategory = findCategoryBySlug(slug, categories);
const breadcrumbItems = currentCategory?.path?.path?.map((p) => ({ const breadcrumbItems = currentCategory?.current_category?.path?.path?.map((p) => ({
name: p.name, name: p.name,
url: p.url, url: p.url,
})) ?? [ })) ?? [
{ name: 'Trang chủ', url: '/' }, { name: 'Trang chủ', url: '/' },
{ name: currentCategory?.name, url: currentCategory?.url }, { name: currentCategory?.current_category.name, url: currentCategory?.current_category.url },
]; ];
// Trường hợp không tìm thấy danh mục // Trường hợp không tìm thấy danh mục
if (!currentCategory) { if (!currentCategory) {
return ( return (
@@ -67,12 +68,54 @@ const CategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
); );
} }
// lấy sản phẩm
const products = Object.values(currentCategory.product_list);
return ( return (
<div className="page-product-category"> <div className="page-category">
<div className="container"> <div className="container">
<Breadcrumb items={breadcrumbItems} /> <Breadcrumb items={breadcrumbItems} />
<BannerCategory /> <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>
</div> </div>
); );

File diff suppressed because one or more lines are too long

View File

@@ -9,11 +9,8 @@ function getSlug(url: string): string {
/** /**
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"]) * Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
*/ */
export function findCategoryBySlug( export function findCategoryBySlug(slug: string, categories: CategoryData[]): CategoryData | null {
slug: string,
categories: CategoryData[],
): CurrentCategory | null {
const found = categories.find((item) => item.current_category.url === slug); const found = categories.find((item) => item.current_category.url === slug);
return found ? found.current_category : null; return found ?? null;
} }

View File

@@ -429,7 +429,7 @@ textarea::placeholder {
box-shadow: 0 1px 2px 1px #01010133; box-shadow: 0 1px 2px 1px #01010133;
color: #007bff; color: #007bff;
font-size: 14px; font-size: 14px;
height: 65px; height: 50px;
} }
.global-fixed-right .messenger img { .global-fixed-right .messenger img {
width: 40px; width: 40px;

View File

@@ -16,7 +16,7 @@ interface FilterCollection {
key: string; key: string;
name: string; name: string;
} }
interface SortCollection { export interface SortCollection {
url: string; url: string;
key: string; key: string;
name: string; name: string;
@@ -38,7 +38,7 @@ interface CategoryCollection {
count: string; count: string;
is_selected: string; is_selected: string;
} }
interface BrandFilter { export interface BrandFilter {
id: string; id: string;
name: string; name: string;
brand_index: string; brand_index: string;
@@ -48,6 +48,13 @@ interface BrandFilter {
url: string; url: string;
} }
export interface PriceFilter {
count: string;
is_selected: string;
name: string;
url: string;
}
export interface CurrentCategory { export interface CurrentCategory {
id: string; id: string;
catPath: string; catPath: string;
@@ -104,6 +111,23 @@ export interface CurrentCategory {
children?: ChildCategory[]; children?: ChildCategory[];
} }
interface AttributeValue {
id: string;
name: string;
api_key: string;
description: string;
count: string;
is_selected: string;
url: string;
}
export interface AttributeFilterList {
name: string;
filter_code: string;
is_selected: string;
value_list: AttributeValue[];
}
export interface CategoryData { export interface CategoryData {
title: string; title: string;
keywords: string; keywords: string;
@@ -120,8 +144,8 @@ export interface CategoryData {
paging_count: string; paging_count: string;
product_count: string; product_count: string;
product_list: Product[]; product_list: Product[];
price_filter_list: []; price_filter_list: PriceFilter[];
category_collection: CategoryCollection[]; category_collection: CategoryCollection[];
attribute_filter_list: []; attribute_filter_list: AttributeFilterList[];
brand_filter_list: BrandFilter[]; brand_filter_list: BrandFilter[];
} }