Другой роутинг
Some checks failed
CI / main (push) Has been cancelled

This commit is contained in:
Igor Rybakov
2026-03-06 23:24:16 +02:00
parent 6228624805
commit f919ea66f6
19 changed files with 500 additions and 160 deletions

View File

@@ -10,6 +10,15 @@ const nextConfig = {
// Use this to set Nx-specific options // Use this to set Nx-specific options
// See: https://nx.dev/recipes/next/next-config-setup // See: https://nx.dev/recipes/next/next-config-setup
nx: {}, nx: {},
async redirects() {
return [
{
source: '/catalog/:category(gidravlika|pnevmatika|asu|zip)',
destination: '/catalog?category=:category',
permanent: true,
},
];
},
}; };
const plugins = [ const plugins = [

View File

@@ -0,0 +1,58 @@
import { notFound } from 'next/navigation';
import { brands } from '@/data/brands';
import { products } from '@/data/products';
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { CatalogView } from '@/components/CatalogView/CatalogView';
export function generateStaticParams() {
return brands.map((b) => ({ id: String(b.id) }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const brand = brands.find((b) => b.id === Number(id));
return {
title: brand
? `Каталог ${brand.name} | PAN-PROM`
: 'Каталог | PAN-PROM',
};
}
export default async function BrandCatalogPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { id } = await params;
const brand = brands.find((b) => b.id === Number(id));
if (!brand) notFound();
const sp = await searchParams;
const filters = parseSearchParams(sp);
const brandProducts = products.filter((p) => p.brand === brand.name);
const filtered = filterProducts(brandProducts, filters);
const basePath = `/brand/${id}/catalog`;
const breadcrumbItems = [
{ label: 'Главная', href: '/' },
{ label: 'Каталог', href: '/catalog' },
{ label: brand.name, href: `/brand/${id}` },
{ label: 'Каталог' },
];
return (
<CatalogView
products={filtered}
breadcrumbItems={breadcrumbItems}
basePath={basePath}
hiddenFilters={['brand']}
/>
);
}

View File

@@ -14,21 +14,21 @@
} }
.logoBox { .logoBox {
width: 64px; width: 80px;
height: 64px; height: 80px;
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
background: var(--color-slate-100); background: var(--color-slate-100);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: 800; font-weight: 800;
font-size: 22px; font-size: 26px;
color: var(--color-slate-600); color: var(--color-slate-600);
flex-shrink: 0; flex-shrink: 0;
} }
.brandName { .brandName {
font-size: 28px; font-size: 32px;
font-weight: 700; font-weight: 700;
color: var(--color-slate-900); color: var(--color-slate-900);
} }
@@ -39,17 +39,11 @@
margin-top: 4px; margin-top: 4px;
} }
.sectionTitle { .cardGrid {
font-size: 18px;
font-weight: 600;
color: var(--color-slate-800);
margin-bottom: var(--space-md);
}
.grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: var(--space-md); gap: var(--space-md);
margin-bottom: var(--space-xl);
@media (max-width: 1024px) { @media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -60,8 +54,63 @@
} }
} }
.empty { .card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-lg);
padding: var(--space-lg);
text-align: center; text-align: center;
color: var(--color-text-muted); }
padding: var(--space-2xl) 0;
.cardLabel {
font-size: 12px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.cardValue {
font-size: 22px;
font-weight: 700;
color: var(--color-slate-900);
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: var(--color-slate-800);
margin-bottom: var(--space-md);
}
.tagGrid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: var(--space-xl);
}
.tag {
display: inline-block;
padding: 8px 16px;
border-radius: 8px;
background: var(--color-slate-50);
border: 1px solid var(--color-border);
font-size: 13px;
font-weight: 500;
color: var(--color-slate-700);
text-decoration: none;
transition: all 0.15s;
&:hover {
background: var(--color-orange-50);
border-color: var(--color-orange-200);
color: var(--color-orange-600);
}
}
.ctaBlock {
text-align: center;
padding: var(--space-xl) 0;
} }

View File

@@ -1,8 +1,9 @@
import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { brands } from '@/data/brands'; import { brands } from '@/data/brands';
import { products } from '@/data/products'; import { products } from '@/data/products';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { ProductCard } from '@/components/ProductCard/ProductCard'; import { Button } from '@/components/ui/Button/Button';
import styles from './page.module.scss'; import styles from './page.module.scss';
export function generateStaticParams() { export function generateStaticParams() {
@@ -31,6 +32,8 @@ export default async function BrandPage({
if (!brand) notFound(); if (!brand) notFound();
const brandProducts = products.filter((p) => p.brand === brand.name); const brandProducts = products.filter((p) => p.brand === brand.name);
const subcategories = [...new Set(brandProducts.map((p) => p.subcategory))];
const inStockCount = brandProducts.filter((p) => p.inStock).length;
return ( return (
<div className={styles.container}> <div className={styles.container}>
@@ -52,20 +55,47 @@ export default async function BrandPage({
</div> </div>
</div> </div>
<h2 className={styles.sectionTitle}> <div className={styles.cardGrid}>
Продукция ({brandProducts.length}) <div className={styles.card}>
</h2> <div className={styles.cardLabel}>Направление</div>
<div className={styles.grid}> <div className={styles.cardValue}>{brand.category}</div>
{brandProducts.map((p) => ( </div>
<ProductCard key={p.id} product={p} /> <div className={styles.card}>
))} <div className={styles.cardLabel}>Страна</div>
<div className={styles.cardValue}>{brand.country}</div>
</div>
<div className={styles.card}>
<div className={styles.cardLabel}>Позиций в каталоге</div>
<div className={styles.cardValue}>{brandProducts.length}</div>
</div>
<div className={styles.card}>
<div className={styles.cardLabel}>В наличии</div>
<div className={styles.cardValue}>{inStockCount}</div>
</div>
</div> </div>
{brandProducts.length === 0 && ( {subcategories.length > 0 && (
<p className={styles.empty}> <>
Продукция этого бренда скоро появится в каталоге. <h2 className={styles.sectionTitle}>Категории продукции</h2>
</p> <div className={styles.tagGrid}>
{subcategories.map((sub) => (
<Link
key={sub}
href={`/brand/${id}/catalog?subcategory=${encodeURIComponent(sub)}`}
className={styles.tag}
>
{sub}
</Link>
))}
</div>
</>
)} )}
<div className={styles.ctaBlock}>
<Link href={`/brand/${id}/catalog`}>
<Button variant="primary">Открыть каталог {brand.name}</Button>
</Link>
</div>
</div> </div>
); );
} }

View File

@@ -68,7 +68,7 @@ export default function CartPage() {
Позиций: {cart.length} Позиций: {cart.length}
</span> </span>
<Link href="/rfq"> <Link href="/rfq">
<Button>Отправить запрос (RFQ)</Button> <Button>Отправить запрос</Button>
</Link> </Link>
</div> </div>
</> </>

View File

@@ -1,61 +0,0 @@
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { products } from '@/data/products';
import { categories } from '@/data/categories';
import { categoryNameMap, CategorySlug } from '@/types';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { ProductCard } from '@/components/ProductCard/ProductCard';
import styles from '../page.module.scss';
export function generateStaticParams() {
return categories.map((c) => ({ category: c.slug }));
}
export function generateMetadata({
params,
}: {
params: Promise<{ category: string }>;
}) {
return params.then(({ category }) => {
const name = categoryNameMap[category as CategorySlug];
return { title: name ? `${name} | PAN-PROM` : 'Каталог | PAN-PROM' };
});
}
export default async function CategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const { category } = await params;
const catName = categoryNameMap[category as CategorySlug];
if (!catName) notFound();
const filtered = products.filter((p) => p.category === catName);
return (
<div className={styles.layout}>
<Suspense fallback={null}>
<CatalogSidebar activeCategory={category} />
</Suspense>
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb
items={[
{ label: 'Главная', href: '/' },
{ label: 'Каталог', href: '/catalog' },
{ label: catName },
]}
/>
<span className={styles.count}>{filtered.length} позиций</span>
</div>
<div className={styles.grid}>
{filtered.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
</div>
);
}

View File

@@ -1,36 +1,41 @@
import { Suspense } from 'react';
import { products } from '@/data/products'; import { products } from '@/data/products';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import { categoryNameMap, CategorySlug } from '@/types';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar'; import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { ProductCard } from '@/components/ProductCard/ProductCard'; import { CatalogView } from '@/components/CatalogView/CatalogView';
import styles from './page.module.scss';
export const metadata = { export async function generateMetadata({
title: 'Каталог | PAN-PROM', searchParams,
}; }: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const params = await searchParams;
const category = params.category as string | undefined;
const catName = category
? categoryNameMap[category as CategorySlug]
: undefined;
const title = catName
? `${catName} | Каталог | PAN-PROM`
: 'Каталог | PAN-PROM';
return { title };
}
export default async function CatalogPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const params = await searchParams;
const filters = parseSearchParams(params);
const filtered = filterProducts(products, filters);
const breadcrumbItems = [
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
];
export default function CatalogPage() {
return ( return (
<div className={styles.layout}> <CatalogView products={filtered} breadcrumbItems={breadcrumbItems} />
<Suspense fallback={null}>
<CatalogSidebar />
</Suspense>
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb
items={[
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
]}
/>
<span className={styles.count}>{products.length} позиций</span>
</div>
<div className={styles.grid}>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
</div>
); );
} }

View File

@@ -3,7 +3,7 @@ import { RFQForm } from '@/components/RFQForm/RFQForm';
import styles from './page.module.scss'; import styles from './page.module.scss';
export const metadata = { export const metadata = {
title: 'Запрос цены (RFQ) | PAN-PROM', title: 'Запрос цены | PAN-PROM',
}; };
export default function RFQPage() { export default function RFQPage() {
@@ -12,7 +12,7 @@ export default function RFQPage() {
<Breadcrumb <Breadcrumb
items={[ items={[
{ label: 'Главная', href: '/' }, { label: 'Главная', href: '/' },
{ label: 'Запрос цены (RFQ)' }, { label: 'Запрос цены' },
]} ]}
/> />
<h1 className={styles.title}>Запрос коммерческого предложения</h1> <h1 className={styles.title}>Запрос коммерческого предложения</h1>

View File

@@ -1,39 +1,40 @@
'use client'; 'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { Icon } from '@/components/Icon/Icon'; import { Icon } from '@/components/Icon/Icon';
import { categories } from '@/data/categories'; import { categories } from '@/data/categories';
import { brands } from '@/data/brands'; import { brands } from '@/data/brands';
import { products } from '@/data/products';
import { useCatalogFilters } from '@/hooks/useCatalogFilters';
import { getSubcategories } from '@/lib/getSubcategories';
import styles from './CatalogSidebar.module.scss'; import styles from './CatalogSidebar.module.scss';
interface CatalogSidebarProps { interface CatalogSidebarProps {
activeCategory?: string; basePath?: string;
hiddenFilters?: string[];
} }
export function CatalogSidebar({ activeCategory }: CatalogSidebarProps) { export function CatalogSidebar({
const router = useRouter(); basePath = '/catalog',
const searchParams = useSearchParams(); hiddenFilters = [],
const activeBrand = searchParams.get('brand'); }: CatalogSidebarProps) {
const { filters, setFilter } = useCatalogFilters(basePath);
const handleCategoryClick = (slug: string | null) => { const toggleFilter = (
if (slug) { key: 'category' | 'brand' | 'subcategory' | 'sort',
router.push(`/catalog/${slug}`); value: string
} else { ) => {
router.push('/catalog'); setFilter(key, filters[key] === value ? undefined : value);
}
}; };
const handleBrandClick = (brandId: number) => { const filteredBrands = filters.category
router.push(`/brand/${brandId}`);
};
const filteredBrands = activeCategory
? brands.filter((b) => { ? brands.filter((b) => {
const cat = categories.find((c) => c.slug === activeCategory); const cat = categories.find((c) => c.slug === filters.category);
return cat ? b.category === cat.name : true; return cat ? b.category === cat.name : true;
}) })
: brands; : brands;
const subcategories = getSubcategories(products, filters.category);
return ( return (
<aside className={styles.sidebar}> <aside className={styles.sidebar}>
<div className={styles.inner}> <div className={styles.inner}>
@@ -42,35 +43,84 @@ export function CatalogSidebar({ activeCategory }: CatalogSidebarProps) {
Фильтры Фильтры
</div> </div>
{/* Category */}
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<div className={styles.filterLabel}>Направление</div> <div className={styles.filterLabel}>Направление</div>
<button <button
className={`${styles.filterItem} ${!activeCategory ? styles.filterItemActive : ''}`} className={`${styles.filterItem} ${!filters.category ? styles.filterItemActive : ''}`}
onClick={() => handleCategoryClick(null)} onClick={() => setFilter('category', undefined)}
> >
Все Все
</button> </button>
{categories.map((c) => ( {categories.map((c) => (
<button <button
key={c.slug} key={c.slug}
className={`${styles.filterItem} ${activeCategory === c.slug ? styles.filterItemActive : ''}`} className={`${styles.filterItem} ${filters.category === c.slug ? styles.filterItemActive : ''}`}
onClick={() => handleCategoryClick(c.slug)} onClick={() => toggleFilter('category', c.slug)}
> >
{c.name} {c.name}
</button> </button>
))} ))}
</div> </div>
{/* Brand */}
{!hiddenFilters.includes('brand') && (
<div className={styles.filterGroup}>
<div className={styles.filterLabel}>Бренд</div>
{filteredBrands.slice(0, 6).map((b) => (
<button
key={b.id}
className={`${styles.filterItem} ${filters.brand === b.name ? styles.filterItemActive : ''}`}
onClick={() => toggleFilter('brand', b.name)}
>
<span>{b.name}</span>
<span className={styles.brandCountry}>{b.country}</span>
</button>
))}
</div>
)}
{/* Subcategory */}
{subcategories.length > 0 && (
<div className={styles.filterGroup}>
<div className={styles.filterLabel}>Подкатегория</div>
{subcategories.map((sub) => (
<button
key={sub}
className={`${styles.filterItem} ${filters.subcategory === sub ? styles.filterItemActive : ''}`}
onClick={() => toggleFilter('subcategory', sub)}
>
{sub}
</button>
))}
</div>
)}
{/* In Stock */}
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<div className={styles.filterLabel}>Бренд</div> <div className={styles.filterLabel}>Наличие</div>
{filteredBrands.slice(0, 6).map((b) => ( <button
className={`${styles.filterItem} ${filters.inStock ? styles.filterItemActive : ''}`}
onClick={() => setFilter('inStock', filters.inStock ? undefined : true)}
>
В наличии
</button>
</div>
{/* Sort */}
<div className={styles.filterGroup}>
<div className={styles.filterLabel}>Сортировка</div>
{[
{ value: 'name', label: 'По названию' },
{ value: 'brand', label: 'По бренду' },
{ value: 'category', label: 'По категории' },
].map((opt) => (
<button <button
key={b.id} key={opt.value}
className={`${styles.filterItem} ${activeBrand === String(b.id) ? styles.filterItemActive : ''}`} className={`${styles.filterItem} ${filters.sort === opt.value ? styles.filterItemActive : ''}`}
onClick={() => handleBrandClick(b.id)} onClick={() => toggleFilter('sort', opt.value)}
> >
<span>{b.name}</span> {opt.label}
<span className={styles.brandCountry}>{b.country}</span>
</button> </button>
))} ))}
</div> </div>

View File

@@ -35,3 +35,9 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.empty {
text-align: center;
color: var(--color-text-muted);
padding: var(--space-2xl) 0;
}

View File

@@ -0,0 +1,49 @@
import { Suspense } from 'react';
import { Product } from '@/types';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { ProductCard } from '@/components/ProductCard/ProductCard';
import styles from './CatalogView.module.scss';
interface BreadcrumbItem {
label: string;
href?: string;
}
interface CatalogViewProps {
products: Product[];
breadcrumbItems: BreadcrumbItem[];
basePath?: string;
hiddenFilters?: string[];
}
export function CatalogView({
products,
breadcrumbItems,
basePath = '/catalog',
hiddenFilters,
}: CatalogViewProps) {
return (
<div className={styles.layout}>
<Suspense fallback={null}>
<CatalogSidebar basePath={basePath} hiddenFilters={hiddenFilters} />
</Suspense>
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb items={breadcrumbItems} />
<span className={styles.count}>{products.length} позиций</span>
</div>
<div className={styles.grid}>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
{products.length === 0 && (
<p className={styles.empty}>
По выбранным фильтрам ничего не найдено.
</p>
)}
</div>
</div>
);
}

View File

@@ -13,7 +13,7 @@ export function Directions() {
{categories.map((d) => ( {categories.map((d) => (
<Link <Link
key={d.slug} key={d.slug}
href={`/catalog/${d.slug}`} href={`/catalog?category=${d.slug}`}
className={styles.card} className={styles.card}
> >
<div <div

View File

@@ -117,7 +117,7 @@ export function Header() {
)} )}
</Link> </Link>
<Link href="/rfq" className={styles.rfqButton}> <Link href="/rfq" className={styles.rfqButton}>
Запрос цены (RFQ) Запрос цены
</Link> </Link>
<button <button
className={styles.menuToggle} className={styles.menuToggle}

View File

@@ -38,7 +38,7 @@ export function Hero() {
<Icon name="chevronRight" size={16} color="#fff" /> <Icon name="chevronRight" size={16} color="#fff" />
</Link> </Link>
<Link href="/rfq" className={styles.ctaSecondary}> <Link href="/rfq" className={styles.ctaSecondary}>
Отправить запрос (RFQ) Отправить запрос
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -48,7 +48,7 @@ export function ProductDetail({ product }: { product: Product }) {
В корзину В корзину
</Button> </Button>
<Link href="/rfq"> <Link href="/rfq">
<Button variant="secondary">Запросить цену (RFQ)</Button> <Button variant="secondary">Запросить цену</Button>
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -10,10 +10,10 @@ export const footerColumns = [
{ {
title: 'Направления', title: 'Направления',
links: [ links: [
{ label: 'Гидравлика', href: '/catalog/gidravlika' }, { label: 'Гидравлика', href: '/catalog?category=gidravlika' },
{ label: 'Пневматика', href: '/catalog/pnevmatika' }, { label: 'Пневматика', href: '/catalog?category=pnevmatika' },
{ label: 'АСУ', href: '/catalog/asu' }, { label: 'АСУ', href: '/catalog?category=asu' },
{ label: 'ЗИП', href: '/catalog/zip' }, { label: 'ЗИП', href: '/catalog?category=zip' },
], ],
}, },
{ {
@@ -21,7 +21,7 @@ export const footerColumns = [
links: [ links: [
{ label: 'О компании', href: '/about' }, { label: 'О компании', href: '/about' },
{ label: 'Контакты', href: '/contact' }, { label: 'Контакты', href: '/contact' },
{ label: 'Запрос цены (RFQ)', href: '/rfq' }, { label: 'Запрос цены', href: '/rfq' },
], ],
}, },
{ {

View File

@@ -0,0 +1,50 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
import { CatalogFilters } from '@/lib/filterProducts';
export function useCatalogFilters(basePath = '/catalog') {
const router = useRouter();
const searchParams = useSearchParams();
const filters: CatalogFilters = {
category: searchParams.get('category') || undefined,
brand: searchParams.get('brand') || undefined,
subcategory: searchParams.get('subcategory') || undefined,
inStock: searchParams.get('inStock') === 'true' ? true : undefined,
q: searchParams.get('q') || undefined,
sort: searchParams.get('sort') || undefined,
};
const buildFilterUrl = useCallback(
(overrides: Partial<CatalogFilters>) => {
const params = new URLSearchParams();
const merged = { ...filters, ...overrides };
for (const [key, value] of Object.entries(merged)) {
if (value !== undefined && value !== null && value !== false) {
params.set(key, String(value));
}
}
const qs = params.toString();
return qs ? `${basePath}?${qs}` : basePath;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[basePath, searchParams]
);
const setFilter = useCallback(
(key: keyof CatalogFilters, value: string | boolean | undefined) => {
router.push(buildFilterUrl({ [key]: value }));
},
[router, buildFilterUrl]
);
const clearFilters = useCallback(() => {
router.push(basePath);
}, [router, basePath]);
return { filters, setFilter, clearFilters, buildFilterUrl };
}

View File

@@ -0,0 +1,76 @@
import { Product } from '@/types';
import { categoryNameMap, CategorySlug } from '@/types';
export interface CatalogFilters {
category?: string;
brand?: string;
subcategory?: string;
inStock?: boolean;
q?: string;
sort?: string;
}
export function parseSearchParams(
searchParams: Record<string, string | string[] | undefined>
): CatalogFilters {
return {
category: (searchParams.category as string) || undefined,
brand: (searchParams.brand as string) || undefined,
subcategory: (searchParams.subcategory as string) || undefined,
inStock: searchParams.inStock === 'true' ? true : undefined,
q: (searchParams.q as string) || undefined,
sort: (searchParams.sort as string) || undefined,
};
}
export function filterProducts(
products: Product[],
filters: CatalogFilters
): Product[] {
let result = [...products];
if (filters.category) {
const catName = categoryNameMap[filters.category as CategorySlug];
if (catName) {
result = result.filter((p) => p.category === catName);
}
}
if (filters.brand) {
result = result.filter((p) => p.brand === filters.brand);
}
if (filters.subcategory) {
result = result.filter((p) => p.subcategory === filters.subcategory);
}
if (filters.inStock) {
result = result.filter((p) => p.inStock);
}
if (filters.q) {
const query = filters.q.toLowerCase();
result = result.filter(
(p) =>
p.name.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query)
);
}
if (filters.sort) {
result.sort((a, b) => {
switch (filters.sort) {
case 'name':
return a.name.localeCompare(b.name);
case 'brand':
return a.brand.localeCompare(b.brand);
case 'category':
return a.category.localeCompare(b.category);
default:
return 0;
}
});
}
return result;
}

View File

@@ -0,0 +1,19 @@
import { Product } from '@/types';
import { categoryNameMap, CategorySlug } from '@/types';
export function getSubcategories(
products: Product[],
categorySlug?: string
): string[] {
let filtered = products;
if (categorySlug) {
const catName = categoryNameMap[categorySlug as CategorySlug];
if (catName) {
filtered = products.filter((p) => p.category === catName);
}
}
const subcategories = [...new Set(filtered.map((p) => p.subcategory))];
return subcategories.sort((a, b) => a.localeCompare(b));
}