diff --git a/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx
index dae5360..221cfc4 100644
--- a/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx
+++ b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx
@@ -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);
diff --git a/apps/web/src/components/CatalogView/CatalogView.tsx b/apps/web/src/components/CatalogView/CatalogView.tsx
index b9de4d1..49fc00a 100644
--- a/apps/web/src/components/CatalogView/CatalogView.tsx
+++ b/apps/web/src/components/CatalogView/CatalogView.tsx
@@ -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 (
-
+
diff --git a/apps/web/src/components/Directions/Directions.tsx b/apps/web/src/components/Directions/Directions.tsx
index cbb4b60..d51dd88 100644
--- a/apps/web/src/components/Directions/Directions.tsx
+++ b/apps/web/src/components/Directions/Directions.tsx
@@ -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 (
Направления
diff --git a/apps/web/src/components/Header/Header.tsx b/apps/web/src/components/Header/Header.tsx
index 270c8f2..6bbe8ce 100644
--- a/apps/web/src/components/Header/Header.tsx
+++ b/apps/web/src/components/Header/Header.tsx
@@ -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([]);
const searchRef = useRef(null);
+ useEffect(() => {
+ fetch(`${API_URL}/products`)
+ .then((res) => res.json())
+ .then(setProducts)
+ .catch(() => {});
+ }, []);
+
const suggestions =
searchQuery.length > 1
? products
diff --git a/apps/web/src/components/Skeleton/Skeleton.module.scss b/apps/web/src/components/Skeleton/Skeleton.module.scss
new file mode 100644
index 0000000..eadd90f
--- /dev/null
+++ b/apps/web/src/components/Skeleton/Skeleton.module.scss
@@ -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);
+}
diff --git a/apps/web/src/components/Skeleton/Skeleton.tsx b/apps/web/src/components/Skeleton/Skeleton.tsx
new file mode 100644
index 0000000..ed2e481
--- /dev/null
+++ b/apps/web/src/components/Skeleton/Skeleton.tsx
@@ -0,0 +1,74 @@
+import styles from './Skeleton.module.scss';
+
+function Box({ className }: { className?: string }) {
+ return ;
+}
+
+export function DirectionsSkeleton() {
+ return (
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
+
+export function BrandLogosSkeleton() {
+ return (
+
+
+ {Array.from({ length: 12 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
+
+export function CatalogSkeleton() {
+ return (
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function BrandPageSkeleton() {
+ return (
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
+
+export function ProductPageSkeleton() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
new file mode 100644
index 0000000..76b6df5
--- /dev/null
+++ b/apps/web/src/lib/api.ts
@@ -0,0 +1,13 @@
+import { Brand, Product, Category } from '@/types';
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
+
+async function fetchApi(path: string): Promise {
+ const res = await fetch(`${API_URL}${path}`, { cache: 'no-store' });
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
+ return res.json();
+}
+
+export const getProducts = () => fetchApi('/products');
+export const getBrands = () => fetchApi('/brands');
+export const getCategories = () => fetchApi('/categories');
diff --git a/apps/web/src/types/brand.ts b/apps/web/src/types/brand.ts
deleted file mode 100644
index e507263..0000000
--- a/apps/web/src/types/brand.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface Brand {
- id: number;
- name: string;
- country: string;
- logo: string;
- category: string;
-}
diff --git a/apps/web/src/types/category.ts b/apps/web/src/types/category.ts
deleted file mode 100644
index 79a5411..0000000
--- a/apps/web/src/types/category.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type CategorySlug = 'gidravlika' | 'pnevmatika' | 'asu' | 'zip';
-
-export interface Category {
- slug: CategorySlug;
- name: string;
- icon: string;
- description: string;
- brands: string;
- color: string;
-}
-
-export const categorySlugMap: Record = {
- 'Гидравлика': 'gidravlika',
- 'Пневматика': 'pnevmatika',
- 'АСУ': 'asu',
- 'ЗИП': 'zip',
-};
-
-export const categoryNameMap: Record = {
- gidravlika: 'Гидравлика',
- pnevmatika: 'Пневматика',
- asu: 'АСУ',
- zip: 'ЗИП',
-};
diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts
index d694842..9f6f455 100644
--- a/apps/web/src/types/index.ts
+++ b/apps/web/src/types/index.ts
@@ -1,4 +1,2 @@
-export type { Brand } from './brand';
-export type { Product } from './product';
-export type { Category, CategorySlug } from './category';
-export { categorySlugMap, categoryNameMap } from './category';
+export type { Brand, Product, Category, CategorySlug } from '@spairtech/shared';
+export { categorySlugMap, categoryNameMap } from '@spairtech/shared';
diff --git a/apps/web/src/types/product.ts b/apps/web/src/types/product.ts
deleted file mode 100644
index aff43bb..0000000
--- a/apps/web/src/types/product.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface Product {
- id: number;
- sku: string;
- name: string;
- brand: string;
- category: string;
- subcategory: string;
- price: string;
- image: string;
- inStock: boolean;
-}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 11a3c38..412e4bd 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -47,5 +47,10 @@
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
+ ],
+ "references": [
+ {
+ "path": "../../libs/shared"
+ }
]
}
diff --git a/libs/shared/src/lib/shared.ts b/libs/shared/src/lib/shared.ts
index d734544..5f6218d 100644
--- a/libs/shared/src/lib/shared.ts
+++ b/libs/shared/src/lib/shared.ts
@@ -1,3 +1,44 @@
-export function shared(): string {
- return 'shared';
+export interface Brand {
+ id: number;
+ name: string;
+ country: string;
+ logo: string;
+ category: string;
}
+
+export interface Product {
+ id: number;
+ sku: string;
+ name: string;
+ brand: string;
+ category: string;
+ subcategory: string;
+ price: string;
+ image: string;
+ inStock: boolean;
+}
+
+export type CategorySlug = 'gidravlika' | 'pnevmatika' | 'asu' | 'zip';
+
+export interface Category {
+ slug: CategorySlug;
+ name: string;
+ icon: string;
+ description: string;
+ brands: string;
+ color: string;
+}
+
+export const categorySlugMap: Record = {
+ 'Гидравлика': 'gidravlika',
+ 'Пневматика': 'pnevmatika',
+ 'АСУ': 'asu',
+ 'ЗИП': 'zip',
+};
+
+export const categoryNameMap: Record = {
+ gidravlika: 'Гидравлика',
+ pnevmatika: 'Пневматика',
+ asu: 'АСУ',
+ zip: 'ЗИП',
+};