Перенес mockup данные на бэкенд
Some checks failed
CI / main (push) Has been cancelled

This commit is contained in:
Igor Rybakov
2026-03-06 23:51:52 +02:00
parent b9918116ae
commit cb527f4961
35 changed files with 468 additions and 85 deletions

View File

@@ -1,9 +1,10 @@
import Link from 'next/link';
import { Icon } from '@/components/Icon/Icon';
import { brands } from '@/data/brands';
import { getBrands } from '@/lib/api';
import styles from './BrandLogos.module.scss';
export function BrandLogos() {
export async function BrandLogos() {
const brands = await getBrands();
return (
<section className={styles.section}>
<div className={styles.header}>

View File

@@ -1,9 +1,7 @@
'use client';
import { Brand, Category, Product } from '@/types';
import { Icon } from '@/components/Icon/Icon';
import { categories } from '@/data/categories';
import { brands } from '@/data/brands';
import { products } from '@/data/products';
import { useCatalogFilters } from '@/hooks/useCatalogFilters';
import { getSubcategories } from '@/lib/getSubcategories';
import styles from './CatalogSidebar.module.scss';
@@ -11,11 +9,17 @@ import styles from './CatalogSidebar.module.scss';
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);

View File

@@ -1,5 +1,5 @@
import { Suspense } from 'react';
import { Product } from '@/types';
import { Product, Brand, Category } from '@/types';
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
import { ProductCard } from '@/components/ProductCard/ProductCard';
@@ -15,6 +15,9 @@ interface CatalogViewProps {
breadcrumbItems: BreadcrumbItem[];
basePath?: string;
hiddenFilters?: string[];
brands: Brand[];
categories: Category[];
allProducts?: Product[];
}
export function CatalogView({
@@ -22,11 +25,20 @@ export function CatalogView({
breadcrumbItems,
basePath = '/catalog',
hiddenFilters,
brands,
categories,
allProducts,
}: CatalogViewProps) {
return (
<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.header}>

View File

@@ -1,8 +1,9 @@
import Link from 'next/link';
import { categories } from '@/data/categories';
import { getCategories } from '@/lib/api';
import styles from './Directions.module.scss';
export function Directions() {
export async function Directions() {
const categories = await getCategories();
return (
<section className={styles.section}>
<h2 className={styles.title}>Направления</h2>

View File

@@ -1,22 +1,32 @@
'use client';
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Icon } from '@/components/Icon/Icon';
import { useCart } from '@/components/CartProvider/CartProvider';
import { headerNav } from '@/data/navigation';
import { products } from '@/data/products';
import { Product } from '@/types';
import styles from './Header.module.scss';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
export function Header() {
const pathname = usePathname();
const { cart } = useCart();
const [searchQuery, setSearchQuery] = useState('');
const [searchFocused, setSearchFocused] = useState(false);
const [mobileMenu, setMobileMenu] = useState(false);
const [products, setProducts] = useState<Product[]>([]);
const searchRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch(`${API_URL}/products`)
.then((res) => res.json())
.then(setProducts)
.catch(() => {});
}, []);
const suggestions =
searchQuery.length > 1
? products

View File

@@ -0,0 +1,147 @@
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-slate-100) 25%,
var(--color-slate-200) 50%,
var(--color-slate-100) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 8px;
}
.directionsGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-lg);
padding: var(--space-2xl) var(--space-xl);
@media (max-width: 768px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
}
.directionsCard {
height: 160px;
}
.brandsGrid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: var(--space-md);
padding: var(--space-2xl) var(--space-xl);
@media (max-width: 768px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 480px) {
grid-template-columns: repeat(2, 1fr);
}
}
.brandsCard {
height: 100px;
}
.catalogGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-md);
padding: var(--space-md) 0;
}
.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;
}
.titleBar {
height: 24px;
width: 200px;
margin-bottom: var(--space-lg);
}
.pageContainer {
max-width: 1280px;
margin: 0 auto;
padding: var(--space-xl);
}
.brandHeader {
display: flex;
align-items: center;
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.brandLogo {
width: 64px;
height: 64px;
border-radius: 12px;
}
.brandTitle {
height: 28px;
width: 200px;
margin-bottom: var(--space-sm);
}
.brandMeta {
height: 16px;
width: 120px;
}
.cardGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-xl);
@media (max-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
}
.statCard {
height: 80px;
}
.productDetail {
height: 400px;
margin-top: var(--space-lg);
}

View File

@@ -0,0 +1,74 @@
import styles from './Skeleton.module.scss';
function Box({ className }: { className?: string }) {
return <div className={`${styles.skeleton} ${className ?? ''}`} />;
}
export function DirectionsSkeleton() {
return (
<section>
<div className={styles.directionsGrid}>
{Array.from({ length: 4 }).map((_, i) => (
<Box key={i} className={styles.directionsCard} />
))}
</div>
</section>
);
}
export function BrandLogosSkeleton() {
return (
<section>
<div className={styles.brandsGrid}>
{Array.from({ length: 12 }).map((_, i) => (
<Box key={i} className={styles.brandsCard} />
))}
</div>
</section>
);
}
export function CatalogSkeleton() {
return (
<div className={styles.catalogLayout}>
<Box className={styles.catalogSidebar} />
<div className={styles.catalogMain}>
<Box className={styles.titleBar} />
<div className={styles.catalogGrid}>
{Array.from({ length: 6 }).map((_, i) => (
<Box key={i} className={styles.catalogCard} />
))}
</div>
</div>
</div>
);
}
export function BrandPageSkeleton() {
return (
<div className={styles.pageContainer}>
<Box className={styles.titleBar} />
<div className={styles.brandHeader}>
<Box className={styles.brandLogo} />
<div>
<Box className={styles.brandTitle} />
<Box className={styles.brandMeta} />
</div>
</div>
<div className={styles.cardGrid}>
{Array.from({ length: 4 }).map((_, i) => (
<Box key={i} className={styles.statCard} />
))}
</div>
</div>
);
}
export function ProductPageSkeleton() {
return (
<div className={styles.pageContainer}>
<Box className={styles.titleBar} />
<Box className={styles.productDetail} />
</div>
);
}