This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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`;
|
return (
|
||||||
|
<div className={styles.main}>
|
||||||
const breadcrumbItems = [
|
<div className={styles.header}>
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
{ label: 'Главная', href: '/' },
|
{ label: 'Главная', href: '/' },
|
||||||
{ label: 'Каталог', href: '/catalog' },
|
{ label: 'Каталог', href: '/catalog' },
|
||||||
{ label: brand.name, href: `/brand/${id}` },
|
{ label: brand.name, href: `/brand/${id}` },
|
||||||
{ label: 'Каталог' },
|
{ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
7
apps/web/src/app/brand/[id]/catalog/page.module.scss
Normal file
7
apps/web/src/app/brand/[id]/catalog/page.module.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.layout {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.layout}>
|
||||||
|
<CatalogSidebar basePath={basePath} hiddenFilters={['brand']} />
|
||||||
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
||||||
<BrandCatalogContent id={id} params={sp} />
|
<BrandCatalogContent id={id} params={sp} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
apps/web/src/app/catalog/CatalogContent.module.scss
Normal file
35
apps/web/src/app/catalog/CatalogContent.module.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -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 = [
|
return (
|
||||||
|
<div className={styles.main}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
{ label: 'Главная', href: '/' },
|
{ label: 'Главная', href: '/' },
|
||||||
{ label: 'Каталог' },
|
{ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
7
apps/web/src/app/catalog/page.module.scss
Normal file
7
apps/web/src/app/catalog/page.module.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.layout {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.layout}>
|
||||||
|
<CatalogSidebar />
|
||||||
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
<Suspense key={key} fallback={<CatalogSkeleton />}>
|
||||||
<CatalogContent params={params} />
|
<CatalogContent params={params} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ export function BrandLogosSkeleton() {
|
|||||||
|
|
||||||
export function CatalogSkeleton() {
|
export function CatalogSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.catalogLayout}>
|
|
||||||
<Box className={styles.catalogSidebar} />
|
|
||||||
<div className={styles.catalogMain}>
|
<div className={styles.catalogMain}>
|
||||||
<Box className={styles.titleBar} />
|
<Box className={styles.titleBar} />
|
||||||
<div className={styles.catalogGrid}>
|
<div className={styles.catalogGrid}>
|
||||||
@@ -40,7 +38,6 @@ export function CatalogSkeleton() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user