This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { BrandsModule } from './brands/brands.module';
|
||||||
|
import { ProductsModule } from './products/products.module';
|
||||||
|
import { CategoriesModule } from './categories/categories.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [BrandsModule, ProductsModule, CategoriesModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
12
apps/api/src/app/brands/brands.controller.ts
Normal file
12
apps/api/src/app/brands/brands.controller.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Brand } from '@/types';
|
import { Brand } from '@spairtech/shared';
|
||||||
|
|
||||||
export const brands: Brand[] = [
|
export const brands: Brand[] = [
|
||||||
{ id: 1, name: 'Bosch Rexroth', country: 'DE', logo: 'BR', category: 'Гидравлика' },
|
{ id: 1, name: 'Bosch Rexroth', country: 'DE', logo: 'BR', category: 'Гидравлика' },
|
||||||
9
apps/api/src/app/brands/brands.module.ts
Normal file
9
apps/api/src/app/brands/brands.module.ts
Normal file
@@ -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 {}
|
||||||
10
apps/api/src/app/brands/brands.service.ts
Normal file
10
apps/api/src/app/brands/brands.service.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/api/src/app/categories/categories.controller.ts
Normal file
12
apps/api/src/app/categories/categories.controller.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Category } from '@/types';
|
import { Category } from '@spairtech/shared';
|
||||||
|
|
||||||
export const categories: Category[] = [
|
export const categories: Category[] = [
|
||||||
{
|
{
|
||||||
@@ -34,5 +34,3 @@ export const categories: Category[] = [
|
|||||||
color: '#7B1FA2',
|
color: '#7B1FA2',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allCategories = ['Все', ...categories.map((c) => c.name)];
|
|
||||||
9
apps/api/src/app/categories/categories.module.ts
Normal file
9
apps/api/src/app/categories/categories.module.ts
Normal file
@@ -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 {}
|
||||||
10
apps/api/src/app/categories/categories.service.ts
Normal file
10
apps/api/src/app/categories/categories.service.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/api/src/app/products/products.controller.ts
Normal file
12
apps/api/src/app/products/products.controller.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Product } from '@/types';
|
import { Product } from '@spairtech/shared';
|
||||||
|
|
||||||
export const products: Product[] = [
|
export const products: Product[] = [
|
||||||
{ id: 1, sku: '4WRPEH 6 C3B12L', name: 'Пропорциональный распределитель', brand: 'Bosch Rexroth', category: 'Гидравлика', subcategory: 'Распределители', price: 'По запросу', image: '', inStock: true },
|
{ id: 1, sku: '4WRPEH 6 C3B12L', name: 'Пропорциональный распределитель', brand: 'Bosch Rexroth', category: 'Гидравлика', subcategory: 'Распределители', price: 'По запросу', image: '', inStock: true },
|
||||||
9
apps/api/src/app/products/products.module.ts
Normal file
9
apps/api/src/app/products/products.module.ts
Normal file
@@ -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 {}
|
||||||
10
apps/api/src/app/products/products.service.ts
Normal file
10
apps/api/src/app/products/products.service.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { AppModule } from './app/app.module';
|
|||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors();
|
||||||
const globalPrefix = 'api';
|
const globalPrefix = 'api';
|
||||||
app.setGlobalPrefix(globalPrefix);
|
app.setGlobalPrefix(globalPrefix);
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
|
|||||||
@@ -20,5 +20,10 @@
|
|||||||
"eslint.config.js",
|
"eslint.config.js",
|
||||||
"eslint.config.cjs",
|
"eslint.config.cjs",
|
||||||
"eslint.config.mjs"
|
"eslint.config.mjs"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../libs/shared/tsconfig.lib.json"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
"name": "@spairtech/web",
|
"name": "@spairtech/web",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"dev": {
|
||||||
|
"options": {
|
||||||
|
"port": 8888
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "~16.0.1",
|
"next": "~16.0.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { brands } from '@/data/brands';
|
|
||||||
import { products } from '@/data/products';
|
|
||||||
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
||||||
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
||||||
|
import { getBrands, getProducts, getCategories } from '@/lib/api';
|
||||||
export function generateStaticParams() {
|
|
||||||
return brands.map((b) => ({ id: String(b.id) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
@@ -14,6 +9,7 @@ export async function generateMetadata({
|
|||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const brands = await getBrands();
|
||||||
const brand = brands.find((b) => b.id === Number(id));
|
const brand = brands.find((b) => b.id === Number(id));
|
||||||
return {
|
return {
|
||||||
title: brand
|
title: brand
|
||||||
@@ -30,6 +26,11 @@ export default async function BrandCatalogPage({
|
|||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const [brands, products, categories] = await Promise.all([
|
||||||
|
getBrands(),
|
||||||
|
getProducts(),
|
||||||
|
getCategories(),
|
||||||
|
]);
|
||||||
const brand = brands.find((b) => b.id === Number(id));
|
const brand = brands.find((b) => b.id === Number(id));
|
||||||
if (!brand) notFound();
|
if (!brand) notFound();
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ export default async function BrandCatalogPage({
|
|||||||
breadcrumbItems={breadcrumbItems}
|
breadcrumbItems={breadcrumbItems}
|
||||||
basePath={basePath}
|
basePath={basePath}
|
||||||
hiddenFilters={['brand']}
|
hiddenFilters={['brand']}
|
||||||
|
brands={brands}
|
||||||
|
categories={categories}
|
||||||
|
allProducts={brandProducts}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { brands } from '@/data/brands';
|
|
||||||
import { products } from '@/data/products';
|
|
||||||
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
||||||
import { Button } from '@/components/ui/Button/Button';
|
import { Button } from '@/components/ui/Button/Button';
|
||||||
|
import { getBrands, getProducts } from '@/lib/api';
|
||||||
import styles from './page.module.scss';
|
import styles from './page.module.scss';
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return brands.map((b) => ({ id: String(b.id) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const brands = await getBrands();
|
||||||
const brand = brands.find((b) => b.id === Number(id));
|
const brand = brands.find((b) => b.id === Number(id));
|
||||||
return {
|
return {
|
||||||
title: brand ? `${brand.name} | PAN-PROM` : 'Бренд | PAN-PROM',
|
title: brand ? `${brand.name} | PAN-PROM` : 'Бренд | PAN-PROM',
|
||||||
@@ -28,6 +24,7 @@ export default async function BrandPage({
|
|||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const [brands, products] = await Promise.all([getBrands(), getProducts()]);
|
||||||
const brand = brands.find((b) => b.id === Number(id));
|
const brand = brands.find((b) => b.id === Number(id));
|
||||||
if (!brand) notFound();
|
if (!brand) notFound();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { products } from '@/data/products';
|
|
||||||
import { categoryNameMap, CategorySlug } from '@/types';
|
import { categoryNameMap, CategorySlug } from '@/types';
|
||||||
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
import { filterProducts, parseSearchParams } from '@/lib/filterProducts';
|
||||||
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
import { CatalogView } from '@/components/CatalogView/CatalogView';
|
||||||
|
import { getProducts, getBrands, getCategories } from '@/lib/api';
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -27,6 +27,11 @@ export default async function CatalogPage({
|
|||||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
|
const [products, brands, categories] = await Promise.all([
|
||||||
|
getProducts(),
|
||||||
|
getBrands(),
|
||||||
|
getCategories(),
|
||||||
|
]);
|
||||||
const filters = parseSearchParams(params);
|
const filters = parseSearchParams(params);
|
||||||
const filtered = filterProducts(products, filters);
|
const filtered = filterProducts(products, filters);
|
||||||
|
|
||||||
@@ -36,6 +41,12 @@ export default async function CatalogPage({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CatalogView products={filtered} breadcrumbItems={breadcrumbItems} />
|
<CatalogView
|
||||||
|
products={filtered}
|
||||||
|
breadcrumbItems={breadcrumbItems}
|
||||||
|
brands={brands}
|
||||||
|
categories={categories}
|
||||||
|
allProducts={products}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { products } from '@/data/products';
|
|
||||||
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
||||||
import { ProductDetail } from '@/components/ProductDetail/ProductDetail';
|
import { ProductDetail } from '@/components/ProductDetail/ProductDetail';
|
||||||
|
import { getProducts } from '@/lib/api';
|
||||||
import styles from './page.module.scss';
|
import styles from './page.module.scss';
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return products.map((p) => ({ sku: encodeURIComponent(p.sku) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ sku: string }>;
|
params: Promise<{ sku: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { sku } = await params;
|
const { sku } = await params;
|
||||||
|
const products = await getProducts();
|
||||||
const product = products.find((p) => p.sku === decodeURIComponent(sku));
|
const product = products.find((p) => p.sku === decodeURIComponent(sku));
|
||||||
return {
|
return {
|
||||||
title: product ? `${product.name} | PAN-PROM` : 'Продукт | PAN-PROM',
|
title: product ? `${product.name} | PAN-PROM` : 'Продукт | PAN-PROM',
|
||||||
@@ -26,6 +23,7 @@ export default async function ProductPage({
|
|||||||
params: Promise<{ sku: string }>;
|
params: Promise<{ sku: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { sku } = await params;
|
const { sku } = await params;
|
||||||
|
const products = await getProducts();
|
||||||
const product = products.find((p) => p.sku === decodeURIComponent(sku));
|
const product = products.find((p) => p.sku === decodeURIComponent(sku));
|
||||||
|
|
||||||
if (!product) notFound();
|
if (!product) notFound();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@/components/Icon/Icon';
|
import { Icon } from '@/components/Icon/Icon';
|
||||||
import { brands } from '@/data/brands';
|
import { getBrands } from '@/lib/api';
|
||||||
import styles from './BrandLogos.module.scss';
|
import styles from './BrandLogos.module.scss';
|
||||||
|
|
||||||
export function BrandLogos() {
|
export async function BrandLogos() {
|
||||||
|
const brands = await getBrands();
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Brand, Category, Product } from '@/types';
|
||||||
import { Icon } from '@/components/Icon/Icon';
|
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 { useCatalogFilters } from '@/hooks/useCatalogFilters';
|
||||||
import { getSubcategories } from '@/lib/getSubcategories';
|
import { getSubcategories } from '@/lib/getSubcategories';
|
||||||
import styles from './CatalogSidebar.module.scss';
|
import styles from './CatalogSidebar.module.scss';
|
||||||
@@ -11,11 +9,17 @@ import styles from './CatalogSidebar.module.scss';
|
|||||||
interface CatalogSidebarProps {
|
interface CatalogSidebarProps {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
hiddenFilters?: string[];
|
hiddenFilters?: string[];
|
||||||
|
brands: Brand[];
|
||||||
|
categories: Category[];
|
||||||
|
products: Product[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatalogSidebar({
|
export function CatalogSidebar({
|
||||||
basePath = '/catalog',
|
basePath = '/catalog',
|
||||||
hiddenFilters = [],
|
hiddenFilters = [],
|
||||||
|
brands,
|
||||||
|
categories,
|
||||||
|
products,
|
||||||
}: CatalogSidebarProps) {
|
}: CatalogSidebarProps) {
|
||||||
const { filters, setFilter } = useCatalogFilters(basePath);
|
const { filters, setFilter } = useCatalogFilters(basePath);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Product } from '@/types';
|
import { Product, Brand, Category } from '@/types';
|
||||||
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
|
||||||
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
|
import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
|
||||||
import { ProductCard } from '@/components/ProductCard/ProductCard';
|
import { ProductCard } from '@/components/ProductCard/ProductCard';
|
||||||
@@ -15,6 +15,9 @@ interface CatalogViewProps {
|
|||||||
breadcrumbItems: BreadcrumbItem[];
|
breadcrumbItems: BreadcrumbItem[];
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
hiddenFilters?: string[];
|
hiddenFilters?: string[];
|
||||||
|
brands: Brand[];
|
||||||
|
categories: Category[];
|
||||||
|
allProducts?: Product[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatalogView({
|
export function CatalogView({
|
||||||
@@ -22,11 +25,20 @@ export function CatalogView({
|
|||||||
breadcrumbItems,
|
breadcrumbItems,
|
||||||
basePath = '/catalog',
|
basePath = '/catalog',
|
||||||
hiddenFilters,
|
hiddenFilters,
|
||||||
|
brands,
|
||||||
|
categories,
|
||||||
|
allProducts,
|
||||||
}: CatalogViewProps) {
|
}: CatalogViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<CatalogSidebar basePath={basePath} hiddenFilters={hiddenFilters} />
|
<CatalogSidebar
|
||||||
|
basePath={basePath}
|
||||||
|
hiddenFilters={hiddenFilters}
|
||||||
|
brands={brands}
|
||||||
|
categories={categories}
|
||||||
|
products={allProducts ?? products}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { categories } from '@/data/categories';
|
import { getCategories } from '@/lib/api';
|
||||||
import styles from './Directions.module.scss';
|
import styles from './Directions.module.scss';
|
||||||
|
|
||||||
export function Directions() {
|
export async function Directions() {
|
||||||
|
const categories = await getCategories();
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h2 className={styles.title}>Направления</h2>
|
<h2 className={styles.title}>Направления</h2>
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Icon } from '@/components/Icon/Icon';
|
import { Icon } from '@/components/Icon/Icon';
|
||||||
import { useCart } from '@/components/CartProvider/CartProvider';
|
import { useCart } from '@/components/CartProvider/CartProvider';
|
||||||
import { headerNav } from '@/data/navigation';
|
import { headerNav } from '@/data/navigation';
|
||||||
import { products } from '@/data/products';
|
import { Product } from '@/types';
|
||||||
import styles from './Header.module.scss';
|
import styles from './Header.module.scss';
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { cart } = useCart();
|
const { cart } = useCart();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchFocused, setSearchFocused] = useState(false);
|
const [searchFocused, setSearchFocused] = useState(false);
|
||||||
const [mobileMenu, setMobileMenu] = useState(false);
|
const [mobileMenu, setMobileMenu] = useState(false);
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const searchRef = useRef<HTMLDivElement>(null);
|
const searchRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_URL}/products`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(setProducts)
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const suggestions =
|
const suggestions =
|
||||||
searchQuery.length > 1
|
searchQuery.length > 1
|
||||||
? products
|
? products
|
||||||
|
|||||||
147
apps/web/src/components/Skeleton/Skeleton.module.scss
Normal file
147
apps/web/src/components/Skeleton/Skeleton.module.scss
Normal 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);
|
||||||
|
}
|
||||||
74
apps/web/src/components/Skeleton/Skeleton.tsx
Normal file
74
apps/web/src/components/Skeleton/Skeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/web/src/lib/api.ts
Normal file
13
apps/web/src/lib/api.ts
Normal file
@@ -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<T>(path: string): Promise<T> {
|
||||||
|
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<Product[]>('/products');
|
||||||
|
export const getBrands = () => fetchApi<Brand[]>('/brands');
|
||||||
|
export const getCategories = () => fetchApi<Category[]>('/categories');
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Brand {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
country: string;
|
|
||||||
logo: string;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
@@ -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<string, CategorySlug> = {
|
|
||||||
'Гидравлика': 'gidravlika',
|
|
||||||
'Пневматика': 'pnevmatika',
|
|
||||||
'АСУ': 'asu',
|
|
||||||
'ЗИП': 'zip',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const categoryNameMap: Record<CategorySlug, string> = {
|
|
||||||
gidravlika: 'Гидравлика',
|
|
||||||
pnevmatika: 'Пневматика',
|
|
||||||
asu: 'АСУ',
|
|
||||||
zip: 'ЗИП',
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
export type { Brand } from './brand';
|
export type { Brand, Product, Category, CategorySlug } from '@spairtech/shared';
|
||||||
export type { Product } from './product';
|
export { categorySlugMap, categoryNameMap } from '@spairtech/shared';
|
||||||
export type { Category, CategorySlug } from './category';
|
|
||||||
export { categorySlugMap, categoryNameMap } from './category';
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -47,5 +47,10 @@
|
|||||||
"eslint.config.js",
|
"eslint.config.js",
|
||||||
"eslint.config.cjs",
|
"eslint.config.cjs",
|
||||||
"eslint.config.mjs"
|
"eslint.config.mjs"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../libs/shared"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
export function shared(): string {
|
export interface Brand {
|
||||||
return 'shared';
|
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<string, CategorySlug> = {
|
||||||
|
'Гидравлика': 'gidravlika',
|
||||||
|
'Пневматика': 'pnevmatika',
|
||||||
|
'АСУ': 'asu',
|
||||||
|
'ЗИП': 'zip',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categoryNameMap: Record<CategorySlug, string> = {
|
||||||
|
gidravlika: 'Гидравлика',
|
||||||
|
pnevmatika: 'Пневматика',
|
||||||
|
asu: 'АСУ',
|
||||||
|
zip: 'ЗИП',
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user