Skeleton на загрузку
Some checks failed
CI / main (push) Has been cancelled

This commit is contained in:
Igor Rybakov
2026-03-06 23:59:33 +02:00
parent cb527f4961
commit a68fad6078
11 changed files with 126 additions and 82 deletions

View 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']}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { notFound } from 'next/navigation';
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { CatalogView } from '@/components/CatalogView/CatalogView';
import { getBrands, getProducts, getCategories } from '@/lib/api';
import { Suspense } from 'react';
import { getBrands } from '@/lib/api';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { BrandCatalogContent } from './BrandCatalogContent';
export async function generateMetadata({
params,
@@ -26,37 +26,12 @@ export default async function BrandCatalogPage({
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
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 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: 'Каталог' },
];
const key = JSON.stringify(sp);
return (
<CatalogView
products={filtered}
breadcrumbItems={breadcrumbItems}
basePath={basePath}
hiddenFilters={['brand']}
brands={brands}
categories={categories}
allProducts={brandProducts}
/>
<Suspense key={key} fallback={<CatalogSkeleton />}>
<BrandCatalogContent id={id} params={sp} />
</Suspense>
);
}

View File

@@ -0,0 +1,5 @@
import { BrandPageSkeleton } from '@/components/Skeleton/Skeleton';
export default function Loading() {
return <BrandPageSkeleton />;
}

View 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}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { Suspense } from 'react';
import { categoryNameMap, CategorySlug } from '@/types';
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { CatalogView } from '@/components/CatalogView/CatalogView';
import { getProducts, getBrands, getCategories } from '@/lib/api';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { CatalogContent } from './CatalogContent';
export async function generateMetadata({
searchParams,
@@ -27,26 +27,11 @@ export default async function CatalogPage({
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const params = await searchParams;
const [products, brands, categories] = await Promise.all([
getProducts(),
getBrands(),
getCategories(),
]);
const filters = parseSearchParams(params);
const filtered = filterProducts(products, filters);
const breadcrumbItems = [
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
];
const key = JSON.stringify(params);
return (
<CatalogView
products={filtered}
breadcrumbItems={breadcrumbItems}
brands={brands}
categories={categories}
allProducts={products}
/>
<Suspense key={key} fallback={<CatalogSkeleton />}>
<CatalogContent params={params} />
</Suspense>
);
}

View File

@@ -1,15 +1,24 @@
import { Suspense } from 'react';
import { Hero } from '@/components/Hero/Hero';
import { Directions } from '@/components/Directions/Directions';
import { TrustStrip } from '@/components/TrustStrip/TrustStrip';
import { BrandLogos } from '@/components/BrandLogos/BrandLogos';
import {
DirectionsSkeleton,
BrandLogosSkeleton,
} from '@/components/Skeleton/Skeleton';
export default function HomePage() {
return (
<>
<Hero />
<Directions />
<Suspense fallback={<DirectionsSkeleton />}>
<Directions />
</Suspense>
<TrustStrip />
<BrandLogos />
<Suspense fallback={<BrandLogosSkeleton />}>
<BrandLogos />
</Suspense>
</>
);
}

View File

@@ -0,0 +1,5 @@
import { ProductPageSkeleton } from '@/components/Skeleton/Skeleton';
export default function Loading() {
return <ProductPageSkeleton />;
}

View File

@@ -1,27 +1,41 @@
'use client';
import { useEffect, useState } from 'react';
import { Brand, Category, Product } from '@/types';
import { Icon } from '@/components/Icon/Icon';
import { useCatalogFilters } from '@/hooks/useCatalogFilters';
import { getSubcategories } from '@/lib/getSubcategories';
import styles from './CatalogSidebar.module.scss';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
interface CatalogSidebarProps {
basePath?: string;
hiddenFilters?: string[];
brands: Brand[];
categories: Category[];
products: Product[];
}
export function CatalogSidebar({
basePath = '/catalog',
hiddenFilters = [],
brands,
categories,
products,
}: CatalogSidebarProps) {
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 = (
key: 'category' | 'brand' | 'subcategory' | 'sort',

View File

@@ -1,5 +1,4 @@
import { Suspense } from 'react';
import { Product, Brand, Category } from '@/types';
import { Product } from '@/types';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { ProductCard } from '@/components/ProductCard/ProductCard';
@@ -15,9 +14,6 @@ interface CatalogViewProps {
breadcrumbItems: BreadcrumbItem[];
basePath?: string;
hiddenFilters?: string[];
brands: Brand[];
categories: Category[];
allProducts?: Product[];
}
export function CatalogView({
@@ -25,21 +21,10 @@ export function CatalogView({
breadcrumbItems,
basePath = '/catalog',
hiddenFilters,
brands,
categories,
allProducts,
}: CatalogViewProps) {
return (
<div className={styles.layout}>
<Suspense fallback={null}>
<CatalogSidebar
basePath={basePath}
hiddenFilters={hiddenFilters}
brands={brands}
categories={categories}
products={allProducts ?? products}
/>
</Suspense>
<CatalogSidebar basePath={basePath} hiddenFilters={hiddenFilters} />
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb items={breadcrumbItems} />

View File

@@ -24,7 +24,7 @@ export function Header() {
fetch(`${API_URL}/products`)
.then((res) => res.json())
.then(setProducts)
.catch(() => {});
.catch((error) => console.error(error));
}, []);
const suggestions =

View File

@@ -31,7 +31,7 @@ export function useCatalogFilters(basePath = '/catalog') {
const qs = params.toString();
return qs ? `${basePath}?${qs}` : basePath;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[basePath, searchParams]
);