This commit is contained in:
41
apps/web/src/app/brand/[id]/catalog/BrandCatalogContent.tsx
Normal file
41
apps/web/src/app/brand/[id]/catalog/BrandCatalogContent.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
||||||
|
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
||||||
|
import { getBrands, getProducts } from '@/lib/api';
|
||||||
|
|
||||||
|
export async function BrandCatalogContent({
|
||||||
|
id,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
params: Record<string, string | string[] | undefined>;
|
||||||
|
}) {
|
||||||
|
const [brands, products] = await Promise.all([
|
||||||
|
getBrands(),
|
||||||
|
getProducts(),
|
||||||
|
]);
|
||||||
|
const brand = brands.find((b) => b.id === Number(id));
|
||||||
|
if (!brand) notFound();
|
||||||
|
|
||||||
|
const filters = parseSearchParams(params);
|
||||||
|
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']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { Suspense } from 'react';
|
||||||
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
import { getBrands } from '@/lib/api';
|
||||||
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
|
||||||
import { getBrands, getProducts, getCategories } from '@/lib/api';
|
import { BrandCatalogContent } from './BrandCatalogContent';
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
@@ -26,37 +26,12 @@ export default async function BrandCatalogPage({
|
|||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const [brands, products, categories] = await Promise.all([
|
|
||||||
getBrands(),
|
|
||||||
getProducts(),
|
|
||||||
getCategories(),
|
|
||||||
]);
|
|
||||||
const brand = brands.find((b) => b.id === Number(id));
|
|
||||||
if (!brand) notFound();
|
|
||||||
|
|
||||||
const sp = await searchParams;
|
const sp = await searchParams;
|
||||||
const filters = parseSearchParams(sp);
|
const key = JSON.stringify(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 (
|
return (
|
||||||
<CatalogView
|
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
||||||
products={filtered}
|
<BrandCatalogContent id={id} params={sp} />
|
||||||
breadcrumbItems={breadcrumbItems}
|
</Suspense>
|
||||||
basePath={basePath}
|
|
||||||
hiddenFilters={['brand']}
|
|
||||||
brands={brands}
|
|
||||||
categories={categories}
|
|
||||||
allProducts={brandProducts}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
apps/web/src/app/brand/[id]/loading.tsx
Normal file
5
apps/web/src/app/brand/[id]/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { BrandPageSkeleton } from '@/components/Skeleton/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <BrandPageSkeleton />;
|
||||||
|
}
|
||||||
25
apps/web/src/app/catalog/CatalogContent.tsx
Normal file
25
apps/web/src/app/catalog/CatalogContent.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
||||||
|
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
||||||
|
import { getProducts } from '@/lib/api';
|
||||||
|
|
||||||
|
export async function CatalogContent({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Record<string, string | string[] | undefined>;
|
||||||
|
}) {
|
||||||
|
const products = await getProducts();
|
||||||
|
const filters = parseSearchParams(params);
|
||||||
|
const filtered = filterProducts(products, filters);
|
||||||
|
|
||||||
|
const breadcrumbItems = [
|
||||||
|
{ label: 'Главная', href: '/' },
|
||||||
|
{ label: 'Каталог' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CatalogView
|
||||||
|
products={filtered}
|
||||||
|
breadcrumbItems={breadcrumbItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
import { categoryNameMap, CategorySlug } from '@/types';
|
import { categoryNameMap, CategorySlug } from '@/types';
|
||||||
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
|
||||||
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
import { CatalogContent } from './CatalogContent';
|
||||||
import { getProducts, getBrands, getCategories } from '@/lib/api';
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -27,26 +27,11 @@ export default async function CatalogPage({
|
|||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const [products, brands, categories] = await Promise.all([
|
const key = JSON.stringify(params);
|
||||||
getProducts(),
|
|
||||||
getBrands(),
|
|
||||||
getCategories(),
|
|
||||||
]);
|
|
||||||
const filters = parseSearchParams(params);
|
|
||||||
const filtered = filterProducts(products, filters);
|
|
||||||
|
|
||||||
const breadcrumbItems = [
|
|
||||||
{ label: 'Главная', href: '/' },
|
|
||||||
{ label: 'Каталог' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CatalogView
|
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
||||||
products={filtered}
|
<CatalogContent params={params} />
|
||||||
breadcrumbItems={breadcrumbItems}
|
</Suspense>
|
||||||
brands={brands}
|
|
||||||
categories={categories}
|
|
||||||
allProducts={products}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
import { Hero } from '@/components/Hero/Hero';
|
import { Hero } from '@/components/Hero/Hero';
|
||||||
import { Directions } from '@/components/Directions/Directions';
|
import { Directions } from '@/components/Directions/Directions';
|
||||||
import { TrustStrip } from '@/components/TrustStrip/TrustStrip';
|
import { TrustStrip } from '@/components/TrustStrip/TrustStrip';
|
||||||
import { BrandLogos } from '@/components/BrandLogos/BrandLogos';
|
import { BrandLogos } from '@/components/BrandLogos/BrandLogos';
|
||||||
|
import {
|
||||||
|
DirectionsSkeleton,
|
||||||
|
BrandLogosSkeleton,
|
||||||
|
} from '@/components/Skeleton/Skeleton';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero />
|
||||||
|
<Suspense fallback={<DirectionsSkeleton />}>
|
||||||
<Directions />
|
<Directions />
|
||||||
|
</Suspense>
|
||||||
<TrustStrip />
|
<TrustStrip />
|
||||||
|
<Suspense fallback={<BrandLogosSkeleton />}>
|
||||||
<BrandLogos />
|
<BrandLogos />
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
apps/web/src/app/product/[sku]/loading.tsx
Normal file
5
apps/web/src/app/product/[sku]/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ProductPageSkeleton } from '@/components/Skeleton/Skeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <ProductPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -1,27 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Brand, Category, Product } from '@/types';
|
import { Brand, Category, Product } from '@/types';
|
||||||
import { Icon } from '@/components/Icon/Icon';
|
import { Icon } from '@/components/Icon/Icon';
|
||||||
import { useCatalogFilters } from '@/hooks/useCatalogFilters';
|
import { useCatalogFilters } from '@/hooks/useCatalogFilters';
|
||||||
import { getSubcategories } from '@/lib/getSubcategories';
|
import { getSubcategories } from '@/lib/getSubcategories';
|
||||||
import styles from './CatalogSidebar.module.scss';
|
import styles from './CatalogSidebar.module.scss';
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
|
||||||
|
|
||||||
interface CatalogSidebarProps {
|
interface CatalogSidebarProps {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
hiddenFilters?: string[];
|
hiddenFilters?: string[];
|
||||||
brands: Brand[];
|
|
||||||
categories: Category[];
|
|
||||||
products: Product[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatalogSidebar({
|
export function CatalogSidebar({
|
||||||
basePath = '/catalog',
|
basePath = '/catalog',
|
||||||
hiddenFilters = [],
|
hiddenFilters = [],
|
||||||
brands,
|
|
||||||
categories,
|
|
||||||
products,
|
|
||||||
}: CatalogSidebarProps) {
|
}: CatalogSidebarProps) {
|
||||||
const { filters, setFilter } = useCatalogFilters(basePath);
|
const { filters, setFilter } = useCatalogFilters(basePath);
|
||||||
|
const [brands, setBrands] = useState<Brand[]>([]);
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch(`${API_URL}/brands`).then((r) => r.json()),
|
||||||
|
fetch(`${API_URL}/categories`).then((r) => r.json()),
|
||||||
|
fetch(`${API_URL}/products`).then((r) => r.json()),
|
||||||
|
])
|
||||||
|
.then(([b, c, p]) => {
|
||||||
|
setBrands(b);
|
||||||
|
setCategories(c);
|
||||||
|
setProducts(p);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleFilter = (
|
const toggleFilter = (
|
||||||
key: 'category' | 'brand' | 'subcategory' | 'sort',
|
key: 'category' | 'brand' | 'subcategory' | 'sort',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Suspense } from 'react';
|
import { Product } from '@/types';
|
||||||
import { Product, Brand, Category } from '@/types';
|
|
||||||
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
||||||
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
|
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
|
||||||
import { ProductCard } from '@/components/ProductCard/ProductCard';
|
import { ProductCard } from '@/components/ProductCard/ProductCard';
|
||||||
@@ -15,9 +14,6 @@ interface CatalogViewProps {
|
|||||||
breadcrumbItems: BreadcrumbItem[];
|
breadcrumbItems: BreadcrumbItem[];
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
hiddenFilters?: string[];
|
hiddenFilters?: string[];
|
||||||
brands: Brand[];
|
|
||||||
categories: Category[];
|
|
||||||
allProducts?: Product[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatalogView({
|
export function CatalogView({
|
||||||
@@ -25,21 +21,10 @@ export function CatalogView({
|
|||||||
breadcrumbItems,
|
breadcrumbItems,
|
||||||
basePath = '/catalog',
|
basePath = '/catalog',
|
||||||
hiddenFilters,
|
hiddenFilters,
|
||||||
brands,
|
|
||||||
categories,
|
|
||||||
allProducts,
|
|
||||||
}: CatalogViewProps) {
|
}: CatalogViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<Suspense fallback={null}>
|
<CatalogSidebar basePath={basePath} hiddenFilters={hiddenFilters} />
|
||||||
<CatalogSidebar
|
|
||||||
basePath={basePath}
|
|
||||||
hiddenFilters={hiddenFilters}
|
|
||||||
brands={brands}
|
|
||||||
categories={categories}
|
|
||||||
products={allProducts ?? products}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Breadcrumb items={breadcrumbItems} />
|
<Breadcrumb items={breadcrumbItems} />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function Header() {
|
|||||||
fetch(`${API_URL}/products`)
|
fetch(`${API_URL}/products`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(setProducts)
|
.then(setProducts)
|
||||||
.catch(() => {});
|
.catch((error) => console.error(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const suggestions =
|
const suggestions =
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function useCatalogFilters(basePath = '/catalog') {
|
|||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return qs ? `${basePath}?${qs}` : basePath;
|
return qs ? `${basePath}?${qs}` : basePath;
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[basePath, searchParams]
|
[basePath, searchParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user