This commit is contained in:
@@ -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],
|
||||
})
|
||||
|
||||
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[] = [
|
||||
{ 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[] = [
|
||||
{
|
||||
@@ -34,5 +34,3 @@ export const categories: Category[] = [
|
||||
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[] = [
|
||||
{ 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() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors();
|
||||
const globalPrefix = 'api';
|
||||
app.setGlobalPrefix(globalPrefix);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
@@ -20,5 +20,10 @@
|
||||
"eslint.config.js",
|
||||
"eslint.config.cjs",
|
||||
"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/image-types/global" />
|
||||
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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
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 (
|
||||
<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 { 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();
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
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 { 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';
|
||||
|
||||
@@ -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.cjs",
|
||||
"eslint.config.mjs"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../libs/shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<string, CategorySlug> = {
|
||||
'Гидравлика': 'gidravlika',
|
||||
'Пневматика': 'pnevmatika',
|
||||
'АСУ': 'asu',
|
||||
'ЗИП': 'zip',
|
||||
};
|
||||
|
||||
export const categoryNameMap: Record<CategorySlug, string> = {
|
||||
gidravlika: 'Гидравлика',
|
||||
pnevmatika: 'Пневматика',
|
||||
asu: 'АСУ',
|
||||
zip: 'ЗИП',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user