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

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

View File

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

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'; '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',

View File

@@ -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} />

View File

@@ -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 =

View File

@@ -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]
); );