diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index 1511519..9edff1c 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/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/src/app/about/page.module.scss b/apps/web/src/app/about/page.module.scss
new file mode 100644
index 0000000..1740061
--- /dev/null
+++ b/apps/web/src/app/about/page.module.scss
@@ -0,0 +1,53 @@
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.title {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ margin-bottom: var(--space-xl);
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+ font-size: 15px;
+ line-height: 1.7;
+ color: var(--color-slate-700);
+ margin-bottom: var(--space-2xl);
+}
+
+.stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: var(--space-md);
+ padding: var(--space-xl);
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ margin-bottom: var(--space-2xl);
+
+ @media (max-width: 640px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+.statItem {
+ text-align: center;
+}
+
+.statNum {
+ font-size: 28px;
+ font-weight: 800;
+ color: var(--color-orange-500);
+}
+
+.statLabel {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 4px;
+}
diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx
new file mode 100644
index 0000000..707b754
--- /dev/null
+++ b/apps/web/src/app/about/page.tsx
@@ -0,0 +1,53 @@
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { TrustStrip } from '@/components/TrustStrip/TrustStrip';
+import styles from './page.module.scss';
+
+export const metadata = {
+ title: 'О компании | PAN-PROM',
+};
+
+export default function AboutPage() {
+ return (
+ <>
+
+
+
О компании PAN-PROM
+
+
+
+ PAN-PROM — поставщик промышленного оборудования и комплектующих от
+ ведущих европейских производителей. Мы специализируемся на
+ гидравлике, пневматике, системах автоматизации (АСУ) и запасных
+ частях.
+
+
+ Наша компания работает напрямую с такими брендами, как Bosch Rexroth,
+ Festo, Siemens, Parker, HYDAC, Beckhoff и многими другими. Это
+ гарантирует оригинальность продукции и оптимальные сроки поставки.
+
+
+ Склады в Берлине и Гуанчжоу позволяют нам обеспечивать быструю
+ логистику и конкурентные цены для наших клиентов.
+
+
+
+
+ {[
+ { num: '50+', label: 'Европейских брендов' },
+ { num: '10 000+', label: 'Позиций в каталоге' },
+ { num: '14 дней', label: 'Средний срок поставки' },
+ { num: '100%', label: 'Оригинальная продукция' },
+ ].map((s, i) => (
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/brand/[id]/page.module.scss b/apps/web/src/app/brand/[id]/page.module.scss
new file mode 100644
index 0000000..44b4580
--- /dev/null
+++ b/apps/web/src/app/brand/[id]/page.module.scss
@@ -0,0 +1,67 @@
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.brandHeader {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin: var(--space-lg) 0;
+ padding-bottom: var(--space-lg);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.logoBox {
+ width: 64px;
+ height: 64px;
+ border-radius: var(--border-radius-lg);
+ background: var(--color-slate-100);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ font-size: 22px;
+ color: var(--color-slate-600);
+ flex-shrink: 0;
+}
+
+.brandName {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+}
+
+.brandMeta {
+ font-size: 14px;
+ color: var(--color-text-muted);
+ margin-top: 4px;
+}
+
+.sectionTitle {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--color-slate-800);
+ margin-bottom: var(--space-md);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-md);
+
+ @media (max-width: 1024px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.empty {
+ text-align: center;
+ color: var(--color-text-muted);
+ padding: var(--space-2xl) 0;
+}
diff --git a/apps/web/src/app/brand/[id]/page.tsx b/apps/web/src/app/brand/[id]/page.tsx
new file mode 100644
index 0000000..35b945c
--- /dev/null
+++ b/apps/web/src/app/brand/[id]/page.tsx
@@ -0,0 +1,71 @@
+import { notFound } from 'next/navigation';
+import { brands } from '@/data/brands';
+import { products } from '@/data/products';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { ProductCard } from '@/components/ProductCard/ProductCard';
+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 brand = brands.find((b) => b.id === Number(id));
+ return {
+ title: brand ? `${brand.name} | PAN-PROM` : 'Бренд | PAN-PROM',
+ };
+}
+
+export default async function BrandPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const brand = brands.find((b) => b.id === Number(id));
+ if (!brand) notFound();
+
+ const brandProducts = products.filter((p) => p.brand === brand.name);
+
+ return (
+
+
+
+
+
{brand.logo}
+
+
{brand.name}
+
+ {brand.country} · {brand.category}
+
+
+
+
+
+ Продукция ({brandProducts.length})
+
+
+ {brandProducts.map((p) => (
+
+ ))}
+
+
+ {brandProducts.length === 0 && (
+
+ Продукция этого бренда скоро появится в каталоге.
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/cart/layout.tsx b/apps/web/src/app/cart/layout.tsx
new file mode 100644
index 0000000..2a789b2
--- /dev/null
+++ b/apps/web/src/app/cart/layout.tsx
@@ -0,0 +1,11 @@
+export const metadata = {
+ title: 'Корзина | PAN-PROM',
+};
+
+export default function CartLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return children;
+}
diff --git a/apps/web/src/app/cart/page.module.scss b/apps/web/src/app/cart/page.module.scss
new file mode 100644
index 0000000..205e54f
--- /dev/null
+++ b/apps/web/src/app/cart/page.module.scss
@@ -0,0 +1,122 @@
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.title {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ margin-bottom: var(--space-lg);
+}
+
+.empty {
+ text-align: center;
+ padding: var(--space-2xl) 0;
+ color: var(--color-text-muted);
+
+ p {
+ margin-bottom: var(--space-md);
+ font-size: 16px;
+ }
+}
+
+.items {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ background: var(--color-border);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ overflow: hidden;
+}
+
+.item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ background: var(--color-surface);
+}
+
+.itemSku {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--color-orange-500);
+ font-weight: 600;
+}
+
+.itemName {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-slate-800);
+ margin-top: 2px;
+}
+
+.itemBrand {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 2px;
+}
+
+.itemActions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+}
+
+.qtyControl {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ padding: 4px;
+}
+
+.qtyBtn {
+ background: none;
+ border: none;
+ padding: 4px;
+ display: flex;
+ cursor: pointer;
+ color: var(--color-slate-500);
+
+ &:hover {
+ color: var(--color-slate-800);
+ }
+}
+
+.qty {
+ font-size: 14px;
+ font-weight: 600;
+ min-width: 24px;
+ text-align: center;
+}
+
+.removeBtn {
+ background: none;
+ border: none;
+ padding: 4px;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.7;
+ }
+}
+
+.footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: var(--space-lg);
+ padding-top: var(--space-lg);
+ border-top: 1px solid var(--color-border);
+}
+
+.totalLabel {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-slate-600);
+}
diff --git a/apps/web/src/app/cart/page.tsx b/apps/web/src/app/cart/page.tsx
new file mode 100644
index 0000000..0508764
--- /dev/null
+++ b/apps/web/src/app/cart/page.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import Link from 'next/link';
+import { useCart } from '@/components/CartProvider/CartProvider';
+import { Icon } from '@/components/Icon/Icon';
+import { Button } from '@/components/ui/Button/Button';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import styles from './page.module.scss';
+
+export default function CartPage() {
+ const { cart, removeFromCart, updateQuantity } = useCart();
+
+ return (
+
+
+
Корзина запроса
+
+ {cart.length === 0 ? (
+
+
Корзина пуста
+
+
Перейти в каталог
+
+
+ ) : (
+ <>
+
+ {cart.map((item) => (
+
+
+
{item.sku}
+
{item.name}
+
{item.brand}
+
+
+
+ updateQuantity(item.id, item.qty - 1)}
+ aria-label="Уменьшить количество"
+ >
+
+
+ {item.qty}
+ updateQuantity(item.id, item.qty + 1)}
+ aria-label="Увеличить количество"
+ >
+
+
+
+
removeFromCart(item.id)}
+ aria-label="Удалить из корзины"
+ >
+
+
+
+
+ ))}
+
+
+
+ Позиций: {cart.length}
+
+
+ Отправить запрос (RFQ)
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/catalog/[category]/page.tsx b/apps/web/src/app/catalog/[category]/page.tsx
new file mode 100644
index 0000000..45ba61b
--- /dev/null
+++ b/apps/web/src/app/catalog/[category]/page.tsx
@@ -0,0 +1,61 @@
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { products } from '@/data/products';
+import { categories } from '@/data/categories';
+import { categoryNameMap, CategorySlug } from '@/types';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
+import { ProductCard } from '@/components/ProductCard/ProductCard';
+import styles from '../page.module.scss';
+
+export function generateStaticParams() {
+ return categories.map((c) => ({ category: c.slug }));
+}
+
+export function generateMetadata({
+ params,
+}: {
+ params: Promise<{ category: string }>;
+}) {
+ return params.then(({ category }) => {
+ const name = categoryNameMap[category as CategorySlug];
+ return { title: name ? `${name} | PAN-PROM` : 'Каталог | PAN-PROM' };
+ });
+}
+
+export default async function CategoryPage({
+ params,
+}: {
+ params: Promise<{ category: string }>;
+}) {
+ const { category } = await params;
+ const catName = categoryNameMap[category as CategorySlug];
+ if (!catName) notFound();
+
+ const filtered = products.filter((p) => p.category === catName);
+
+ return (
+
+
+
+
+
+
+
+ {filtered.length} позиций
+
+
+ {filtered.map((p) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/catalog/page.module.scss b/apps/web/src/app/catalog/page.module.scss
new file mode 100644
index 0000000..48a27f6
--- /dev/null
+++ b/apps/web/src/app/catalog/page.module.scss
@@ -0,0 +1,37 @@
+.layout {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: var(--space-lg);
+ display: flex;
+ gap: var(--space-lg);
+}
+
+.main {
+ flex: 1;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-md);
+}
+
+.count {
+ font-size: 12px;
+ color: var(--color-slate-400);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-md);
+
+ @media (max-width: 1024px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/apps/web/src/app/catalog/page.tsx b/apps/web/src/app/catalog/page.tsx
new file mode 100644
index 0000000..5e43059
--- /dev/null
+++ b/apps/web/src/app/catalog/page.tsx
@@ -0,0 +1,36 @@
+import { Suspense } from 'react';
+import { products } from '@/data/products';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar';
+import { ProductCard } from '@/components/ProductCard/ProductCard';
+import styles from './page.module.scss';
+
+export const metadata = {
+ title: 'Каталог | PAN-PROM',
+};
+
+export default function CatalogPage() {
+ return (
+
+
+
+
+
+
+
+ {products.length} позиций
+
+
+ {products.map((p) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/contact/page.module.scss b/apps/web/src/app/contact/page.module.scss
new file mode 100644
index 0000000..3e15385
--- /dev/null
+++ b/apps/web/src/app/contact/page.module.scss
@@ -0,0 +1,60 @@
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.title {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ margin-bottom: var(--space-xl);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-lg);
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ padding: var(--space-xl);
+ text-align: center;
+}
+
+.iconBox {
+ width: 56px;
+ height: 56px;
+ border-radius: var(--border-radius-lg);
+ background: var(--color-orange-50);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto var(--space-md);
+}
+
+.cardTitle {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-slate-800);
+ margin-bottom: 8px;
+}
+
+.cardText {
+ font-size: 15px;
+ color: var(--color-slate-700);
+ font-weight: 500;
+}
+
+.cardNote {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 4px;
+}
diff --git a/apps/web/src/app/contact/page.tsx b/apps/web/src/app/contact/page.tsx
new file mode 100644
index 0000000..8e6b4f8
--- /dev/null
+++ b/apps/web/src/app/contact/page.tsx
@@ -0,0 +1,47 @@
+import { Icon } from '@/components/Icon/Icon';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import styles from './page.module.scss';
+
+export const metadata = {
+ title: 'Контакты | PAN-PROM',
+};
+
+export default function ContactPage() {
+ return (
+
+
+
Контакты
+
+
+
+
+
+
+
Телефон
+
+49 (0) 40 123-4567
+
Пн-Пт: 09:00 - 18:00 CET
+
+
+
+
+
+
+
Email
+
info@pan-prom.eu
+
Ответ в течение 24 часов
+
+
+
+
+
+
+
Офис
+
Hamburg, Germany
+
Склады: Берлин, Гуанчжоу
+
+
+
+ );
+}
diff --git a/apps/web/src/app/global.css b/apps/web/src/app/global.css
index bb41dd8..6e32479 100644
--- a/apps/web/src/app/global.css
+++ b/apps/web/src/app/global.css
@@ -1,501 +1,122 @@
-html {
- -webkit-text-size-adjust: 100%;
- font-family:
- ui-sans-serif,
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- Segoe UI,
- Roboto,
- Helvetica Neue,
- Arial,
- Noto Sans,
- sans-serif,
- Apple Color Emoji,
- Segoe UI Emoji,
- Segoe UI Symbol,
- Noto Color Emoji;
- line-height: 1.5;
- tab-size: 4;
- scroll-behavior: smooth;
-}
-body {
- font-family: inherit;
- line-height: inherit;
- margin: 0;
-}
-h1,
-h2,
-p,
-pre {
- margin: 0;
-}
+/* Reset */
*,
-::before,
-::after {
+*::before,
+*::after {
box-sizing: border-box;
- border-width: 0;
- border-style: solid;
- border-color: currentColor;
-}
-h1,
-h2 {
- font-size: inherit;
- font-weight: inherit;
-}
-a {
- color: inherit;
- text-decoration: inherit;
-}
-pre {
- font-family:
- ui-monospace,
- SFMono-Regular,
- Menlo,
- Monaco,
- Consolas,
- Liberation Mono,
- Courier New,
- monospace;
-}
-svg {
- display: block;
- vertical-align: middle;
- shape-rendering: auto;
- text-rendering: optimizeLegibility;
-}
-pre {
- background-color: rgba(55, 65, 81, 1);
- border-radius: 0.25rem;
- color: rgba(229, 231, 235, 1);
- font-family:
- ui-monospace,
- SFMono-Regular,
- Menlo,
- Monaco,
- Consolas,
- Liberation Mono,
- Courier New,
- monospace;
- overflow: scroll;
- padding: 0.5rem 0.75rem;
+ margin: 0;
+ padding: 0;
}
-.shadow {
- box-shadow:
- 0 0 #0000,
- 0 0 #0000,
- 0 10px 15px -3px rgba(0, 0, 0, 0.1),
- 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+html {
+ -webkit-text-size-adjust: 100%;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
-.rounded {
- border-radius: 1.5rem;
+
+body {
+ min-height: 100vh;
+ font-family: var(--font-sans);
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--color-text);
+ background: var(--color-bg);
}
-.wrapper {
- width: 100%;
+
+a {
+ color: inherit;
+ text-decoration: none;
}
-.container {
- margin-left: auto;
- margin-right: auto;
- max-width: 768px;
- padding-bottom: 3rem;
- padding-left: 1rem;
- padding-right: 1rem;
- color: rgba(55, 65, 81, 1);
- width: 100%;
-}
-#welcome {
- margin-top: 2.5rem;
-}
-#welcome h1 {
- font-size: 3rem;
- font-weight: 500;
- letter-spacing: -0.025em;
- line-height: 1;
-}
-#welcome span {
- display: block;
- font-size: 1.875rem;
- font-weight: 300;
- line-height: 2.25rem;
- margin-bottom: 0.5rem;
-}
-#hero {
- align-items: center;
- background-color: hsla(214, 62%, 21%, 1);
- border: none;
- box-sizing: border-box;
- color: rgba(55, 65, 81, 1);
- display: grid;
- grid-template-columns: 1fr;
- margin-top: 3.5rem;
-}
-#hero .text-container {
- color: rgba(255, 255, 255, 1);
- padding: 3rem 2rem;
-}
-#hero .text-container h2 {
- font-size: 1.5rem;
- line-height: 2rem;
- position: relative;
-}
-#hero .text-container h2 svg {
- color: hsla(162, 47%, 50%, 1);
- height: 2rem;
- left: -0.25rem;
- position: absolute;
- top: 0;
- width: 2rem;
-}
-#hero .text-container h2 span {
- margin-left: 2.5rem;
-}
-#hero .text-container a {
- background-color: rgba(255, 255, 255, 1);
- border-radius: 0.75rem;
- color: rgba(55, 65, 81, 1);
- display: inline-block;
- margin-top: 1.5rem;
- padding: 1rem 2rem;
- text-decoration: inherit;
-}
-#hero .logo-container {
- display: none;
- justify-content: center;
- padding-left: 2rem;
- padding-right: 2rem;
-}
-#hero .logo-container svg {
- color: rgba(255, 255, 255, 1);
- width: 66.666667%;
-}
-#middle-content {
- align-items: flex-start;
- display: grid;
- gap: 4rem;
- grid-template-columns: 1fr;
- margin-top: 3.5rem;
-}
-#learning-materials {
- padding: 2.5rem 2rem;
-}
-#learning-materials h2 {
- font-weight: 500;
- font-size: 1.25rem;
- letter-spacing: -0.025em;
- line-height: 1.75rem;
- padding-left: 1rem;
- padding-right: 1rem;
-}
-.list-item-link {
- align-items: center;
- border-radius: 0.75rem;
- display: flex;
- margin-top: 1rem;
- padding: 1rem;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
- width: 100%;
-}
-.list-item-link svg:first-child {
- margin-right: 1rem;
- height: 1.5rem;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
- width: 1.5rem;
-}
-.list-item-link > span {
- flex-grow: 1;
- font-weight: 400;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-.list-item-link > span > span {
- color: rgba(107, 114, 128, 1);
- display: block;
- flex-grow: 1;
- font-size: 0.75rem;
- font-weight: 300;
- line-height: 1rem;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
-}
-.list-item-link svg:last-child {
- height: 1rem;
- transition-property: all;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
- width: 1rem;
-}
-.list-item-link:hover {
- color: rgba(255, 255, 255, 1);
- background-color: hsla(162, 47%, 50%, 1);
-}
-.list-item-link:hover > span {
-}
-.list-item-link:hover > span > span {
- color: rgba(243, 244, 246, 1);
-}
-.list-item-link:hover svg:last-child {
- transform: translateX(0.25rem);
-}
-#other-links {
-}
-.button-pill {
- padding: 1.5rem 2rem;
- transition-duration: 300ms;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- align-items: center;
- display: flex;
-}
-.button-pill svg {
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
- flex-shrink: 0;
- width: 3rem;
-}
-.button-pill > span {
- letter-spacing: -0.025em;
- font-weight: 400;
- font-size: 1.125rem;
- line-height: 1.75rem;
- padding-left: 1rem;
- padding-right: 1rem;
-}
-.button-pill span span {
- display: block;
- font-size: 0.875rem;
- font-weight: 300;
- line-height: 1.25rem;
-}
-.button-pill:hover svg,
-.button-pill:hover {
- color: rgba(255, 255, 255, 1) !important;
-}
-#nx-console:hover {
- background-color: rgba(0, 122, 204, 1);
-}
-#nx-console svg {
- color: rgba(0, 122, 204, 1);
-}
-#nx-console-jetbrains {
- margin-top: 2rem;
-}
-#nx-console-jetbrains:hover {
- background-color: rgba(255, 49, 140, 1);
-}
-#nx-console-jetbrains svg {
- color: rgba(255, 49, 140, 1);
-}
-#nx-repo:hover {
- background-color: rgba(24, 23, 23, 1);
-}
-#nx-repo svg {
- color: rgba(24, 23, 23, 1);
-}
-#nx-cloud {
- margin-bottom: 2rem;
- margin-top: 2rem;
- padding: 2.5rem 2rem;
-}
-#nx-cloud > div {
- align-items: center;
- display: flex;
-}
-#nx-cloud > div svg {
- border-radius: 0.375rem;
- flex-shrink: 0;
- width: 3rem;
-}
-#nx-cloud > div h2 {
- font-size: 1.125rem;
- font-weight: 400;
- letter-spacing: -0.025em;
- line-height: 1.75rem;
- padding-left: 1rem;
- padding-right: 1rem;
-}
-#nx-cloud > div h2 span {
- display: block;
- font-size: 0.875rem;
- font-weight: 300;
- line-height: 1.25rem;
-}
-#nx-cloud p {
- font-size: 1rem;
- line-height: 1.5rem;
- margin-top: 1rem;
-}
-#nx-cloud pre {
- margin-top: 1rem;
-}
-#nx-cloud a {
- color: rgba(107, 114, 128, 1);
- display: block;
- font-size: 0.875rem;
- line-height: 1.25rem;
- margin-top: 1.5rem;
- text-align: right;
-}
-#nx-cloud a:hover {
- text-decoration: underline;
-}
-#commands {
- padding: 2.5rem 2rem;
- margin-top: 3.5rem;
-}
-#commands h2 {
- font-size: 1.25rem;
- font-weight: 400;
- letter-spacing: -0.025em;
- line-height: 1.75rem;
- padding-left: 1rem;
- padding-right: 1rem;
-}
-#commands p {
- font-size: 1rem;
- font-weight: 300;
- line-height: 1.5rem;
- margin-top: 1rem;
- padding-left: 1rem;
- padding-right: 1rem;
-}
-details {
- align-items: center;
- display: flex;
- margin-top: 1rem;
- padding-left: 1rem;
- padding-right: 1rem;
- width: 100%;
-}
-details pre > span {
- color: rgba(181, 181, 181, 1);
+
+img {
+ max-width: 100%;
display: block;
}
-summary {
- border-radius: 0.5rem;
- display: flex;
- font-weight: 400;
- padding: 0.5rem;
+
+button {
+ font-family: inherit;
cursor: pointer;
- transition-property:
- background-color,
- border-color,
- color,
- fill,
- stroke,
- opacity,
- box-shadow,
- transform,
- filter,
- backdrop-filter,
- -webkit-backdrop-filter;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 150ms;
}
-summary:hover {
- background-color: rgba(243, 244, 246, 1);
+
+input,
+textarea,
+select {
+ font-family: inherit;
+ font-size: inherit;
}
-summary svg {
- height: 1.5rem;
- margin-right: 1rem;
- width: 1.5rem;
-}
-#love {
- color: rgba(107, 114, 128, 1);
- font-size: 0.875rem;
- line-height: 1.25rem;
- margin-top: 3.5rem;
- opacity: 0.6;
- text-align: center;
-}
-#love svg {
- color: rgba(252, 165, 165, 1);
- width: 1.25rem;
- height: 1.25rem;
- display: inline;
- margin-top: -0.25rem;
-}
-@media screen and (min-width: 768px) {
- #hero {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- #hero .logo-container {
- display: flex;
- }
- #middle-content {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
+
+/* Design Tokens */
+:root {
+ /* Orange */
+ --color-orange-50: #fff3e0;
+ --color-orange-100: #ffe0b2;
+ --color-orange-200: #ffcc80;
+ --color-orange-400: #ffa726;
+ --color-orange-500: #e8600a;
+ --color-orange-600: #d4560a;
+ --color-orange-700: #bf4d09;
+ --color-orange-800: #993e07;
+ --color-orange-900: #733006;
+
+ /* Navy */
+ --color-navy-50: #e8eaf6;
+ --color-navy-100: #c5cae9;
+ --color-navy-200: #9fa8da;
+ --color-navy-300: #7986cb;
+ --color-navy-400: #5c6bc0;
+ --color-navy-500: #1a237e;
+ --color-navy-600: #151b6b;
+ --color-navy-700: #101358;
+ --color-navy-800: #0b0d45;
+ --color-navy-900: #060732;
+
+ /* Teal */
+ --color-teal-50: #e0f2f1;
+ --color-teal-100: #b2dfdb;
+ --color-teal-200: #80cbc4;
+ --color-teal-300: #4db6ac;
+ --color-teal-400: #26a69a;
+ --color-teal-500: #00695c;
+
+ /* Slate */
+ --color-slate-50: #f8fafc;
+ --color-slate-100: #f1f5f9;
+ --color-slate-200: #e2e8f0;
+ --color-slate-300: #cbd5e1;
+ --color-slate-400: #94a3b8;
+ --color-slate-500: #64748b;
+ --color-slate-600: #475569;
+ --color-slate-700: #334155;
+ --color-slate-800: #1e293b;
+ --color-slate-900: #0f172a;
+
+ /* Semantic */
+ --color-primary: var(--color-orange-500);
+ --color-primary-hover: var(--color-orange-600);
+ --color-bg: var(--color-slate-50);
+ --color-surface: #ffffff;
+ --color-text: var(--color-slate-800);
+ --color-text-muted: var(--color-slate-500);
+ --color-border: var(--color-slate-200);
+
+ /* Spacing */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+
+ /* Typography */
+ --font-sans: -apple-system, 'Segoe UI', Arial, sans-serif;
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
+
+ /* Layout */
+ --max-width: 1280px;
+ --header-height: 120px;
+
+ /* Border radius */
+ --border-radius-sm: 6px;
+ --border-radius-md: 10px;
+ --border-radius-lg: 12px;
+ --border-radius-xl: 16px;
}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 8580125..e89202a 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,8 +1,12 @@
import './global.css';
+import { Header } from '@/components/Header/Header';
+import { Footer } from '@/components/Footer/Footer';
+import { CartProvider } from '@/components/CartProvider/CartProvider';
export const metadata = {
- title: 'Welcome to web',
- description: 'Generated by create-nx-workspace',
+ title: 'PAN-PROM | Промышленное оборудование из Европы',
+ description:
+ 'Гидравлика, пневматика, АСУ, запасные части. Прямые поставки оригинальных комплектующих от ведущих европейских производителей.',
};
export default function RootLayout({
@@ -11,8 +15,14 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
- {children}
+
+
+
+
+ {children}
+
+
+
);
}
diff --git a/apps/web/src/app/page.module.scss b/apps/web/src/app/page.module.scss
index 8a13e21..b2444b2 100644
--- a/apps/web/src/app/page.module.scss
+++ b/apps/web/src/app/page.module.scss
@@ -1,2 +1 @@
-.page {
-}
+/* Home page styles — currently handled by section components */
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index f9074f8..c6cd258 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,469 +1,15 @@
-import styles from './page.module.scss';
+import { Hero } from '@/components/Hero/Hero';
+import { Directions } from '@/components/Directions/Directions';
+import { TrustStrip } from '@/components/TrustStrip/TrustStrip';
+import { BrandLogos } from '@/components/BrandLogos/BrandLogos';
-export default function Index() {
- /*
- * Replace the elements below with your own.
- *
- * Note: The corresponding styles are in the ./index.scss file.
- */
+export default function HomePage() {
return (
-
-
-
-
-
- Hello there,
- Welcome @my-monorepo/web 👋
-
-
-
-
-
-
-
-
-
Next steps
-
Here are some things you can do with Nx:
-
-
-
-
-
- Add UI library
-
-
- # Generate UI lib
- nx g @nx/next:library ui
- # Add a component
- nx g @nx/next:component ui/src/lib/button
-
-
-
-
-
-
-
- View project details
-
- nx show project @my-monorepo/web --web
-
-
-
-
-
-
- View interactive project graph
-
- nx graph
-
-
-
-
-
-
- Run affected commands
-
-
- # see what's been affected by changes
- nx affected:graph
- # run tests for current changes
- nx affected:test
- # run e2e tests for current changes
- nx affected:e2e
-
-
-
-
-
- Carefully crafted with
-
-
-
-
-
-
-
+ <>
+
+
+
+
+ >
);
}
diff --git a/apps/web/src/app/product/[sku]/page.module.scss b/apps/web/src/app/product/[sku]/page.module.scss
new file mode 100644
index 0000000..e0f06b9
--- /dev/null
+++ b/apps/web/src/app/product/[sku]/page.module.scss
@@ -0,0 +1,105 @@
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-xl);
+ margin-top: var(--space-lg);
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.imageArea {
+ background: var(--color-slate-50);
+ border-radius: var(--border-radius-lg);
+ border: 1px solid var(--color-border);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.imagePlaceholder {
+ width: 120px;
+ height: 120px;
+ background: var(--color-slate-200);
+ border-radius: var(--border-radius-md);
+}
+
+.details {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.brand {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.name {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ line-height: 1.2;
+}
+
+.sku {
+ font-size: 14px;
+ color: var(--color-slate-500);
+
+ span {
+ font-family: var(--font-mono);
+ color: var(--color-orange-500);
+ font-weight: 600;
+ }
+}
+
+.meta {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: var(--space-md) 0;
+ border-top: 1px solid var(--color-border);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.metaItem {
+ display: flex;
+ justify-content: space-between;
+ font-size: 13px;
+}
+
+.metaLabel {
+ color: var(--color-slate-400);
+}
+
+.inStock {
+ color: var(--color-teal-500);
+ font-weight: 600;
+}
+
+.outOfStock {
+ color: var(--color-slate-400);
+}
+
+.price {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--color-navy-500);
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
diff --git a/apps/web/src/app/product/[sku]/page.tsx b/apps/web/src/app/product/[sku]/page.tsx
new file mode 100644
index 0000000..bd5019b
--- /dev/null
+++ b/apps/web/src/app/product/[sku]/page.tsx
@@ -0,0 +1,45 @@
+import { notFound } from 'next/navigation';
+import { products } from '@/data/products';
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { ProductDetail } from '@/components/ProductDetail/ProductDetail';
+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 product = products.find((p) => p.sku === decodeURIComponent(sku));
+ return {
+ title: product ? `${product.name} | PAN-PROM` : 'Продукт | PAN-PROM',
+ };
+}
+
+export default async function ProductPage({
+ params,
+}: {
+ params: Promise<{ sku: string }>;
+}) {
+ const { sku } = await params;
+ const product = products.find((p) => p.sku === decodeURIComponent(sku));
+
+ if (!product) notFound();
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/rfq/page.module.scss b/apps/web/src/app/rfq/page.module.scss
new file mode 100644
index 0000000..ff46caf
--- /dev/null
+++ b/apps/web/src/app/rfq/page.module.scss
@@ -0,0 +1,90 @@
+.container {
+ max-width: 720px;
+ margin: 0 auto;
+ padding: var(--space-lg);
+}
+
+.title {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ margin-bottom: 8px;
+}
+
+.subtitle {
+ color: var(--color-text-muted);
+ font-size: 15px;
+ margin-bottom: var(--space-xl);
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-md);
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-slate-700);
+}
+
+.input {
+ padding: 10px 14px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+
+ &:focus {
+ border-color: var(--color-orange-500);
+ }
+}
+
+.textarea {
+ padding: 10px 14px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ outline: none;
+ resize: vertical;
+ transition: border-color 0.2s;
+
+ &:focus {
+ border-color: var(--color-orange-500);
+ }
+}
+
+.submit {
+ background: var(--color-primary);
+ color: #fff;
+ border: none;
+ padding: 14px 32px;
+ border-radius: var(--border-radius-md);
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ align-self: flex-start;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background: var(--color-primary-hover);
+ }
+}
diff --git a/apps/web/src/app/rfq/page.tsx b/apps/web/src/app/rfq/page.tsx
new file mode 100644
index 0000000..dc26e9c
--- /dev/null
+++ b/apps/web/src/app/rfq/page.tsx
@@ -0,0 +1,26 @@
+import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb';
+import { RFQForm } from '@/components/RFQForm/RFQForm';
+import styles from './page.module.scss';
+
+export const metadata = {
+ title: 'Запрос цены (RFQ) | PAN-PROM',
+};
+
+export default function RFQPage() {
+ return (
+
+
+
Запрос коммерческого предложения
+
+ Заполните форму, и мы подготовим коммерческое предложение в течение 24
+ часов.
+
+
+
+ );
+}
diff --git a/apps/web/src/components/BrandLogos/BrandLogos.module.scss b/apps/web/src/components/BrandLogos/BrandLogos.module.scss
new file mode 100644
index 0000000..e1d9a73
--- /dev/null
+++ b/apps/web/src/components/BrandLogos/BrandLogos.module.scss
@@ -0,0 +1,90 @@
+.section {
+ padding: var(--space-2xl) var(--space-lg);
+ max-width: var(--max-width);
+ margin: 0 auto;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+}
+
+.title {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--color-navy-500);
+ margin: 0;
+}
+
+.allLink {
+ background: none;
+ border: none;
+ color: var(--color-orange-500);
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
+ gap: 12px;
+
+ @media (max-width: 1024px) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ @media (max-width: 640px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+.card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-md);
+ padding: 16px 12px;
+ text-align: center;
+ cursor: pointer;
+ transition: box-shadow 0.2s;
+ display: block;
+
+ &:hover {
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+ }
+}
+
+.logoBox {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--border-radius-md);
+ background: var(--color-slate-100);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 8px;
+ font-weight: 800;
+ font-size: 15px;
+ color: var(--color-slate-600);
+}
+
+.name {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-slate-700);
+}
+
+.meta {
+ font-size: 10px;
+ color: var(--color-slate-400);
+ margin-top: 2px;
+}
diff --git a/apps/web/src/components/BrandLogos/BrandLogos.tsx b/apps/web/src/components/BrandLogos/BrandLogos.tsx
new file mode 100644
index 0000000..89a1899
--- /dev/null
+++ b/apps/web/src/components/BrandLogos/BrandLogos.tsx
@@ -0,0 +1,33 @@
+import Link from 'next/link';
+import { Icon } from '@/components/Icon/Icon';
+import { brands } from '@/data/brands';
+import styles from './BrandLogos.module.scss';
+
+export function BrandLogos() {
+ return (
+
+
+
Производители
+
+ Все бренды
+
+
+
+
+ {brands.map((b) => (
+
+
{b.logo}
+
{b.name}
+
+ {b.country} · {b.category}
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/Breadcrumb/Breadcrumb.module.scss b/apps/web/src/components/Breadcrumb/Breadcrumb.module.scss
new file mode 100644
index 0000000..2a4cca3
--- /dev/null
+++ b/apps/web/src/components/Breadcrumb/Breadcrumb.module.scss
@@ -0,0 +1,21 @@
+.breadcrumb {
+ font-size: 12px;
+ color: var(--color-slate-400);
+ margin-bottom: var(--space-md);
+}
+
+.separator {
+ margin: 0 6px;
+}
+
+.link {
+ color: var(--color-slate-400);
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+.current {
+ color: var(--color-slate-600);
+}
diff --git a/apps/web/src/components/Breadcrumb/Breadcrumb.tsx b/apps/web/src/components/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 0000000..3d66530
--- /dev/null
+++ b/apps/web/src/components/Breadcrumb/Breadcrumb.tsx
@@ -0,0 +1,26 @@
+import Link from 'next/link';
+import styles from './Breadcrumb.module.scss';
+
+interface BreadcrumbItem {
+ label: string;
+ href?: string;
+}
+
+export function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {
+ return (
+
+ {items.map((item, i) => (
+
+ {i > 0 && / }
+ {item.href ? (
+
+ {item.label}
+
+ ) : (
+ {item.label}
+ )}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/components/CartProvider/CartProvider.tsx b/apps/web/src/components/CartProvider/CartProvider.tsx
new file mode 100644
index 0000000..cc9166f
--- /dev/null
+++ b/apps/web/src/components/CartProvider/CartProvider.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ ReactNode,
+} from 'react';
+import { Product } from '@/types';
+
+interface CartItem extends Product {
+ qty: number;
+}
+
+interface CartContextValue {
+ cart: CartItem[];
+ addToCart: (product: Product) => void;
+ removeFromCart: (id: number) => void;
+ updateQuantity: (id: number, qty: number) => void;
+}
+
+const CartContext = createContext(null);
+
+export function CartProvider({ children }: { children: ReactNode }) {
+ const [cart, setCart] = useState([]);
+
+ const addToCart = useCallback((product: Product) => {
+ setCart((prev) => {
+ const exists = prev.find((i) => i.id === product.id);
+ if (exists) return prev;
+ return [...prev, { ...product, qty: 1 }];
+ });
+ }, []);
+
+ const removeFromCart = useCallback((id: number) => {
+ setCart((prev) => prev.filter((i) => i.id !== id));
+ }, []);
+
+ const updateQuantity = useCallback((id: number, qty: number) => {
+ if (qty < 1) return;
+ setCart((prev) =>
+ prev.map((i) => (i.id === id ? { ...i, qty } : i))
+ );
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useCart() {
+ const ctx = useContext(CartContext);
+ if (!ctx) throw new Error('useCart must be used within CartProvider');
+ return ctx;
+}
diff --git a/apps/web/src/components/CatalogSidebar/CatalogSidebar.module.scss b/apps/web/src/components/CatalogSidebar/CatalogSidebar.module.scss
new file mode 100644
index 0000000..008148a
--- /dev/null
+++ b/apps/web/src/components/CatalogSidebar/CatalogSidebar.module.scss
@@ -0,0 +1,68 @@
+.sidebar {
+ width: 240px;
+ flex-shrink: 0;
+}
+
+.inner {
+ background: var(--color-surface);
+ border-radius: var(--border-radius-lg);
+ border: 1px solid var(--color-border);
+ padding: 20px;
+ position: sticky;
+ top: 140px;
+}
+
+.sectionTitle {
+ font-weight: 700;
+ font-size: 14px;
+ color: var(--color-slate-800);
+ margin-bottom: var(--space-md);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.filterGroup {
+ margin-bottom: 20px;
+}
+
+.filterLabel {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ margin-bottom: 8px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.filterItem {
+ padding: 8px 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--color-slate-600);
+ margin-bottom: 2px;
+ transition: all 0.15s;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ border: none;
+ background: none;
+ text-align: left;
+
+ &:hover {
+ background: var(--color-slate-50);
+ }
+}
+
+.filterItemActive {
+ font-weight: 600;
+ background: var(--color-orange-50);
+ color: var(--color-orange-600);
+}
+
+.brandCountry {
+ font-size: 11px;
+ color: var(--color-slate-400);
+}
diff --git a/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx
new file mode 100644
index 0000000..a2e71ab
--- /dev/null
+++ b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import { Icon } from '@/components/Icon/Icon';
+import { categories } from '@/data/categories';
+import { brands } from '@/data/brands';
+import styles from './CatalogSidebar.module.scss';
+
+interface CatalogSidebarProps {
+ activeCategory?: string;
+}
+
+export function CatalogSidebar({ activeCategory }: CatalogSidebarProps) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const activeBrand = searchParams.get('brand');
+
+ const handleCategoryClick = (slug: string | null) => {
+ if (slug) {
+ router.push(`/catalog/${slug}`);
+ } else {
+ router.push('/catalog');
+ }
+ };
+
+ const handleBrandClick = (brandId: number) => {
+ router.push(`/brand/${brandId}`);
+ };
+
+ const filteredBrands = activeCategory
+ ? brands.filter((b) => {
+ const cat = categories.find((c) => c.slug === activeCategory);
+ return cat ? b.category === cat.name : true;
+ })
+ : brands;
+
+ return (
+
+
+
+
+ Фильтры
+
+
+
+
Направление
+
handleCategoryClick(null)}
+ >
+ Все
+
+ {categories.map((c) => (
+
handleCategoryClick(c.slug)}
+ >
+ {c.name}
+
+ ))}
+
+
+
+
Бренд
+ {filteredBrands.slice(0, 6).map((b) => (
+
handleBrandClick(b.id)}
+ >
+ {b.name}
+ {b.country}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Directions/Directions.module.scss b/apps/web/src/components/Directions/Directions.module.scss
new file mode 100644
index 0000000..c8f6e8c
--- /dev/null
+++ b/apps/web/src/components/Directions/Directions.module.scss
@@ -0,0 +1,76 @@
+.section {
+ padding: var(--space-2xl) var(--space-lg);
+ max-width: var(--max-width);
+ margin: 0 auto;
+}
+
+.title {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--color-navy-500);
+ margin: 0 0 8px;
+}
+
+.subtitle {
+ color: var(--color-text-muted);
+ font-size: 14px;
+ margin: 0 0 24px;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 16px;
+
+ @media (max-width: 1024px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.card {
+ background: var(--color-surface);
+ border-radius: var(--border-radius-lg);
+ padding: 24px;
+ cursor: pointer;
+ border: 1px solid var(--color-border);
+ transition: box-shadow 0.2s, transform 0.2s;
+ position: relative;
+ overflow: hidden;
+ display: block;
+
+ &:hover {
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
+ }
+}
+
+.cardAccent {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+}
+
+.cardTitle {
+ font-size: 17px;
+ font-weight: 700;
+ color: var(--color-slate-800);
+ margin: 0 0 8px;
+}
+
+.cardDesc {
+ font-size: 13px;
+ color: var(--color-text-muted);
+ line-height: 1.5;
+ margin: 0 0 12px;
+}
+
+.cardBrands {
+ font-size: 11px;
+ color: var(--color-slate-400);
+}
diff --git a/apps/web/src/components/Directions/Directions.tsx b/apps/web/src/components/Directions/Directions.tsx
new file mode 100644
index 0000000..6147744
--- /dev/null
+++ b/apps/web/src/components/Directions/Directions.tsx
@@ -0,0 +1,31 @@
+import Link from 'next/link';
+import { categories } from '@/data/categories';
+import styles from './Directions.module.scss';
+
+export function Directions() {
+ return (
+
+ Направления
+
+ Четыре ключевых направления поставок — выберите ваше
+
+
+ {categories.map((d) => (
+
+
+
{d.name}
+
{d.description}
+
{d.brands}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/Footer/Footer.module.scss b/apps/web/src/components/Footer/Footer.module.scss
new file mode 100644
index 0000000..e1a281b
--- /dev/null
+++ b/apps/web/src/components/Footer/Footer.module.scss
@@ -0,0 +1,92 @@
+.footer {
+ background: var(--color-slate-900);
+ color: var(--color-slate-300);
+ padding: var(--space-2xl) var(--space-lg) 0;
+ margin-top: auto;
+}
+
+.inner {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ gap: var(--space-2xl);
+ flex-wrap: wrap;
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.logoIcon {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ background: linear-gradient(135deg, var(--color-orange-500), var(--color-orange-700));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-weight: 900;
+ font-size: 18px;
+ letter-spacing: -1px;
+ flex-shrink: 0;
+}
+
+.logoTitle {
+ font-weight: 700;
+ font-size: 17px;
+ color: #fff;
+}
+
+.logoSubtitle {
+ font-size: 11px;
+ color: var(--color-slate-400);
+}
+
+.columns {
+ display: flex;
+ gap: var(--space-2xl);
+ flex-wrap: wrap;
+}
+
+.column {
+ min-width: 160px;
+}
+
+.columnTitle {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-slate-400);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: var(--space-md);
+}
+
+.columnList {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.columnLink {
+ font-size: 13px;
+ color: var(--color-slate-300);
+ transition: color 0.2s;
+
+ &:hover {
+ color: var(--color-orange-400);
+ }
+}
+
+.bottom {
+ max-width: var(--max-width);
+ margin: var(--space-xl) auto 0;
+ padding: var(--space-lg) 0;
+ border-top: 1px solid var(--color-slate-700);
+ font-size: 12px;
+ color: var(--color-slate-500);
+}
diff --git a/apps/web/src/components/Footer/Footer.tsx b/apps/web/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000..a620f93
--- /dev/null
+++ b/apps/web/src/components/Footer/Footer.tsx
@@ -0,0 +1,42 @@
+import Link from 'next/link';
+import { footerColumns } from '@/data/navigation';
+import styles from './Footer.module.scss';
+
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/Header/Header.module.scss b/apps/web/src/components/Header/Header.module.scss
new file mode 100644
index 0000000..b88bbfe
--- /dev/null
+++ b/apps/web/src/components/Header/Header.module.scss
@@ -0,0 +1,292 @@
+@use '../../styles/variables' as *;
+
+.header {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+}
+
+/* Top bar */
+.topBar {
+ background: var(--color-navy-500);
+ color: #fff;
+ font-size: 12px;
+ padding: 6px var(--space-lg);
+}
+
+.topBarInner {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.topBarContacts {
+ display: flex;
+ gap: var(--space-md);
+ align-items: center;
+}
+
+.topBarItem {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.langSwitcher {
+ display: flex;
+ gap: 12px;
+}
+
+.langActive {
+ opacity: 0.7;
+ cursor: pointer;
+}
+
+.langInactive {
+ opacity: 0.5;
+ cursor: pointer;
+}
+
+.langDivider {
+ opacity: 0.4;
+}
+
+/* Main nav */
+.mainNav {
+ padding: 12px var(--space-lg);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: var(--max-width);
+ margin: 0 auto;
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+}
+
+.logoIcon {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ background: linear-gradient(135deg, var(--color-orange-500), var(--color-orange-700));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-weight: 900;
+ font-size: 18px;
+ letter-spacing: -1px;
+}
+
+.logoTitle {
+ font-weight: 700;
+ font-size: 17px;
+ color: var(--color-navy-500);
+ line-height: 1.1;
+ letter-spacing: 0.5px;
+}
+
+.logoSubtitle {
+ font-size: 10px;
+ color: var(--color-slate-400);
+ letter-spacing: 1.5px;
+ text-transform: uppercase;
+}
+
+/* Search */
+.searchWrapper {
+ flex: 1;
+ max-width: 520px;
+ margin: 0 var(--space-xl);
+ position: relative;
+
+ @media (max-width: $breakpoint-md) {
+ display: none;
+ }
+}
+
+.searchBar {
+ display: flex;
+ align-items: center;
+ border: 2px solid var(--color-border);
+ border-radius: var(--border-radius-md);
+ overflow: hidden;
+ transition: border 0.2s;
+ background: var(--color-slate-50);
+}
+
+.searchFocused .searchBar {
+ border-color: var(--color-orange-500);
+}
+
+.searchInput {
+ flex: 1;
+ border: none;
+ outline: none;
+ padding: 10px 16px;
+ font-size: 14px;
+ background: transparent;
+ color: var(--color-slate-800);
+}
+
+.searchButton {
+ background: var(--color-orange-500);
+ border: none;
+ padding: 10px 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+}
+
+.suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ margin-top: 4px;
+ background: var(--color-surface);
+ border-radius: var(--border-radius-md);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
+ border: 1px solid var(--color-border);
+ overflow: hidden;
+ z-index: 200;
+}
+
+.suggestionItem {
+ padding: 10px 16px;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--color-slate-100);
+ font-size: 13px;
+
+ &:hover {
+ background: var(--color-slate-50);
+ }
+}
+
+.suggestionSku {
+ color: var(--color-orange-500);
+ font-weight: 600;
+ font-family: var(--font-mono);
+}
+
+.suggestionName {
+ color: var(--color-text-muted);
+ margin-left: var(--space-sm);
+}
+
+.suggestionBrand {
+ font-size: 11px;
+ color: var(--color-slate-400);
+}
+
+/* Actions */
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.cartLink {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.cartBadge {
+ position: absolute;
+ top: -6px;
+ right: -8px;
+ background: var(--color-orange-500);
+ color: #fff;
+ font-size: 10px;
+ font-weight: 700;
+ width: 18px;
+ height: 18px;
+ border-radius: 9px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.rfqButton {
+ background: var(--color-orange-500);
+ color: #fff;
+ border: none;
+ padding: 8px 20px;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ white-space: nowrap;
+ letter-spacing: 0.3px;
+
+ @media (max-width: $breakpoint-md) {
+ display: none;
+ }
+}
+
+.menuToggle {
+ display: none;
+ background: none;
+ border: none;
+ padding: 4px;
+
+ @media (max-width: $breakpoint-md) {
+ display: flex;
+ }
+}
+
+/* Nav tabs */
+.navTabs {
+ border-top: 1px solid var(--color-slate-100);
+ display: flex;
+ align-items: center;
+ gap: 0;
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 0 var(--space-lg);
+
+ @media (max-width: $breakpoint-md) {
+ display: none;
+ flex-direction: column;
+ align-items: stretch;
+
+ &.navTabsOpen {
+ display: flex;
+ }
+ }
+}
+
+.navTab {
+ background: none;
+ border: none;
+ padding: 12px 20px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-slate-600);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+.navTabActive {
+ border-bottom-color: var(--color-orange-500);
+ color: var(--color-orange-500);
+}
diff --git a/apps/web/src/components/Header/Header.tsx b/apps/web/src/components/Header/Header.tsx
new file mode 100644
index 0000000..bf73f98
--- /dev/null
+++ b/apps/web/src/components/Header/Header.tsx
@@ -0,0 +1,147 @@
+'use client';
+
+import { useState, useRef } 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 styles from './Header.module.scss';
+
+export function Header() {
+ const pathname = usePathname();
+ const { cart } = useCart();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchFocused, setSearchFocused] = useState(false);
+ const [mobileMenu, setMobileMenu] = useState(false);
+ const searchRef = useRef(null);
+
+ const suggestions =
+ searchQuery.length > 1
+ ? products
+ .filter(
+ (p) =>
+ p.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ p.name.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ .slice(0, 5)
+ : [];
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/Hero/Hero.module.scss b/apps/web/src/components/Hero/Hero.module.scss
new file mode 100644
index 0000000..dc9da96
--- /dev/null
+++ b/apps/web/src/components/Hero/Hero.module.scss
@@ -0,0 +1,129 @@
+.hero {
+ background: linear-gradient(135deg, var(--color-navy-500) 0%, var(--color-navy-700) 50%, var(--color-navy-900) 100%);
+ padding: 64px var(--space-lg);
+ color: #fff;
+ position: relative;
+ overflow: hidden;
+}
+
+.pattern {
+ position: absolute;
+ inset: 0;
+ opacity: 0.05;
+}
+
+.circle {
+ position: absolute;
+ border: 1px solid var(--color-orange-400);
+ border-radius: 50%;
+}
+
+.inner {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ position: relative;
+ z-index: 1;
+}
+
+.content {
+ max-width: 640px;
+}
+
+.badge {
+ display: inline-block;
+ background: rgba(232, 96, 10, 0.13);
+ color: var(--color-orange-400);
+ padding: 4px 12px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ border: 1px solid rgba(232, 96, 10, 0.2);
+ letter-spacing: 1px;
+ text-transform: uppercase;
+}
+
+.title {
+ font-size: 40px;
+ font-weight: 800;
+ line-height: 1.15;
+ margin: 0 0 16px;
+}
+
+.titleAccent {
+ color: var(--color-orange-400);
+}
+
+.description {
+ font-size: 17px;
+ color: var(--color-slate-300);
+ line-height: 1.6;
+ margin: 0 0 32px;
+}
+
+.cta {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.ctaPrimary {
+ background: var(--color-orange-500);
+ color: #fff;
+ border: none;
+ padding: 14px 32px;
+ border-radius: var(--border-radius-md);
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+
+ &:hover {
+ background: var(--color-orange-600);
+ }
+}
+
+.ctaSecondary {
+ background: transparent;
+ color: #fff;
+ border: 1px solid var(--color-slate-400);
+ padding: 14px 32px;
+ border-radius: var(--border-radius-md);
+ font-weight: 600;
+ font-size: 15px;
+ cursor: pointer;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.05);
+ }
+}
+
+.stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 16px;
+ margin-top: 48px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: var(--border-radius-lg);
+ padding: 20px;
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.statItem {
+ text-align: center;
+}
+
+.statNum {
+ font-size: 28px;
+ font-weight: 800;
+ color: var(--color-orange-400);
+}
+
+.statLabel {
+ font-size: 12px;
+ color: var(--color-slate-300);
+ margin-top: 4px;
+}
diff --git a/apps/web/src/components/Hero/Hero.tsx b/apps/web/src/components/Hero/Hero.tsx
new file mode 100644
index 0000000..9e2a7e0
--- /dev/null
+++ b/apps/web/src/components/Hero/Hero.tsx
@@ -0,0 +1,62 @@
+import Link from 'next/link';
+import { Icon } from '@/components/Icon/Icon';
+import styles from './Hero.module.scss';
+
+export function Hero() {
+ return (
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
German Engineering. European Standards.
+
+ Промышленное оборудование
+ из Европы
+
+
+ Гидравлика, пневматика, АСУ, запасные части. Прямые поставки
+ оригинальных комплектующих от ведущих европейских производителей.
+ Склады в Берлине и Гуанчжоу.
+
+
+
+ Каталог оборудования
+
+
+
+ Отправить запрос (RFQ)
+
+
+
+
+
+ {[
+ { num: '50+', label: 'Европейских брендов' },
+ { num: '10 000+', label: 'Позиций в каталоге' },
+ { num: '14', label: 'Дней средняя поставка' },
+ { num: '100%', label: 'Оригинальная продукция' },
+ ].map((s, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Icon/Icon.tsx b/apps/web/src/components/Icon/Icon.tsx
new file mode 100644
index 0000000..2aae63d
--- /dev/null
+++ b/apps/web/src/components/Icon/Icon.tsx
@@ -0,0 +1,31 @@
+import { iconPaths, IconName } from './icons';
+
+interface IconProps {
+ name: IconName;
+ size?: number;
+ color?: string;
+ className?: string;
+}
+
+export function Icon({
+ name,
+ size = 20,
+ color = 'currentColor',
+ className,
+}: IconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Icon/icons.ts b/apps/web/src/components/Icon/icons.ts
new file mode 100644
index 0000000..aed8fce
--- /dev/null
+++ b/apps/web/src/components/Icon/icons.ts
@@ -0,0 +1,24 @@
+export const iconPaths = {
+ search: 'M21 21l-4.35-4.35M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
+ cart: 'M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4zM3 6h18M16 10a4 4 0 0 1-8 0',
+ menu: 'M3 12h18M3 6h18M3 18h18',
+ chevronRight: 'M9 18l6-6-6-6',
+ phone:
+ 'M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z',
+ mail: 'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zM22 6l-10 7L2 6',
+ map: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
+ filter: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z',
+ star: 'M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z',
+ truck:
+ 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8zM5.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM18.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
+ shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
+ clock:
+ 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 6v6l4 2',
+ x: 'M18 6L6 18M6 6l12 12',
+ arrowLeft: 'M19 12H5M12 19l-7-7 7-7',
+ check: 'M20 6L9 17l-5-5',
+ plus: 'M12 5v14M5 12h14',
+ minus: 'M5 12h14',
+} as const;
+
+export type IconName = keyof typeof iconPaths;
diff --git a/apps/web/src/components/ProductCard/ProductCard.module.scss b/apps/web/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000..783973c
--- /dev/null
+++ b/apps/web/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,73 @@
+.card {
+ background: var(--color-surface);
+ border-radius: var(--border-radius-lg);
+ border: 1px solid var(--color-border);
+ overflow: hidden;
+ cursor: pointer;
+ transition: box-shadow 0.2s;
+ display: block;
+
+ &:hover {
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+ }
+}
+
+.imageArea {
+ background: var(--color-slate-50);
+ height: 140px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40px;
+ position: relative;
+}
+
+.stockBadge {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: var(--color-teal-50);
+ color: var(--color-teal-500);
+ font-size: 10px;
+ font-weight: 600;
+ padding: 3px 8px;
+ border-radius: 4px;
+}
+
+.info {
+ padding: var(--space-md);
+}
+
+.sku {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--color-orange-500);
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-slate-800);
+ margin-bottom: 8px;
+ line-height: 1.3;
+}
+
+.meta {
+ display: flex;
+ gap: 8px;
+ font-size: 12px;
+ color: var(--color-slate-400);
+ margin-bottom: 8px;
+}
+
+.brand {
+ color: var(--color-slate-600);
+}
+
+.price {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-navy-500);
+}
diff --git a/apps/web/src/components/ProductCard/ProductCard.tsx b/apps/web/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000..38aa7c3
--- /dev/null
+++ b/apps/web/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,25 @@
+import Link from 'next/link';
+import { Product } from '@/types';
+import styles from './ProductCard.module.scss';
+
+export function ProductCard({ product }: { product: Product }) {
+ return (
+
+
+ {product.inStock && В наличии }
+
+
+
{product.sku}
+
{product.name}
+
+ {product.brand}
+ {product.subcategory}
+
+
{product.price}
+
+
+ );
+}
diff --git a/apps/web/src/components/ProductDetail/ProductDetail.module.scss b/apps/web/src/components/ProductDetail/ProductDetail.module.scss
new file mode 100644
index 0000000..caf31f9
--- /dev/null
+++ b/apps/web/src/components/ProductDetail/ProductDetail.module.scss
@@ -0,0 +1,99 @@
+.layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-xl);
+ margin-top: var(--space-lg);
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.imageArea {
+ background: var(--color-slate-50);
+ border-radius: var(--border-radius-lg);
+ border: 1px solid var(--color-border);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.imagePlaceholder {
+ width: 120px;
+ height: 120px;
+ background: var(--color-slate-200);
+ border-radius: var(--border-radius-md);
+}
+
+.details {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.brand {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.name {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-slate-900);
+ line-height: 1.2;
+}
+
+.sku {
+ font-size: 14px;
+ color: var(--color-slate-500);
+
+ span {
+ font-family: var(--font-mono);
+ color: var(--color-orange-500);
+ font-weight: 600;
+ }
+}
+
+.meta {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: var(--space-md) 0;
+ border-top: 1px solid var(--color-border);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.metaItem {
+ display: flex;
+ justify-content: space-between;
+ font-size: 13px;
+}
+
+.metaLabel {
+ color: var(--color-slate-400);
+}
+
+.inStock {
+ color: var(--color-teal-500);
+ font-weight: 600;
+}
+
+.outOfStock {
+ color: var(--color-slate-400);
+}
+
+.price {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--color-navy-500);
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
diff --git a/apps/web/src/components/ProductDetail/ProductDetail.tsx b/apps/web/src/components/ProductDetail/ProductDetail.tsx
new file mode 100644
index 0000000..3c2b807
--- /dev/null
+++ b/apps/web/src/components/ProductDetail/ProductDetail.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import Link from 'next/link';
+import { Product } from '@/types';
+import { useCart } from '@/components/CartProvider/CartProvider';
+import { Icon } from '@/components/Icon/Icon';
+import { Button } from '@/components/ui/Button/Button';
+import styles from './ProductDetail.module.scss';
+
+export function ProductDetail({ product }: { product: Product }) {
+ const { addToCart } = useCart();
+
+ return (
+
+
+
+
+
{product.brand}
+
{product.name}
+
+ Артикул: {product.sku}
+
+
+
+
+ Категория
+ {product.category}
+
+
+ Подкатегория
+ {product.subcategory}
+
+
+ Наличие
+
+ {product.inStock ? 'В наличии' : 'Под заказ'}
+
+
+
+
+
{product.price}
+
+
+ addToCart(product)}>
+
+ В корзину
+
+
+ Запросить цену (RFQ)
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/RFQForm/RFQForm.module.scss b/apps/web/src/components/RFQForm/RFQForm.module.scss
new file mode 100644
index 0000000..1814e40
--- /dev/null
+++ b/apps/web/src/components/RFQForm/RFQForm.module.scss
@@ -0,0 +1,71 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-md);
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-slate-700);
+}
+
+.input {
+ padding: 10px 14px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+
+ &:focus {
+ border-color: var(--color-orange-500);
+ }
+}
+
+.textarea {
+ padding: 10px 14px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ outline: none;
+ resize: vertical;
+ transition: border-color 0.2s;
+
+ &:focus {
+ border-color: var(--color-orange-500);
+ }
+}
+
+.submit {
+ background: var(--color-primary);
+ color: #fff;
+ border: none;
+ padding: 14px 32px;
+ border-radius: var(--border-radius-md);
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ align-self: flex-start;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background: var(--color-primary-hover);
+ }
+}
diff --git a/apps/web/src/components/RFQForm/RFQForm.tsx b/apps/web/src/components/RFQForm/RFQForm.tsx
new file mode 100644
index 0000000..5bb09a8
--- /dev/null
+++ b/apps/web/src/components/RFQForm/RFQForm.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { FormEvent } from 'react';
+import styles from './RFQForm.module.scss';
+
+export function RFQForm() {
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ // TODO: integrate with API
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/TrustStrip/TrustStrip.module.scss b/apps/web/src/components/TrustStrip/TrustStrip.module.scss
new file mode 100644
index 0000000..85ecf9b
--- /dev/null
+++ b/apps/web/src/components/TrustStrip/TrustStrip.module.scss
@@ -0,0 +1,49 @@
+.section {
+ background: var(--color-slate-50);
+ padding: 40px var(--space-lg);
+}
+
+.inner {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 24px;
+
+ @media (max-width: 1024px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (max-width: 640px) {
+ grid-template-columns: 1fr;
+ }
+}
+
+.item {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+}
+
+.iconBox {
+ width: 44px;
+ height: 44px;
+ border-radius: var(--border-radius-md);
+ background: var(--color-orange-50);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.title {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--color-slate-800);
+}
+
+.desc {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 2px;
+}
diff --git a/apps/web/src/components/TrustStrip/TrustStrip.tsx b/apps/web/src/components/TrustStrip/TrustStrip.tsx
new file mode 100644
index 0000000..09abd7b
--- /dev/null
+++ b/apps/web/src/components/TrustStrip/TrustStrip.tsx
@@ -0,0 +1,30 @@
+import { Icon } from '@/components/Icon/Icon';
+import { IconName } from '@/components/Icon/icons';
+import styles from './TrustStrip.module.scss';
+
+const items: { icon: IconName; title: string; desc: string }[] = [
+ { icon: 'truck', title: 'Доставка по РФ', desc: 'Склады в Европе, логистика до вашего города' },
+ { icon: 'shield', title: 'Гарантия', desc: 'Оригинальная продукция с сертификатами' },
+ { icon: 'clock', title: 'Быстрый отклик', desc: 'КП в течение 24 часов после запроса' },
+ { icon: 'star', title: 'Экспертиза', desc: 'Подбор аналогов и техническая поддержка' },
+];
+
+export function TrustStrip() {
+ return (
+
+
+ {items.map((t, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/ui/Button/Button.module.scss b/apps/web/src/components/ui/Button/Button.module.scss
new file mode 100644
index 0000000..54f6e4e
--- /dev/null
+++ b/apps/web/src/components/ui/Button/Button.module.scss
@@ -0,0 +1,42 @@
+.button {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: 10px 24px;
+ border: none;
+ border-radius: var(--border-radius-md);
+ font-weight: 600;
+ font-size: 14px;
+ cursor: pointer;
+ transition: background-color 0.2s, color 0.2s;
+ white-space: nowrap;
+}
+
+.primary {
+ background: var(--color-primary);
+ color: #fff;
+
+ &:hover {
+ background: var(--color-primary-hover);
+ }
+}
+
+.secondary {
+ background: transparent;
+ color: var(--color-text);
+ border: 1px solid var(--color-border);
+
+ &:hover {
+ background: var(--color-slate-100);
+ }
+}
+
+.ghost {
+ background: transparent;
+ color: var(--color-primary);
+ padding: 10px 12px;
+
+ &:hover {
+ background: var(--color-orange-50);
+ }
+}
diff --git a/apps/web/src/components/ui/Button/Button.tsx b/apps/web/src/components/ui/Button/Button.tsx
new file mode 100644
index 0000000..9f0f1a7
--- /dev/null
+++ b/apps/web/src/components/ui/Button/Button.tsx
@@ -0,0 +1,22 @@
+import { ButtonHTMLAttributes } from 'react';
+import styles from './Button.module.scss';
+
+interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'ghost';
+}
+
+export function Button({
+ variant = 'primary',
+ className,
+ children,
+ ...props
+}: ButtonProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/components/ui/Container/Container.module.scss b/apps/web/src/components/ui/Container/Container.module.scss
new file mode 100644
index 0000000..4b7db23
--- /dev/null
+++ b/apps/web/src/components/ui/Container/Container.module.scss
@@ -0,0 +1,5 @@
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 0 var(--space-lg);
+}
diff --git a/apps/web/src/components/ui/Container/Container.tsx b/apps/web/src/components/ui/Container/Container.tsx
new file mode 100644
index 0000000..825e43d
--- /dev/null
+++ b/apps/web/src/components/ui/Container/Container.tsx
@@ -0,0 +1,13 @@
+import { ReactNode } from 'react';
+import styles from './Container.module.scss';
+
+interface ContainerProps {
+ children: ReactNode;
+ className?: string;
+}
+
+export function Container({ children, className }: ContainerProps) {
+ return (
+ {children}
+ );
+}
diff --git a/apps/web/src/data/brands.ts b/apps/web/src/data/brands.ts
new file mode 100644
index 0000000..ea0b621
--- /dev/null
+++ b/apps/web/src/data/brands.ts
@@ -0,0 +1,16 @@
+import { Brand } from '@/types';
+
+export const brands: Brand[] = [
+ { id: 1, name: 'Bosch Rexroth', country: 'DE', logo: 'BR', category: 'Гидравлика' },
+ { id: 2, name: 'Festo', country: 'DE', logo: 'FE', category: 'Пневматика' },
+ { id: 3, name: 'Siemens', country: 'DE', logo: 'SI', category: 'АСУ' },
+ { id: 4, name: 'Parker', country: 'US', logo: 'PK', category: 'Гидравлика' },
+ { id: 5, name: 'HYDAC', country: 'DE', logo: 'HY', category: 'Гидравлика' },
+ { id: 6, name: 'Camozzi', country: 'IT', logo: 'CZ', category: 'Пневматика' },
+ { id: 7, name: 'Beckhoff', country: 'DE', logo: 'BK', category: 'АСУ' },
+ { id: 8, name: 'IMI Norgren', country: 'UK', logo: 'IN', category: 'Пневматика' },
+ { id: 9, name: 'ABB', country: 'CH', logo: 'AB', category: 'АСУ' },
+ { id: 10, name: 'Danfoss', country: 'DK', logo: 'DF', category: 'Гидравлика' },
+ { id: 11, name: 'Schneider Electric', country: 'FR', logo: 'SE', category: 'АСУ' },
+ { id: 12, name: 'HAWE', country: 'DE', logo: 'HW', category: 'Гидравлика' },
+];
diff --git a/apps/web/src/data/categories.ts b/apps/web/src/data/categories.ts
new file mode 100644
index 0000000..145cee5
--- /dev/null
+++ b/apps/web/src/data/categories.ts
@@ -0,0 +1,38 @@
+import { Category } from '@/types';
+
+export const categories: Category[] = [
+ {
+ slug: 'gidravlika',
+ name: 'Гидравлика',
+ icon: 'hydraulic',
+ description: 'Насосы, распределители, фильтры, цилиндры, гидростанции',
+ brands: 'Bosch Rexroth · Parker · HYDAC · Danfoss',
+ color: 'var(--color-orange-500)',
+ },
+ {
+ slug: 'pnevmatika',
+ name: 'Пневматика',
+ icon: 'pneumatic',
+ description: 'Цилиндры, клапаны, FRL-модули, фитинги, вакуум',
+ brands: 'Festo · Camozzi · IMI Norgren · Metal Work',
+ color: 'var(--color-teal-500)',
+ },
+ {
+ slug: 'asu',
+ name: 'АСУ',
+ icon: 'automation',
+ description: 'ПЛК, частотники, сервоприводы, датчики, HMI, энкодеры',
+ brands: 'Siemens · Beckhoff · ABB · Pepperl+Fuchs',
+ color: 'var(--color-navy-400)',
+ },
+ {
+ slug: 'zip',
+ name: 'ЗИП',
+ icon: 'spares',
+ description: 'Запасные части, контакторы, реле, вспомогательные компоненты',
+ brands: 'Siemens · Schneider · Phoenix Contact · WAGO',
+ color: '#7B1FA2',
+ },
+];
+
+export const allCategories = ['Все', ...categories.map((c) => c.name)];
diff --git a/apps/web/src/data/navigation.ts b/apps/web/src/data/navigation.ts
new file mode 100644
index 0000000..f30ab6a
--- /dev/null
+++ b/apps/web/src/data/navigation.ts
@@ -0,0 +1,34 @@
+export const headerNav = [
+ { label: 'Каталог', href: '/catalog' },
+ { label: 'Бренды', href: '/catalog' },
+ { label: 'О компании', href: '/about' },
+ { label: 'RFQ', href: '/rfq' },
+ { label: 'Контакты', href: '/contact' },
+];
+
+export const footerColumns = [
+ {
+ title: 'Направления',
+ links: [
+ { label: 'Гидравлика', href: '/catalog/gidravlika' },
+ { label: 'Пневматика', href: '/catalog/pnevmatika' },
+ { label: 'АСУ', href: '/catalog/asu' },
+ { label: 'ЗИП', href: '/catalog/zip' },
+ ],
+ },
+ {
+ title: 'Компания',
+ links: [
+ { label: 'О компании', href: '/about' },
+ { label: 'Контакты', href: '/contact' },
+ { label: 'Запрос цены (RFQ)', href: '/rfq' },
+ ],
+ },
+ {
+ title: 'Контакты',
+ links: [
+ { label: '+49 (0) 40 123-4567', href: 'tel:+494012345678' },
+ { label: 'info@pan-prom.eu', href: 'mailto:info@pan-prom.eu' },
+ ],
+ },
+];
diff --git a/apps/web/src/data/products.ts b/apps/web/src/data/products.ts
new file mode 100644
index 0000000..4a3f959
--- /dev/null
+++ b/apps/web/src/data/products.ts
@@ -0,0 +1,14 @@
+import { Product } from '@/types';
+
+export const products: Product[] = [
+ { id: 1, sku: '4WRPEH 6 C3B12L', name: 'Пропорциональный распределитель', brand: 'Bosch Rexroth', category: 'Гидравлика', subcategory: 'Распределители', price: 'По запросу', image: '', inStock: true },
+ { id: 2, sku: 'DSBC-50-200-PA', name: 'Пневмоцилиндр стандартный ISO 15552', brand: 'Festo', category: 'Пневматика', subcategory: 'Цилиндры', price: 'По запросу', image: '', inStock: true },
+ { id: 3, sku: '6ES7 511-1AK02', name: 'CPU 1511-1 PN', brand: 'Siemens', category: 'АСУ', subcategory: 'Контроллеры', price: 'По запросу', image: '', inStock: false },
+ { id: 4, sku: 'D1VW020BNJW', name: 'Гидрораспределитель с электроуправлением', brand: 'Parker', category: 'Гидравлика', subcategory: 'Распределители', price: 'По запросу', image: '', inStock: true },
+ { id: 5, sku: '0160 D 010 BH4HC', name: 'Фильтроэлемент напорный', brand: 'HYDAC', category: 'Гидравлика', subcategory: 'Фильтры', price: 'По запросу', image: '', inStock: true },
+ { id: 6, sku: '61F-2H-060-T9', name: 'Пневмоцилиндр серии 61', brand: 'Camozzi', category: 'Пневматика', subcategory: 'Цилиндры', price: 'По запросу', image: '', inStock: false },
+ { id: 7, sku: 'CX2040-0135', name: 'Embedded PC', brand: 'Beckhoff', category: 'АСУ', subcategory: 'Контроллеры', price: 'По запросу', image: '', inStock: true },
+ { id: 8, sku: 'MVA-D-3-FES', name: 'Клапан управляющий', brand: 'HAWE', category: 'Гидравлика', subcategory: 'Клапаны', price: 'По запросу', image: '', inStock: true },
+ { id: 9, sku: 'ENI58IL-H12DA5-1024', name: 'Энкодер инкрементальный', brand: 'Pepperl+Fuchs', category: 'АСУ', subcategory: 'Энкодеры', price: 'По запросу', image: '', inStock: true },
+ { id: 10, sku: '3RH1921-1FA22', name: 'Блок вспомогательных контактов', brand: 'Siemens', category: 'ЗИП', subcategory: 'Контакторы', price: 'По запросу', image: '', inStock: true },
+];
diff --git a/apps/web/src/styles/_mixins.scss b/apps/web/src/styles/_mixins.scss
new file mode 100644
index 0000000..badf67f
--- /dev/null
+++ b/apps/web/src/styles/_mixins.scss
@@ -0,0 +1,19 @@
+@use 'variables' as *;
+
+@mixin container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 0 var(--space-lg);
+}
+
+@mixin respond-to($bp) {
+ @media (min-width: $bp) {
+ @content;
+ }
+}
+
+@mixin card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+}
diff --git a/apps/web/src/styles/_variables.scss b/apps/web/src/styles/_variables.scss
new file mode 100644
index 0000000..d3b396d
--- /dev/null
+++ b/apps/web/src/styles/_variables.scss
@@ -0,0 +1,4 @@
+$breakpoint-sm: 640px;
+$breakpoint-md: 768px;
+$breakpoint-lg: 1024px;
+$breakpoint-xl: 1280px;
diff --git a/apps/web/src/types/brand.ts b/apps/web/src/types/brand.ts
new file mode 100644
index 0000000..e507263
--- /dev/null
+++ b/apps/web/src/types/brand.ts
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 0000000..79a5411
--- /dev/null
+++ b/apps/web/src/types/category.ts
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..d694842
--- /dev/null
+++ b/apps/web/src/types/index.ts
@@ -0,0 +1,4 @@
+export type { Brand } from './brand';
+export type { Product } from './product';
+export type { Category, CategorySlug } from './category';
+export { categorySlugMap, categoryNameMap } from './category';
diff --git a/apps/web/src/types/product.ts b/apps/web/src/types/product.ts
new file mode 100644
index 0000000..aff43bb
--- /dev/null
+++ b/apps/web/src/types/product.ts
@@ -0,0 +1,11 @@
+export interface Product {
+ id: number;
+ sku: string;
+ name: string;
+ brand: string;
+ category: string;
+ subcategory: string;
+ price: string;
+ image: string;
+ inStock: boolean;
+}
diff --git a/error.log b/error.log
deleted file mode 100644
index 7850b6c..0000000
--- a/error.log
+++ /dev/null
@@ -1,2 +0,0 @@
-
-/bin/sh: gh: command not found