update
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { findCategoryBySlug } from '@/lib/category';
|
||||
|
||||
const CategoryPage: React.FC = () => {
|
||||
const params = useParams();
|
||||
const slugArray = params?.slug as string[];
|
||||
|
||||
// tìm danh mục hiện tại theo slug
|
||||
const category: Category | null = findCategoryBySlug(slugArray, categories);
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<h1 className="text-2xl font-bold">Không tìm thấy danh mục</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-4 text-sm text-gray-600">
|
||||
{slugArray.map((slug, idx) => (
|
||||
<span key={idx}>
|
||||
<Link href={`/${slugArray.slice(0, idx + 1).join('/')}`}>{slug}</Link>
|
||||
{idx < slugArray.length - 1 && ' / '}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Tiêu đề danh mục */}
|
||||
<h1 className="mb-6 text-2xl font-bold">{category.name}</h1>
|
||||
|
||||
{/* Nếu có danh mục con thì hiển thị */}
|
||||
{category.children && category.children.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-6 md:grid-cols-4">
|
||||
{category.children.map((child) => (
|
||||
<div key={child.id} className="rounded-lg border p-4">
|
||||
<Link href={`/${slugArray.join('/')}/${child.slug}`}>{child.name}</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>Danh mục này chưa có danh mục con hoặc sản phẩm.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
17
src/app/[slug]/page.tsx
Normal file
17
src/app/[slug]/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { productCategoryData } from '@/data/product/category';
|
||||
|
||||
// import component
|
||||
import CategoryPage from '@components/layout/product/Category';
|
||||
|
||||
export default function DynamicPage() {
|
||||
const params = useParams();
|
||||
const slug = ('/' + params?.slug) as string;
|
||||
|
||||
if (productCategoryData.find((c) => c.current_category.url == slug)) {
|
||||
return <CategoryPage slug={slug} />;
|
||||
}
|
||||
|
||||
return <div>404 Không tìm thấy</div>;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export default function RootLayout({
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
{children}
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
)}
|
||||
46
src/components/common/Breadcrumb.tsx
Normal file
46
src/components/common/Breadcrumb.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { FaHouse, FaAngleRight } from 'react-icons/fa6';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string | undefined;
|
||||
url: string | undefined;
|
||||
}
|
||||
|
||||
export const Breadcrumb = ({ items }: { items: BreadcrumbItem[] }) => {
|
||||
return (
|
||||
<nav className="box-breadcrumb-global mb-4 text-sm text-gray-600">
|
||||
<ol itemScope itemType="http://schema.org/BreadcrumbList" className="flex gap-2">
|
||||
<li
|
||||
itemProp="itemListElement"
|
||||
itemScope
|
||||
itemType="http://schema.org/ListItem"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link href="/" itemProp="item">
|
||||
<span itemProp="name" className="flex items-center gap-2">
|
||||
<span style={{ fontSize: 0 }}>Trang chủ</span> <FaHouse className="text-gray-700" />
|
||||
</span>
|
||||
</Link>{' '}
|
||||
<FaAngleRight className="text-gray-700" />
|
||||
<meta itemProp="position" content="1" />
|
||||
</li>
|
||||
{items.map((item, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
itemProp="itemListElement"
|
||||
itemScope
|
||||
itemType="http://schema.org/ListItem"
|
||||
className="flex items-center"
|
||||
>
|
||||
<Link href={item.url ?? '/'} itemProp="item">
|
||||
<span itemProp="name">{item?.name}</span>
|
||||
</Link>
|
||||
<meta itemProp="position" content={(idx + 1).toString()} />
|
||||
{idx < items.length - 1 && <span className="mx-1">/</span>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ 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 './bannerData';
|
||||
import { bannerData } from '@/data/banner';
|
||||
|
||||
const SliderHome: React.FC = () => {
|
||||
// data banner slider
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
81
src/components/layout/product/Category/index.tsx
Normal file
81
src/components/layout/product/Category/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'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/category';
|
||||
import { Breadcrumb } from '@components/common/Breadcrumb';
|
||||
import BannerCategory from './BannerCategory';
|
||||
|
||||
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[] nếu cần
|
||||
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 breadcrumbItems = currentCategory?.path?.path?.map((p) => ({
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
})) ?? [
|
||||
{ name: 'Trang chủ', url: '/' },
|
||||
{ name: currentCategory?.name, url: currentCategory?.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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-product-category">
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<BannerCategory />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
@@ -753,5 +753,74 @@ export const bannerData: BannerType = [
|
||||
},
|
||||
],
|
||||
},
|
||||
product_list: {
|
||||
banner_category_2023: [
|
||||
{
|
||||
id: '487',
|
||||
display:
|
||||
'<img border=0 src="\/media\/banner\/02_Decf1b77a9ec940fcf31fa7359fe29e35f1.webp" width=\'1200\' height=\'161\' alt=""\/>',
|
||||
fileUrl:
|
||||
'https://nguyencongpc.vn\/media\/banner\/02_Decf1b77a9ec940fcf31fa7359fe29e35f1.webp',
|
||||
desUrl: '\/ad.php?id=487',
|
||||
title: '',
|
||||
width: 1200,
|
||||
height: 161,
|
||||
fileType: '',
|
||||
summary: '',
|
||||
},
|
||||
{
|
||||
id: '446',
|
||||
display:
|
||||
"<a href=\"\/ad.php?id=446\" target='_blank' rel='nofollow'>\r\n <img border=0 src=\"\/media\/banner\/06_May6f05ef9a99dc3e76c80669c1f693db8e.jpg\" width='2392' height='320' alt=\"\"\/><\/a>",
|
||||
fileUrl:
|
||||
'https://nguyencongpc.vn\/media\/banner\/06_May6f05ef9a99dc3e76c80669c1f693db8e.jpg',
|
||||
desUrl: '\/ad.php?id=446',
|
||||
title: '',
|
||||
width: 2392,
|
||||
height: 320,
|
||||
fileType: '',
|
||||
summary: '',
|
||||
},
|
||||
{
|
||||
id: '359',
|
||||
display:
|
||||
"<a href=\"\/ad.php?id=359\" target='_blank' rel='nofollow'>\r\n <img border=0 src=\"\/media\/banner\/05_Febeb00e3c3f19ceda40733b5d4d8f335ed.webp\" width='1500' height='201' alt=\"\"\/><\/a>",
|
||||
fileUrl:
|
||||
'https://nguyencongpc.vn\/media\/banner\/05_Febeb00e3c3f19ceda40733b5d4d8f335ed.webp',
|
||||
desUrl: '\/ad.php?id=359',
|
||||
title: '',
|
||||
width: 1500,
|
||||
height: 201,
|
||||
fileType: 'banner',
|
||||
summary: '',
|
||||
},
|
||||
{
|
||||
id: '331',
|
||||
display:
|
||||
"<a href=\"\/ad.php?id=331\" target='_blank' rel='nofollow'>\r\n <img border=0 src=\"\/media\/banner\/07_Febc979a257d61b60e2e33a39a767dbc909.webp\" width='1200' height='161' alt=\"\"\/><\/a>",
|
||||
fileUrl:
|
||||
'https://nguyencongpc.vn\/media\/banner\/07_Febc979a257d61b60e2e33a39a767dbc909.webp',
|
||||
desUrl: '\/ad.php?id=331',
|
||||
title: '',
|
||||
width: 1200,
|
||||
height: 161,
|
||||
fileType: 'banner',
|
||||
summary: '',
|
||||
},
|
||||
{
|
||||
id: '443',
|
||||
display:
|
||||
"<a href=\"\/ad.php?id=443\" target='_blank' rel='nofollow'>\r\n <img border=0 src=\"\/media\/banner\/07_Febf902e9f33cf7cd48e7649ea8f8d33122.webp\" width='2392' height='320' alt=\"\"\/><\/a>",
|
||||
fileUrl:
|
||||
'https://nguyencongpc.vn\/media\/banner\/07_Febf902e9f33cf7cd48e7649ea8f8d33122.webp',
|
||||
desUrl: '\/ad.php?id=443',
|
||||
title: '',
|
||||
width: 2392,
|
||||
height: 320,
|
||||
fileType: 'banner',
|
||||
summary: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,59 +1,19 @@
|
||||
import { CategoryDetail } from '@/types';
|
||||
import { CategoryData, CurrentCategory } from '@/types';
|
||||
|
||||
// Hàm helper để lấy URL an toàn từ các cấu trúc dữ liệu khác nhau
|
||||
function getSlug(url: string): string {
|
||||
const parts = url.split('/').filter(Boolean);
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tìm danh mục theo mảng slug (ví dụ: ["pc-gaming","cao-cap","rtx-4090"])
|
||||
*/
|
||||
export function findCategoryBySlug(
|
||||
slugs: string[],
|
||||
categories: CategoryDetail[],
|
||||
): CategoryDetail | null {
|
||||
let currentList: CategoryDetail[] = categories;
|
||||
let currentCategory: CategoryDetail | null = null;
|
||||
slug: string,
|
||||
categories: CategoryData[],
|
||||
): CurrentCategory | null {
|
||||
const found = categories.find((item) => item.current_category.url === slug);
|
||||
|
||||
for (const slug of slugs) {
|
||||
const found = currentList.find((cat) => cat.slug === slug);
|
||||
if (!found) return null;
|
||||
currentCategory = found;
|
||||
currentList = found.children || [];
|
||||
}
|
||||
|
||||
return currentCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy tất cả danh mục con của một danh mục
|
||||
*/
|
||||
export function getChildren(category: CategoryDetail): CategoryDetail[] {
|
||||
return category.children || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tìm danh mục theo id
|
||||
*/
|
||||
export function findCategoryById(id: number, categories: CategoryDetail[]): CategoryDetail | null {
|
||||
for (const cat of categories) {
|
||||
if (cat.id === id) return cat;
|
||||
if (cat.children) {
|
||||
const found = findCategoryById(id, cat.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tìm đường dẫn breadcrumb từ root đến danh mục hiện tại
|
||||
*/
|
||||
export function getBreadcrumb(slugs: string[], categories: CategoryDetail[]): CategoryDetail[] {
|
||||
const breadcrumb: CategoryDetail[] = [];
|
||||
let currentList: CategoryDetail[] = categories;
|
||||
|
||||
for (const slug of slugs) {
|
||||
const found = currentList.find((cat) => cat.slug === slug);
|
||||
if (!found) break;
|
||||
breadcrumb.push(found);
|
||||
currentList = found.children || [];
|
||||
}
|
||||
|
||||
return breadcrumb;
|
||||
return found ? found.current_category : null;
|
||||
}
|
||||
|
||||
@@ -63,15 +63,6 @@ html {
|
||||
scroll-behavior: smooth;
|
||||
line-height: 16px;
|
||||
}
|
||||
a,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
-webkit-transition: color 0.2s ease-out;
|
||||
-o-transition: color 0.2s ease-out;
|
||||
transition: color 0.2s ease-out;
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -38,10 +38,15 @@ export interface BannerHomePageData {
|
||||
banner_collection_pc: BannerItem[];
|
||||
}
|
||||
|
||||
export interface BannerCategory {
|
||||
banner_category_2023: BannerItem[];
|
||||
}
|
||||
|
||||
export interface TemplateBanner {
|
||||
footer: BannerFooterData;
|
||||
header: BannerHeaderData;
|
||||
homepage: BannerHomePageData;
|
||||
product_list: BannerCategory;
|
||||
}
|
||||
|
||||
export type BannerType = TemplateBanner[];
|
||||
@@ -1 +1,2 @@
|
||||
export * from '@/types/global/Menu';
|
||||
export * from '@/types/global/TypeListProduct';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from '@/types/home/Banner';
|
||||
export * from '@/types/global/Banner';
|
||||
export * from '@/types/home/TypeArticle';
|
||||
export * from '@/types/home/TypeListProduct';
|
||||
export * from '@/types/home/TypeListProductDeal';
|
||||
|
||||
@@ -1,24 +1,127 @@
|
||||
export interface CategoryDetail {
|
||||
id: number;
|
||||
import { Product } from '@/types';
|
||||
|
||||
export interface ChildCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
parentId: string;
|
||||
thumnail: string;
|
||||
big_image: string;
|
||||
isParent: string;
|
||||
url: string;
|
||||
is_featured: string;
|
||||
summary: string;
|
||||
}
|
||||
interface FilterCollection {
|
||||
url: string;
|
||||
key: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
parentId?: number;
|
||||
children?: CategoryDetail[];
|
||||
}
|
||||
interface SortCollection {
|
||||
url: string;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
interface DisplayCollection {
|
||||
url: string;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
interface PagingCollection {
|
||||
name: string;
|
||||
url: string;
|
||||
is_active: string;
|
||||
}
|
||||
interface CategoryCollection {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
count: string;
|
||||
is_selected: string;
|
||||
}
|
||||
interface BrandFilter {
|
||||
id: string;
|
||||
name: string;
|
||||
brand_index: string;
|
||||
image: string;
|
||||
count: string;
|
||||
is_selected: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CategoryDetailNew {
|
||||
export interface CurrentCategory {
|
||||
id: string;
|
||||
catPath: string;
|
||||
childListId: string;
|
||||
display_option: string;
|
||||
request_path: string;
|
||||
url: string;
|
||||
url_hash: string;
|
||||
name: string;
|
||||
summary: string;
|
||||
static_html: string;
|
||||
tags: string[];
|
||||
isParent: string;
|
||||
imgUrl: string;
|
||||
img_big: string;
|
||||
useImg: string;
|
||||
toUrl: string;
|
||||
parentId: string;
|
||||
proCount: string;
|
||||
attr_count: string;
|
||||
priceRange: string;
|
||||
keyword: string;
|
||||
ordering: string;
|
||||
status: string;
|
||||
createDate: string;
|
||||
createBy: string;
|
||||
lastUpdate: string;
|
||||
lastUpdateBy: string;
|
||||
meta_title: string;
|
||||
meta_keyword: string;
|
||||
meta_description: string;
|
||||
url_canonical: string;
|
||||
visit: string;
|
||||
like_count: string;
|
||||
is_featured: string;
|
||||
redirect_url: string;
|
||||
template: string;
|
||||
number_display: string;
|
||||
brand_url: string;
|
||||
hide_menu: string;
|
||||
extend: string;
|
||||
price_filter: [];
|
||||
path: {
|
||||
path: { id: string; url: string; name: string }[];
|
||||
path_url: string;
|
||||
};
|
||||
image: string;
|
||||
big_image: string;
|
||||
is_parent: string;
|
||||
parent_id: string;
|
||||
path_id: string;
|
||||
display_info: string;
|
||||
related: [];
|
||||
children?: ChildCategory[];
|
||||
}
|
||||
|
||||
export interface CategoryData {
|
||||
title: string;
|
||||
keywords: string;
|
||||
description: string;
|
||||
canonical: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface current_category {
|
||||
id: string;
|
||||
catPath: string;
|
||||
childListId: string;
|
||||
display_option: string;
|
||||
h1: string;
|
||||
current_category: CurrentCategory;
|
||||
other_filter_collection: FilterCollection[];
|
||||
sort_by_collection: SortCollection[];
|
||||
display_by_collection: DisplayCollection[];
|
||||
paging_collection: PagingCollection[];
|
||||
paging: string;
|
||||
paging_count: string;
|
||||
product_count: string;
|
||||
product_list: Product[];
|
||||
price_filter_list: [];
|
||||
category_collection: CategoryCollection[];
|
||||
attribute_filter_list: [];
|
||||
brand_filter_list: BrandFilter[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user