From cb527f4961313614bfa041076b9f300463f2fefd Mon Sep 17 00:00:00 2001 From: Igor Rybakov Date: Fri, 6 Mar 2026 23:51:52 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=20mo?= =?UTF-8?q?ckup=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/app/app.module.ts | 5 +- apps/api/src/app/brands/brands.controller.ts | 12 ++ .../src/app/brands/brands.data.ts} | 2 +- apps/api/src/app/brands/brands.module.ts | 9 ++ apps/api/src/app/brands/brands.service.ts | 10 ++ .../app/categories/categories.controller.ts | 12 ++ .../src/app/categories/categories.data.ts} | 4 +- .../src/app/categories/categories.module.ts | 9 ++ .../src/app/categories/categories.service.ts | 10 ++ .../src/app/products/products.controller.ts | 12 ++ .../src/app/products/products.data.ts} | 2 +- apps/api/src/app/products/products.module.ts | 9 ++ apps/api/src/app/products/products.service.ts | 10 ++ apps/api/src/main.ts | 1 + apps/api/tsconfig.app.json | 5 + apps/web/next-env.d.ts | 2 +- apps/web/package.json | 9 ++ apps/web/src/app/brand/[id]/catalog/page.tsx | 16 +- apps/web/src/app/brand/[id]/page.tsx | 9 +- apps/web/src/app/catalog/page.tsx | 15 +- apps/web/src/app/product/[sku]/page.tsx | 8 +- .../src/components/BrandLogos/BrandLogos.tsx | 5 +- .../CatalogSidebar/CatalogSidebar.tsx | 10 +- .../components/CatalogView/CatalogView.tsx | 16 +- .../src/components/Directions/Directions.tsx | 5 +- apps/web/src/components/Header/Header.tsx | 14 +- .../components/Skeleton/Skeleton.module.scss | 147 ++++++++++++++++++ apps/web/src/components/Skeleton/Skeleton.tsx | 74 +++++++++ apps/web/src/lib/api.ts | 13 ++ apps/web/src/types/brand.ts | 7 - apps/web/src/types/category.ts | 24 --- apps/web/src/types/index.ts | 6 +- apps/web/src/types/product.ts | 11 -- apps/web/tsconfig.json | 5 + libs/shared/src/lib/shared.ts | 45 +++++- 35 files changed, 468 insertions(+), 85 deletions(-) create mode 100644 apps/api/src/app/brands/brands.controller.ts rename apps/{web/src/data/brands.ts => api/src/app/brands/brands.data.ts} (96%) create mode 100644 apps/api/src/app/brands/brands.module.ts create mode 100644 apps/api/src/app/brands/brands.service.ts create mode 100644 apps/api/src/app/categories/categories.controller.ts rename apps/{web/src/data/categories.ts => api/src/app/categories/categories.data.ts} (91%) create mode 100644 apps/api/src/app/categories/categories.module.ts create mode 100644 apps/api/src/app/categories/categories.service.ts create mode 100644 apps/api/src/app/products/products.controller.ts rename apps/{web/src/data/products.ts => api/src/app/products/products.data.ts} (98%) create mode 100644 apps/api/src/app/products/products.module.ts create mode 100644 apps/api/src/app/products/products.service.ts create mode 100644 apps/web/src/components/Skeleton/Skeleton.module.scss create mode 100644 apps/web/src/components/Skeleton/Skeleton.tsx create mode 100644 apps/web/src/lib/api.ts delete mode 100644 apps/web/src/types/brand.ts delete mode 100644 apps/web/src/types/category.ts delete mode 100644 apps/web/src/types/product.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 8662803..80b0e03 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { BrandsModule } from './brands/brands.module'; +import { ProductsModule } from './products/products.module'; +import { CategoriesModule } from './categories/categories.module'; @Module({ - imports: [], + imports: [BrandsModule, ProductsModule, CategoriesModule], controllers: [AppController], providers: [AppService], }) diff --git a/apps/api/src/app/brands/brands.controller.ts b/apps/api/src/app/brands/brands.controller.ts new file mode 100644 index 0000000..92088f6 --- /dev/null +++ b/apps/api/src/app/brands/brands.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { BrandsService } from './brands.service'; + +@Controller('brands') +export class BrandsController { + constructor(private readonly brandsService: BrandsService) {} + + @Get() + findAll() { + return this.brandsService.findAll(); + } +} diff --git a/apps/web/src/data/brands.ts b/apps/api/src/app/brands/brands.data.ts similarity index 96% rename from apps/web/src/data/brands.ts rename to apps/api/src/app/brands/brands.data.ts index ea0b621..386d6d4 100644 --- a/apps/web/src/data/brands.ts +++ b/apps/api/src/app/brands/brands.data.ts @@ -1,4 +1,4 @@ -import { Brand } from '@/types'; +import { Brand } from '@spairtech/shared'; export const brands: Brand[] = [ { id: 1, name: 'Bosch Rexroth', country: 'DE', logo: 'BR', category: 'Гидравлика' }, diff --git a/apps/api/src/app/brands/brands.module.ts b/apps/api/src/app/brands/brands.module.ts new file mode 100644 index 0000000..c49e13a --- /dev/null +++ b/apps/api/src/app/brands/brands.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { BrandsController } from './brands.controller'; +import { BrandsService } from './brands.service'; + +@Module({ + controllers: [BrandsController], + providers: [BrandsService], +}) +export class BrandsModule {} diff --git a/apps/api/src/app/brands/brands.service.ts b/apps/api/src/app/brands/brands.service.ts new file mode 100644 index 0000000..41639da --- /dev/null +++ b/apps/api/src/app/brands/brands.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { brands } from './brands.data'; + +@Injectable() +export class BrandsService { + async findAll() { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return brands; + } +} diff --git a/apps/api/src/app/categories/categories.controller.ts b/apps/api/src/app/categories/categories.controller.ts new file mode 100644 index 0000000..5eb10ba --- /dev/null +++ b/apps/api/src/app/categories/categories.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { CategoriesService } from './categories.service'; + +@Controller('categories') +export class CategoriesController { + constructor(private readonly categoriesService: CategoriesService) {} + + @Get() + findAll() { + return this.categoriesService.findAll(); + } +} diff --git a/apps/web/src/data/categories.ts b/apps/api/src/app/categories/categories.data.ts similarity index 91% rename from apps/web/src/data/categories.ts rename to apps/api/src/app/categories/categories.data.ts index 145cee5..7a1bb96 100644 --- a/apps/web/src/data/categories.ts +++ b/apps/api/src/app/categories/categories.data.ts @@ -1,4 +1,4 @@ -import { Category } from '@/types'; +import { Category } from '@spairtech/shared'; export const categories: Category[] = [ { @@ -34,5 +34,3 @@ export const categories: Category[] = [ color: '#7B1FA2', }, ]; - -export const allCategories = ['Все', ...categories.map((c) => c.name)]; diff --git a/apps/api/src/app/categories/categories.module.ts b/apps/api/src/app/categories/categories.module.ts new file mode 100644 index 0000000..47daf6d --- /dev/null +++ b/apps/api/src/app/categories/categories.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CategoriesController } from './categories.controller'; +import { CategoriesService } from './categories.service'; + +@Module({ + controllers: [CategoriesController], + providers: [CategoriesService], +}) +export class CategoriesModule {} diff --git a/apps/api/src/app/categories/categories.service.ts b/apps/api/src/app/categories/categories.service.ts new file mode 100644 index 0000000..bdfc19e --- /dev/null +++ b/apps/api/src/app/categories/categories.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { categories } from './categories.data'; + +@Injectable() +export class CategoriesService { + async findAll() { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return categories; + } +} diff --git a/apps/api/src/app/products/products.controller.ts b/apps/api/src/app/products/products.controller.ts new file mode 100644 index 0000000..7fc667f --- /dev/null +++ b/apps/api/src/app/products/products.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { ProductsService } from './products.service'; + +@Controller('products') +export class ProductsController { + constructor(private readonly productsService: ProductsService) {} + + @Get() + findAll() { + return this.productsService.findAll(); + } +} diff --git a/apps/web/src/data/products.ts b/apps/api/src/app/products/products.data.ts similarity index 98% rename from apps/web/src/data/products.ts rename to apps/api/src/app/products/products.data.ts index 4a3f959..0e8bd41 100644 --- a/apps/web/src/data/products.ts +++ b/apps/api/src/app/products/products.data.ts @@ -1,4 +1,4 @@ -import { Product } from '@/types'; +import { Product } from '@spairtech/shared'; export const products: Product[] = [ { id: 1, sku: '4WRPEH 6 C3B12L', name: 'Пропорциональный распределитель', brand: 'Bosch Rexroth', category: 'Гидравлика', subcategory: 'Распределители', price: 'По запросу', image: '', inStock: true }, diff --git a/apps/api/src/app/products/products.module.ts b/apps/api/src/app/products/products.module.ts new file mode 100644 index 0000000..9b00f47 --- /dev/null +++ b/apps/api/src/app/products/products.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ProductsController } from './products.controller'; +import { ProductsService } from './products.service'; + +@Module({ + controllers: [ProductsController], + providers: [ProductsService], +}) +export class ProductsModule {} diff --git a/apps/api/src/app/products/products.service.ts b/apps/api/src/app/products/products.service.ts new file mode 100644 index 0000000..873d88d --- /dev/null +++ b/apps/api/src/app/products/products.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { products } from './products.data'; + +@Injectable() +export class ProductsService { + async findAll() { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return products; + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 27cc058..c6a079d 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -9,6 +9,7 @@ import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors(); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); const port = process.env.PORT || 3000; diff --git a/apps/api/tsconfig.app.json b/apps/api/tsconfig.app.json index da8f8a0..48c6f3f 100644 --- a/apps/api/tsconfig.app.json +++ b/apps/api/tsconfig.app.json @@ -20,5 +20,10 @@ "eslint.config.js", "eslint.config.cjs", "eslint.config.mjs" + ], + "references": [ + { + "path": "../../libs/shared/tsconfig.lib.json" + } ] } diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index e9c1bcd..1173a7f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,6 +2,15 @@ "name": "@spairtech/web", "version": "0.0.1", "private": true, + "nx": { + "targets": { + "dev": { + "options": { + "port": 8888 + } + } + } + }, "dependencies": { "next": "~16.0.1", "react": "^19.0.0", diff --git a/apps/web/src/app/brand/[id]/catalog/page.tsx b/apps/web/src/app/brand/[id]/catalog/page.tsx index 95939fe..8e4e2a5 100644 --- a/apps/web/src/app/brand/[id]/catalog/page.tsx +++ b/apps/web/src/app/brand/[id]/catalog/page.tsx @@ -1,12 +1,7 @@ import { notFound } from 'next/navigation'; -import { brands } from '@/data/brands'; -import { products } from '@/data/products'; import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; import { CatalogView } from '@/components/CatalogView/CatalogView'; - -export function generateStaticParams() { - return brands.map((b) => ({ id: String(b.id) })); -} +import { getBrands, getProducts, getCategories } from '@/lib/api'; export async function generateMetadata({ params, @@ -14,6 +9,7 @@ export async function generateMetadata({ params: Promise<{ id: string }>; }) { const { id } = await params; + const brands = await getBrands(); const brand = brands.find((b) => b.id === Number(id)); return { title: brand @@ -30,6 +26,11 @@ export default async function BrandCatalogPage({ searchParams: Promise>; }) { const { id } = await params; + const [brands, products, categories] = await Promise.all([ + getBrands(), + getProducts(), + getCategories(), + ]); const brand = brands.find((b) => b.id === Number(id)); if (!brand) notFound(); @@ -53,6 +54,9 @@ export default async function BrandCatalogPage({ breadcrumbItems={breadcrumbItems} basePath={basePath} hiddenFilters={['brand']} + brands={brands} + categories={categories} + allProducts={brandProducts} /> ); } diff --git a/apps/web/src/app/brand/[id]/page.tsx b/apps/web/src/app/brand/[id]/page.tsx index 8ac1bcd..30c8ba7 100644 --- a/apps/web/src/app/brand/[id]/page.tsx +++ b/apps/web/src/app/brand/[id]/page.tsx @@ -1,21 +1,17 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { brands } from '@/data/brands'; -import { products } from '@/data/products'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import { Button } from '@/components/ui/Button/Button'; +import { getBrands, getProducts } from '@/lib/api'; import styles from './page.module.scss'; -export function generateStaticParams() { - return brands.map((b) => ({ id: String(b.id) })); -} - export async function generateMetadata({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; + const brands = await getBrands(); const brand = brands.find((b) => b.id === Number(id)); return { title: brand ? `${brand.name} | PAN-PROM` : 'Бренд | PAN-PROM', @@ -28,6 +24,7 @@ export default async function BrandPage({ params: Promise<{ id: string }>; }) { const { id } = await params; + const [brands, products] = await Promise.all([getBrands(), getProducts()]); const brand = brands.find((b) => b.id === Number(id)); if (!brand) notFound(); diff --git a/apps/web/src/app/catalog/page.tsx b/apps/web/src/app/catalog/page.tsx index 6cf1d1a..0559786 100644 --- a/apps/web/src/app/catalog/page.tsx +++ b/apps/web/src/app/catalog/page.tsx @@ -1,7 +1,7 @@ -import { products } from '@/data/products'; import { categoryNameMap, CategorySlug } from '@/types'; import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; import { CatalogView } from '@/components/CatalogView/CatalogView'; +import { getProducts, getBrands, getCategories } from '@/lib/api'; export async function generateMetadata({ searchParams, @@ -27,6 +27,11 @@ export default async function CatalogPage({ searchParams: Promise>; }) { const params = await searchParams; + const [products, brands, categories] = await Promise.all([ + getProducts(), + getBrands(), + getCategories(), + ]); const filters = parseSearchParams(params); const filtered = filterProducts(products, filters); @@ -36,6 +41,12 @@ export default async function CatalogPage({ ]; return ( - + ); } diff --git a/apps/web/src/app/product/[sku]/page.tsx b/apps/web/src/app/product/[sku]/page.tsx index bd5019b..9528d71 100644 --- a/apps/web/src/app/product/[sku]/page.tsx +++ b/apps/web/src/app/product/[sku]/page.tsx @@ -1,19 +1,16 @@ import { notFound } from 'next/navigation'; -import { products } from '@/data/products'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import { ProductDetail } from '@/components/ProductDetail/ProductDetail'; +import { getProducts } from '@/lib/api'; import styles from './page.module.scss'; -export function generateStaticParams() { - return products.map((p) => ({ sku: encodeURIComponent(p.sku) })); -} - export async function generateMetadata({ params, }: { params: Promise<{ sku: string }>; }) { const { sku } = await params; + const products = await getProducts(); const product = products.find((p) => p.sku === decodeURIComponent(sku)); return { title: product ? `${product.name} | PAN-PROM` : 'Продукт | PAN-PROM', @@ -26,6 +23,7 @@ export default async function ProductPage({ params: Promise<{ sku: string }>; }) { const { sku } = await params; + const products = await getProducts(); const product = products.find((p) => p.sku === decodeURIComponent(sku)); if (!product) notFound(); diff --git a/apps/web/src/components/BrandLogos/BrandLogos.tsx b/apps/web/src/components/BrandLogos/BrandLogos.tsx index 89a1899..545b145 100644 --- a/apps/web/src/components/BrandLogos/BrandLogos.tsx +++ b/apps/web/src/components/BrandLogos/BrandLogos.tsx @@ -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 (
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: 'ЗИП', +};