Улучшен Skeleton
Some checks failed
CI / main (push) Has been cancelled

This commit is contained in:
Igor Rybakov
2026-03-07 00:01:56 +02:00
parent a68fad6078
commit c62ff0394a
10 changed files with 166 additions and 61 deletions

View File

@@ -0,0 +1,35 @@
.main {
flex: 1;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-md);
}
.count {
font-size: 12px;
color: var(--color-slate-400);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-md);
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
.empty {
text-align: center;
color: var(--color-text-muted);
padding: var(--space-2xl) 0;
}

View File

@@ -1,7 +1,9 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { CatalogView } from '@/components/CatalogView/CatalogView'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { ProductCard } from '@/components/ProductCard/ProductCard';
import { getBrands, getProducts } from '@/lib/api'; import { getBrands, getProducts } from '@/lib/api';
import styles from './BrandCatalogContent.module.scss';
export async function BrandCatalogContent({ export async function BrandCatalogContent({
id, id,
@@ -21,21 +23,29 @@ export async function BrandCatalogContent({
const brandProducts = products.filter((p) => p.brand === brand.name); const brandProducts = products.filter((p) => p.brand === brand.name);
const filtered = filterProducts(brandProducts, filters); 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 <div className={styles.main}>
products={filtered} <div className={styles.header}>
breadcrumbItems={breadcrumbItems} <Breadcrumb
basePath={basePath} items={[
hiddenFilters={['brand']} { label: 'Главная', href: '/' },
/> { label: 'Каталог', href: '/catalog' },
{ label: brand.name, href: `/brand/${id}` },
{ label: 'Каталог' },
]}
/>
<span className={styles.count}>{filtered.length} позиций</span>
</div>
<div className={styles.grid}>
{filtered.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
{filtered.length === 0 && (
<p className={styles.empty}>
По выбранным фильтрам ничего не найдено.
</p>
)}
</div>
); );
} }

View File

@@ -0,0 +1,7 @@
.layout {
max-width: var(--max-width);
margin: 0 auto;
padding: var(--space-lg);
display: flex;
gap: var(--space-lg);
}

View File

@@ -1,7 +1,9 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { getBrands } from '@/lib/api'; import { getBrands } from '@/lib/api';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton'; import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { BrandCatalogContent } from './BrandCatalogContent'; import { BrandCatalogContent } from './BrandCatalogContent';
import styles from './page.module.scss';
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -28,10 +30,14 @@ export default async function BrandCatalogPage({
const { id } = await params; const { id } = await params;
const sp = await searchParams; const sp = await searchParams;
const key = JSON.stringify(sp); const key = JSON.stringify(sp);
const basePath = `/brand/${id}/catalog`;
return ( return (
<Suspense key={key} fallback={<CatalogSkeleton />}> <div className={styles.layout}>
<BrandCatalogContent id={id} params={sp} /> <CatalogSidebar basePath={basePath} hiddenFilters={['brand']} />
</Suspense> <Suspense key={key} fallback={<CatalogSkeleton />}>
<BrandCatalogContent id={id} params={sp} />
</Suspense>
</div>
); );
} }

View File

@@ -0,0 +1,35 @@
.main {
flex: 1;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-md);
}
.count {
font-size: 12px;
color: var(--color-slate-400);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-md);
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
.empty {
text-align: center;
color: var(--color-text-muted);
padding: var(--space-2xl) 0;
}

View File

@@ -1,6 +1,8 @@
import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
import { CatalogView } from '@/components/CatalogView/CatalogView'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { ProductCard } from '@/components/ProductCard/ProductCard';
import { getProducts } from '@/lib/api'; import { getProducts } from '@/lib/api';
import styles from './CatalogContent.module.scss';
export async function CatalogContent({ export async function CatalogContent({
params, params,
@@ -11,15 +13,27 @@ export async function CatalogContent({
const filters = parseSearchParams(params); const filters = parseSearchParams(params);
const filtered = filterProducts(products, filters); const filtered = filterProducts(products, filters);
const breadcrumbItems = [
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
];
return ( return (
<CatalogView <div className={styles.main}>
products={filtered} <div className={styles.header}>
breadcrumbItems={breadcrumbItems} <Breadcrumb
/> items={[
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
]}
/>
<span className={styles.count}>{filtered.length} позиций</span>
</div>
<div className={styles.grid}>
{filtered.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
{filtered.length === 0 && (
<p className={styles.empty}>
По выбранным фильтрам ничего не найдено.
</p>
)}
</div>
); );
} }

View File

@@ -0,0 +1,7 @@
.layout {
max-width: var(--max-width);
margin: 0 auto;
padding: var(--space-lg);
display: flex;
gap: var(--space-lg);
}

View File

@@ -1,7 +1,9 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { categoryNameMap, CategorySlug } from '@/types'; import { categoryNameMap, CategorySlug } from '@/types';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton'; import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { CatalogContent } from './CatalogContent'; import { CatalogContent } from './CatalogContent';
import styles from './page.module.scss';
export async function generateMetadata({ export async function generateMetadata({
searchParams, searchParams,
@@ -30,8 +32,11 @@ export default async function CatalogPage({
const key = JSON.stringify(params); const key = JSON.stringify(params);
return ( return (
<Suspense key={key} fallback={<CatalogSkeleton />}> <div className={styles.layout}>
<CatalogContent params={params} /> <CatalogSidebar />
</Suspense> <Suspense key={key} fallback={<CatalogSkeleton />}>
<CatalogContent params={params} />
</Suspense>
</div>
); );
} }

View File

@@ -59,33 +59,22 @@
.catalogGrid { .catalogGrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: var(--space-md); gap: var(--space-md);
padding: var(--space-md) 0;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
} }
.catalogCard { .catalogCard {
height: 180px; height: 180px;
} }
.catalogLayout {
display: flex;
gap: var(--space-xl);
max-width: 1280px;
margin: 0 auto;
padding: var(--space-xl);
}
.catalogSidebar {
width: 240px;
height: 400px;
flex-shrink: 0;
@media (max-width: 768px) {
display: none;
}
}
.catalogMain { .catalogMain {
flex: 1; flex: 1;
} }

View File

@@ -30,15 +30,12 @@ export function BrandLogosSkeleton() {
export function CatalogSkeleton() { export function CatalogSkeleton() {
return ( return (
<div className={styles.catalogLayout}> <div className={styles.catalogMain}>
<Box className={styles.catalogSidebar} /> <Box className={styles.titleBar} />
<div className={styles.catalogMain}> <div className={styles.catalogGrid}>
<Box className={styles.titleBar} /> {Array.from({ length: 6 }).map((_, i) => (
<div className={styles.catalogGrid}> <Box key={i} className={styles.catalogCard} />
{Array.from({ length: 6 }).map((_, i) => ( ))}
<Box key={i} className={styles.catalogCard} />
))}
</div>
</div> </div>
</div> </div>
); );