Улучшен 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 { 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 styles from './BrandCatalogContent.module.scss';
export async function BrandCatalogContent({
id,
@@ -21,21 +23,29 @@ export async function BrandCatalogContent({
const brandProducts = products.filter((p) => p.brand === brand.name);
const filtered = filterProducts(brandProducts, filters);
const basePath = `/brand/${id}/catalog`;
const breadcrumbItems = [
return (
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb
items={[
{ label: 'Главная', href: '/' },
{ label: 'Каталог', href: '/catalog' },
{ label: brand.name, href: `/brand/${id}` },
{ label: 'Каталог' },
];
return (
<CatalogView
products={filtered}
breadcrumbItems={breadcrumbItems}
basePath={basePath}
hiddenFilters={['brand']}
]}
/>
<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 { getBrands } from '@/lib/api';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { BrandCatalogContent } from './BrandCatalogContent';
import styles from './page.module.scss';
export async function generateMetadata({
params,
@@ -28,10 +30,14 @@ export default async function BrandCatalogPage({
const { id } = await params;
const sp = await searchParams;
const key = JSON.stringify(sp);
const basePath = `/brand/${id}/catalog`;
return (
<div className={styles.layout}>
<CatalogSidebar basePath={basePath} hiddenFilters={['brand']} />
<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 { CatalogView } from '@/components/CatalogView/CatalogView';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { ProductCard } from '@/components/ProductCard/ProductCard';
import { getProducts } from '@/lib/api';
import styles from './CatalogContent.module.scss';
export async function CatalogContent({
params,
@@ -11,15 +13,27 @@ export async function CatalogContent({
const filters = parseSearchParams(params);
const filtered = filterProducts(products, filters);
const breadcrumbItems = [
return (
<div className={styles.main}>
<div className={styles.header}>
<Breadcrumb
items={[
{ label: 'Главная', href: '/' },
{ label: 'Каталог' },
];
return (
<CatalogView
products={filtered}
breadcrumbItems={breadcrumbItems}
]}
/>
<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 { categoryNameMap, CategorySlug } from '@/types';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { CatalogSkeleton } from '@/components/Skeleton/Skeleton';
import { CatalogContent } from './CatalogContent';
import styles from './page.module.scss';
export async function generateMetadata({
searchParams,
@@ -30,8 +32,11 @@ export default async function CatalogPage({
const key = JSON.stringify(params);
return (
<div className={styles.layout}>
<CatalogSidebar />
<Suspense key={key} fallback={<CatalogSkeleton />}>
<CatalogContent params={params} />
</Suspense>
</div>
);
}

View File

@@ -59,33 +59,22 @@
.catalogGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-template-columns: repeat(3, 1fr);
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 {
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 {
flex: 1;
}

View File

@@ -30,8 +30,6 @@ export function BrandLogosSkeleton() {
export function CatalogSkeleton() {
return (
<div className={styles.catalogLayout}>
<Box className={styles.catalogSidebar} />
<div className={styles.catalogMain}>
<Box className={styles.titleBar} />
<div className={styles.catalogGrid}>
@@ -40,7 +38,6 @@ export function CatalogSkeleton() {
))}
</div>
</div>
</div>
);
}