first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:59 +03:00
commit d44874775c
450 changed files with 76635 additions and 0 deletions

86
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,86 @@
import "@/styles/globals.css";
import "@/styles/normalize.css";
import "@/styles/webflow.css";
import "@/styles/protekproject.webflow.css";
import "@/styles/my.css";
import "@/styles/maintenance.css";
import type { AppProps } from "next/app";
import Script from "next/script";
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '@/lib/apollo';
import React, { useState } from "react";
import MaintenanceMode from '@/components/MaintenanceMode';
import { CartProvider } from '@/contexts/CartContext';
import { FavoritesProvider } from '@/contexts/FavoritesContext';
import Layout from "@/components/Layout";
import { Toaster } from 'react-hot-toast';
export default function App({ Component, pageProps }: AppProps) {
const [isMaintenanceMode, setIsMaintenanceMode] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
React.useEffect(() => {
const maintenanceMode = process.env.NEXT_PUBLIC_MAINTENANCE_MODE === 'true';
const savedAuth = typeof window !== 'undefined' ? localStorage.getItem('maintenance_authenticated') : null;
setIsMaintenanceMode(maintenanceMode);
setIsAuthenticated(savedAuth === 'true');
setIsLoading(false);
}, []);
const handlePasswordCorrect = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('maintenance_authenticated', 'true');
}
setIsAuthenticated(true);
};
if (isLoading) {
return null;
}
if (isMaintenanceMode && !isAuthenticated) {
return <MaintenanceMode onPasswordCorrect={handlePasswordCorrect} />;
}
return (
<ApolloProvider client={apolloClient}>
<FavoritesProvider>
<CartProvider>
<Layout>
<Component {...pageProps} />
</Layout>
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
success: {
duration: 3000,
iconTheme: {
primary: '#4ade80',
secondary: '#fff',
},
},
error: {
duration: 5000,
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
},
}}
/>
<Script src="/js/webflow.js" strategy="beforeInteractive" />
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
strategy="afterInteractive"
/>
</CartProvider>
</FavoritesProvider>
</ApolloProvider>
);
}

13
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

56
src/pages/about.tsx Normal file
View File

@ -0,0 +1,56 @@
import Head from "next/head";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import CatalogInfoHeader from "@/components/CatalogInfoHeader";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import AboutIntro from "@/components/about/AboutIntro";
import AboutOffers from "@/components/about/AboutOffers";
import AboutProtekInfo from "@/components/about/AboutProtekInfo";
import AboutHelp from "@/components/about/AboutHelp";
export default function About() {
return (
<>
<Head>
<title>About</title>
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CatalogInfoHeader
title="О компании"
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "О компании" }
]}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<AboutIntro />
<AboutOffers />
<AboutProtekInfo />
<AboutHelp />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
<style jsx>{`
.text-block-36 { font-size: 14px; }
.submit-button.w-button { font-size: 16px; }
.heading-14 { font-size: 20px; }
.heading-13 { font-size: 24px; }
.text-block-37 { font-size: 14px; }
.text-block-38 { font-size: 14px; }
.text-block-19 { font-size: 16px; }
`}</style>
</>
);
}

View File

@ -0,0 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// Проверяем только публичные переменные для безопасности
const envVars = {
NEXT_PUBLIC_MAINTENANCE_MODE: process.env.NEXT_PUBLIC_MAINTENANCE_MODE,
NEXT_PUBLIC_CMS_GRAPHQL_URL: process.env.NEXT_PUBLIC_CMS_GRAPHQL_URL,
NODE_ENV: process.env.NODE_ENV,
};
res.status(200).json({
message: 'Environment Variables Debug',
env: envVars,
timestamp: new Date().toISOString(),
});
}

13
src/pages/api/hello.ts Normal file
View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}

View File

@ -0,0 +1,142 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useQuery } from "@apollo/client";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import { DOC_FIND_OEM } from "@/lib/graphql";
import { LaximoDocFindOEMResult } from "@/types/laximo";
const InfoArticleSearch = () => (
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block-2 w-inline-block">
<div>Поиск деталей по артикулу</div>
</a>
</div>
<div className="w-layout-hflex flex-block-8">
<div className="w-layout-hflex flex-block-10">
<h1 className="heading">Поиск деталей по артикулу</h1>
</div>
</div>
</div>
</div>
</section>
);
const ArticleSearchPage = () => {
const router = useRouter();
const { article } = router.query;
const [searchQuery, setSearchQuery] = useState<string>("");
useEffect(() => {
if (article && typeof article === 'string') {
setSearchQuery(article.trim().toUpperCase());
}
}, [article]);
const { data, loading, error } = useQuery(DOC_FIND_OEM, {
variables: {
oemNumber: searchQuery
},
skip: !searchQuery,
errorPolicy: 'all'
});
const handleFindOffers = (articleNumber: string, brand: string) => {
router.push(`/search-result?article=${encodeURIComponent(articleNumber)}&brand=${encodeURIComponent(brand)}`);
};
const result: LaximoDocFindOEMResult | null = data?.laximoDocFindOEM || null;
const hasResults = result && result.details && result.details.length > 0;
return (
<>
<Head>
<title>Поиск деталей по артикулу {searchQuery} - Protek</title>
<meta name="description" content={`Результаты поиска деталей по артикулу ${searchQuery}`} />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoArticleSearch />
<div className="page-wrapper bg-[#F5F8FB] min-h-screen">
<div className="flex flex-col px-32 pt-10 pb-16 max-md:px-5">
<div className="flex flex-col items-center w-full">
<div className="w-full max-w-[1200px]">
{loading && (
<div className="bg-white rounded-2xl shadow p-10 flex flex-col items-center justify-center min-h-[300px]">
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-red-600 mb-6"></div>
<p className="text-lg text-gray-600">Поиск деталей по артикулу...</p>
</div>
)}
{error && !loading && (
<div className="bg-red-50 border border-red-200 rounded-2xl shadow p-10 mb-6">
<div className="flex items-center">
<svg className="w-6 h-6 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="text-lg font-medium text-red-800">Ошибка поиска</h3>
<p className="text-red-700 mt-1">Произошла ошибка при поиске деталей. Попробуйте еще раз.</p>
</div>
</div>
</div>
)}
{!hasResults && !loading && !error && (
<div className="bg-yellow-50 border border-yellow-200 rounded-2xl shadow p-10 text-center">
<svg className="w-16 h-16 text-yellow-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.562M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<h3 className="text-xl font-semibold text-yellow-800 mb-2">Детали не найдены</h3>
<p className="text-yellow-700 mb-4">
По артикулу <span className="font-mono font-semibold">{searchQuery}</span> детали не найдены.
</p>
<p className="text-sm text-yellow-600">
Попробуйте изменить запрос или проверьте правильность написания артикула.
</p>
</div>
)}
{hasResults && (
<div className="bg-white rounded-2xl shadow p-10">
<div className="border-b border-gray-200 pb-4">
<h2 className="text-xl font-semibold text-gray-900">
Поиск деталей по артикулу: {searchQuery}
</h2>
<p className="text-sm text-gray-600 mt-1">
Выберите нужную деталь
</p>
</div>
<div className="divide-y divide-gray-200">
{result.details.map((detail, index) => (
<div key={detail.detailid || index}>
<button
onClick={() => handleFindOffers(detail.formattedoem || detail.oem, detail.manufacturer)}
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block"
>
<div className="self-stretch my-auto font-bold leading-snug text-gray-950 max-md:w-full hover:text-[#EC1C24] transition-colors">
{detail.manufacturer}: {detail.formattedoem || detail.oem} {detail.name}
</div>
</button>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
<MobileMenuBottomSection />
<Footer />
</div>
</>
);
};
export default ArticleSearchPage;

118
src/pages/brands.tsx Normal file
View File

@ -0,0 +1,118 @@
import React, { useState, useMemo } from "react";
import { useRouter } from "next/router";
import { useQuery } from "@apollo/client";
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import { GET_LAXIMO_BRANDS } from "@/lib/graphql";
import { LaximoBrand } from "@/types/laximo";
import BrandWizardSearchSection from "@/components/BrandWizardSearchSection";
const InfoBrands = () => (
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block-2 w-inline-block">
<div>Все марки</div>
</a>
</div>
<div className="w-layout-hflex flex-block-8">
<div className="w-layout-hflex flex-block-10">
<h1 className="heading">Все марки автомобилей</h1>
</div>
</div>
</div>
</div>
</section>
);
const BrandsPage = () => {
const router = useRouter();
const [selectedLetter, setSelectedLetter] = useState<string>('');
const { data, loading, error } = useQuery<{ laximoBrands: LaximoBrand[] }>(GET_LAXIMO_BRANDS, { errorPolicy: 'all' });
const staticBrands = [
{ name: "Audi", code: "audi" },
{ name: "BMW", code: "bmw" },
{ name: "Cadillac", code: "cadillac" },
{ name: "Chevrolet", code: "chevrolet" },
{ name: "Citroen", code: "citroen" },
{ name: "Fiat", code: "fiat" },
{ name: "Mazda", code: "mazda" },
{ name: "Mercedes-Benz", code: "mercedes" },
{ name: "Nissan", code: "nissan" },
{ name: "Opel", code: "opel" },
{ name: "Peugeot", code: "peugeot" },
{ name: "Renault", code: "renault" },
{ name: "Toyota", code: "toyota" },
{ name: "Volkswagen", code: "volkswagen" },
{ name: "Volvo", code: "volvo" }
];
let brands = staticBrands;
if (data?.laximoBrands && data.laximoBrands.length > 0) {
brands = data.laximoBrands.map(brand => ({ name: brand.name, code: brand.code }));
} else if (error) {
console.warn('Laximo API недоступен, используются статические данные:', error.message);
}
const brandsByLetter = useMemo(() => {
const grouped: { [key: string]: typeof brands } = {};
brands.forEach(brand => {
const firstLetter = brand.name.charAt(0).toUpperCase();
if (!grouped[firstLetter]) grouped[firstLetter] = [];
grouped[firstLetter].push(brand);
});
Object.keys(grouped).forEach(letter => {
grouped[letter].sort((a, b) => a.name.localeCompare(b.name));
});
return grouped;
}, [brands]);
const letters = Object.keys(brandsByLetter).sort();
const handleBrandClick = (brand: { name: string; code?: string }) => {
if (brand.code) router.push(`/brands?selected=${brand.code}`);
};
const handleLetterClick = (letter: string) => {
setSelectedLetter(selectedLetter === letter ? '' : letter);
};
return (
<>
<Head>
<title>Все марки автомобилей - Protek</title>
<meta name="description" content="Полный каталог автомобильных брендов для поиска запчастей" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoBrands />
<BrandWizardSearchSection />
<Footer />
<MobileMenuBottomSection />
</>
);
};
const BrandGrid = ({ brands, onBrandClick }: { brands: { name: string; code?: string }[]; onBrandClick: (brand: { name: string; code?: string }) => void }) => (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{brands.map((brand, idx) => (
<button
key={idx}
onClick={() => onBrandClick(brand)}
className="bg-[#F5F8FB] hover:bg-white border border-gray-200 hover:border-[#EC1C24] rounded-xl p-4 text-left font-medium text-[#000814] shadow-sm transition-all duration-200"
>
{brand.name}
</button>
))}
</div>
);
export default BrandsPage;

277
src/pages/card.tsx Normal file
View File

@ -0,0 +1,277 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState, useMemo } from "react";
import { useQuery, useLazyQuery } from "@apollo/client";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import CartRecommended from "@/components/CartRecommended";
import InfoCard from "@/components/card/InfoCard";
import ProductImageGallery from "@/components/card/ProductImageGallery";
import ProductSortHeader from "@/components/card/ProductSortHeader";
import ProductList from "@/components/card/ProductList";
import ShowMoreOffers from "@/components/card/ShowMoreOffers";
import ProductCharacteristics from "@/components/card/ProductCharacteristics";
import ProductDescriptionTabs from "@/components/card/ProductDescriptionTabs";
import { SEARCH_PRODUCT_OFFERS, PARTS_INDEX_SEARCH_BY_ARTICLE, GET_ANALOG_OFFERS } from "@/lib/graphql";
import { useArticleImage } from "@/hooks/useArticleImage";
import { useRecommendedProducts } from "../hooks/useRecommendedProducts";
const INITIAL_OFFERS_COUNT = 4;
export default function CardPage() {
const router = useRouter();
const { article, brand, q, artId } = router.query;
const [searchQuery, setSearchQuery] = useState<string>("");
const [brandQuery, setBrandQuery] = useState<string>("");
const [sortBy, setSortBy] = useState<string>("price"); // price, quantity, delivery
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_COUNT);
const [analogsWithOffers, setAnalogsWithOffers] = useState<any[]>([]);
useEffect(() => {
if (article && typeof article === 'string') {
setSearchQuery(article.trim().toUpperCase());
}
if (brand && typeof brand === 'string') {
setBrandQuery(brand.trim());
}
setVisibleOffersCount(INITIAL_OFFERS_COUNT);
}, [article, brand]);
const { data, loading, error } = useQuery(SEARCH_PRODUCT_OFFERS, {
variables: {
articleNumber: searchQuery,
brand: brandQuery || ''
},
skip: !searchQuery,
errorPolicy: 'all'
});
// УБИРАЕМ ЗАПРОС К PARTSINDEX ДЛЯ ОПТИМИЗАЦИИ
// Теперь данные PartsIndex будут получаться только по требованию
// const { data: partsIndexData, loading: partsIndexLoading } = useQuery(PARTS_INDEX_SEARCH_BY_ARTICLE, {
// variables: {
// articleNumber: searchQuery,
// brandName: brandQuery || '',
// lang: 'ru'
// },
// skip: !searchQuery || !brandQuery,
// errorPolicy: 'ignore'
// });
const { imageUrl: mainImageUrl } = useArticleImage(artId as string, { enabled: !!artId });
const result = data?.searchProductOffers;
// Запрос для получения предложений аналогов
const [getAnalogOffers, { loading: analogsLoading }] = useLazyQuery(GET_ANALOG_OFFERS, {
onCompleted: (analogsData) => {
if (analogsData?.getAnalogOffers) {
setAnalogsWithOffers(analogsData.getAnalogOffers);
}
},
errorPolicy: 'ignore'
});
// Автоматически загружаем предложения для аналогов
useEffect(() => {
if (result?.analogs && result.analogs.length > 0) {
const analogsToLoad = result.analogs.slice(0, 5).map((analog: any) => ({
articleNumber: analog.articleNumber,
brand: analog.brand,
name: analog.name,
type: analog.type
}));
getAnalogOffers({ variables: { analogs: analogsToLoad } });
}
}, [result?.analogs, getAnalogOffers]);
// Рекомендуемые товары из той же категории с AutoEuro предложениями
const { recommendedProducts, isLoading: isLoadingRecommendedPrices } = useRecommendedProducts(
result?.name || '',
result?.articleNumber || searchQuery || '',
result?.brand || brandQuery || ''
);
// Если это поиск по параметру q, используем q как article
useEffect(() => {
if (q && typeof q === 'string' && !article) {
setSearchQuery(q.trim().toUpperCase());
const catalogFromUrl = router.query.catalog as string;
if (catalogFromUrl) {
const catalogToBrandMap: { [key: string]: string } = {
'AU1587': 'AUDI',
'VW1587': 'VOLKSWAGEN',
'BMW1587': 'BMW',
'MB1587': 'MERCEDES-BENZ',
};
setBrandQuery(catalogToBrandMap[catalogFromUrl] || '');
} else {
setBrandQuery('');
}
}
}, [q, article, router.query]);
// Собираем и сортируем все предложения (БЕЗ аналогов)
const allOffers = useMemo(() => {
if (!result) return [];
const offers: any[] = [];
// Добавляем только предложения основного товара (НЕ аналоги)
if (result.internalOffers) {
result.internalOffers.forEach((offer: any) => {
// Показываем только предложения с ценой больше 0
if (offer.price && offer.price > 0) {
offers.push({
...offer,
type: 'internal',
brand: result.brand,
articleNumber: result.articleNumber,
name: result.name,
isAnalog: false,
deliveryTime: offer.deliveryDays,
sortPrice: offer.price
});
}
});
}
if (result.externalOffers) {
result.externalOffers.forEach((offer: any) => {
// Показываем только предложения с ценой больше 0
if (offer.price && offer.price > 0) {
offers.push({
...offer,
type: 'external',
brand: offer.brand || result.brand,
articleNumber: offer.code || result.articleNumber,
name: offer.name || result.name,
isAnalog: false,
deliveryTime: offer.deliveryTime,
sortPrice: offer.price
});
}
});
}
// Сортировка предложений
const sortedOffers = [...offers].sort((a, b) => {
switch (sortBy) {
case 'price':
return a.sortPrice - b.sortPrice;
case 'quantity':
return (b.quantity || 0) - (a.quantity || 0);
case 'delivery':
return (a.deliveryTime || 999) - (b.deliveryTime || 999);
default:
return a.sortPrice - b.sortPrice;
}
});
return sortedOffers;
}, [result, sortBy]);
// Видимые предложения
const visibleOffers = allOffers.slice(0, visibleOffersCount);
const hasMoreOffers = allOffers.length > visibleOffersCount;
const handleShowMoreOffers = () => {
setVisibleOffersCount(prev => Math.min(prev + 4, allOffers.length));
};
const handleSortChange = (newSortBy: string) => {
setSortBy(newSortBy);
setVisibleOffersCount(INITIAL_OFFERS_COUNT); // Сбрасываем к начальному количеству
};
if (loading) {
return (
<>
<Head>
<title>Загрузка товара - Protek</title>
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Загрузка данных товара...</p>
</div>
</main>
<Footer />
</>
);
}
return (
<>
<Head>
<title>{result ? `${result.brand} ${result.articleNumber} - ${result.name}` : `Карточка товара`} - Protek</title>
<meta name="description" content={`Подробная информация о товаре ${result?.name}`} />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="/images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="/images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoCard
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}
name={result ? result.name : "деталь"}
productId={artId ? String(artId) : undefined}
price={allOffers.length > 0 ? Math.min(...allOffers.map(offer => offer.sortPrice)) : 0}
currency="RUB"
image={mainImageUrl || (result?.partsIndexImages && result.partsIndexImages.length > 0 ? result.partsIndexImages[0].url : undefined)}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex flex-block-14">
<div className="w-layout-hflex core-product-card-copy">
<ProductImageGallery
imageUrl={mainImageUrl}
partsIndexImages={result?.partsIndexImages?.map((img: any) => img.url) || []}
/>
<div className="w-layout-vflex flex-block-48">
<ProductSortHeader
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}
name={result ? result.name : "деталь"}
sortBy={sortBy}
onSortChange={handleSortChange}
/>
<ProductList
offers={visibleOffers}
isLoading={loading}
/>
<ShowMoreOffers
hasMoreOffers={hasMoreOffers}
onShowMore={handleShowMoreOffers}
remainingCount={allOffers.length - visibleOffersCount}
/>
<div className="w-layout-vflex description-item">
<ProductCharacteristics
result={result}
/>
</div>
</div>
</div>
<CartRecommended
recommendedProducts={recommendedProducts}
isLoadingPrices={analogsLoading || isLoadingRecommendedPrices}
/>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

43
src/pages/cart-step-2.tsx Normal file
View File

@ -0,0 +1,43 @@
import React from "react";
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import CartRecommended from "../components/CartRecommended";
import CartInfo from "../components/CartInfo";
import CartList2 from "../components/CartList2";
import CartSummary2 from "../components/CartSummary2";
import MobileMenuBottomSection from "../components/MobileMenuBottomSection";
export default function CartStep2() {
return (
<>
<Head>
<title>Cart Step 2</title>
<meta name="description" content="Cart Step 2" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CartInfo />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex cart-list">
<div className="w-layout-hflex core-product-card">
<CartList2 />
<CartSummary2 />
</div>
<CartRecommended />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

45
src/pages/cart.tsx Normal file
View File

@ -0,0 +1,45 @@
import Header from "@/components/Header";
import Head from "next/head";
import Footer from "@/components/Footer";
import CartInfo from "@/components/CartInfo";
import CartList from "@/components/CartList";
import CartSummary from "@/components/CartSummary";
import CartRecommended from "../components/CartRecommended";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import CartDebug from "@/components/CartDebug";
export default function CartPage() {
return (
<><Head>
<title>Cart</title>
<meta name="description" content="Cart" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CartInfo />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex cart-list">
<div className="w-layout-hflex core-product-card">
<CartList />
<CartSummary />
</div>
<CartRecommended />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
<CartDebug />
</>
);
}

850
src/pages/catalog.tsx Normal file
View File

@ -0,0 +1,850 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import ProductListCard from "@/components/ProductListCard";
import Filters, { FilterConfig } from "@/components/Filters";
import FiltersWithSearch from "@/components/FiltersWithSearch";
import CatalogProductCard from "@/components/CatalogProductCard";
import CatalogPagination from "@/components/CatalogPagination";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import CatalogInfoHeader from "@/components/CatalogInfoHeader";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { useQuery } from '@apollo/client';
import FiltersPanelMobile from '@/components/FiltersPanelMobile';
import MobileMenuBottomSection from '../components/MobileMenuBottomSection';
import { GET_PARTSAPI_ARTICLES, GET_PARTSAPI_MAIN_IMAGE, SEARCH_PRODUCT_OFFERS, GET_PARTSINDEX_CATALOG_ENTITIES, GET_PARTSINDEX_CATALOG_PARAMS } from '@/lib/graphql';
import { PartsAPIArticlesData, PartsAPIArticlesVariables, PartsAPIArticle, PartsAPIMainImageData, PartsAPIMainImageVariables } from '@/types/partsapi';
import { PartsIndexEntitiesData, PartsIndexEntitiesVariables, PartsIndexEntity, PartsIndexParamsData, PartsIndexParamsVariables } from '@/types/partsindex';
import LoadingSpinner from '@/components/LoadingSpinner';
import ArticleCard from '@/components/ArticleCard';
import CatalogEmptyState from '@/components/CatalogEmptyState';
import { useProductPrices } from '@/hooks/useProductPrices';
import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton';
import { useCart } from '@/contexts/CartContext';
import toast from 'react-hot-toast';
const mockData = Array(12).fill({
image: "",
discount: "-35%",
price: "от 17 087 ₽",
oldPrice: "22 347 ₽",
title: 'Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60',
brand: "Borsehung",
});
const ITEMS_PER_PAGE = 20;
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
export default function Catalog() {
const router = useRouter();
const { addItem } = useCart();
const {
partsApiCategory: strId,
categoryName,
partsIndexCatalog: catalogId,
partsIndexCategory: groupId
} = router.query;
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
const [showSortMobile, setShowSortMobile] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({});
const [visibleArticles, setVisibleArticles] = useState<PartsAPIArticle[]>([]);
const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [entitiesPage, setEntitiesPage] = useState(1); // Страница для PartsIndex
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [hasMoreEntities, setHasMoreEntities] = useState(true); // Есть ли еще товары на сервере
const [showAllBrands, setShowAllBrands] = useState(false);
const [catalogFilters, setCatalogFilters] = useState<FilterConfig[]>([]);
const [filtersLoading, setFiltersLoading] = useState(true);
const [sortActive, setSortActive] = useState(0);
const [visibleProductsCount, setVisibleProductsCount] = useState(0); // Счетчик товаров с предложениями
const [filtersGenerating, setFiltersGenerating] = useState(false); // Состояние генерации фильтров
const [targetVisibleCount, setTargetVisibleCount] = useState(ITEMS_PER_PAGE); // Целевое количество видимых товаров
const [loadedArticlesCount, setLoadedArticlesCount] = useState(ITEMS_PER_PAGE); // Количество загруженных артикулов
const [showEmptyState, setShowEmptyState] = useState(false);
const [partsIndexPage, setPartsIndexPage] = useState(1); // Текущая страница для PartsIndex
const [totalPages, setTotalPages] = useState(1); // Общее количество страниц
// Карта видимости товаров по индексу
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
// Обработчик изменения видимости товара
const handleVisibilityChange = useCallback((index: number, isVisible: boolean) => {
setVisibilityMap(prev => {
const currentVisibility = prev.get(index);
// Обновляем только если значение действительно изменилось
if (currentVisibility === isVisible) {
return prev;
}
const newMap = new Map(prev);
newMap.set(index, isVisible);
return newMap;
});
}, []);
// Пересчитываем количество видимых товаров при изменении карты видимости
useEffect(() => {
const visibleCount = Array.from(visibilityMap.values()).filter(Boolean).length;
setVisibleProductsCount(visibleCount);
}, [visibilityMap]);
// Определяем режим работы
const isPartsAPIMode = Boolean(strId && categoryName);
const isPartsIndexMode = Boolean(catalogId && categoryName && groupId); // Требуем groupId для PartsIndex
const isPartsIndexCatalogOnly = Boolean(catalogId && categoryName && !groupId); // Каталог без группы
// Отладочная информация
console.log('🔍 Режимы работы каталога:', {
catalogId,
groupId,
categoryName,
isPartsAPIMode,
isPartsIndexMode,
isPartsIndexCatalogOnly
});
// Загружаем артикулы PartsAPI
const { data: articlesData, loading: articlesLoading, error: articlesError } = useQuery<PartsAPIArticlesData, PartsAPIArticlesVariables>(
GET_PARTSAPI_ARTICLES,
{
variables: {
strId: parseInt(strId as string),
carId: 9877,
carType: 'PC'
},
skip: !isPartsAPIMode,
fetchPolicy: 'cache-first'
}
);
const allArticles = articlesData?.partsAPIArticles || [];
// Загружаем товары PartsIndex
const { data: entitiesData, loading: entitiesLoading, error: entitiesError, refetch: refetchEntities } = useQuery<PartsIndexEntitiesData, PartsIndexEntitiesVariables>(
GET_PARTSINDEX_CATALOG_ENTITIES,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
limit: ITEMS_PER_PAGE,
page: partsIndexPage,
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-and-network'
}
);
// Загружаем параметры фильтрации для PartsIndex
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
GET_PARTSINDEX_CATALOG_PARAMS,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-first'
}
);
// allEntities больше не используется - используем allLoadedEntities
// Хук для загрузки цен товаров PartsIndex
const productsForPrices = visibleEntities.map(entity => ({
id: entity.id,
code: entity.code,
brand: entity.brand.name
}));
const { getPrice, isLoadingPrice, loadPriceOnDemand } = useProductPrices(productsForPrices);
useEffect(() => {
if (articlesData?.partsAPIArticles) {
// Загружаем изначально только ITEMS_PER_PAGE товаров
const initialLoadCount = Math.min(ITEMS_PER_PAGE, articlesData.partsAPIArticles.length);
setVisibleArticles(articlesData.partsAPIArticles.slice(0, initialLoadCount));
setLoadedArticlesCount(initialLoadCount);
setTargetVisibleCount(ITEMS_PER_PAGE);
setCurrentPage(1);
}
}, [articlesData]);
useEffect(() => {
if (entitiesData?.partsIndexCatalogEntities?.list) {
console.log('📊 Обновляем entitiesData:', {
listLength: entitiesData.partsIndexCatalogEntities.list.length,
pagination: entitiesData.partsIndexCatalogEntities.pagination,
currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1
});
const newEntities = entitiesData.partsIndexCatalogEntities.list;
const pagination = entitiesData.partsIndexCatalogEntities.pagination;
// Обновляем список товаров
setVisibleEntities(newEntities);
// Обновляем информацию о пагинации
const currentPage = pagination?.page?.current || 1;
const hasNext = pagination?.page?.next !== null;
const hasPrev = pagination?.page?.prev !== null;
setPartsIndexPage(currentPage);
setHasMoreEntities(hasNext);
// Вычисляем общее количество страниц (приблизительно)
if (hasNext) {
setTotalPages(currentPage + 1); // Минимум еще одна страница
} else {
setTotalPages(currentPage); // Это последняя страница
}
console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev });
}
}, [entitiesData]);
// Генерация фильтров для PartsIndex на основе параметров API
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
if (!paramsData?.partsIndexCatalogParams?.list) {
return [];
}
return paramsData.partsIndexCatalogParams.list.map(param => {
if (param.type === 'range') {
// Для range фильтров ищем min и max значения
const numericValues = param.values
.map(v => parseFloat(v.value))
.filter(v => !isNaN(v));
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
return {
type: 'range' as const,
title: param.name,
min,
max
};
} else {
// Для dropdown фильтров
return {
type: 'dropdown' as const,
title: param.name,
options: param.values
.filter(value => value.available) // Показываем только доступные
.map(value => value.title || value.value),
multi: true,
showAll: true
};
}
});
}, [paramsData]);
useEffect(() => {
if (isPartsIndexMode) {
// Для PartsIndex генерируем фильтры на основе параметров API
const filters = generatePartsIndexFilters();
setCatalogFilters(filters);
setFiltersLoading(paramsLoading);
} else {
// Для других режимов убираем запрос на catalog-filters
setFiltersLoading(false);
}
}, [isPartsIndexMode, generatePartsIndexFilters, paramsLoading]);
// Генерируем динамические фильтры для PartsAPI
const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
if (!allArticles.length) return [];
// Получаем список видимых товаров из карты видимости
const visibleIndices = Array.from(visibilityMap.entries())
.filter(([_, isVisible]) => isVisible)
.map(([index]) => index);
// Если еще нет данных о видимости, используем все товары (для начальной загрузки)
const articlesToProcess = visibilityMap.size === 0 ? allArticles :
visibleIndices.map(index => allArticles[index]).filter(Boolean);
const brandCounts = new Map<string, number>();
const productGroups = new Set<string>();
// Подсчитываем количество товаров для каждого бренда (только видимые)
articlesToProcess.forEach(article => {
if (article?.artSupBrand) {
brandCounts.set(article.artSupBrand, (brandCounts.get(article.artSupBrand) || 0) + 1);
}
if (article?.productGroup) productGroups.add(article.productGroup);
});
const filters: FilterConfig[] = [];
if (brandCounts.size > 1) {
// Сортируем бренды по количеству товаров (по убыванию)
const sortedBrands = Array.from(brandCounts.entries())
.sort((a, b) => b[1] - a[1]) // Сортируем по количеству товаров
.map(([brand]) => brand);
// Показываем либо первые N брендов, либо все (если нажата кнопка "Показать еще")
const brandsToShow = showAllBrands ? sortedBrands : sortedBrands.slice(0, MAX_BRANDS_DISPLAY);
filters.push({
type: "dropdown",
title: "Бренд",
options: brandsToShow.sort(), // Сортируем по алфавиту для удобства
multi: true,
showAll: true,
defaultOpen: true,
hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY,
onShowMore: () => setShowAllBrands(true)
});
}
if (productGroups.size > 1) {
filters.push({
type: "dropdown",
title: "Группа товаров",
options: Array.from(productGroups).sort(),
multi: true,
showAll: true,
defaultOpen: true,
});
}
return filters;
}, [allArticles, showAllBrands, visibilityMap]);
const dynamicFilters = useMemo(() => {
if (isPartsIndexMode) {
return generatePartsIndexFilters();
} else if (isPartsAPIMode) {
return generatePartsAPIFilters();
}
return [];
}, [isPartsIndexMode, isPartsAPIMode, generatePartsIndexFilters, generatePartsAPIFilters]);
// Отдельный useEffect для управления состоянием загрузки фильтров
useEffect(() => {
if ((isPartsAPIMode && allArticles.length > 0) || (isPartsIndexMode && visibleEntities.length > 0)) {
setFiltersGenerating(true);
const timer = setTimeout(() => {
setFiltersGenerating(false);
}, 300);
return () => clearTimeout(timer);
} else {
setFiltersGenerating(false);
}
}, [isPartsAPIMode, allArticles.length, isPartsIndexMode, visibleEntities.length]);
const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => {
setSelectedFilters(prev => ({
...prev,
[filterTitle]: Array.isArray(value) ? value : [value]
}));
};
const handleMobileFilterChange = (type: string, value: any) => {
setSelectedFilters(prev => ({
...prev,
[type]: Array.isArray(value) ? value : [value]
}));
};
// Функция для сброса всех фильтров
const handleResetFilters = useCallback(() => {
setSearchQuery('');
setSelectedFilters({});
setShowAllBrands(false);
setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую
}, []);
// Фильтрация по поиску и фильтрам для PartsAPI
const filteredArticles = useMemo(() => {
return allArticles.filter(article => {
// Фильтрация по поиску
if (searchQuery.trim()) {
const searchLower = searchQuery.toLowerCase();
const articleTitle = [
article.artSupBrand || '',
article.artArticleNr || '',
article.productGroup || ''
].join(' ').toLowerCase();
if (!articleTitle.includes(searchLower)) {
return false;
}
}
// Фильтрация по выбранным фильтрам
const brandFilter = selectedFilters['Бренд'] || [];
if (brandFilter.length > 0 && !brandFilter.includes(article.artSupBrand || '')) {
return false;
}
const groupFilter = selectedFilters['Группа товаров'] || [];
if (groupFilter.length > 0 && !groupFilter.includes(article.productGroup || '')) {
return false;
}
return true;
});
}, [allArticles, searchQuery, selectedFilters]);
// Упрощенная логика - показываем все загруженные товары без клиентской фильтрации
const filteredEntities = visibleEntities;
// Обновляем видимые артикулы при изменении поиска или фильтров для PartsAPI
useEffect(() => {
if (isPartsAPIMode) {
setVisibleArticles(filteredArticles.slice(0, ITEMS_PER_PAGE));
setCurrentPage(1);
setIsLoadingMore(false);
setVisibilityMap(new Map()); // Сбрасываем карту видимости при изменении фильтров
setVisibleProductsCount(0); // Сбрасываем счетчик
setLoadedArticlesCount(ITEMS_PER_PAGE); // Сбрасываем счетчик загруженных
setShowEmptyState(false); // Сбрасываем пустое состояние
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsAPIMode, searchQuery, JSON.stringify(selectedFilters), filteredArticles.length]);
// Обновляем видимые товары при изменении поиска или фильтров для PartsIndex
useEffect(() => {
if (isPartsIndexMode) {
// При изменении поиска или фильтров сбрасываем пагинацию
setShowEmptyState(false);
// Если изменился поисковый запрос, нужно перезагрузить данные с сервера
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
setPartsIndexPage(1);
setHasMoreEntities(true);
// refetch будет автоматически вызван при изменении partsIndexPage
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]);
// Управляем показом пустого состояния с задержкой
useEffect(() => {
if (isPartsAPIMode && !articlesLoading && !articlesError) {
// Если товаров вообще нет - показываем сразу
if (allArticles.length === 0) {
setShowEmptyState(true);
return;
}
// Если товары есть, но нет видимых - ждем 2 секунды
const timer = setTimeout(() => {
setShowEmptyState(visibleProductsCount === 0 && allArticles.length > 0);
}, 2000); // Даем 2 секунды на загрузку данных о предложениях
return () => clearTimeout(timer);
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
// Для PartsIndex показываем пустое состояние если нет товаров
setShowEmptyState(visibleEntities.length === 0);
} else {
setShowEmptyState(false);
}
}, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length,
isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, filteredEntities.length]);
// Функции для навигации по страницам PartsIndex
const handleNextPage = useCallback(() => {
if (hasMoreEntities && !entitiesLoading) {
setPartsIndexPage(prev => prev + 1);
}
}, [hasMoreEntities, entitiesLoading]);
const handlePrevPage = useCallback(() => {
if (partsIndexPage > 1 && !entitiesLoading) {
setPartsIndexPage(prev => prev - 1);
}
}, [partsIndexPage, entitiesLoading]);
// Функция для загрузки следующей порции товаров по кнопке (только для PartsAPI)
const handleLoadMorePartsAPI = useCallback(async () => {
if (isLoadingMore || !isPartsAPIMode) {
return;
}
setIsLoadingMore(true);
try {
const additionalCount = Math.min(ITEMS_PER_PAGE, filteredArticles.length - loadedArticlesCount);
if (additionalCount > 0) {
const newArticles = filteredArticles.slice(loadedArticlesCount, loadedArticlesCount + additionalCount);
setVisibleArticles(prev => [...prev, ...newArticles]);
setLoadedArticlesCount(prev => prev + additionalCount);
setTargetVisibleCount(prev => prev + ITEMS_PER_PAGE);
}
} catch (error) {
console.error('❌ Ошибка загрузки дополнительных товаров:', error);
} finally {
setIsLoadingMore(false);
}
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles, isLoadingMore]);
// Определяем есть ли еще товары для загрузки (только для PartsAPI)
const hasMoreItems = useMemo(() => {
if (isPartsAPIMode) {
return loadedArticlesCount < filteredArticles.length;
}
return false;
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]);
if (filtersLoading) {
return <div className="py-8 text-center">Загрузка фильтров...</div>;
}
return (
<>
<Head>
<title>Catalog</title>
<meta name="description" content="Catalog" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CatalogInfoHeader
title={
isPartsAPIMode ? decodeURIComponent(categoryName as string || 'Запчасти') :
isPartsIndexMode ? decodeURIComponent(categoryName as string || 'Товары') :
"Аккумуляторы"
}
count={
isPartsAPIMode ?
(visibilityMap.size === 0 && allArticles.length > 0 ? undefined : visibleProductsCount) :
isPartsIndexMode ?
(searchQuery.trim() || Object.keys(selectedFilters).length > 0 ?
filteredEntities.length :
entitiesData?.partsIndexCatalogEntities?.pagination?.limit || visibleEntities.length) :
3587
}
productName={
isPartsAPIMode ? "запчасть" :
isPartsIndexMode ? "товар" :
"аккумулятор"
}
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Каталог" },
...((isPartsAPIMode || isPartsIndexMode) ? [{ label: decodeURIComponent(categoryName as string || 'Товары') }] : [])
]}
showCount={true}
showProductHelp={true}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-13">
<div className="w-layout-hflex flex-block-84">
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
<div className="code-embed-9 w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>Фильтры</div>
</div>
</div>
{isPartsAPIMode ? (
<div className="filters-desktop">
<Filters
filters={dynamicFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersGenerating}
/>
</div>
) : isPartsIndexMode ? (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
) : (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
)}
<FiltersPanelMobile
open={showFiltersMobile}
onClose={() => setShowFiltersMobile(false)}
filters={isPartsAPIMode ? dynamicFilters : catalogFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filterValues={selectedFilters}
onFilterChange={handleMobileFilterChange}
/>
<div className="w-layout-vflex flex-block-14-copy-copy">
{/* Индикатор загрузки для PartsAPI */}
{isPartsAPIMode && articlesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем артикулы..." />
</div>
)}
{/* Индикатор загрузки для PartsIndex */}
{isPartsIndexMode && entitiesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем товары..." />
</div>
)}
{/* Сообщение об ошибке */}
{isPartsAPIMode && articlesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки артикулов: {articlesError.message}</div>
</div>
)}
{/* Сообщение об ошибке для PartsIndex */}
{isPartsIndexMode && entitiesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки товаров: {entitiesError.message}</div>
</div>
)}
{/* Отображение артикулов PartsAPI */}
{isPartsAPIMode && visibleArticles.length > 0 && (
<>
{visibleArticles.map((article, idx) => (
<ArticleCard
key={`${article.artId}_${idx}`}
article={article}
index={idx}
onVisibilityChange={handleVisibilityChange}
/>
))}
{/* Кнопка "Показать еще" */}
{hasMoreItems && (
<div className="w-layout-hflex pagination">
<button
onClick={handleLoadMorePartsAPI}
disabled={isLoadingMore}
className="button_strock w-button"
>
{isLoadingMore ? (
<>
Загружаем...
</>
) : (
<>
Показать еще
</>
)}
</button>
</div>
)}
</>
)}
{/* Отображение товаров PartsIndex */}
{isPartsIndexMode && filteredEntities.length > 0 && (
<>
{filteredEntities
.map((entity, idx) => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
const isLoadingPriceData = isLoadingPrice(productForPrice);
return {
entity,
idx,
productForPrice,
priceData,
isLoadingPriceData,
hasOffer: priceData !== null || isLoadingPriceData
};
})
.filter(item => item.hasOffer) // Показываем только товары с предложениями или загружающиеся
.map(({ entity, idx, productForPrice, priceData, isLoadingPriceData }) => {
// Определяем цену для отображения
let displayPrice = "Цена по запросу";
let displayCurrency = "RUB";
let priceElement;
if (isLoadingPriceData) {
priceElement = <PriceSkeleton />;
} else if (priceData && priceData.price) {
displayPrice = `${priceData.price.toLocaleString('ru-RU')}`;
displayCurrency = priceData.currency || "RUB";
}
return (
<CatalogProductCard
key={`${entity.id}_${idx}`}
title={entity.originalName || entity.name?.name || 'Товар без названия'}
brand={entity.brand.name}
articleNumber={entity.code}
brandName={entity.brand.name}
image={entity.images?.[0] || ''}
price={isLoadingPriceData ? "" : displayPrice}
priceElement={priceElement}
oldPrice=""
discount=""
currency={displayCurrency}
productId={entity.id}
artId={entity.id}
offerKey={priceData?.offerKey}
onAddToCart={() => {
// Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) {
loadPriceOnDemand(productForPrice);
console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name);
return;
}
// Если цена есть, добавляем в корзину
if (priceData && priceData.price) {
const itemToAdd = {
productId: entity.id,
offerKey: priceData.offerKey,
name: entity.originalName || entity.name?.name || 'Товар без названия',
description: `${entity.brand.name} ${entity.code}`,
brand: entity.brand.name,
article: entity.code,
price: priceData.price,
currency: priceData.currency || 'RUB',
quantity: 1,
deliveryTime: '1-3 дня',
warehouse: 'Parts Index',
supplier: 'Parts Index',
isExternal: true,
image: entity.images?.[0] || '',
};
addItem(itemToAdd);
// Показываем уведомление
toast.success(`Товар "${entity.brand.name} ${entity.code}" добавлен в корзину за ${priceData.price.toLocaleString('ru-RU')}`);
} else {
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
}
}}
/>
);
})}
{/* Пагинация для PartsIndex */}
<div className="w-layout-hflex pagination">
<button
onClick={handlePrevPage}
disabled={partsIndexPage <= 1 || entitiesLoading}
className="button_strock w-button mr-2"
>
Назад
</button>
<span className="flex items-center px-4 text-gray-600">
Страница {partsIndexPage} {totalPages > partsIndexPage && `из ${totalPages}+`}
</span>
<button
onClick={handleNextPage}
disabled={!hasMoreEntities || entitiesLoading}
className="button_strock w-button ml-2"
>
{entitiesLoading ? 'Загрузка...' : 'Вперед →'}
</button>
</div>
{/* Отладочная информация */}
{isPartsIndexMode && (
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<div>🔍 Отладка PartsIndex:</div>
<div> hasMoreItems: {hasMoreItems ? 'да' : 'нет'}</div>
<div> hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}</div>
<div> entitiesPage: {entitiesPage}</div>
<div> visibleEntities: {visibleEntities.length}</div>
<div> filteredEntities: {filteredEntities.length}</div>
<div> groupId: {groupId || 'отсутствует'}</div>
<div> isLoadingMore: {isLoadingMore ? 'да' : 'нет'}</div>
<div> entitiesLoading: {entitiesLoading ? 'да' : 'нет'}</div>
<div> catalogId: {catalogId || 'отсутствует'}</div>
<div> Пагинация: {JSON.stringify(entitiesData?.partsIndexCatalogEntities?.pagination)}</div>
</div>
)}
</>
)}
{/* Пустое состояние для PartsAPI */}
{isPartsAPIMode && !articlesLoading && !articlesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Пустое состояние для PartsIndex */}
{isPartsIndexMode && !entitiesLoading && !entitiesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Каталог PartsIndex без группы */}
{isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Выберите подкатегорию</div>
<div className="text-gray-400 text-sm">Для просмотра товаров необходимо выбрать конкретную подкатегорию из меню.</div>
</div>
)}
{/* Обычные товары (не PartsAPI/PartsIndex) */}
{!isPartsAPIMode && !isPartsIndexMode && !isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Раздел в разработке</div>
<div className="text-gray-400 text-sm">Данные для этой категории скоро появятся.</div>
</div>
)}
</div>
</div>
</div>
</section>
{!isPartsAPIMode && !isPartsIndexMode && <CatalogPagination />}
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

20
src/pages/checkout.tsx Normal file
View File

@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function Checkout() {
return (
<>
<Head>
<title>Checkout</title>
<meta name="description" content="Checkout" />
</Head>
<Header />
{/* Вставь сюда содержимое <body> из checkout.html, преобразовав в JSX. Все пути к картинкам и svg поменяй на /images/... */}
{/* Пример: <img src="/images/logo.svg" ... /> */}
{/* Сохрани все классы для стилей. */}
{/* TODO: Перевести формы и интерактив на React позже */}
<Footer />
</>
);
}

44
src/pages/contacts.tsx Normal file
View File

@ -0,0 +1,44 @@
import React from "react";
import Head from "next/head";
import Header from "@/components/Header";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import InfoContacts from "@/components/contacts/InfoContacts";
import MapContacts from "@/components/contacts/MapContacts";
import OrderContacts from "@/components/contacts/OrderContacts";
import LegalContacts from "@/components/contacts/LegalContacts";
const Contacts = () => (
<>
<Head>
<title>Contacts</title>
<meta name="description" content="Contacts" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoContacts />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<div className="w-layout-vflex flex-block-72">
<div className="w-layout-vflex flex-block-97">
<OrderContacts />
<LegalContacts />
</div>
<MapContacts />
</div>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
export default Contacts;

View File

@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function DetailCategory() {
return (
<>
<Head>
<title>Detail Category</title>
<meta name="description" content="Detail Category" />
</Head>
<Header />
{/* Вставь сюда содержимое <body> из detail_category.html, преобразовав в JSX. Все пути к картинкам и svg поменяй на /images/... */}
{/* Пример: <img src="/images/logo.svg" ... /> */}
{/* Сохрани все классы для стилей. */}
{/* TODO: Перевести формы и интерактив на React позже */}
<Footer />
</>
);
}

View File

@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function DetailProduct() {
return (
<>
<Head>
<title>Detail Product</title>
<meta name="description" content="Detail Product" />
</Head>
<Header />
{/* Вставь сюда содержимое <body> из detail_product.html, преобразовав в JSX. Все пути к картинкам и svg поменяй на /images/... */}
{/* Пример: <img src="/images/logo.svg" ... /> */}
{/* Сохрани все классы для стилей. */}
{/* TODO: Перевести формы и интерактив на React позже */}
<Footer />
</>
);
}

20
src/pages/detail_sku.tsx Normal file
View File

@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function DetailSku() {
return (
<>
<Head>
<title>Detail SKU</title>
<meta name="description" content="Detail SKU" />
</Head>
<Header />
{/* Вставь сюда содержимое <body> из detail_sku.html, преобразовав в JSX. Все пути к картинкам и svg поменяй на /images/... */}
{/* Пример: <img src="/images/logo.svg" ... /> */}
{/* Сохрани все классы для стилей. */}
{/* TODO: Перевести формы и интерактив на React позже */}
<Footer />
</>
);
}

178
src/pages/favorite.tsx Normal file
View File

@ -0,0 +1,178 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "../components/MobileMenuBottomSection";
import FavoriteInfo from "@/components/FavoriteInfo";
import Filters, { FilterConfig } from "@/components/Filters";
import FiltersPanelMobile from "@/components/FiltersPanelMobile";
import React, { useState, useMemo } from "react";
import CartRecommended from "../components/CartRecommended";
import FavoriteList from "../components/FavoriteList";
import { useFavorites } from "@/contexts/FavoritesContext";
export default function Favorite() {
const { favorites } = useFavorites();
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [filterValues, setFilterValues] = useState<{[key: string]: any}>({});
const [sortBy, setSortBy] = useState<'name' | 'brand' | 'price' | 'date'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Создаем динамические фильтры на основе данных избранного
const dynamicFilters: FilterConfig[] = useMemo(() => {
const filters: FilterConfig[] = [];
if (favorites.length === 0) {
return filters;
}
// Фильтр по производителю
const brands = [...new Set(favorites.map(item => item.brand).filter(Boolean))].sort();
if (brands.length > 1) {
filters.push({
type: "dropdown",
title: "Производитель",
options: brands,
multi: true,
showAll: true,
});
}
// Фильтр по цене
const prices = favorites
.map(item => item.price)
.filter((price): price is number => typeof price === 'number' && price > 0);
if (prices.length > 0) {
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
if (maxPrice > minPrice) {
filters.push({
type: "range",
title: "Цена (₽)",
min: Math.floor(minPrice),
max: Math.ceil(maxPrice),
});
}
}
return filters;
}, [favorites]);
const handleFilterChange = (type: string, value: any) => {
setFilterValues(prev => ({
...prev,
[type]: value
}));
};
const handleSearchChange = (value: string) => {
setSearchQuery(value);
};
const handleApplyFilters = () => {
setShowFiltersMobile(false);
};
const handleClearFilters = () => {
setFilterValues({});
setSearchQuery('');
};
// Подсчитываем количество активных фильтров
const activeFiltersCount = useMemo(() => {
let count = 0;
if (searchQuery.trim()) count++;
Object.entries(filterValues).forEach(([key, value]) => {
if (Array.isArray(value) && value.length > 0) count++;
else if (value && !Array.isArray(value)) count++;
});
return count;
}, [searchQuery, filterValues]);
return (
<>
<Head>
<title>Избранное - Protek Auto</title>
<meta name="description" content="Ваши избранные товары" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<FavoriteInfo />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-84">
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
<div className="code-embed-9 w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>Фильтры</div>
{activeFiltersCount > 0 && (
<div className="ml-2 bg-red-600 text-white text-xs rounded-full px-2 py-1 min-w-[20px] text-center">
{activeFiltersCount}
</div>
)}
</div>
{activeFiltersCount > 0 && (
<button
onClick={handleClearFilters}
className="text-red-600 hover:text-red-800 text-sm underline ml-4"
>
Сбросить фильтры
</button>
)}
</div>
<FiltersPanelMobile
filters={dynamicFilters}
open={showFiltersMobile}
onClose={() => setShowFiltersMobile(false)}
onApply={handleApplyFilters}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
filterValues={filterValues}
onFilterChange={handleFilterChange}
/>
</div>
</section>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex cart-list">
<FavoriteList
filters={dynamicFilters}
filterValues={filterValues}
onFilterChange={handleFilterChange}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
sortBy={sortBy}
sortOrder={sortOrder}
onSortChange={setSortBy}
onSortOrderChange={setSortOrder}
/>
<CartRecommended />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

50
src/pages/index.tsx Normal file
View File

@ -0,0 +1,50 @@
import Head from "next/head";
import Image from "next/image";
import { Geist, Geist_Mono } from "next/font/google";
import styles from "@/styles/Home.module.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import HeroSlider from "@/components/index/HeroSlider";
import CatalogSection from "@/components/index/CatalogSection";
import AvailableParts from "@/components/index/AvailableParts";
import NewsAndPromos from "@/components/index/NewsAndPromos";
import AboutHelp from "@/components/about/AboutHelp";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export default function Home() {
return (
<>
<Head>
<title>Protek</title>
<meta name="description" content="Protek" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<HeroSlider />
<CatalogSection />
<div className="w-layout-blockcontainer container w-container">
<AboutHelp />
</div>
<AvailableParts />
<NewsAndPromos />
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

64
src/pages/news-open.tsx Normal file
View File

@ -0,0 +1,64 @@
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Header from "@/components/Header";
import Head from "next/head";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import InfoNewsOpen from "@/components/news-open/InfoNewsOpen";
import ContentNews from "@/components/news-open/ContentNews";
import NewsCard from "@/components/news/NewsCard";
export default function NewsOpen() {
return (
<>
<Head>
<title>news open</title>
<meta content="news open" property="og:title" />
<meta content="news open" property="twitter:title" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta content="Webflow" name="generator" />
<link href="/css/normalize.css" rel="stylesheet" type="text/css" />
<link href="/css/webflow.css" rel="stylesheet" type="text/css" />
<link href="/css/protekproject.webflow.css" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoNewsOpen />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<div className="w-layout-vflex flex-block-72">
<div className="w-layout-vflex flex-block-97">
<ContentNews />
</div>
<div className="w-layout-vflex lastnews">
<NewsCard
key={1}
title="Kia Syros будет выделяться необычным стилем"
description="Компания Kia готова представить новый кроссовер Syros"
category="Новости компании"
date="17.12.2024"
image="/images/news_img.png"
/>
<NewsCard
key={2}
title="Kia Syros будет выделяться необычным стилем"
description="Компания Kia готова представить новый кроссовер Syros"
category="Новости компании"
date="17.12.2024"
image="/images/news_img.png"
/>
</div>
</div>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

52
src/pages/news.tsx Normal file
View File

@ -0,0 +1,52 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import InfoNews from "@/components/news/InfoNews";
import NewsMenu from "@/components/news/NewsMenu";
import NewsCard from "@/components/news/NewsCard";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
export default function News() {
return (
<>
<Head>
<title>News</title>
<meta name="description" content="News" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoNews />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex">
<NewsMenu />
<div className="w-layout-hflex main-news">
{Array(12).fill(0).map((_, i) => (
<NewsCard
key={i}
title="Kia Syros будет выделяться необычным стилем"
description="Компания Kia готова представить новый кроссовер Syros"
category="Новости компании"
date="17.12.2024"
image="/images/news_img.png"
/>
))}
<div className="w-layout-hflex pagination">
<a href="#" className="button_strock w-button">Показать ещё</a>
</div>
</div>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

View File

@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default function OrderConfirmation() {
return (
<>
<Head>
<title>Order Confirmation</title>
<meta name="description" content="Order Confirmation" />
</Head>
<Header />
{/* Вставь сюда содержимое <body> из order-confirmation.html, преобразовав в JSX. Все пути к картинкам и svg поменяй на /images/... */}
{/* Пример: <img src="/images/logo.svg" ... /> */}
{/* Сохрани все классы для стилей. */}
{/* TODO: Перевести формы и интерактив на React позже */}
<Footer />
</>
);
}

View File

@ -0,0 +1,170 @@
import React, { useEffect, useState } from "react";
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useRouter } from "next/router";
export default function PaymentCancelled() {
const router = useRouter();
const [paymentId, setPaymentId] = useState<string | null>(null);
const [orderId, setOrderId] = useState<string | null>(null);
useEffect(() => {
// Получаем параметры из URL
const { payment_id, order_id } = router.query;
if (payment_id) {
setPaymentId(payment_id as string);
}
if (order_id) {
setOrderId(order_id as string);
}
}, [router.query]);
const handleReturnToCart = () => {
router.push('/cart');
};
const handleContinueShopping = () => {
router.push('/catalog');
};
return (
<>
<Head>
<title>Оплата отменена - Protekauto</title>
<meta name="description" content="Оплата заказа была отменена" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="/images/webclip.png" rel="apple-touch-icon" />
</Head>
<Header />
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<div className="text-block-3">Оплата отменена</div>
</div>
</div>
</div>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex" style={{ alignItems: 'center', textAlign: 'center', padding: '4rem 0' }}>
{/* Иконка отмены */}
<div style={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#F59E0B',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '2rem'
}}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 18L18 6M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Заголовок */}
<h1 className="heading" style={{ marginBottom: '1rem', color: '#F59E0B' }}>
Оплата отменена
</h1>
{/* Описание */}
<div className="text-block-4" style={{ marginBottom: '2rem', maxWidth: 600 }}>
Вы отменили процесс оплаты. Ваш заказ сохранен в корзине,
и вы можете завершить оплату в любое время.
</div>
{/* Информация о заказе */}
{(paymentId || orderId) && (
<div style={{
backgroundColor: '#FFFBEB',
border: '1px solid #FDE68A',
borderRadius: 8,
padding: '1.5rem',
marginBottom: '2rem',
maxWidth: 400,
width: '100%'
}}>
<h3 style={{ margin: '0 0 1rem 0', fontSize: '1.1rem', fontWeight: 600, color: '#92400E' }}>
Информация о заказе
</h3>
{orderId && (
<div style={{ marginBottom: '0.5rem' }}>
<strong>Номер заказа:</strong> {orderId}
</div>
)}
{paymentId && (
<div>
<strong>ID платежа:</strong> {paymentId}
</div>
)}
</div>
)}
{/* Кнопки действий */}
<div className="w-layout-hflex" style={{ gap: '1rem', flexWrap: 'wrap', justifyContent: 'center' }}>
<button
className="submit-button fill w-button"
onClick={handleReturnToCart}
style={{ minWidth: 200 }}
>
Вернуться в корзину
</button>
<button
className="submit-button w-button"
onClick={handleContinueShopping}
style={{
minWidth: 200,
backgroundColor: 'transparent',
border: '2px solid var(--_button---primary)',
color: 'var(--_button---primary)'
}}
>
Продолжить покупки
</button>
</div>
{/* Дополнительная информация */}
<div style={{
marginTop: '3rem',
padding: '1.5rem',
backgroundColor: '#F0F9FF',
border: '1px solid #BAE6FD',
borderRadius: 8,
maxWidth: 600,
width: '100%'
}}>
<h4 style={{ margin: '0 0 1rem 0', color: '#0C4A6E' }}>
Что произошло?
</h4>
<div style={{ color: '#0369A1', lineHeight: 1.6 }}>
Процесс оплаты был прерван по вашему запросу. Это может произойти, если вы:
<ul style={{ margin: '0.5rem 0 0 0', paddingLeft: '1.5rem' }}>
<li>Нажали кнопку "Отмена" на странице оплаты</li>
<li>Закрыли окно браузера во время оплаты</li>
<li>Вернулись на предыдущую страницу</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<Footer />
</>
);
}

View File

@ -0,0 +1,200 @@
import React, { useEffect, useState } from "react";
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useRouter } from "next/router";
export default function PaymentFailed() {
const router = useRouter();
const [paymentId, setPaymentId] = useState<string | null>(null);
const [orderId, setOrderId] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string>("");
useEffect(() => {
// Получаем параметры из URL
const { payment_id, order_id, error } = router.query;
if (payment_id) {
setPaymentId(payment_id as string);
}
if (order_id) {
setOrderId(order_id as string);
}
if (error) {
setErrorMessage(error as string);
}
}, [router.query]);
const handleRetryPayment = () => {
// Возвращаемся в корзину для повторной попытки
router.push('/cart');
};
const handleContactSupport = () => {
router.push('/contacts');
};
const handleContinueShopping = () => {
router.push('/catalog');
};
return (
<>
<Head>
<title>Ошибка оплаты - Protekauto</title>
<meta name="description" content="Произошла ошибка при оплате заказа" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="/images/webclip.png" rel="apple-touch-icon" />
</Head>
<Header />
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<div className="text-block-3">Ошибка оплаты</div>
</div>
</div>
</div>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex" style={{ alignItems: 'center', textAlign: 'center', padding: '4rem 0' }}>
{/* Иконка ошибки */}
<div style={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#EF4444',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '2rem'
}}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 9V13M12 17H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Заголовок */}
<h1 className="heading" style={{ marginBottom: '1rem', color: '#EF4444' }}>
Ошибка оплаты
</h1>
{/* Описание */}
<div className="text-block-4" style={{ marginBottom: '2rem', maxWidth: 600 }}>
К сожалению, произошла ошибка при обработке платежа.
Ваш заказ не был оплачен, но вы можете попробовать еще раз.
</div>
{/* Информация об ошибке */}
{(paymentId || orderId || errorMessage) && (
<div style={{
backgroundColor: '#FEF2F2',
border: '1px solid #FECACA',
borderRadius: 8,
padding: '1.5rem',
marginBottom: '2rem',
maxWidth: 400,
width: '100%'
}}>
<h3 style={{ margin: '0 0 1rem 0', fontSize: '1.1rem', fontWeight: 600, color: '#DC2626' }}>
Детали ошибки
</h3>
{orderId && (
<div style={{ marginBottom: '0.5rem' }}>
<strong>Номер заказа:</strong> {orderId}
</div>
)}
{paymentId && (
<div style={{ marginBottom: '0.5rem' }}>
<strong>ID платежа:</strong> {paymentId}
</div>
)}
{errorMessage && (
<div style={{ color: '#DC2626' }}>
<strong>Ошибка:</strong> {errorMessage}
</div>
)}
</div>
)}
{/* Кнопки действий */}
<div className="w-layout-hflex" style={{ gap: '1rem', flexWrap: 'wrap', justifyContent: 'center' }}>
<button
className="submit-button fill w-button"
onClick={handleRetryPayment}
style={{ minWidth: 200 }}
>
Попробовать снова
</button>
<button
className="submit-button w-button"
onClick={handleContactSupport}
style={{
minWidth: 200,
backgroundColor: 'transparent',
border: '2px solid var(--_button---primary)',
color: 'var(--_button---primary)'
}}
>
Связаться с поддержкой
</button>
</div>
<div style={{ marginTop: '1rem' }}>
<button
onClick={handleContinueShopping}
style={{
background: 'none',
border: 'none',
color: '#6B7280',
textDecoration: 'underline',
cursor: 'pointer'
}}
>
Продолжить покупки
</button>
</div>
{/* Дополнительная информация */}
<div style={{
marginTop: '3rem',
padding: '1.5rem',
backgroundColor: '#F3F4F6',
border: '1px solid #D1D5DB',
borderRadius: 8,
maxWidth: 600,
width: '100%'
}}>
<h4 style={{ margin: '0 0 1rem 0', color: '#374151' }}>
Возможные причины ошибки:
</h4>
<ul style={{ margin: 0, paddingLeft: '1.5rem', color: '#6B7280' }}>
<li>Недостаточно средств на карте</li>
<li>Карта заблокирована или просрочена</li>
<li>Неверно введены данные карты</li>
<li>Технические проблемы платежной системы</li>
<li>Превышен лимит по карте</li>
</ul>
</div>
</div>
</div>
</section>
<Footer />
</>
);
}

View File

@ -0,0 +1,256 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
const InvoicePage: React.FC = () => {
const router = useRouter();
const { orderId, orderNumber } = router.query;
const [orderData, setOrderData] = useState<any>(null);
useEffect(() => {
// Здесь можно загрузить данные заказа если нужно
// Пока используем базовую информацию из query параметров
}, [orderId, orderNumber]);
const handleBackToHome = () => {
router.push('/');
};
const handleGoToProfile = () => {
router.push('/profile/orders');
};
return (
<>
<Head>
<title>Счёт на оплату - Протек Авто</title>
<meta name="description" content="Счёт на оплату заказа" />
</Head>
<div className="w-layout-vflex" style={{
minHeight: '100vh',
backgroundColor: '#f8f9fa',
padding: '40px 20px'
}}>
<div style={{
maxWidth: '600px',
margin: '0 auto',
backgroundColor: 'white',
borderRadius: '8px',
padding: '40px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
{/* Заголовок */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{
width: '64px',
height: '64px',
backgroundColor: '#28a745',
borderRadius: '50%',
margin: '0 auto 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M9 12l2 2 4-4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="12" cy="12" r="9" stroke="white" strokeWidth="2"/>
</svg>
</div>
<h1 style={{
fontSize: '24px',
fontWeight: '600',
color: '#333',
marginBottom: '8px'
}}>
Заказ оформлен!
</h1>
<p style={{
fontSize: '16px',
color: '#666',
marginBottom: '0'
}}>
Заказ {orderNumber} успешно создан
</p>
</div>
{/* Информация о счёте */}
<div style={{
backgroundColor: '#f8f9fa',
borderRadius: '8px',
padding: '24px',
marginBottom: '32px'
}}>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#333',
marginBottom: '16px'
}}>
Реквизиты для оплаты
</h3>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
Получатель
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
ООО "Протек Авто"
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
ИНН
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
1234567890
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
КПП
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
123456001
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
Расчётный счёт
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
40702810123456789012
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
Банк
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
ПАО "Сбербанк России"
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
БИК
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
044525225
</div>
</div>
<div style={{ marginBottom: '0' }}>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '4px' }}>
Корреспондентский счёт
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
30101810400000000225
</div>
</div>
</div>
{/* Важная информация */}
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '8px',
padding: '16px',
marginBottom: '32px'
}}>
<h4 style={{
fontSize: '16px',
fontWeight: '600',
color: '#856404',
marginBottom: '8px'
}}>
Важно!
</h4>
<p style={{
fontSize: '14px',
color: '#856404',
marginBottom: '8px'
}}>
В назначении платежа обязательно укажите номер заказа: <strong>{orderNumber}</strong>
</p>
<p style={{
fontSize: '14px',
color: '#856404',
marginBottom: '8px'
}}>
Счёт на оплату будет выслан на указанную при оформлении заказа электронную почту
</p>
<p style={{
fontSize: '14px',
color: '#856404',
marginBottom: '0'
}}>
После поступления оплаты заказ будет передан в обработку
</p>
</div>
{/* Кнопки действий */}
<div style={{
display: 'flex',
gap: '16px',
flexDirection: 'column'
}}>
<button
onClick={handleGoToProfile}
style={{
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '8px',
padding: '12px 24px',
fontSize: '16px',
fontWeight: '500',
cursor: 'pointer',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#0056b3';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#007bff';
}}
>
Мои заказы
</button>
<button
onClick={handleBackToHome}
style={{
backgroundColor: 'transparent',
color: '#007bff',
border: '1px solid #007bff',
borderRadius: '8px',
padding: '12px 24px',
fontSize: '16px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#007bff';
e.currentTarget.style.color = 'white';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = '#007bff';
}}
>
На главную
</button>
</div>
</div>
</div>
</>
);
};
export default InvoicePage;

View File

@ -0,0 +1,212 @@
import React, { useEffect, useState } from "react";
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useRouter } from "next/router";
import { useMutation, ApolloProvider } from "@apollo/client";
import { gql } from "@apollo/client";
import { apolloClient } from "@/lib/apollo";
const CONFIRM_PAYMENT = gql`
mutation ConfirmPayment($orderId: ID!) {
confirmPayment(orderId: $orderId) {
id
orderNumber
status
}
}
`;
function PaymentSuccessContent() {
const router = useRouter();
const [paymentId, setPaymentId] = useState<string | null>(null);
const [orderId, setOrderId] = useState<string | null>(null);
const [confirmPayment] = useMutation(CONFIRM_PAYMENT);
useEffect(() => {
// Получаем параметры из URL
const { payment_id, orderId, orderNumber } = router.query;
if (payment_id) {
setPaymentId(payment_id as string);
}
if (orderId) {
setOrderId(orderId as string);
// Проверяем авторизацию перед обновлением статуса
const userData = localStorage.getItem('userData');
console.log('PaymentSuccess: userData из localStorage:', userData);
if (!userData) {
console.log('PaymentSuccess: пользователь не авторизован, пропускаем обновление статуса');
return;
}
// Автоматически подтверждаем оплату заказа
console.log('PaymentSuccess: подтверждаем оплату заказа', orderId);
confirmPayment({
variables: {
orderId: orderId as string
}
}).then(() => {
console.log('Оплата заказа подтверждена');
}).catch((error: any) => {
console.error('Ошибка подтверждения оплаты:', error);
});
}
}, [router.query, confirmPayment]);
const handleContinueShopping = () => {
router.push('/catalog');
};
const handleViewOrders = () => {
router.push('/profile-orders');
};
return (
<>
<Head>
<title>Оплата прошла успешно - Protekauto</title>
<meta name="description" content="Ваш заказ успешно оплачен" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link href="/images/webclip.png" rel="apple-touch-icon" />
</Head>
<Header />
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<div className="text-block-3">Оплата завершена</div>
</div>
</div>
</div>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex" style={{ alignItems: 'center', textAlign: 'center', padding: '4rem 0' }}>
{/* Иконка успеха */}
<div style={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#10B981',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '2rem'
}}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Заголовок */}
<h1 className="heading" style={{ marginBottom: '1rem', color: '#10B981' }}>
Оплата прошла успешно!
</h1>
{/* Описание */}
<div className="text-block-4" style={{ marginBottom: '2rem', maxWidth: 600 }}>
Спасибо за ваш заказ! Оплата была успешно обработана.
Мы отправили подтверждение на вашу электронную почту.
</div>
{/* Информация о заказе */}
{(paymentId || orderId) && (
<div style={{
backgroundColor: '#F9FAFB',
border: '1px solid #E5E7EB',
borderRadius: 8,
padding: '1.5rem',
marginBottom: '2rem',
maxWidth: 400,
width: '100%'
}}>
<h3 style={{ margin: '0 0 1rem 0', fontSize: '1.1rem', fontWeight: 600 }}>
Детали платежа
</h3>
{orderId && (
<div style={{ marginBottom: '0.5rem' }}>
<strong>Номер заказа:</strong> {orderId}
</div>
)}
{paymentId && (
<div>
<strong>ID платежа:</strong> {paymentId}
</div>
)}
</div>
)}
{/* Кнопки действий */}
<div className="w-layout-hflex" style={{ gap: '1rem', flexWrap: 'wrap', justifyContent: 'center' }}>
<button
className="submit-button fill w-button"
onClick={handleViewOrders}
style={{ minWidth: 200 }}
>
Мои заказы
</button>
<button
className="submit-button w-button"
onClick={handleContinueShopping}
style={{
minWidth: 200,
backgroundColor: 'transparent',
border: '2px solid var(--_button---primary)',
color: 'var(--_button---primary)'
}}
>
Продолжить покупки
</button>
</div>
{/* Дополнительная информация */}
<div style={{
marginTop: '3rem',
padding: '1.5rem',
backgroundColor: '#FEF3C7',
border: '1px solid #F59E0B',
borderRadius: 8,
maxWidth: 600,
width: '100%'
}}>
<h4 style={{ margin: '0 0 1rem 0', color: '#92400E' }}>
Что дальше?
</h4>
<ul style={{ margin: 0, paddingLeft: '1.5rem', color: '#92400E' }}>
<li>Мы обработаем ваш заказ в течение 1-2 рабочих дней</li>
<li>Вы получите уведомление о статусе заказа на email</li>
<li>Отслеживать заказ можно в разделе "Мои заказы"</li>
<li>При вопросах обращайтесь в службу поддержки</li>
</ul>
</div>
</div>
</div>
</section>
<Footer />
</>
);
}
export default function PaymentSuccess() {
return (
<ApolloProvider client={apolloClient}>
<PaymentSuccessContent />
</ApolloProvider>
);
}

View File

@ -0,0 +1,39 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import Help from "@/components/Help";
import InfoPayments from "@/components/payments/InfoPayments";
import PaymentsDetails from "@/components/payments/PaymentsDetails";
import DeliveryInfo from "@/components/payments/DeliveryInfo";
import PaymentsCompony from "@/components/payments/PaymentsCompony";
export default function PaymentsMethod() {
return (
<>
<Head>
<title>Payments Method</title>
<meta name="description" content="Payments Method" />
</Head>
<InfoPayments />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<PaymentsDetails />
<DeliveryInfo />
<PaymentsCompony />
<Help />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const PaypalCheckout: React.FC = () => (
<div className="w-commerce-commercepaypalcheckoutformcontainer">
<div className="w-commerce-commercelayoutcontainer w-container">
<div className="w-commerce-commercelayoutmain">
{/* Shipping Method */}
<form className="w-commerce-commercecheckoutshippingmethodswrapper">
<div className="w-commerce-commercecheckoutblockheader">
<h2>Shipping Method</h2>
</div>
<fieldset>
<div className="w-commerce-commercecheckoutshippingmethodslist">
<label className="w-commerce-commercecheckoutshippingmethoditem">
<input required type="radio" name="shipping-method-choice" />
<div className="w-commerce-commercecheckoutshippingmethoddescriptionblock">
<div className="w-commerce-commerceboldtextblock"></div>
<div></div>
</div>
<div></div>
</label>
</div>
<div style={{display:'none'}} className="w-commerce-commercecheckoutshippingmethodsemptystate">
<div>No shipping methods are available for the address given.</div>
</div>
</fieldset>
</form>
{/* Customer Info */}
<div className="w-commerce-commercecheckoutcustomerinfosummarywrapper">
<div className="w-commerce-commercecheckoutsummaryblockheader">
<h2>Customer Information</h2>
</div>
<fieldset className="w-commerce-commercecheckoutblockcontent">
<div className="w-commerce-commercecheckoutrow">
<div className="w-commerce-commercecheckoutcolumn">
<div className="w-commerce-commercecheckoutsummaryitem">
<label className="w-commerce-commercecheckoutsummarylabel">Email</label>
<div></div>
</div>
</div>
<div className="w-commerce-commercecheckoutcolumn">
<div className="w-commerce-commercecheckoutsummaryitem">
<label className="w-commerce-commercecheckoutsummarylabel">Shipping Address</label>
<div></div>
<div></div>
<div></div>
<div className="w-commerce-commercecheckoutsummaryflexboxdiv">
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
</div>
<div></div>
</div>
</div>
</div>
</fieldset>
</div>
{/* Payment Info */}
<div className="w-commerce-commercecheckoutpaymentsummarywrapper">
<div className="w-commerce-commercecheckoutsummaryblockheader">
<h2>Payment Info</h2>
</div>
<fieldset className="w-commerce-commercecheckoutblockcontent">
<div className="w-commerce-commercecheckoutrow">
<div className="w-commerce-commercecheckoutcolumn">
<div className="w-commerce-commercecheckoutsummaryitem">
<label className="w-commerce-commercecheckoutsummarylabel">Payment Info</label>
<div className="w-commerce-commercecheckoutsummaryflexboxdiv">
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
</div>
<div className="w-commerce-commercecheckoutsummaryflexboxdiv">
<div></div>
<div> / </div>
<div></div>
</div>
</div>
</div>
<div className="w-commerce-commercecheckoutcolumn">
<div className="w-commerce-commercecheckoutsummaryitem">
<label className="w-commerce-commercecheckoutsummarylabel">Billing Address</label>
<div></div>
<div></div>
<div></div>
<div className="w-commerce-commercecheckoutsummaryflexboxdiv">
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
<div className="w-commerce-commercecheckoutsummarytextspacingondiv"></div>
</div>
<div></div>
</div>
</div>
</div>
</fieldset>
</div>
{/* Items in Order */}
<div className="w-commerce-commercecheckoutorderitemswrapper">
<div className="w-commerce-commercecheckoutsummaryblockheader">
<h2>Items in Order</h2>
</div>
<fieldset className="w-commerce-commercecheckoutblockcontent">
<div role="list" className="w-commerce-commercecheckoutorderitemslist"></div>
</fieldset>
</div>
</div>
<div className="w-commerce-commercelayoutsidebar">
<div className="w-commerce-commercecheckoutordersummarywrapper">
<div className="w-commerce-commercecheckoutsummaryblockheader">
<h2>Order Summary</h2>
</div>
<fieldset className="w-commerce-commercecheckoutblockcontent">
<div className="w-commerce-commercecheckoutsummarylineitem">
<div>Subtotal</div>
<div></div>
</div>
<div className="w-commerce-commercecheckoutordersummaryextraitemslist">
<div className="w-commerce-commercecheckoutordersummaryextraitemslistitem">
<div></div>
<div></div>
</div>
</div>
<div className="w-commerce-commercecheckoutsummarylineitem">
<div>Total</div>
<div className="w-commerce-commercecheckoutsummarytotal"></div>
</div>
</fieldset>
</div>
<button className="w-commerce-commercecheckoutplaceorderbutton">Place Order</button>
<div style={{display:'none'}} className="w-commerce-commercepaypalcheckouterrorstate">
<div aria-live="polite" className="w-checkout-error-msg">
There was an error processing your customer info. Please try again, or contact us if you continue to have problems.
</div>
</div>
</div>
</div>
</div>
);
export default PaypalCheckout;

View File

@ -0,0 +1,39 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProfileSidebar from '@/components/ProfileSidebar';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileActsMain from '@/components/profile/ProfileActsMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import NotificationMane from "@/components/profile/NotificationMane";
import Head from "next/head";
const ProfileActsPage = () => {
return (
<div className="page-wrapper">
<Head>
<title>ProfileActs</title>
<meta content="ProfileActs" property="og:title" />
<meta content="ProfileActs" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileActsMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileActsPage;

View File

@ -0,0 +1,40 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileAddressesMain from '@/components/profile/ProfileAddressesMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileAddressesPage = () => {
return (
<div className="page-wrapper">
<Head>
<title>ProfileAddresses</title>
<meta content="ProfileAddresses" property="og:title" />
<meta content="ProfileAddresses" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileAddressesMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileAddressesPage;

View File

@ -0,0 +1,32 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProfileSidebar from '@/components/ProfileSidebar';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileAnnouncementMain from '@/components/profile/ProfileAnnouncementMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import NotificationMane from "@/components/profile/NotificationMane";
const ProfileAnnouncementPage = () => {
return (
<div className="page-wrapper">
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px]">
<LKMenu />
<ProfileAnnouncementMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileAnnouncementPage;

View File

@ -0,0 +1,84 @@
import * as React from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useQuery } from '@apollo/client';
import { GET_CLIENT_ME } from '@/lib/graphql';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileBalanceMain from '@/components/profile/ProfileBalanceMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileBalancePage = () => {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const { data: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME, {
skip: !isAuthenticated,
onCompleted: (data) => {
// Проверяем есть ли у клиента юридические лица
if (!data?.clientMe?.legalEntities?.length) {
// Если нет юридических лиц, перенаправляем на настройки
router.push('/profile-settings?tab=legal');
return;
}
},
onError: (error) => {
console.error('Ошибка загрузки данных клиента:', error);
// Если ошибка авторизации, перенаправляем на главную
router.push('/');
}
});
useEffect(() => {
// Проверяем авторизацию
const token = localStorage.getItem('authToken');
if (!token) {
router.push('/');
return;
}
setIsAuthenticated(true);
}, [router]);
// Показываем загрузку пока проверяем авторизацию и данные
if (!isAuthenticated || clientLoading) {
return (
<div className="page-wrapper">
<div className="flex flex-col justify-center items-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
<div className="mt-4 text-gray-600">Загрузка...</div>
</div>
<Footer />
</div>
);
}
return (
<div className="page-wrapper">
<Head>
<title>ProfileBalance</title>
<meta content="ProfileBalance" property="og:title" />
<meta content="ProfileBalance" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileBalanceMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileBalancePage;

40
src/pages/profile-gar.tsx Normal file
View File

@ -0,0 +1,40 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileGarageMain from '@/components/profile/ProfileGarageMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileGaragePage = () => {
return (
<div className="page-wrapper">
<Head>
<title>ProfileGarage</title>
<meta content="ProfileGarage" property="og:title" />
<meta content="ProfileGarage" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileGarageMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileGaragePage;

View File

@ -0,0 +1,698 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery, useMutation } from '@apollo/client';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProfileSidebar from '@/components/ProfileSidebar';
import {
GET_USER_VEHICLES,
GET_VEHICLE_SEARCH_HISTORY,
CREATE_USER_VEHICLE,
DELETE_USER_VEHICLE,
ADD_VEHICLE_FROM_SEARCH,
DELETE_SEARCH_HISTORY_ITEM,
SEARCH_VEHICLE_BY_VIN,
UserVehicle,
VehicleSearchHistory,
UserVehicleInput
} from '@/lib/graphql/garage';
const ProfileGaragePage = () => {
const [searchQuery, setSearchQuery] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
const [newCar, setNewCar] = useState<UserVehicleInput>({
name: '',
vin: '',
comment: ''
});
const router = useRouter();
// GraphQL queries
const { data: vehiclesData, loading: vehiclesLoading, refetch: refetchVehicles } = useQuery(GET_USER_VEHICLES);
const { data: historyData, loading: historyLoading, refetch: refetchHistory } = useQuery(GET_VEHICLE_SEARCH_HISTORY);
// GraphQL mutations
const [createVehicle] = useMutation(CREATE_USER_VEHICLE, {
onCompleted: () => {
refetchVehicles();
setNewCar({ name: '', vin: '', comment: '' });
setShowAddForm(false);
},
onError: (error) => {
console.error('Ошибка создания автомобиля:', error);
alert('Ошибка при добавлении автомобиля');
}
});
const [deleteVehicle] = useMutation(DELETE_USER_VEHICLE, {
onCompleted: () => {
refetchVehicles();
},
onError: (error) => {
console.error('Ошибка удаления автомобиля:', error);
alert('Ошибка при удалении автомобиля');
}
});
const [addFromSearch] = useMutation(ADD_VEHICLE_FROM_SEARCH, {
onCompleted: () => {
refetchVehicles();
},
onError: (error) => {
console.error('Ошибка добавления из истории:', error);
alert('Ошибка при добавлении автомобиля из истории');
}
});
const [deleteHistoryItem] = useMutation(DELETE_SEARCH_HISTORY_ITEM, {
onCompleted: () => {
refetchHistory();
},
onError: (error) => {
console.error('Ошибка удаления истории:', error);
alert('Ошибка при удалении из истории');
}
});
const cars: UserVehicle[] = vehiclesData?.userVehicles || [];
const searchHistory: VehicleSearchHistory[] = historyData?.vehicleSearchHistory || [];
const handleAddCar = () => {
setShowAddForm(true);
};
const handleSaveCar = async () => {
if (!newCar.vin?.trim()) {
alert('Введите VIN номер');
return;
}
if (!newCar.name?.trim()) {
alert('Введите название автомобиля');
return;
}
try {
await createVehicle({
variables: {
input: newCar
}
});
} catch (error) {
console.error('Ошибка сохранения автомобиля:', error);
}
};
const handleCancelAdd = () => {
setNewCar({ name: '', vin: '', comment: '' });
setShowAddForm(false);
};
const handleDeleteCar = async (carId: string) => {
if (confirm('Вы уверены, что хотите удалить этот автомобиль?')) {
try {
await deleteVehicle({
variables: { id: carId }
});
} catch (error) {
console.error('Ошибка удаления автомобиля:', error);
}
}
};
const handleAddFromHistory = async (historyCar: VehicleSearchHistory) => {
try {
await addFromSearch({
variables: {
vin: historyCar.vin,
comment: ''
}
});
} catch (error) {
console.error('Ошибка добавления из истории:', error);
}
};
const handleDeleteFromHistory = async (historyId: string) => {
try {
await deleteHistoryItem({
variables: { id: historyId }
});
} catch (error) {
console.error('Ошибка удаления истории:', error);
}
};
return (
<div className="page-wrapper">
{/* Хлебные крошки */}
<section className="breadcrumbs">
<div className="w-layout-blockcontainer container w-container">
<div className="breadcrumb-wrapper">
<a href="/" className="breadcrumb-link">Главная</a>
<span className="breadcrumb-separator"></span>
<a href="/profile" className="breadcrumb-link">Личный кабинет</a>
<span className="breadcrumb-separator"></span>
<span className="breadcrumb-current">Гараж</span>
</div>
<h1 className="profile-title">Гараж</h1>
</div>
</section>
{/* Основной контент */}
<section className="profile-content">
<div className="w-layout-blockcontainer container w-container">
<div className="profile-layout">
{/* Боковое меню */}
<ProfileSidebar activeItem="garage" />
{/* Основной контент */}
<div className="profile-main">
{/* Поиск */}
<div className="search-section">
<div className="search-wrapper">
<input
type="text"
placeholder="Поиск по гаражу"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
<div className="search-icon">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M19 19L14.65 14.65" stroke="#8893A1" strokeWidth="2" strokeLinecap="round"/>
<circle cx="9" cy="9" r="7" stroke="#8893A1" strokeWidth="2"/>
</svg>
</div>
</div>
</div>
{/* Мои автомобили */}
<div className="garage-section">
<h2 className="section-title">Мои автомобили</h2>
<div className="cars-grid">
{cars.map((car) => (
<div key={car.id} className="car-card">
<div className="car-info">
<div className="car-details">
<h3 className="car-name">{car.name}</h3>
<p className="car-vin">{car.vin}</p>
</div>
{car.comment && (
<div className="car-comment-display">
{car.comment}
</div>
)}
<div className="car-actions">
<button
onClick={() => handleDeleteCar(car.id)}
className="action-button delete"
>
<svg width="18" height="16" viewBox="0 0 18 16" fill="none">
<path d="M2 4H16M7 4V2H11V4M3 4L4 14H14L15 4H3Z" stroke="#D0D0D0" strokeWidth="2" strokeLinecap="round"/>
</svg>
Удалить
</button>
<button className="action-button expand">
Развернуть
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M2 4L7 9L12 4" stroke="#000814" strokeWidth="2" strokeLinecap="round"/>
</svg>
</button>
</div>
</div>
</div>
))}
</div>
{/* Форма добавления автомобиля */}
{showAddForm && (
<div className="add-car-form">
<h3 className="form-title">Добавить авто в гараж</h3>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Название</label>
<input
type="text"
value={newCar.name}
onChange={(e) => setNewCar({...newCar, name: e.target.value})}
placeholder="Название автомобиля"
className="form-input"
/>
</div>
<div className="form-group">
<label className="form-label">VIN</label>
<input
type="text"
value={newCar.vin || ''}
onChange={(e) => setNewCar({...newCar, vin: e.target.value})}
placeholder="VIN"
className="form-input"
/>
</div>
<div className="form-group">
<label className="form-label">Комментарий</label>
<input
type="text"
value={newCar.comment || ''}
onChange={(e) => setNewCar({...newCar, comment: e.target.value})}
placeholder="Комментарий"
className="form-input"
/>
</div>
</div>
<div className="form-actions">
<button onClick={handleSaveCar} className="btn-primary">
Сохранить
</button>
<button onClick={handleCancelAdd} className="btn-secondary">
Отменить
</button>
</div>
</div>
)}
{!showAddForm && (
<button onClick={handleAddCar} className="btn-primary add-car-btn">
Добавить авто
</button>
)}
</div>
{/* История поиска */}
<div className="garage-section">
<h2 className="section-title">Ранее вы искали</h2>
<div className="history-grid">
{searchHistory.map((item) => (
<div key={item.id} className="history-card">
<div className="history-info">
<div className="car-details">
<h3 className="car-name-history">{item.brand && item.model ? `${item.brand} ${item.model}` : 'Неизвестный автомобиль'}</h3>
<p className="car-vin">{item.vin}</p>
</div>
<button
onClick={() => handleAddFromHistory(item)}
className="action-button add"
>
<svg width="18" height="16" viewBox="0 0 18 16" fill="none">
<path d="M9 3V13M3 8H15" stroke="#D0D0D0" strokeWidth="2" strokeLinecap="round"/>
</svg>
Добавить в гараж
</button>
<div className="history-meta">
<span className="search-date">{item.searchDate}</span>
<button
onClick={() => handleDeleteFromHistory(item.id)}
className="action-button delete small"
>
<svg width="18" height="16" viewBox="0 0 18 16" fill="none">
<path d="M2 4H16M7 4V2H11V4M3 4L4 14H14L15 4H3Z" stroke="#D0D0D0" strokeWidth="2" strokeLinecap="round"/>
</svg>
Удалить
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</section>
<Footer />
<style jsx>{`
.page-wrapper {
background-color: #F5F8FB;
min-height: 100vh;
}
.breadcrumbs {
background: white;
padding: 30px 0;
}
.breadcrumb-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.breadcrumb-link {
color: #000000;
text-decoration: none;
font-size: 14px;
font-weight: 400;
}
.breadcrumb-link:hover {
color: #EC1C24;
}
.breadcrumb-separator {
color: #8E9AAC;
font-size: 14px;
}
.breadcrumb-current {
color: #8E9AAC;
font-size: 14px;
}
.profile-title {
font-size: 36px;
font-weight: 800;
color: #000814;
margin: 0;
}
.profile-content {
padding: 40px 0 60px;
}
.profile-layout {
display: flex;
gap: 30px;
align-items: flex-start;
}
.profile-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.search-section {
margin-bottom: 20px;
}
.search-wrapper {
position: relative;
width: 100%;
}
.search-input {
width: 100%;
padding: 12px 50px 12px 30px;
border: none;
border-radius: 8px;
background: white;
font-size: 16px;
color: #000814;
box-sizing: border-box;
}
.search-input::placeholder {
color: #8893A1;
}
.search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
.garage-section {
background: white;
border-radius: 16px;
padding: 30px;
}
.section-title {
font-size: 30px;
font-weight: 700;
color: #000814;
margin: 0 0 30px 0;
}
.cars-grid {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 30px;
}
.car-card {
background: #F5F8FB;
border-radius: 8px;
padding: 12px 20px;
}
.car-info {
display: flex;
align-items: center;
gap: 30px;
}
.car-details {
display: flex;
flex-direction: column;
gap: 5px;
}
.car-name {
font-size: 20px;
font-weight: 700;
color: #000814;
margin: 0;
}
.car-name-history {
font-size: 18px;
font-weight: 700;
color: #000814;
margin: 0;
}
.car-vin {
font-size: 14px;
color: #424F60;
margin: 0;
}
.car-comment-display {
padding: 6px 14px;
background: white;
border: 1px solid #F0F0F0;
border-radius: 4px;
font-size: 14px;
color: #D0D0D0;
height: 32px;
display: flex;
align-items: center;
flex: 1;
max-width: 200px;
}
.car-actions {
display: flex;
align-items: center;
gap: 20px;
margin-left: auto;
}
.action-button {
display: flex;
align-items: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: #424F60;
padding: 0;
}
.action-button:hover {
color: #EC1C24;
}
.action-button:hover svg path {
stroke: #EC1C24;
}
.action-button.small {
font-size: 12px;
}
.add-car-form {
border-top: 1px solid #E6EDF6;
padding-top: 20px;
}
.form-title {
font-size: 30px;
font-weight: 700;
color: #000814;
margin: 0 0 20px 0;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
font-weight: 400;
color: #000814;
}
.form-input {
padding: 16px 24px;
border: 1px solid #D0D0D0;
border-radius: 4px;
font-size: 14px;
color: #000814;
}
.form-input:focus {
outline: none;
border-color: #EC1C24;
}
.form-actions {
display: flex;
gap: 30px;
}
.btn-primary {
padding: 14px 20px;
background: #EC1C24;
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary:hover {
background: #d91920;
}
.btn-secondary {
padding: 14px 20px;
background: transparent;
color: #000814;
border: 1px solid #EC1C24;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-secondary:hover {
background: #f8f9fa;
}
.add-car-btn {
align-self: flex-start;
}
.history-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.history-card {
background: #F5F8FB;
border-radius: 8px;
padding: 12px 20px;
height: 44px;
display: flex;
align-items: center;
}
.history-info {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 30px;
}
.history-meta {
display: flex;
align-items: center;
gap: 20px;
}
.search-date {
font-size: 14px;
color: #424F60;
}
@media (max-width: 768px) {
.profile-layout {
flex-direction: column;
gap: 20px;
}
.form-grid {
grid-template-columns: 1fr;
}
.car-info {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.car-actions {
margin-left: 0;
width: 100%;
justify-content: space-between;
}
.history-info {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.history-card {
height: auto;
padding: 15px 20px;
}
.form-actions {
flex-direction: column;
gap: 15px;
}
}
`}</style>
</div>
);
};
export default ProfileGaragePage;

View File

@ -0,0 +1,60 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileHistoryMain from '@/components/profile/ProfileHistoryMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileHistoryPage = () => {
const menuRef = React.useRef<HTMLDivElement>(null);
const [menuHeight, setMenuHeight] = React.useState<number | undefined>(undefined);
React.useEffect(() => {
if (!menuRef.current) return;
const updateHeight = () => {
setMenuHeight(menuRef.current?.offsetHeight);
};
updateHeight();
const observer = new window.ResizeObserver(updateHeight);
observer.observe(menuRef.current);
window.addEventListener('resize', updateHeight);
return () => {
observer.disconnect();
window.removeEventListener('resize', updateHeight);
};
}, []);
return (
<>
<Head>
<title>ProfileHistory</title>
<meta content="ProfileHistory" property="og:title" />
<meta content="ProfileHistory" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<div className="page-wrapper h-full flex flex-col flex-1">
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5 h-full flex-1">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto min-h-[526px] max-w-[1580px] w-full h-full">
<LKMenu ref={menuRef} />
<ProfileHistoryMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
</>
);
};
export default ProfileHistoryPage;

View File

@ -0,0 +1,31 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProfileSidebar from '@/components/ProfileSidebar';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import NotificationMane from '@/components/profile/NotificationMane';
import ProfileInfo from '@/components/profile/ProfileInfo';
const ProfileNotificationPage = () => {
return (
<div className="page-wrapper">
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px]">
<LKMenu />
<NotificationMane />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileNotificationPage;

View File

@ -0,0 +1,42 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProfileSidebar from '@/components/ProfileSidebar';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileOrdersMain from '@/components/profile/ProfileOrdersMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileOrdersPage = () => {
return (
<div className="page-wrapper">
<Head>
<title>ProfileOrders</title>
<meta content="ProfileOrders" property="og:title" />
<meta content="ProfileOrders" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileOrdersMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileOrdersPage;

40
src/pages/profile-req.tsx Normal file
View File

@ -0,0 +1,40 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileRequisitiesMain from '@/components/profile/ProfileRequisitiesMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileRequisitiesPage = () => {
return (
<div className="page-wrapper">
<Head>
<title>ProfileRequisities</title>
<meta content="ProfileRequisities" property="og:title" />
<meta content="ProfileRequisities" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileRequisitiesMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileRequisitiesPage;

View File

@ -0,0 +1,1475 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery, useMutation } from '@apollo/client';
import Header from '../components/Header';
import ProfileSidebar from '../components/ProfileSidebar';
import {
GET_CLIENT_ME,
CREATE_CLIENT_LEGAL_ENTITY,
UPDATE_CLIENT_LEGAL_ENTITY,
DELETE_CLIENT_LEGAL_ENTITY,
CREATE_CLIENT_BANK_DETAILS,
UPDATE_CLIENT_BANK_DETAILS,
DELETE_CLIENT_BANK_DETAILS
} from '../lib/graphql';
interface LegalEntity {
id: string;
shortName: string;
fullName: string;
form: string;
legalAddress: string;
actualAddress?: string;
taxSystem: string;
responsiblePhone?: string;
responsiblePosition?: string;
responsibleName?: string;
accountant?: string;
signatory?: string;
registrationReasonCode?: string;
ogrn?: string;
inn: string;
vatPercent: number;
bankDetails?: BankDetail[];
}
interface BankDetail {
id: string;
name: string;
accountNumber: string;
bankName: string;
bik: string;
correspondentAccount: string;
}
interface NewLegalEntity {
shortName: string;
fullName: string;
form: string;
legalAddress: string;
actualAddress?: string;
taxSystem: string;
responsiblePhone?: string;
responsiblePosition?: string;
responsibleName?: string;
accountant?: string;
signatory?: string;
registrationReasonCode?: string;
ogrn?: string;
inn: string;
vatPercent: number;
}
interface NewBankDetail {
name: string;
accountNumber: string;
bankName: string;
bik: string;
correspondentAccount: string;
}
const ProfileRequisites: React.FC = () => {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [showAddModal, setShowAddModal] = useState<boolean>(false);
const [showBankModal, setShowBankModal] = useState<boolean>(false);
const [editingEntity, setEditingEntity] = useState<LegalEntity | null>(null);
const [editingBankDetail, setEditingBankDetail] = useState<BankDetail | null>(null);
const [selectedLegalEntityId, setSelectedLegalEntityId] = useState<string>('');
const [legalEntities, setLegalEntities] = useState<LegalEntity[]>([]);
const [newEntity, setNewEntity] = useState<NewLegalEntity>({
shortName: '',
fullName: '',
form: 'ООО',
legalAddress: '',
actualAddress: '',
taxSystem: 'УСН',
responsiblePhone: '',
responsiblePosition: '',
responsibleName: '',
accountant: '',
signatory: '',
registrationReasonCode: '',
ogrn: '',
inn: '',
vatPercent: 20
});
const [newBankDetail, setNewBankDetail] = useState<NewBankDetail>({
name: '',
accountNumber: '',
bankName: '',
bik: '',
correspondentAccount: ''
});
// GraphQL запросы и мутации
const { data: clientData, loading: clientLoading, error: clientError, refetch } = useQuery(GET_CLIENT_ME, {
skip: !isAuthenticated,
onCompleted: (data) => {
console.log('GET_CLIENT_ME onCompleted:', data);
if (data?.clientMe?.legalEntities) {
setLegalEntities(data.clientMe.legalEntities);
console.log('Юридические лица загружены:', data.clientMe.legalEntities);
}
},
onError: (error) => {
console.error('GET_CLIENT_ME onError:', error);
console.error('Детали ошибки GET_CLIENT_ME:', JSON.stringify(error, null, 2));
}
});
const [createLegalEntity] = useMutation(CREATE_CLIENT_LEGAL_ENTITY);
const [updateLegalEntity] = useMutation(UPDATE_CLIENT_LEGAL_ENTITY);
const [deleteLegalEntity] = useMutation(DELETE_CLIENT_LEGAL_ENTITY);
const [createBankDetails] = useMutation(CREATE_CLIENT_BANK_DETAILS);
const [updateBankDetails] = useMutation(UPDATE_CLIENT_BANK_DETAILS);
const [deleteBankDetails] = useMutation(DELETE_CLIENT_BANK_DETAILS);
// Проверка авторизации
useEffect(() => {
const userData = localStorage.getItem('userData');
console.log('ProfileRequisites: проверяем userData в localStorage:', userData);
if (!userData) {
console.log('ProfileRequisites: userData не найден, перенаправляем на главную');
router.push('/');
return;
}
try {
const user = JSON.parse(userData);
console.log('ProfileRequisites: parsed user data:', user);
if (!user.id) {
console.log('ProfileRequisites: user.id не найден, перенаправляем на главную');
router.push('/');
return;
}
console.log('ProfileRequisites: авторизация успешна, user.id:', user.id);
setIsAuthenticated(true);
} catch (error) {
console.error('ProfileRequisites: ошибка парсинга userData:', error);
router.push('/');
}
}, [router]);
// Обработчики для юридических лиц
const handleAddEntity = () => {
setEditingEntity(null);
setNewEntity({
shortName: '',
fullName: '',
form: 'ООО',
legalAddress: '',
actualAddress: '',
taxSystem: 'УСН',
responsiblePhone: '',
responsiblePosition: '',
responsibleName: '',
accountant: '',
signatory: '',
registrationReasonCode: '',
ogrn: '',
inn: '',
vatPercent: 20
});
setShowAddModal(true);
};
const handleEditEntity = (entity: LegalEntity) => {
setEditingEntity(entity);
setNewEntity({
shortName: entity.shortName,
fullName: entity.fullName,
form: entity.form,
legalAddress: entity.legalAddress,
actualAddress: entity.actualAddress || '',
taxSystem: entity.taxSystem,
responsiblePhone: entity.responsiblePhone || '',
responsiblePosition: entity.responsiblePosition || '',
responsibleName: entity.responsibleName || '',
accountant: entity.accountant || '',
signatory: entity.signatory || '',
registrationReasonCode: entity.registrationReasonCode || '',
ogrn: entity.ogrn || '',
inn: entity.inn,
vatPercent: entity.vatPercent
});
setShowAddModal(true);
};
const handleSaveEntity = async () => {
try {
console.log('handleSaveEntity: начало сохранения', { editingEntity, newEntity });
if (editingEntity) {
// Обновление существующего юр. лица
console.log('handleSaveEntity: обновление юр. лица', editingEntity.id);
const { data } = await updateLegalEntity({
variables: {
id: editingEntity.id,
input: newEntity
}
});
console.log('handleSaveEntity: результат обновления', data);
if (data?.updateClientLegalEntity) {
setLegalEntities(prev =>
prev.map(entity =>
entity.id === editingEntity.id ? data.updateClientLegalEntity : entity
)
);
}
} else {
// Создание нового юр. лица
console.log('handleSaveEntity: создание нового юр. лица');
const { data } = await createLegalEntity({
variables: {
input: newEntity
}
});
console.log('handleSaveEntity: результат создания', data);
if (data?.createClientLegalEntityMe) {
setLegalEntities(prev => [...prev, data.createClientLegalEntityMe]);
}
}
setShowAddModal(false);
setEditingEntity(null);
} catch (error) {
console.error('Ошибка сохранения юридического лица:', error);
console.error('Детали ошибки:', JSON.stringify(error, null, 2));
alert('Ошибка при сохранении данных: ' + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
}
};
const handleDeleteEntity = async (entityId: string) => {
if (!confirm('Вы уверены, что хотите удалить это юридическое лицо?')) {
return;
}
try {
await deleteLegalEntity({
variables: { id: entityId }
});
setLegalEntities(prev => prev.filter(entity => entity.id !== entityId));
} catch (error) {
console.error('Ошибка удаления юридического лица:', error);
alert('Ошибка при удалении');
}
};
// Обработчики для банковских реквизитов
const handleAddBankDetail = (legalEntityId: string) => {
setSelectedLegalEntityId(legalEntityId);
setEditingBankDetail(null);
setNewBankDetail({
name: '',
accountNumber: '',
bankName: '',
bik: '',
correspondentAccount: ''
});
setShowBankModal(true);
};
const handleEditBankDetail = (bankDetail: BankDetail, legalEntityId: string) => {
setSelectedLegalEntityId(legalEntityId);
setEditingBankDetail(bankDetail);
setNewBankDetail({
name: bankDetail.name,
accountNumber: bankDetail.accountNumber,
bankName: bankDetail.bankName,
bik: bankDetail.bik,
correspondentAccount: bankDetail.correspondentAccount
});
setShowBankModal(true);
};
const handleSaveBankDetail = async () => {
try {
if (editingBankDetail) {
// Обновление существующих банковских реквизитов
const { data } = await updateBankDetails({
variables: {
id: editingBankDetail.id,
input: newBankDetail
}
});
if (data?.updateClientBankDetails) {
// Обновляем локальное состояние
setLegalEntities(prev =>
prev.map(entity =>
entity.id === selectedLegalEntityId
? {
...entity,
bankDetails: entity.bankDetails?.map(bd =>
bd.id === editingBankDetail.id ? data.updateClientBankDetails : bd
) || []
}
: entity
)
);
}
} else {
// Создание новых банковских реквизитов
const { data } = await createBankDetails({
variables: {
legalEntityId: selectedLegalEntityId,
input: newBankDetail
}
});
if (data?.createClientBankDetails) {
// Добавляем новые реквизиты к соответствующему юр. лицу
setLegalEntities(prev =>
prev.map(entity =>
entity.id === selectedLegalEntityId
? {
...entity,
bankDetails: [...(entity.bankDetails || []), data.createClientBankDetails]
}
: entity
)
);
}
}
setShowBankModal(false);
setEditingBankDetail(null);
setSelectedLegalEntityId('');
} catch (error) {
console.error('Ошибка сохранения банковских реквизитов:', error);
alert('Ошибка при сохранении данных');
}
};
const handleDeleteBankDetail = async (bankDetailId: string, legalEntityId: string) => {
if (!confirm('Вы уверены, что хотите удалить эти банковские реквизиты?')) {
return;
}
try {
await deleteBankDetails({
variables: { id: bankDetailId }
});
// Удаляем банковские реквизиты из локального состояния
setLegalEntities(prev =>
prev.map(entity =>
entity.id === legalEntityId
? {
...entity,
bankDetails: entity.bankDetails?.filter(bd => bd.id !== bankDetailId) || []
}
: entity
)
);
} catch (error) {
console.error('Ошибка удаления банковских реквизитов:', error);
alert('Ошибка при удалении');
}
};
const handleInputChange = (field: keyof NewLegalEntity, value: string | number) => {
setNewEntity(prev => ({
...prev,
[field]: value
}));
};
const handleBankInputChange = (field: keyof NewBankDetail, value: string) => {
setNewBankDetail(prev => ({
...prev,
[field]: value
}));
};
if (!isAuthenticated) {
return null;
}
if (clientLoading) {
return (
<div className="loading-wrapper">
<div className="loading-text">Загрузка...</div>
</div>
);
}
return (
<div className="page-wrapper">
{/* Хлебные крошки */}
<section className="breadcrumbs">
<div className="container">
<div className="breadcrumb-wrapper">
<a href="/" className="breadcrumb-link">Главная</a>
<span className="breadcrumb-separator"></span>
<a href="/profile-settings" className="breadcrumb-link">Личный кабинет</a>
<span className="breadcrumb-separator"></span>
<span className="breadcrumb-current">Реквизиты</span>
</div>
<h1 className="profile-title">Реквизиты</h1>
</div>
</section>
{/* Основной контент */}
<section className="profile-section">
<div className="container">
<div className="profile-layout">
<ProfileSidebar activeItem="requisites" />
<div className="profile-content">
{/* Заголовок и кнопка добавления */}
<div className="requisites-header">
<div className="header-content">
<h2 className="section-title">Юридические лица</h2>
<p className="section-description">
Управляйте информацией о ваших юридических лицах и банковских реквизитах
</p>
</div>
<button
onClick={handleAddEntity}
className="btn-add"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4V16M4 10H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
Добавить юр. лицо
</button>
</div>
{/* Список юридических лиц */}
<div className="requisites-list">
{legalEntities.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="16" width="48" height="32" rx="4" stroke="#D0D0D0" strokeWidth="2"/>
<path d="M16 24H48M16 32H40M16 40H32" stroke="#D0D0D0" strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<h3 className="empty-title">Нет добавленных юридических лиц</h3>
<p className="empty-description">
Добавьте информацию о ваших юридических лицах для оформления документов
</p>
<button
onClick={handleAddEntity}
className="btn-primary"
>
Добавить первое юр. лицо
</button>
</div>
) : (
legalEntities.map((entity) => (
<div key={entity.id} className="entity-card">
<div className="entity-main">
<div className="entity-info">
<h3 className="entity-name">{entity.shortName}</h3>
<p className="entity-full-name">{entity.fullName}</p>
<div className="entity-details">
<span className="entity-detail">
<strong>ИНН:</strong> {entity.inn}
</span>
<span className="entity-detail">
<strong>Форма:</strong> {entity.form}
</span>
<span className="entity-detail">
<strong>Налоговая система:</strong> {entity.taxSystem}
</span>
</div>
<p className="entity-address">
<strong>Юридический адрес:</strong> {entity.legalAddress}
</p>
</div>
<div className="entity-actions">
<button
onClick={() => handleEditEntity(entity)}
className="action-btn edit-btn"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.334 2.00004C11.5091 1.82494 11.7169 1.68605 11.9457 1.59129C12.1745 1.49653 12.4197 1.44775 12.6673 1.44775C12.9149 1.44775 13.1601 1.49653 13.3889 1.59129C13.6177 1.68605 13.8255 1.82494 14.0007 2.00004C14.1758 2.17513 14.3147 2.383 14.4094 2.61178C14.5042 2.84055 14.553 3.08575 14.553 3.33337C14.553 3.58099 14.5042 3.82619 14.4094 4.05497C14.3147 4.28374 14.1758 4.49161 14.0007 4.66671L5.00065 13.6667L1.33398 14.6667L2.33398 11L11.334 2.00004Z" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Редактировать
</button>
<button
onClick={() => handleDeleteEntity(entity.id)}
className="action-btn delete-btn"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H14M12.6667 4V13.3333C12.6667 13.687 12.5262 14.0261 12.2761 14.2761C12.0261 14.5262 11.687 14.6667 11.3333 14.6667H4.66667C4.31304 14.6667 3.97391 14.5262 3.72386 14.2761C3.47381 14.0261 3.33333 13.687 3.33333 13.3333V4M5.33333 4V2.66667C5.33333 2.31304 5.47381 1.97391 5.72386 1.72386C5.97391 1.47381 6.31304 1.33333 6.66667 1.33333H9.33333C9.687 1.33333 10.0261 1.47381 10.2761 1.72386C10.5262 1.97391 10.6667 2.31304 10.6667 2.66667V4" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M6.66699 7.33337V11.3334M9.33366 7.33337V11.3334" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Удалить
</button>
</div>
</div>
{/* Банковские реквизиты */}
<div className="bank-details">
<div className="bank-details-header">
<h4 className="bank-details-title">Банковские реквизиты</h4>
<button
onClick={() => handleAddBankDetail(entity.id)}
className="btn-add small"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3.2V12.8M3.2 8H12.8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
Добавить реквизиты
</button>
</div>
{entity.bankDetails && entity.bankDetails.length > 0 ? (
<div className="bank-details-list">
{entity.bankDetails.map((bankDetail) => (
<div key={bankDetail.id} className="bank-detail-item">
<div className="bank-info">
<p className="bank-name">{bankDetail.name}</p>
<p className="bank-account">Р/с: {bankDetail.accountNumber}</p>
<p className="bank-bik">БИК: {bankDetail.bik}</p>
<p className="bank-correspondent">К/с: {bankDetail.correspondentAccount}</p>
<p className="bank-bank-name">Банк: {bankDetail.bankName}</p>
</div>
<div className="bank-actions">
<button
onClick={() => handleEditBankDetail(bankDetail, entity.id)}
className="action-btn edit-btn"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.334 2.00004C11.5091 1.82494 11.7169 1.68605 11.9457 1.59129C12.1745 1.49653 12.4197 1.44775 12.6673 1.44775C12.9149 1.44775 13.1601 1.49653 13.3889 1.59129C13.6177 1.68605 13.8255 1.82494 14.0007 2.00004C14.1758 2.17513 14.3147 2.383 14.4094 2.61178C14.5042 2.84055 14.553 3.08575 14.553 3.33337C14.553 3.58099 14.5042 3.82619 14.4094 4.05497C14.3147 4.28374 14.1758 4.49161 14.0007 4.66671L5.00065 13.6667L1.33398 14.6667L2.33398 11L11.334 2.00004Z" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Редактировать
</button>
<button
onClick={() => handleDeleteBankDetail(bankDetail.id, entity.id)}
className="action-btn delete-btn"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H14M12.6667 4V13.3333C12.6667 13.687 12.5262 14.0261 12.2761 14.2761C12.0261 14.5262 11.687 14.6667 11.3333 14.6667H4.66667C4.31304 14.6667 3.97391 14.5262 3.72386 14.2761C3.47381 14.0261 3.33333 13.687 3.33333 13.3333V4M5.33333 4V2.66667C5.33333 2.31304 5.47381 1.97391 5.72386 1.72386C5.97391 1.47381 6.31304 1.33333 6.66667 1.33333H9.33333C9.687 1.33333 10.0261 1.47381 10.2761 1.72386C10.5262 1.97391 10.6667 2.31304 10.6667 2.66667V4" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M6.66699 7.33337V11.3334M9.33366 7.33337V11.3334" stroke="currentColor" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Удалить
</button>
</div>
</div>
))}
</div>
) : (
<div className="bank-details-empty">
<p className="empty-text">Банковские реквизиты не добавлены</p>
</div>
)}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</section>
{/* Модальное окно добавления/редактирования */}
{showAddModal && (
<div className="modal-overlay" onClick={() => setShowAddModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">
{editingEntity ? 'Редактировать юридическое лицо' : 'Добавить юридическое лицо'}
</h2>
<button
onClick={() => setShowAddModal(false)}
className="modal-close"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</div>
<div className="modal-form">
<div className="form-row">
<div className="form-field">
<label className="form-label">Краткое наименование *</label>
<input
type="text"
value={newEntity.shortName}
onChange={(e) => handleInputChange('shortName', e.target.value)}
className="form-input"
placeholder="ООО «Компания»"
required
/>
</div>
<div className="form-field">
<label className="form-label">Форма *</label>
<select
value={newEntity.form}
onChange={(e) => handleInputChange('form', e.target.value)}
className="form-select"
required
>
<option value="ООО">ООО</option>
<option value="ИП">ИП</option>
<option value="АО">АО</option>
<option value="ПАО">ПАО</option>
<option value="ЗАО">ЗАО</option>
</select>
</div>
</div>
<div className="form-field">
<label className="form-label">Полное наименование *</label>
<input
type="text"
value={newEntity.fullName}
onChange={(e) => handleInputChange('fullName', e.target.value)}
className="form-input"
placeholder="Общество с ограниченной ответственностью «Компания»"
required
/>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">ИНН *</label>
<input
type="text"
value={newEntity.inn}
onChange={(e) => handleInputChange('inn', e.target.value)}
className="form-input"
placeholder="1234567890"
required
/>
</div>
<div className="form-field">
<label className="form-label">ОГРН</label>
<input
type="text"
value={newEntity.ogrn}
onChange={(e) => handleInputChange('ogrn', e.target.value)}
className="form-input"
placeholder="1234567890123"
/>
</div>
</div>
<div className="form-field">
<label className="form-label">Юридический адрес *</label>
<input
type="text"
value={newEntity.legalAddress}
onChange={(e) => handleInputChange('legalAddress', e.target.value)}
className="form-input"
placeholder="г. Москва, ул. Примерная, д. 1"
required
/>
</div>
<div className="form-field">
<label className="form-label">Фактический адрес</label>
<input
type="text"
value={newEntity.actualAddress}
onChange={(e) => handleInputChange('actualAddress', e.target.value)}
className="form-input"
placeholder="г. Москва, ул. Примерная, д. 1"
/>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Налоговая система *</label>
<select
value={newEntity.taxSystem}
onChange={(e) => handleInputChange('taxSystem', e.target.value)}
className="form-select"
required
>
<option value="УСН">УСН</option>
<option value="ОСНО">ОСНО</option>
<option value="ЕНВД">ЕНВД</option>
<option value="ПСН">ПСН</option>
</select>
</div>
<div className="form-field">
<label className="form-label">НДС (%)</label>
<input
type="number"
value={newEntity.vatPercent}
onChange={(e) => handleInputChange('vatPercent', Number(e.target.value))}
className="form-input"
min="0"
max="100"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">ФИО ответственного</label>
<input
type="text"
value={newEntity.responsibleName}
onChange={(e) => handleInputChange('responsibleName', e.target.value)}
className="form-input"
placeholder="Иванов Иван Иванович"
/>
</div>
<div className="form-field">
<label className="form-label">Должность ответственного</label>
<input
type="text"
value={newEntity.responsiblePosition}
onChange={(e) => handleInputChange('responsiblePosition', e.target.value)}
className="form-input"
placeholder="Генеральный директор"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Телефон ответственного</label>
<input
type="tel"
value={newEntity.responsiblePhone}
onChange={(e) => handleInputChange('responsiblePhone', e.target.value)}
className="form-input"
placeholder="+7 (999) 123-45-67"
/>
</div>
<div className="form-field">
<label className="form-label">Бухгалтер</label>
<input
type="text"
value={newEntity.accountant}
onChange={(e) => handleInputChange('accountant', e.target.value)}
className="form-input"
placeholder="Петрова Анна Сергеевна"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Подписант</label>
<input
type="text"
value={newEntity.signatory}
onChange={(e) => handleInputChange('signatory', e.target.value)}
className="form-input"
placeholder="Иванов И.И."
/>
</div>
<div className="form-field">
<label className="form-label">КПП</label>
<input
type="text"
value={newEntity.registrationReasonCode}
onChange={(e) => handleInputChange('registrationReasonCode', e.target.value)}
className="form-input"
placeholder="123456789"
/>
</div>
</div>
</div>
<div className="modal-actions">
<button
onClick={() => setShowAddModal(false)}
className="btn-secondary"
>
Отмена
</button>
<button
onClick={handleSaveEntity}
className="btn-primary"
>
{editingEntity ? 'Сохранить изменения' : 'Добавить юр. лицо'}
</button>
</div>
</div>
</div>
)}
{/* Модальное окно добавления/редактирования банковских реквизитов */}
{showBankModal && (
<div className="modal-overlay" onClick={() => setShowBankModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">
{editingBankDetail ? 'Редактировать банковские реквизиты' : 'Добавить банковские реквизиты'}
</h2>
<button
onClick={() => setShowBankModal(false)}
className="modal-close"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</div>
<div className="modal-form">
<div className="form-row">
<div className="form-field">
<label className="form-label">Название счета *</label>
<input
type="text"
value={newBankDetail.name}
onChange={(e) => handleBankInputChange('name', e.target.value)}
className="form-input"
placeholder="Расчетный счет"
required
/>
</div>
<div className="form-field">
<label className="form-label"> Расчетного счета *</label>
<input
type="text"
value={newBankDetail.accountNumber}
onChange={(e) => handleBankInputChange('accountNumber', e.target.value)}
className="form-input"
placeholder="40702810000000000000"
required
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">БИК *</label>
<input
type="text"
value={newBankDetail.bik}
onChange={(e) => handleBankInputChange('bik', e.target.value)}
className="form-input"
placeholder="044525225"
required
/>
</div>
<div className="form-field">
<label className="form-label">Наименование банка *</label>
<input
type="text"
value={newBankDetail.bankName}
onChange={(e) => handleBankInputChange('bankName', e.target.value)}
className="form-input"
placeholder="ПАО СБЕРБАНК"
required
/>
</div>
</div>
<div className="form-field">
<label className="form-label">Корреспондентский счет *</label>
<input
type="text"
value={newBankDetail.correspondentAccount}
onChange={(e) => handleBankInputChange('correspondentAccount', e.target.value)}
className="form-input"
placeholder="30101810400000000225"
required
/>
</div>
</div>
<div className="modal-actions">
<button
onClick={() => setShowBankModal(false)}
className="btn-secondary"
>
Отмена
</button>
<button
onClick={handleSaveBankDetail}
className="btn-primary"
>
{editingBankDetail ? 'Сохранить изменения' : 'Добавить реквизиты'}
</button>
</div>
</div>
</div>
)}
<style jsx>{`
.page-wrapper {
background-color: #F5F8FB;
min-height: 100vh;
font-family: 'Onest', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.breadcrumbs {
background: white;
padding: 30px 0;
}
.container {
max-width: 1660px;
margin: 0 auto;
padding: 0 130px;
}
.breadcrumb-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.breadcrumb-link {
color: #000000;
text-decoration: none;
font-size: 14px;
font-weight: 400;
line-height: 1.4;
cursor: pointer;
}
.breadcrumb-link:hover {
color: #EC1C24;
}
.breadcrumb-separator {
color: #8E9AAC;
font-size: 14px;
line-height: 1.3;
}
.breadcrumb-current {
color: #8E9AAC;
font-size: 14px;
line-height: 1.3;
}
.profile-title {
font-size: 36px;
font-weight: 800;
color: #000814;
margin: 0;
line-height: 1;
}
.profile-section {
padding: 40px 0 60px;
}
.profile-layout {
display: flex;
gap: 30px;
align-items: flex-start;
}
.profile-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.requisites-header {
background: white;
border-radius: 16px;
padding: 30px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 30px;
}
.header-content {
flex: 1;
}
.section-title {
font-size: 30px;
font-weight: 700;
color: #000814;
margin: 0 0 8px 0;
line-height: 1.2;
}
.section-description {
font-size: 16px;
color: #8E9AAC;
margin: 0;
line-height: 1.4;
}
.btn-add {
display: flex;
align-items: center;
gap: 8px;
background: #EC1C24;
color: white;
border: none;
border-radius: 12px;
padding: 14px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
}
.btn-add:hover {
background: #d91920;
}
.requisites-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.empty-state {
background: white;
border-radius: 16px;
padding: 60px 30px;
text-align: center;
}
.empty-icon {
margin: 0 auto 24px;
width: 64px;
height: 64px;
}
.empty-title {
font-size: 24px;
font-weight: 600;
color: #000814;
margin: 0 0 8px 0;
}
.empty-description {
font-size: 16px;
color: #8E9AAC;
margin: 0 0 32px 0;
line-height: 1.4;
}
.btn-primary {
background: #EC1C24;
color: white;
border: none;
border-radius: 12px;
padding: 14px 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary:hover {
background: #d91920;
}
.entity-card {
background: white;
border-radius: 16px;
padding: 30px;
display: flex;
flex-direction: column;
gap: 24px;
}
.entity-main {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 30px;
}
.entity-info {
flex: 1;
}
.entity-name {
font-size: 24px;
font-weight: 700;
color: #000814;
margin: 0 0 4px 0;
line-height: 1.2;
}
.entity-full-name {
font-size: 16px;
color: #424F60;
margin: 0 0 16px 0;
line-height: 1.4;
}
.entity-details {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-bottom: 12px;
}
.entity-detail {
font-size: 14px;
color: #424F60;
}
.entity-detail strong {
color: #000814;
}
.entity-address {
font-size: 14px;
color: #424F60;
margin: 0;
line-height: 1.4;
}
.entity-address strong {
color: #000814;
}
.entity-actions {
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-end;
}
.action-btn {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: 1px solid #D0D0D0;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.edit-btn {
color: #424F60;
}
.edit-btn:hover {
border-color: #EC1C24;
color: #EC1C24;
}
.delete-btn {
color: #DC2626;
border-color: #DC2626;
}
.delete-btn:hover {
background: #DC2626;
color: white;
}
.bank-details {
border-top: 1px solid #E6EDF6;
padding-top: 24px;
}
.bank-details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.bank-details-title {
font-size: 18px;
font-weight: 600;
color: #000814;
margin: 0;
}
.btn-add.small {
padding: 8px 12px;
font-size: 14px;
gap: 6px;
}
.bank-details-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.bank-details-empty {
padding: 20px;
text-align: center;
background: #F5F8FB;
border-radius: 8px;
border: 1px dashed #D0D0D0;
}
.empty-text {
font-size: 14px;
color: #8E9AAC;
margin: 0;
}
.bank-detail-item {
background: #F5F8FB;
border-radius: 8px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.bank-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.bank-info p {
margin: 0;
font-size: 14px;
color: #424F60;
}
.bank-name {
font-weight: 600;
color: #000814 !important;
font-size: 16px !important;
}
.bank-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 16px;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30px 30px 0;
}
.modal-title {
font-size: 24px;
font-weight: 700;
color: #000814;
margin: 0;
}
.modal-close {
background: none;
border: none;
cursor: pointer;
color: #8E9AAC;
padding: 4px;
border-radius: 4px;
transition: color 0.2s;
}
.modal-close:hover {
color: #424F60;
}
.modal-form {
padding: 30px;
display: flex;
flex-direction: column;
gap: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
color: #000814;
font-weight: 500;
}
.form-input,
.form-select {
border: 1px solid #D0D0D0;
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
color: #424F60;
font-family: 'Onest', sans-serif;
outline: none;
transition: border-color 0.2s;
background: white;
}
.form-input:focus,
.form-select:focus {
border-color: #EC1C24;
}
.form-select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23424F60' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px;
appearance: none;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 16px;
padding: 0 30px 30px;
border-top: 1px solid #E6EDF6;
margin-top: 20px;
padding-top: 20px;
}
.btn-secondary {
background: transparent;
color: #424F60;
border: 1px solid #D0D0D0;
border-radius: 8px;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
border-color: #424F60;
color: #000814;
}
.loading-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #F5F8FB;
}
.loading-text {
font-size: 18px;
color: #000814;
}
@media (max-width: 768px) {
.container {
padding: 0 20px;
}
.profile-layout {
flex-direction: column;
gap: 20px;
}
.requisites-header {
flex-direction: column;
align-items: stretch;
gap: 20px;
}
.entity-main {
flex-direction: column;
gap: 20px;
}
.entity-actions {
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.form-row {
grid-template-columns: 1fr;
}
.modal-content {
margin: 10px;
}
.modal-header,
.modal-form,
.modal-actions {
padding-left: 20px;
padding-right: 20px;
}
.modal-actions {
flex-direction: column;
}
.profile-title {
font-size: 28px;
}
.section-title {
font-size: 24px;
}
.entity-name {
font-size: 20px;
}
}
`}</style>
</div>
);
};
export default ProfileRequisites;

40
src/pages/profile-set.tsx Normal file
View File

@ -0,0 +1,40 @@
import * as React from "react";
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import LKMenu from '@/components/LKMenu';
import ProfileSettingsMain from '@/components/profile/ProfileSettingsMain';
import ProfileInfo from '@/components/profile/ProfileInfo';
import Head from "next/head";
const ProfileSettingsPage = () => {
return (
<div className="page-wrapper h-full flex flex-col flex-1">
<Head>
<title>ProfileHistory</title>
<meta content="ProfileSettings" property="og:title" />
<meta content="ProfileSettings" property="twitter:title" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ProfileInfo />
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
<LKMenu />
<ProfileSettingsMain />
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<MobileMenuBottomSection />
<Footer />
</div>
);
};
export default ProfileSettingsPage;

View File

@ -0,0 +1,1165 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery, useMutation } from '@apollo/client';
import Header from '../components/Header';
import ProfileSidebar from '../components/ProfileSidebar';
import {
GET_CLIENT_ME,
UPDATE_CLIENT_PERSONAL_DATA,
CREATE_CLIENT_LEGAL_ENTITY,
UPDATE_CLIENT_LEGAL_ENTITY,
DELETE_CLIENT_LEGAL_ENTITY
} from '../lib/graphql';
interface PersonalData {
name: string;
phone: string;
email: string;
emailNotifications: boolean;
smsNotifications: boolean;
pushNotifications: boolean;
}
interface LegalEntity {
id: string;
shortName: string;
fullName: string;
form: string;
legalAddress: string;
actualAddress?: string;
taxSystem: string;
responsiblePhone?: string;
responsiblePosition?: string;
responsibleName?: string;
accountant?: string;
signatory?: string;
registrationReasonCode?: string;
ogrn?: string;
inn: string;
vatPercent: number;
}
interface NewLegalEntity {
shortName: string;
fullName: string;
form: string;
legalAddress: string;
actualAddress?: string;
taxSystem: string;
responsiblePhone?: string;
responsiblePosition?: string;
responsibleName?: string;
accountant?: string;
signatory?: string;
registrationReasonCode?: string;
ogrn?: string;
inn: string;
vatPercent?: number;
}
const ProfileSettings: React.FC = () => {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [showAddModal, setShowAddModal] = useState<boolean>(false);
const [editingEntity, setEditingEntity] = useState<LegalEntity | null>(null);
const [clientId, setClientId] = useState<string>('');
const [personalData, setPersonalData] = useState<PersonalData>({
name: '',
phone: '',
email: '',
emailNotifications: true,
smsNotifications: false,
pushNotifications: false
});
const [newEntity, setNewEntity] = useState<NewLegalEntity>({
shortName: '',
fullName: '',
form: '',
legalAddress: '',
actualAddress: '',
taxSystem: '',
responsiblePhone: '',
responsiblePosition: '',
responsibleName: '',
accountant: '',
signatory: '',
registrationReasonCode: '',
ogrn: '',
inn: '',
vatPercent: 0
});
// GraphQL запросы и мутации
const { data: clientData, loading: clientLoading, error: clientError, refetch } = useQuery(GET_CLIENT_ME, {
skip: !isAuthenticated,
onCompleted: (data) => {
console.log('GET_CLIENT_ME onCompleted:', data);
if (data?.clientMe) {
setPersonalData({
name: data.clientMe.name || '',
phone: data.clientMe.phone || '',
email: data.clientMe.email || '',
emailNotifications: data.clientMe.emailNotifications ?? true,
smsNotifications: data.clientMe.smsNotifications ?? false,
pushNotifications: data.clientMe.pushNotifications ?? false
});
}
},
onError: (error) => {
console.error('GET_CLIENT_ME onError:', error);
}
});
const [updatePersonalData] = useMutation(UPDATE_CLIENT_PERSONAL_DATA);
const [createLegalEntity] = useMutation(CREATE_CLIENT_LEGAL_ENTITY);
const [updateLegalEntity] = useMutation(UPDATE_CLIENT_LEGAL_ENTITY);
const [deleteLegalEntity] = useMutation(DELETE_CLIENT_LEGAL_ENTITY);
useEffect(() => {
const checkAuth = () => {
const userData = localStorage.getItem('userData');
if (userData) {
const user = JSON.parse(userData);
setIsAuthenticated(true);
setClientId(user.id);
} else {
router.push('/');
return;
}
};
checkAuth();
}, [router]);
const handlePersonalDataChange = (field: keyof PersonalData, value: string | boolean) => {
setPersonalData(prev => ({
...prev,
[field]: value
}));
};
const handleNewEntityChange = (field: keyof NewLegalEntity, value: string | number) => {
setNewEntity(prev => ({
...prev,
[field]: value
}));
};
const handleSavePersonalData = async () => {
try {
await updatePersonalData({
variables: {
input: {
type: 'INDIVIDUAL', // Добавляем обязательное поле
name: personalData.name,
email: personalData.email,
phone: personalData.phone,
emailNotifications: personalData.emailNotifications,
smsNotifications: personalData.smsNotifications,
pushNotifications: personalData.pushNotifications
}
}
});
alert('Персональные данные сохранены');
} catch (error) {
console.error('Ошибка сохранения данных:', error);
alert('Ошибка сохранения данных');
}
};
const handleAddEntity = async () => {
if (!newEntity.shortName || !newEntity.inn) {
alert('Заполните обязательные поля');
return;
}
try {
await createLegalEntity({
variables: {
input: newEntity
}
});
resetForm();
setShowAddModal(false);
refetch();
} catch (error) {
console.error('Ошибка создания юр. лица:', error);
alert('Ошибка создания юридического лица');
}
};
const resetForm = () => {
setNewEntity({
shortName: '',
fullName: '',
form: '',
legalAddress: '',
actualAddress: '',
taxSystem: '',
responsiblePhone: '',
responsiblePosition: '',
responsibleName: '',
accountant: '',
signatory: '',
registrationReasonCode: '',
ogrn: '',
inn: '',
vatPercent: 0
});
setEditingEntity(null);
};
const handleDeleteEntity = async (id: string) => {
if (window.confirm('Удалить юридическое лицо?')) {
try {
await deleteLegalEntity({
variables: { id }
});
refetch();
} catch (error) {
console.error('Ошибка удаления юр. лица:', error);
alert('Ошибка удаления юридического лица');
}
}
};
const handleEditEntity = (entity: LegalEntity) => {
setEditingEntity(entity);
setNewEntity({
shortName: entity.shortName,
fullName: entity.fullName,
form: entity.form,
legalAddress: entity.legalAddress,
actualAddress: entity.actualAddress || '',
taxSystem: entity.taxSystem,
responsiblePhone: entity.responsiblePhone || '',
responsiblePosition: entity.responsiblePosition || '',
responsibleName: entity.responsibleName || '',
accountant: entity.accountant || '',
signatory: entity.signatory || '',
registrationReasonCode: entity.registrationReasonCode || '',
ogrn: entity.ogrn || '',
inn: entity.inn,
vatPercent: entity.vatPercent
});
setShowAddModal(true);
};
const handleUpdateEntity = async () => {
if (!editingEntity || !newEntity.shortName || !newEntity.inn) {
alert('Заполните обязательные поля');
return;
}
try {
await updateLegalEntity({
variables: {
id: editingEntity.id,
input: newEntity
}
});
resetForm();
setShowAddModal(false);
refetch();
} catch (error) {
console.error('Ошибка обновления юр. лица:', error);
alert('Ошибка обновления юридического лица');
}
};
const closeModal = () => {
setShowAddModal(false);
resetForm();
};
if (clientLoading || !isAuthenticated) {
return (
<div className="loading-wrapper">
<div className="loading-text">Загрузка...</div>
</div>
);
}
if (clientError) {
console.error('GraphQL Error:', clientError);
return (
<div className="error-wrapper">
<div className="error-text">Ошибка загрузки данных: {clientError.message}</div>
<button onClick={() => window.location.reload()}>Перезагрузить</button>
</div>
);
}
const legalEntities = clientData?.clientMe?.legalEntities || [];
return (
<div className="page-wrapper">
{/* Хлебные крошки */}
<div className="breadcrumbs">
<div className="container">
<div className="breadcrumb-wrapper">
<span className="breadcrumb-link">Главная</span>
<span className="breadcrumb-separator"></span>
<span className="breadcrumb-link">Личный кабинет</span>
<span className="breadcrumb-separator"></span>
<span className="breadcrumb-current">Настройки аккаунта</span>
</div>
<h1 className="profile-title">
Настройки аккаунта
</h1>
</div>
</div>
{/* Основной контент */}
<div className="profile-section">
<div className="container">
<div className="profile-layout">
<ProfileSidebar activeItem="Настройки" />
<div className="profile-content">
{/* Персональные данные */}
<div className="settings-card">
<h2 className="card-title">
Персональные данные
</h2>
<div className="personal-form">
<div className="form-grid">
<div className="form-field">
<label className="form-label">Имя</label>
<input
type="text"
value={personalData.name}
onChange={(e) => handlePersonalDataChange('name', e.target.value)}
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Телефон</label>
<input
type="tel"
value={personalData.phone}
onChange={(e) => handlePersonalDataChange('phone', e.target.value)}
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">E-mail</label>
<input
type="email"
value={personalData.email}
onChange={(e) => handlePersonalDataChange('email', e.target.value)}
placeholder="@"
className="form-input email-input"
/>
</div>
</div>
<div className="notification-section">
<div className="notification-toggle">
<label className="toggle-label">
<div className="toggle-container">
<input
type="checkbox"
checked={personalData.emailNotifications}
onChange={(e) => handlePersonalDataChange('emailNotifications', e.target.checked)}
className="hidden-checkbox"
/>
<div className={`toggle-switch ${personalData.emailNotifications ? 'active' : 'inactive'}`}>
<div className={`toggle-knob ${personalData.emailNotifications ? 'active' : 'inactive'}`} />
</div>
</div>
<span className="toggle-text">
Получать уведомления по Email
</span>
</label>
</div>
<div className="notification-toggle">
<label className="toggle-label">
<div className="toggle-container">
<input
type="checkbox"
checked={personalData.smsNotifications}
onChange={(e) => handlePersonalDataChange('smsNotifications', e.target.checked)}
className="hidden-checkbox"
/>
<div className={`toggle-switch ${personalData.smsNotifications ? 'active' : 'inactive'}`}>
<div className={`toggle-knob ${personalData.smsNotifications ? 'active' : 'inactive'}`} />
</div>
</div>
<span className="toggle-text">
Получать SMS уведомления
</span>
</label>
</div>
</div>
</div>
</div>
{/* Юридические лица */}
<div className="settings-card">
<h2 className="card-title">
Юридические лица
</h2>
{legalEntities.length > 0 ? (
<div className="legal-entities">
{legalEntities.map((entity: LegalEntity) => (
<div key={entity.id} className="entity-card">
<div className="entity-header">
<div className="entity-info">
<div>
<h3>{entity.shortName}</h3>
<p>ИНН {entity.inn}</p>
<div className="entity-actions">
<button className="entity-link">
<svg width="16" height="14" viewBox="0 0 16 14" fill="none">
<path d="M2 3H14M2 7H14M2 11H14" stroke="currentColor" strokeWidth="1.5"/>
</svg>
Реквизиты компании
</button>
</div>
</div>
</div>
<div className="entity-controls">
<button
onClick={() => handleEditEntity(entity)}
className="control-button"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M2 10L10 2L12 4L4 12H2V10Z" stroke="currentColor" strokeWidth="1.5"/>
</svg>
Редактировать
</button>
<button
onClick={() => handleDeleteEntity(entity.id)}
className="control-button"
>
<svg width="14" height="16" viewBox="0 0 14 16" fill="none">
<path d="M1 4H13M5 1H9M5 7V13M9 7V13" stroke="currentColor" strokeWidth="1.5"/>
</svg>
Удалить
</button>
</div>
</div>
</div>
))}
</div>
) : (
<div className="empty-state">
<p>У вас пока нет добавленных юридических лиц</p>
</div>
)}
</div>
{/* Кнопки управления */}
<div className="action-buttons">
<button
onClick={handleSavePersonalData}
className="btn-primary"
>
Сохранить изменения
</button>
<button
onClick={() => setShowAddModal(true)}
className="btn-secondary"
>
Добавить юридическое лицо
</button>
</div>
</div>
</div>
</div>
</div>
{/* Модальное окно */}
{showAddModal && (
<div className="modal-overlay">
<div className="modal-content">
<h2 className="modal-title">
{editingEntity ? 'Редактирование юридического лица' : 'Добавление юридического лица'}
</h2>
<div className="modal-form">
<div className="form-row">
<div className="form-field">
<label className="form-label">ИНН *</label>
<input
type="text"
value={newEntity.inn}
onChange={(e) => handleNewEntityChange('inn', e.target.value)}
placeholder="ИНН"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Краткое наименование *</label>
<input
type="text"
value={newEntity.shortName}
onChange={(e) => handleNewEntityChange('shortName', e.target.value)}
placeholder="Название компании"
className="form-input"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Форма</label>
<select
value={newEntity.form}
onChange={(e) => handleNewEntityChange('form', e.target.value)}
className="select-input"
>
<option value="">Выбрать</option>
<option value="ООО">ООО</option>
<option value="ИП">ИП</option>
<option value="АО">АО</option>
<option value="ЗАО">ЗАО</option>
</select>
</div>
<div className="form-field">
<label className="form-label">ОГРН</label>
<input
type="text"
value={newEntity.ogrn}
onChange={(e) => handleNewEntityChange('ogrn', e.target.value)}
placeholder="ОГРН"
className="form-input"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Юридический адрес</label>
<input
type="text"
value={newEntity.legalAddress}
onChange={(e) => handleNewEntityChange('legalAddress', e.target.value)}
placeholder="Юридический адрес"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Полное наименование</label>
<input
type="text"
value={newEntity.fullName}
onChange={(e) => handleNewEntityChange('fullName', e.target.value)}
placeholder="Полное наименование"
className="form-input"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Фактический адрес</label>
<input
type="text"
value={newEntity.actualAddress}
onChange={(e) => handleNewEntityChange('actualAddress', e.target.value)}
placeholder="Фактический адрес"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Система налогообложения</label>
<select
value={newEntity.taxSystem}
onChange={(e) => handleNewEntityChange('taxSystem', e.target.value)}
className="select-input"
>
<option value="">Выбрать</option>
<option value="УСН">УСН</option>
<option value="ОСНО">ОСНО</option>
<option value="ЕНВД">ЕНВД</option>
<option value="ПСН">ПСН</option>
</select>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">НДС %</label>
<input
type="number"
value={newEntity.vatPercent}
onChange={(e) => handleNewEntityChange('vatPercent', parseFloat(e.target.value) || 0)}
placeholder="0"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Бухгалтер</label>
<input
type="text"
value={newEntity.accountant}
onChange={(e) => handleNewEntityChange('accountant', e.target.value)}
placeholder="ФИО"
className="form-input"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Ответственный</label>
<input
type="text"
value={newEntity.responsibleName}
onChange={(e) => handleNewEntityChange('responsibleName', e.target.value)}
placeholder="ФИО"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Должность ответственного</label>
<input
type="text"
value={newEntity.responsiblePosition}
onChange={(e) => handleNewEntityChange('responsiblePosition', e.target.value)}
placeholder="Должность"
className="form-input"
/>
</div>
</div>
<div className="form-row">
<div className="form-field">
<label className="form-label">Телефон ответственного</label>
<input
type="tel"
value={newEntity.responsiblePhone}
onChange={(e) => handleNewEntityChange('responsiblePhone', e.target.value)}
placeholder="+7"
className="form-input"
/>
</div>
<div className="form-field">
<label className="form-label">Подписант</label>
<input
type="text"
value={newEntity.signatory}
onChange={(e) => handleNewEntityChange('signatory', e.target.value)}
placeholder="ФИО"
className="form-input"
/>
</div>
</div>
<div className="modal-buttons">
<button
onClick={editingEntity ? handleUpdateEntity : handleAddEntity}
className="btn-primary"
>
{editingEntity ? 'Сохранить' : 'Добавить'}
</button>
<button
onClick={closeModal}
className="btn-secondary"
>
Отменить
</button>
</div>
</div>
</div>
</div>
)}
<style jsx>{`
.page-wrapper {
background-color: #F5F8FB;
min-height: 100vh;
font-family: 'Onest', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.breadcrumbs {
background: white;
padding: 30px 0;
}
.container {
max-width: 1660px;
margin: 0 auto;
padding: 0 130px;
}
.breadcrumb-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.breadcrumb-link {
color: #000000;
text-decoration: none;
font-size: 14px;
font-weight: 400;
line-height: 1.4;
cursor: pointer;
}
.breadcrumb-link:hover {
color: #EC1C24;
}
.breadcrumb-separator {
color: #8E9AAC;
font-size: 14px;
line-height: 1.3;
}
.breadcrumb-current {
color: #8E9AAC;
font-size: 14px;
line-height: 1.3;
}
.profile-title {
font-size: 36px;
font-weight: 800;
color: #000814;
margin: 0;
line-height: 1;
}
.profile-section {
padding: 40px 0 60px;
}
.profile-layout {
display: flex;
gap: 30px;
align-items: flex-start;
}
.profile-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.settings-card {
background: white;
border-radius: 16px;
padding: 30px;
}
.card-title {
font-size: 30px;
font-weight: 700;
color: #000814;
margin: 0 0 30px 0;
line-height: 1;
}
.personal-form {
display: flex;
flex-direction: column;
gap: 30px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
color: #000814;
font-weight: 400;
}
.form-input {
border: 1px solid #D0D0D0;
border-radius: 4px;
padding: 16px 24px;
font-size: 14px;
color: #424F60;
font-family: 'Onest', sans-serif;
outline: none;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: #EC1C24;
}
.email-input::placeholder {
color: #747474;
}
.notification-section {
display: flex;
flex-direction: column;
gap: 15px;
}
.notification-toggle {
display: flex;
align-items: center;
}
.toggle-label {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
}
.toggle-container {
position: relative;
}
.hidden-checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.toggle-switch {
width: 36px;
height: 20px;
border-radius: 10px;
position: relative;
transition: background-color 0.2s;
}
.toggle-switch.active {
background-color: #000814;
}
.toggle-switch.inactive {
background-color: #D0D0D0;
}
.toggle-knob {
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
position: absolute;
top: 2px;
transition: transform 0.2s;
}
.toggle-knob.active {
transform: translateX(18px);
}
.toggle-knob.inactive {
transform: translateX(2px);
}
.toggle-text {
font-size: 16px;
color: #424F60;
}
.legal-entities {
display: flex;
flex-direction: column;
gap: 10px;
}
.entity-card {
background: #F5F8FB;
border-radius: 8px;
padding: 12px 20px;
}
.entity-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.entity-info h3 {
font-size: 20px;
font-weight: 700;
color: #000814;
margin: 0;
line-height: 1.2;
}
.entity-info p {
font-size: 14px;
color: #424F60;
margin: 4px 0 8px 0;
}
.entity-actions {
display: flex;
gap: 20px;
}
.entity-link {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
color: #424F60;
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
}
.entity-link:hover {
color: #EC1C24;
}
.entity-controls {
display: flex;
align-items: center;
gap: 20px;
padding-right: 10px;
}
.control-button {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
color: #424F60;
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
}
.control-button:hover {
color: #EC1C24;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-state p {
font-size: 16px;
color: #8E9AAC;
margin: 0;
}
.action-buttons {
background: white;
border-radius: 16px;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-primary {
background: #EC1C24;
color: white;
border: none;
border-radius: 12px;
padding: 14px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary:hover {
background: #d91920;
}
.btn-secondary {
background: transparent;
color: #000814;
border: 1px solid #EC1C24;
border-radius: 12px;
padding: 14px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #EC1C24;
color: white;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 16px;
padding: 30px;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-title {
font-size: 30px;
font-weight: 700;
color: #000814;
margin: 0 0 30px 0;
line-height: 1;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.select-input {
border: 1px solid #D0D0D0;
border-radius: 4px;
padding: 16px 24px;
font-size: 14px;
color: #747474;
font-family: 'Onest', sans-serif;
background: white;
outline: none;
transition: border-color 0.2s;
cursor: pointer;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23747474' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px;
appearance: none;
}
.select-input:focus {
border-color: #EC1C24;
}
.modal-buttons {
display: flex;
gap: 30px;
padding-top: 20px;
}
.loading-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 18px;
color: #000814;
}
@media (max-width: 768px) {
.container {
padding: 0 20px;
}
.profile-layout {
flex-direction: column;
gap: 20px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.form-row {
grid-template-columns: 1fr;
}
.entity-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.entity-controls {
flex-direction: column;
align-items: flex-start;
gap: 10px;
padding-right: 0;
}
.action-buttons {
flex-direction: column;
gap: 15px;
}
.modal-buttons {
flex-direction: column;
gap: 15px;
}
.breadcrumb-wrapper {
flex-wrap: wrap;
}
.profile-title {
font-size: 28px;
}
.settings-card {
padding: 20px;
}
.modal-content {
padding: 20px;
margin: 20px;
}
}
`}</style>
</div>
);
};
export default ProfileSettings;

678
src/pages/search-result.tsx Normal file
View File

@ -0,0 +1,678 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState, useMemo } from "react";
import { useQuery, useLazyQuery } from "@apollo/client";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Filters, { FilterConfig } from "@/components/Filters";
import BestPriceCard from "@/components/BestPriceCard";
import CoreProductCard from "@/components/CoreProductCard";
import AnalogueBlock from "@/components/AnalogueBlock";
import InfoSearch from "@/components/InfoSearch";
import FiltersPanelMobile from "@/components/FiltersPanelMobile";
import CatalogSortDropdown from "@/components/CatalogSortDropdown";
import MobileMenuBottomSection from '../components/MobileMenuBottomSection';
import { SEARCH_PRODUCT_OFFERS, GET_ANALOG_OFFERS } from "@/lib/graphql";
import { useArticleImage } from "@/hooks/useArticleImage";
const ANALOGS_CHUNK_SIZE = 5;
const sortOptions = [
"По цене",
"По рейтингу",
"По количеству"
];
// Функция для создания динамических фильтров
const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
const filters: FilterConfig[] = [];
if (result) {
// Фильтр по производителю
const brands = new Set<string>();
brands.add(result.brand);
result.externalOffers?.forEach((offer: any) => {
if (offer.brand) brands.add(offer.brand);
});
result.analogs?.forEach((analog: any) => {
if (analog.brand) brands.add(analog.brand);
});
if (brands.size > 1) {
filters.push({
type: "dropdown",
title: "Производитель",
options: Array.from(brands).sort(),
multi: true,
showAll: true,
});
}
// Фильтр по цене
const prices: number[] = [];
result.internalOffers?.forEach((offer: any) => {
if (offer.price > 0) prices.push(offer.price);
});
result.externalOffers?.forEach((offer: any) => {
if (offer.price > 0) prices.push(offer.price);
});
// Добавляем цены аналогов
Object.values(loadedAnalogs).forEach((analog: any) => {
analog.internalOffers?.forEach((offer: any) => {
if (offer.price > 0) prices.push(offer.price);
});
analog.externalOffers?.forEach((offer: any) => {
if (offer.price > 0) prices.push(offer.price);
});
});
if (prices.length > 0) {
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
if (maxPrice > minPrice) {
filters.push({
type: "range",
title: "Цена (₽)",
min: Math.floor(minPrice),
max: Math.ceil(maxPrice),
});
}
}
// Фильтр по сроку доставки
const deliveryDays: number[] = [];
result.internalOffers?.forEach((offer: any) => {
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
});
result.externalOffers?.forEach((offer: any) => {
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
});
// Добавляем сроки доставки аналогов
Object.values(loadedAnalogs).forEach((analog: any) => {
analog.internalOffers?.forEach((offer: any) => {
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
});
analog.externalOffers?.forEach((offer: any) => {
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
});
});
if (deliveryDays.length > 0) {
const minDays = Math.min(...deliveryDays);
const maxDays = Math.max(...deliveryDays);
if (maxDays > minDays) {
filters.push({
type: "range",
title: "Срок доставки (дни)",
min: minDays,
max: maxDays,
});
}
}
// Фильтр по количеству наличия
const quantities: number[] = [];
result.internalOffers?.forEach((offer: any) => {
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
});
result.externalOffers?.forEach((offer: any) => {
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
});
// Добавляем количества аналогов
Object.values(loadedAnalogs).forEach((analog: any) => {
analog.internalOffers?.forEach((offer: any) => {
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
});
analog.externalOffers?.forEach((offer: any) => {
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
});
});
if (quantities.length > 0) {
const minQuantity = Math.min(...quantities);
const maxQuantity = Math.max(...quantities);
if (maxQuantity > minQuantity) {
filters.push({
type: "range",
title: "Количество (шт.)",
min: minQuantity,
max: maxQuantity,
});
}
}
}
return filters;
};
// Функция для получения лучших предложений по заданным критериям
const getBestOffers = (offers: any[]) => {
const validOffers = offers.filter(offer => offer.price > 0 && typeof offer.deliveryDuration !== 'undefined');
if (validOffers.length === 0) return [];
const result: { offer: any; type: string }[] = [];
const usedOfferIds = new Set<string>();
// 1. Самая низкая цена (среди всех предложений)
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
if (lowestPriceOffer) {
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
usedOfferIds.add(`${lowestPriceOffer.articleNumber}-${lowestPriceOffer.price}-${lowestPriceOffer.deliveryDuration}`);
}
// 2. Самый дешевый аналог (только среди аналогов)
const analogOffers = validOffers.filter(offer => offer.isAnalog);
if (analogOffers.length > 0) {
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
const analogId = `${cheapestAnalogOffer.articleNumber}-${cheapestAnalogOffer.price}-${cheapestAnalogOffer.deliveryDuration}`;
if (!usedOfferIds.has(analogId)) {
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
usedOfferIds.add(analogId);
}
}
// 3. Самая быстрая доставка (среди всех предложений)
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
if (fastestDeliveryOffer) {
const fastestId = `${fastestDeliveryOffer.articleNumber}-${fastestDeliveryOffer.price}-${fastestDeliveryOffer.deliveryDuration}`;
if (!usedOfferIds.has(fastestId)) {
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
}
}
return result;
};
const transformOffersForCard = (offers: any[]) => {
return offers.map(offer => {
const isExternal = offer.type === 'external';
return {
id: offer.id,
productId: offer.productId,
offerKey: offer.offerKey,
pcs: `${offer.quantity} шт.`,
days: `${isExternal ? offer.deliveryTime : offer.deliveryDays} дн.`,
recommended: !isExternal && offer.available,
price: `${offer.price.toLocaleString('ru-RU')}`,
count: "1",
isExternal,
currency: offer.currency || "RUB",
warehouse: offer.warehouse,
supplier: offer.supplier,
deliveryTime: isExternal ? offer.deliveryTime : offer.deliveryDays,
};
});
};
export default function SearchResult() {
const router = useRouter();
const { article, brand, q, artId } = router.query;
const [sortActive, setSortActive] = useState(0);
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
const [showSortMobile, setShowSortMobile] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [brandQuery, setBrandQuery] = useState<string>("");
const [loadedAnalogs, setLoadedAnalogs] = useState<{ [key: string]: any }>({});
const [visibleAnalogsCount, setVisibleAnalogsCount] = useState(ANALOGS_CHUNK_SIZE);
// Состояния для фильтров
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
const [priceRange, setPriceRange] = useState<[number, number] | null>(null);
const [deliveryRange, setDeliveryRange] = useState<[number, number] | null>(null);
const [quantityRange, setQuantityRange] = useState<[number, number] | null>(null);
const [filterSearchTerm, setFilterSearchTerm] = useState<string>('');
useEffect(() => {
if (article && typeof article === 'string') {
setSearchQuery(article.trim().toUpperCase());
}
if (brand && typeof brand === 'string') {
setBrandQuery(brand.trim());
}
setLoadedAnalogs({});
setVisibleAnalogsCount(ANALOGS_CHUNK_SIZE);
}, [article, brand]);
const { data, loading, error } = useQuery(SEARCH_PRODUCT_OFFERS, {
variables: {
articleNumber: searchQuery,
brand: brandQuery || '' // Используем пустую строку если бренд не указан
},
skip: !searchQuery,
errorPolicy: 'all'
});
const { imageUrl: mainImageUrl } = useArticleImage(artId as string, { enabled: !!artId });
const [
getAnalogOffers,
{ loading: analogsLoading, data: analogsData }
] = useLazyQuery(GET_ANALOG_OFFERS, {
onCompleted: (data) => {
if (data && data.getAnalogOffers) {
const newAnalogs = data.getAnalogOffers.reduce((acc: any, analog: any) => {
const key = `${analog.brand}-${analog.articleNumber}`;
// Сразу трансформируем, но пока не используем
// const offers = transformOffersForCard(analog.internalOffers, analog.externalOffers);
acc[key] = { ...analog }; // offers убрали, т.к. allOffers - единый источник
return acc;
}, {});
setLoadedAnalogs(prev => ({ ...prev, ...newAnalogs }));
}
}
});
// Эффект для автоматической загрузки предложений видимых аналогов
useEffect(() => {
if (data?.searchProductOffers?.analogs) {
const analogsToLoad = data.searchProductOffers.analogs
.slice(0, visibleAnalogsCount)
.filter((a: any) => !loadedAnalogs[`${a.brand}-${a.articleNumber}`])
.map((a: any) => ({ brand: a.brand, articleNumber: a.articleNumber }));
if (analogsToLoad.length > 0) {
getAnalogOffers({ variables: { analogs: analogsToLoad } });
}
}
}, [visibleAnalogsCount, data, getAnalogOffers, loadedAnalogs]);
const result = data?.searchProductOffers;
const allOffers = useMemo(() => {
if (!result) return [];
const offers: any[] = [];
// Основной товар
result.internalOffers.forEach((o: any) => offers.push({ ...o, deliveryDuration: o.deliveryDays, type: 'internal', brand: result.brand, articleNumber: result.articleNumber, name: result.name }));
result.externalOffers.forEach((o: any) => offers.push({ ...o, deliveryDuration: o.deliveryTime, type: 'external', articleNumber: o.code, name: o.name }));
// Аналоги
Object.values(loadedAnalogs).forEach((analog: any) => {
analog.internalOffers.forEach((o: any) => offers.push({ ...o, deliveryDuration: o.deliveryDays, type: 'internal', brand: analog.brand, articleNumber: analog.articleNumber, name: analog.name, isAnalog: true }));
analog.externalOffers.forEach((o: any) => offers.push({ ...o, deliveryDuration: o.deliveryTime, type: 'external', brand: o.brand || analog.brand, articleNumber: o.code || analog.articleNumber, name: o.name, isAnalog: true }));
});
return offers;
}, [result, loadedAnalogs]);
const filteredOffers = useMemo(() => {
return allOffers.filter(offer => {
// Фильтр по бренду
if (selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
return false;
}
// Фильтр по цене
if (priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
return false;
}
// Фильтр по сроку доставки
if (deliveryRange) {
const deliveryDays = offer.deliveryDuration;
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
return false;
}
}
// Фильтр по количеству наличия
if (quantityRange) {
const quantity = offer.quantity;
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
return false;
}
}
// Фильтр по поисковой строке
if (filterSearchTerm) {
const searchTerm = filterSearchTerm.toLowerCase();
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
if (!brandMatch && !articleMatch && !nameMatch) {
return false;
}
}
return true;
});
}, [allOffers, selectedBrands, priceRange, deliveryRange, quantityRange, filterSearchTerm]);
const handleFilterChange = (type: string, value: any) => {
if (type === 'Производитель') {
setSelectedBrands(value);
} else if (type === 'Цена (₽)') {
setPriceRange(value);
} else if (type === 'Срок доставки (дни)') {
setDeliveryRange(value);
} else if (type === 'Количество (шт.)') {
setQuantityRange(value);
} else if (type === 'search') {
setFilterSearchTerm(value);
}
};
const initialOffersExist = allOffers.length > 0;
const filtersAreActive = selectedBrands.length > 0 || priceRange !== null || deliveryRange !== null || quantityRange !== null || filterSearchTerm !== '';
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
const hasAnalogs = result && result.analogs.length > 0;
const searchResultFilters = createFilters(result, loadedAnalogs);
const bestOffersData = getBestOffers(filteredOffers);
// Если это поиск по параметру q (из UnitDetailsSection), используем q как article
useEffect(() => {
if (q && typeof q === 'string' && !article) {
setSearchQuery(q.trim().toUpperCase());
// Определяем бренд из каталога или используем дефолтный
const catalogFromUrl = router.query.catalog as string;
const vehicleFromUrl = router.query.vehicle as string;
if (catalogFromUrl) {
// Маппинг каталогов к брендам
const catalogToBrandMap: { [key: string]: string } = {
'AU1587': 'AUDI',
'VW1587': 'VOLKSWAGEN',
'BMW1587': 'BMW',
'MB1587': 'MERCEDES-BENZ',
// Добавьте другие маппинги по необходимости
};
setBrandQuery(catalogToBrandMap[catalogFromUrl] || '');
} else {
setBrandQuery('');
}
}
}, [q, article, router.query]);
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
const minPrice = useMemo(() => {
if (result && result.minPrice) return result.minPrice;
if (allOffers.length > 0) {
const prices = allOffers.filter(o => o.price > 0).map(o => o.price);
if (prices.length > 0) {
return `${Math.min(...prices).toLocaleString('ru-RU')}`;
}
}
return "-";
}, [result, allOffers]);
if (loading) {
return (
<>
<Head>
<title>Поиск предложений {searchQuery} - Protek</title>
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Поиск предложений...</p>
</div>
</main>
<Footer />
</>
);
}
return (
<>
<Head>
<title>{result ? `${result.brand} ${result.articleNumber} - ${result.name}` : `Результаты поиска`} - Protek</title>
<meta name="description" content={`Лучшие предложения и аналоги для ${result?.name}`} />
<meta content="Search result" property="og:title" />
<meta content="Search result" property="twitter:title" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta content="Webflow" name="generator" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoSearch
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}
name={result ? result.name : "деталь"}
offersCount={result ? result.totalOffers : 0}
minPrice={minPrice}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-84">
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
<span className="code-embed-9 w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</span>
<div>Фильтры</div>
</div>
</div>
</div>
</section>
{/* Мобильная панель фильтров */}
<FiltersPanelMobile
filters={searchResultFilters}
open={showFiltersMobile}
onClose={() => setShowFiltersMobile(false)}
searchQuery={filterSearchTerm}
onSearchChange={(value) => handleFilterChange('search', value)}
/>
{/* Лучшие предложения */}
{bestOffersData.length > 0 && (
<section className="section-6">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex flex-block-36">
{bestOffersData.map(({ offer, type }, index) => (
<BestPriceCard
key={`best-${type}-${index}`}
bestOfferType={type}
title={`${offer.brand} ${offer.articleNumber}${offer.isAnalog ? ' (аналог)' : ''}`}
description={offer.name}
price={`${offer.price.toLocaleString()}`}
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
stock={`${offer.quantity} шт.`}
/>
))}
</div>
</div>
</section>
)}
{/* Если нет предложений после фильтрации, но они были изначально */}
{initialOffersExist && filteredOffers.length === 0 && !loading && (
<section>
<div className="w-layout-blockcontainer container w-container">
<div className="text-center py-12">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
Нет предложений, соответствующих вашим фильтрам
</h3>
<p className="text-gray-600">
Попробуйте изменить или сбросить фильтры.
</p>
</div>
</div>
</section>
)}
{/* Если изначально не было предложений */}
{!initialOffersExist && !loading && (
<section>
<div className="w-layout-blockcontainer container w-container">
<div className="text-center py-12">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
К сожалению, предложений по данному артикулу не найдено
</h3>
<p className="text-gray-600 mb-6">
Попробуйте изменить параметры поиска или обратитесь к нашим менеджерам
</p>
<div className="space-y-2">
<p className="text-sm text-gray-500">Телефон: +7 (495) 123-45-67</p>
<p className="text-sm text-gray-500">Email: info@protek.ru</p>
</div>
</div>
</div>
</section>
)}
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-13-copy">
{/* Фильтры для десктопа */}
<div style={{ width: '300px', marginRight: '20px' }}>
<Filters
filters={searchResultFilters}
onFilterChange={handleFilterChange}
filterValues={{
'Производитель': selectedBrands,
'Цена (₽)': priceRange,
'Срок доставки (дни)': deliveryRange,
'Количество (шт.)': quantityRange
}}
searchQuery={filterSearchTerm}
onSearchChange={(value) => handleFilterChange('search', value)}
/>
</div>
{/* Основной товар */}
<div className="w-layout-vflex flex-block-14-copy">
{hasOffers && result && (() => {
const mainProductOffers = transformOffersForCard(
filteredOffers.filter(o => !o.isAnalog)
);
// Не показываем основной товар, если у него нет предложений
if (mainProductOffers.length === 0) {
return null;
}
return (
<>
<CoreProductCard
brand={result.brand}
article={result.articleNumber}
name={result.name}
image={mainImageUrl}
offers={mainProductOffers}
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
/>
</>
);
})()}
{/* Аналоги */}
{hasAnalogs && result && (() => {
// Фильтруем аналоги с предложениями
const analogsWithOffers = result.analogs.slice(0, visibleAnalogsCount).filter((analog: any) => {
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
if (!loadedAnalogData) {
return true; // Показываем загружающиеся аналоги
}
const analogOffers = transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
);
// Показываем аналог только если у него есть предложения
return analogOffers.length > 0;
});
// Если нет аналогов с предложениями, не показываем секцию
if (analogsWithOffers.length === 0) {
return null;
}
return (
<>
<h2 className="heading-11">Аналоги</h2>
{analogsWithOffers.map((analog: any, index: number) => {
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
const analogOffers = loadedAnalogData
? transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
)
: [];
return (
<CoreProductCard
key={analogKey}
brand={analog.brand}
article={analog.articleNumber}
name={analog.name}
offers={analogOffers}
isAnalog
isLoadingOffers={!loadedAnalogData}
/>
)
})}
{(() => {
// Проверяем, есть ли еще аналоги с предложениями для загрузки
const remainingAnalogs = result.analogs.slice(visibleAnalogsCount);
const hasMoreAnalogsWithOffers = remainingAnalogs.some((analog: any) => {
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
if (!loadedAnalogData) {
return true; // Могут быть предложения у незагруженных аналогов
}
const analogOffers = transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
);
return analogOffers.length > 0;
});
return hasMoreAnalogsWithOffers && (
<div className="w-layout-hflex pagination">
<button
onClick={() => setVisibleAnalogsCount(prev => prev + ANALOGS_CHUNK_SIZE)}
disabled={analogsLoading}
className="button_strock w-button"
>
{analogsLoading ? "Загружаем..." : "Показать еще"}
</button>
</div>
);
})()}
</>
);
})()}
</div>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

314
src/pages/search.tsx Normal file
View File

@ -0,0 +1,314 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import MobileMenuBottomSection from '@/components/MobileMenuBottomSection';
import { DOC_FIND_OEM, FIND_LAXIMO_VEHICLES_BY_PART_NUMBER } from '@/lib/graphql';
import { LaximoDocFindOEMResult, LaximoVehiclesByPartResult, LaximoVehicleSearchResult } from '@/types/laximo';
type SearchMode = 'parts' | 'vehicles';
const InfoSearch = () => (
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block-2 w-inline-block">
<div>Поиск деталей по артикулу</div>
</a>
</div>
<div className="w-layout-hflex flex-block-8">
<div className="w-layout-hflex flex-block-10">
<h1 className="heading">Поиск деталей по артикулу</h1>
</div>
</div>
</div>
</div>
</section>
);
const SearchPage = () => {
const router = useRouter();
const { q, mode } = router.query;
const [searchQuery, setSearchQuery] = useState<string>("");
const [searchMode, setSearchMode] = useState<SearchMode>('parts');
useEffect(() => {
if (q && typeof q === 'string') {
setSearchQuery(q.trim().toUpperCase());
}
if (mode && typeof mode === 'string' && (mode === 'parts' || mode === 'vehicles')) {
setSearchMode(mode);
}
}, [q, mode]);
const { data: partsData, loading: partsLoading, error: partsError } = useQuery(DOC_FIND_OEM, {
variables: { oemNumber: searchQuery },
skip: !searchQuery || searchMode !== 'parts',
errorPolicy: 'all'
});
const { data: vehiclesData, loading: vehiclesLoading, error: vehiclesError } = useQuery(FIND_LAXIMO_VEHICLES_BY_PART_NUMBER, {
variables: { partNumber: searchQuery },
skip: !searchQuery || searchMode !== 'vehicles',
errorPolicy: 'all'
});
const handleSearchModeChange = (mode: SearchMode) => {
setSearchMode(mode);
if (searchQuery) {
router.push(`/search?q=${encodeURIComponent(searchQuery)}&mode=${mode}`, undefined, { shallow: true });
}
};
const handleSearchSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!searchQuery.trim()) return;
router.push(`/search?q=${encodeURIComponent(searchQuery.trim().toUpperCase())}&mode=${searchMode}`);
};
const handlePartDetail = (detail: any) => {
router.push(`/search-result?article=${encodeURIComponent(detail.formattedoem)}&brand=${encodeURIComponent(detail.manufacturer)}`);
};
const handleVehicleSelect = (vehicle: LaximoVehicleSearchResult) => {
const vehicleBrand = vehicle.brand || vehicle.name?.split(' ')[0] || 'UNKNOWN';
const url = `/search-result?article=${encodeURIComponent(searchQuery)}&brand=${encodeURIComponent(vehicleBrand)}`;
router.push(url);
};
const handleShowAllVehicles = (catalogCode?: string) => {
const url = catalogCode
? `/vehicles-by-part?partNumber=${encodeURIComponent(searchQuery)}&catalogCode=${catalogCode}`
: `/vehicles-by-part?partNumber=${encodeURIComponent(searchQuery)}`;
router.push(url);
};
const isLoading = (searchMode === 'parts' && partsLoading) || (searchMode === 'vehicles' && vehiclesLoading);
const hasError = (searchMode === 'parts' && partsError) || (searchMode === 'vehicles' && vehiclesError);
const partsResult: LaximoDocFindOEMResult | null = partsData?.laximoDocFindOEM || null;
const vehiclesResult: LaximoVehiclesByPartResult | null = vehiclesData?.laximoFindVehiclesByPartNumber || null;
const hasPartsResults = partsResult && partsResult.details && partsResult.details.length > 0;
const hasVehiclesResults = vehiclesResult && vehiclesResult.totalVehicles > 0;
return (
<>
<Head>
<title>Поиск по артикулу {searchQuery} - Protek</title>
<meta name="description" content={`Результаты поиска по артикулу ${searchQuery}`} />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoSearch />
<div className="page-wrapper bg-[#F5F8FB] min-h-screen">
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
<div className="flex flex-col items-center w-full">
<div className="w-full max-w-[1580px]">
{/* Переключатель режима поиска */}
{/* {searchQuery && (
<div className="bg-white rounded-2xl shadow p-6 mb-6 flex flex-col items-center">
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1 w-full max-w-md">
<button
onClick={() => handleSearchModeChange('parts')}
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
searchMode === 'parts'
? 'bg-white text-[#EC1C24] shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
🔧 Найти запчасти
</button>
<button
onClick={() => handleSearchModeChange('vehicles')}
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
searchMode === 'vehicles'
? 'bg-white text-[#EC1C24] shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
🚗 Найти автомобили
</button>
</div>
</div>
)} */}
{/* Обработка ошибок */}
{searchQuery && hasError && (
<div className="bg-red-50 border border-red-200 rounded-2xl shadow p-10 mb-6">
<div className="flex items-center">
<svg className="w-6 h-6 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="text-lg font-medium text-red-800">Ошибка поиска</h3>
<p className="text-red-700 mt-1">Произошла ошибка при поиске. Попробуйте еще раз.</p>
</div>
</div>
</div>
)}
{/* Загрузка */}
{searchQuery && isLoading && (
<div className="bg-white rounded-2xl shadow p-10 flex flex-col items-center justify-center min-h-[300px]">
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-red-600 mb-6"></div>
<p className="text-lg text-gray-600">Поиск по артикулу...</p>
</div>
)}
{/* Результаты поиска */}
{searchQuery && !isLoading && !hasError && (
<div className="space-y-6">
{/* Результаты поиска запчастей */}
{searchMode === 'parts' && (
<>
{!hasPartsResults && (
<div className="bg-[#eaf0fa] border border-[#b3c6e6] rounded-2xl shadow p-10 text-center">
<svg className="w-16 h-16 mx-auto mb-4" style={{ color: '#0d336c' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.562M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<h3 className="text-xl font-semibold mb-2" style={{ color: '#0d336c' }}>
Детали не найдены
</h3>
<p className="mb-4" style={{ color: '#0d336c' }}>
По артикулу <span className="font-mono font-semibold">{searchQuery}</span> детали не найдены.
</p>
<p className="text-sm" style={{ color: '#3b5a99' }}>
Попробуйте изменить запрос или проверьте правильность написания артикула.
</p>
</div>
)}
{hasPartsResults && (
<div className="bg-white rounded-2xl shadow p-10">
<div className="border-b border-gray-200 pb-4">
<h2 className="text-xl font-semibold text-gray-900">
Поиск деталей по артикулу: {searchQuery}
</h2>
<p className="text-sm text-gray-600 mt-1">
Выберите нужную деталь
</p>
</div>
<div className="divide-y divide-gray-200">
{partsResult!.details.map((detail, index) => (
<div key={detail.detailid || index}>
<button
onClick={() => handlePartDetail(detail)}
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
>
<div className="flex w-full items-center gap-2">
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>{detail.manufacturer}</div>
<div className="w-1/5 max-md:text-center max-md:w-1/3 font-bold text-left truncate group-hover:text-[#EC1C24] transition-colors">{detail.formattedoem || detail.oem}</div>
<div className="w-3/5 max-md:w-1/3 text-left truncate">{detail.name}</div>
</div>
</button>
</div>
))}
</div>
</div>
)}
</>
)}
{/* Результаты поиска автомобилей */}
{searchMode === 'vehicles' && (
<div className="bg-white rounded-2xl shadow p-10">
<div className="border-b border-gray-200 pb-4 mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
Поиск автомобилей по артикулу: {searchQuery}
</h2>
{hasVehiclesResults && (
<span className="text-sm text-gray-600">
Найдено {vehiclesResult!.totalVehicles} автомобилей в {vehiclesResult!.catalogs.length} каталогах
</span>
)}
</div>
{hasVehiclesResults ? (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Бренд
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Артикул
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Наименование
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Рынок
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{vehiclesResult!.catalogs.map((catalog) => (
<tr
key={catalog.catalogCode}
className="hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => {
router.push(`/search-result?article=${encodeURIComponent(searchQuery)}&brand=${encodeURIComponent(catalog.brand)}`);
}}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="text-sm font-medium text-gray-900">
{catalog.brand}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{searchQuery}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">ГАЕЧНЫЙ КЛЮЧ</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-500">
{catalog.catalogCode}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pt-6">
<button
onClick={() => handleShowAllVehicles()}
className="w-full bg-[#EC1C24] text-white py-3 px-6 rounded-lg hover:bg-[#b91c1c] transition-colors font-medium"
>
Показать все {vehiclesResult!.totalVehicles} автомобилей
</button>
</div>
</>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-2xl shadow p-10 text-center">
<svg className="w-16 h-16 text-yellow-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 14.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h3 className="text-xl font-semibold text-yellow-800 mb-2">Автомобили не найдены</h3>
<p className="text-yellow-700 mb-4">Автомобили с артикулом {searchQuery} не найдены в каталогах</p>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
<MobileMenuBottomSection />
<Footer />
</div>
</>
);
};
export default SearchPage;

41
src/pages/set-token.tsx Normal file
View File

@ -0,0 +1,41 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
const SetToken: React.FC = () => {
const router = useRouter();
useEffect(() => {
// Очищаем все старые данные из localStorage
localStorage.clear();
// Устанавливаем данные пользователя в localStorage
const userData = {
id: 'cmbntpesd0000rq19p7jsszrz',
name: 'Лев Данилов',
phone: '+79611177205',
email: ''
};
localStorage.setItem('userData', JSON.stringify(userData));
localStorage.setItem('authToken', `client_${userData.id}`);
console.log('localStorage очищен и токен установлен:', `client_${userData.id}`);
console.log('userData установлен:', userData);
// Перенаправляем на страницу реквизитов
setTimeout(() => {
router.push('/profile-requisites');
}, 2000);
}, [router]);
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>Установка токена...</h1>
<p>Токен устанавливается, вы будете перенаправлены на страницу реквизитов.</p>
<p>ID клиента: cmbntpesd0000rq19p7jsszrz</p>
<p>Токен: client_cmbntpesd0000rq19p7jsszrz</p>
</div>
);
};
export default SetToken;

View File

@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
const TestProfilePage = () => {
const router = useRouter();
useEffect(() => {
// Устанавливаем тестового пользователя в localStorage
const testUser = {
id: 'cmboumhyx0002mq0i6wibydxv',
name: 'Лев Данилов',
phone: '+79611177205',
email: 'lev@example.com'
};
localStorage.setItem('userData', JSON.stringify(testUser));
localStorage.setItem('authToken', `client_${testUser.id}`);
console.log('Тестовый пользователь установлен:', testUser);
// Перенаправляем на страницу профиля
setTimeout(() => {
router.push('/profile-set');
}, 1000);
}, [router]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Настройка тестового пользователя</h1>
<p className="text-gray-600 mb-4">
Устанавливаем данные тестового пользователя и перенаправляем на страницу профиля...
</p>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600 mx-auto"></div>
</div>
</div>
);
};
export default TestProfilePage;

View File

@ -0,0 +1,69 @@
import Head from 'next/head';
import ThankInfo from "@/components/ThankInfo";
import Header from "@/components/Header";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Footer from "@/components/Footer";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import Link from 'next/link';
export default function ThankYouPage() {
return (
<>
<Head>
<meta charSet="utf-8" />
<title>thankyoupage</title>
<meta content="thankyoupage" property="og:title" />
<meta content="thankyoupage" property="twitter:title" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js" type="text/javascript"></script>
<script type="text/javascript" dangerouslySetInnerHTML={{__html: `WebFont.load({ google: { families: [\"Onest:regular,600,700,800,900:cyrillic-ext,latin\"] }});`}} />
<script type="text/javascript" dangerouslySetInnerHTML={{__html: `!function(o,c){var n=c.documentElement,t=\" w-mod-\";n.className+=t+\"js\",(\"ontouchstart\"in o||o.DocumentTouch&&c instanceof DocumentTouch)&&(n.className+=t+\"touch\")}(window,document);`}} />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<ThankInfo />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<div className="w-layout-vflex flex-block-72">
<div className="w-layout-vflex image-thx"></div>
<div className="w-layout-vflex desc-wholesale">
<div className="w-layout-hflex thxcontent">
<h3 className="heading-14">Ваш заказ <span className="text-span-4">2024ABCD123</span> успешно оплачен </h3>
<div className="w-layout-vflex flex-block-103">
<div className="w-layout-hflex flex-block-75">
<div className="txtpthx">Номер вашего заказа</div>
<div className="text-block-36">2024ABCD123</div>
</div>
<div className="w-layout-hflex flex-block-75">
<div className="txtpthx">Дата и время заказа</div>
<div className="text-block-36">16:33 | 5 апреля 2025</div>
</div>
<div className="w-layout-hflex flex-block-75">
<div className="txtpthx">Сумма заказа</div>
<div className="text-block-36">18 000 </div>
</div>
</div>
<h3 className="thxsubtitle">📦 Что дальше?</h3>
<div className="text-block-36"> Мы уже обрабатываем ваш заказ.<br />🚚 Отправка ожидается в течение 13 рабочих дней.<br />📬 Как только посылка будет передана в службу доставки, вы получите уведомление с трек-номером.</div>
<div className="w-layout-hflex flex-block-104">
<Link href="/cart" legacyBehavior><a className="submit-button-s w-button">Продолжить покупки</a></Link>
<Link href="/cart" legacyBehavior><a className="button_strock-s w-button">К списку заказазов</a></Link>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

View File

@ -0,0 +1,497 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import { useLazyQuery } from '@apollo/client';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { FIND_LAXIMO_VEHICLE, FIND_LAXIMO_VEHICLE_BY_PLATE_GLOBAL } from '@/lib/graphql';
import { LaximoVehicleSearchResult } from '@/types/laximo';
import Link from 'next/link';
interface VehicleSearchResultsPageProps {}
const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () => {
const router = useRouter();
const { query: routerQuery } = router;
const [vehicles, setVehicles] = useState<LaximoVehicleSearchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchType, setSearchType] = useState<'vin' | 'plate' | ''>('');
const [isRedirecting, setIsRedirecting] = useState(false);
// Query для поиска по VIN
const [findVehicleByVin] = useLazyQuery(FIND_LAXIMO_VEHICLE, {
onCompleted: (data) => {
const results = data.laximoFindVehicle || [];
setVehicles(results);
setIsLoading(false);
},
onError: (error) => {
console.error('❌ Ошибка поиска по VIN:', error);
setVehicles([]);
setIsLoading(false);
}
});
// Query для поиска по госномеру
const [findVehicleByPlate] = useLazyQuery(FIND_LAXIMO_VEHICLE_BY_PLATE_GLOBAL, {
onCompleted: (data) => {
const results = data.laximoFindVehicleByPlateGlobal || [];
setVehicles(results);
setIsLoading(false);
},
onError: (error) => {
console.error('❌ Ошибка поиска по госномеру:', error);
setVehicles([]);
setIsLoading(false);
}
});
// Проверяем тип поиска
const isVinNumber = (query: string): boolean => {
const cleanQuery = query.trim().toUpperCase();
return /^[A-HJ-NPR-Z0-9]{17}$/.test(cleanQuery);
};
const isPlateNumber = (query: string): boolean => {
const cleanQuery = query.trim().toUpperCase().replace(/\s+/g, '');
const platePatterns = [
/^[АВЕКМНОРСТУХ]\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}$/,
/^[АВЕКМНОРСТУХ]{2}\d{3}[АВЕКМНОРСТУХ]\d{2,3}$/,
/^[АВЕКМНОРСТУХ]\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}$/,
];
return platePatterns.some(pattern => pattern.test(cleanQuery));
};
// Выполняем поиск при загрузке страницы
useEffect(() => {
if (routerQuery.q && typeof routerQuery.q === 'string') {
const query = routerQuery.q.trim();
setSearchQuery(query);
setIsLoading(true);
if (isVinNumber(query)) {
setSearchType('vin');
findVehicleByVin({
variables: {
catalogCode: '', // Глобальный поиск
vin: query.toUpperCase()
}
});
} else if (isPlateNumber(query)) {
setSearchType('plate');
findVehicleByPlate({
variables: {
plateNumber: query.toUpperCase().replace(/\s+/g, '')
}
});
} else {
setIsLoading(false);
}
}
}, [routerQuery.q, findVehicleByVin, findVehicleByPlate]);
const handleVehicleSelect = useCallback((vehicle: LaximoVehicleSearchResult, skipToCategories = false) => {
console.log('🚗 handleVehicleSelect вызвана для автомобиля:', vehicle, 'skipToCategories:', skipToCategories);
// Переходим к выбору групп запчастей для найденного автомобиля
const catalogCode = vehicle.catalog || vehicle.brand?.toLowerCase() || '';
const vehicleId = vehicle.vehicleid || '';
const ssd = vehicle.ssd || '';
console.log('🔧 Выбранные параметры:', {
catalogCode,
vehicleId,
ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
ssdLength: ssd.length
});
// Если есть SSD, сохраняем его в localStorage для безопасной передачи
if (ssd && ssd.trim() !== '') {
const vehicleKey = `vehicle_ssd_${catalogCode}_${vehicleId}`;
console.log('💾 Сохраняем SSD в localStorage, ключ:', vehicleKey);
// Очищаем все предыдущие SSD для других автомобилей
const keysToRemove = Object.keys(localStorage).filter(key => key.startsWith('vehicle_ssd_'));
keysToRemove.forEach(key => {
if (key !== vehicleKey) {
console.log('🗑️ Удаляем старый SSD ключ:', key);
localStorage.removeItem(key);
}
});
localStorage.setItem(vehicleKey, ssd);
// Выбираем URL в зависимости от того, нужно ли пропустить промежуточную страницу
const url = skipToCategories
? `/vehicle-search/${catalogCode}/${vehicleId}?use_storage=1&ssd_length=${ssd.length}&searchType=categories`
: `/vehicle-search/${catalogCode}/${vehicleId}?use_storage=1&ssd_length=${ssd.length}`;
console.log('🔗 Переходим на URL с localStorage:', url);
// Используем replace вместо push для моментального перехода
router.replace(url);
} else {
// Выбираем URL в зависимости от того, нужно ли пропустить промежуточную страницу
const url = skipToCategories
? `/vehicle-search/${catalogCode}/${vehicleId}?searchType=categories`
: `/vehicle-search/${catalogCode}/${vehicleId}`;
console.log('🔗 Переходим на URL без SSD:', url);
// Используем replace вместо push для моментального перехода
router.replace(url);
}
}, [router]);
// Предзагрузка и автоматический переход при поиске по VIN, если найден только один автомобиль
useEffect(() => {
if (!isLoading && searchType === 'vin' && vehicles.length === 1 && !isRedirecting) {
console.log('🚗 Найден один автомобиль по VIN, подготавливаем мгновенный переход');
const vehicle = vehicles[0];
const catalogCode = vehicle.catalog || vehicle.brand?.toLowerCase() || '';
const vehicleId = vehicle.vehicleid || '';
// Предзагружаем целевую страницу для ускорения перехода (сразу с категориями)
const targetUrl = `/vehicle-search/${catalogCode}/${vehicleId}?searchType=categories`;
router.prefetch(targetUrl);
console.log('🔄 Предзагружаем страницу с категориями:', targetUrl);
setIsRedirecting(true);
// Мгновенный переход сразу к категориям
handleVehicleSelect(vehicle, true);
}
}, [isLoading, searchType, vehicles, handleVehicleSelect, isRedirecting, router]);
const handleCancelRedirect = () => {
setIsRedirecting(false);
};
return (
<>
<main className="bg-gray-50 min-h-screen">
{/* Breadcrumb */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<nav className="flex" aria-label="Breadcrumb">
<ol className="flex items-center space-x-4">
<li>
<Link href="/" className="text-gray-400 hover:text-gray-500">
Главная
</Link>
</li>
<li>
<div className="flex items-center">
<svg className="flex-shrink-0 h-5 w-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<span className="ml-4 text-sm font-medium text-red-600">
{searchType === 'vin' ? 'Найденные автомобили' : 'Найденные автомобили'}
</span>
</div>
</li>
</ol>
</nav>
</div>
</div>
{/* Search Results Header */}
<div className="bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
{searchType === 'vin' ? 'Поиск по VIN номеру' : 'Поиск по государственному номеру'}
</h1>
<p className="text-lg text-gray-600">
Запрос: <span className="font-mono font-bold">{searchQuery}</span>
</p>
{!isLoading && vehicles.length > 0 && !isRedirecting && (
<p className="text-sm text-gray-500 mt-2">
Найдено {vehicles.length} автомобилей
</p>
)}
</div>
{/* Loading State */}
{isLoading && (
<div className="flex items-center justify-center py-12">
<div className="flex items-center space-x-3">
<svg className="animate-spin h-8 w-8 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-lg text-gray-600">Поиск автомобилей...</span>
</div>
</div>
)}
{/* Auto-redirect notification for VIN search with single result */}
{!isLoading && searchType === 'vin' && vehicles.length === 1 && isRedirecting && (
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<svg className="animate-spin h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<div>
<h3 className="text-lg font-medium text-green-900"> Автомобиль найден!</h3>
<p className="text-green-700">
<strong>{vehicles[0]?.brand} {vehicles[0]?.name}</strong>
{vehicles[0]?.year && ` (${vehicles[0].year} г.)`}
{vehicles[0]?.engine && `, двигатель: ${vehicles[0].engine}`}
</p>
<p className="text-sm text-green-600 mt-1">
🚀 Переходим сразу к категориям запчастей...
</p>
</div>
</div>
<button
onClick={() => router.back()}
className="text-green-600 hover:text-green-800 border border-green-300 hover:border-green-400 px-3 py-1 rounded text-sm font-medium transition-colors"
>
Назад
</button>
</div>
</div>
)}
{/* Results Table */}
{!isLoading && vehicles.length > 0 && !isRedirecting && (
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Бренд
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Название
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Модель
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Год
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Двигатель
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
КПП
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Рынок
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Дата выпуска
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Период производства
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Дополнительно
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{vehicles.map((vehicle, index) => {
console.log('🔍 Отображаем автомобиль в таблице:', {
index,
vehicleid: vehicle.vehicleid,
name: vehicle.name,
brand: vehicle.brand,
catalog: vehicle.catalog,
model: vehicle.model,
year: vehicle.year,
engine: vehicle.engine,
ssd: vehicle.ssd ? vehicle.ssd.substring(0, 30) + '...' : 'отсутствует'
});
return (
<tr
key={vehicle.vehicleid || index}
onClick={() => handleVehicleSelect(vehicle)}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{vehicle.brand}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{vehicle.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{vehicle.model}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
const year = vehicle.year || vehicle.manufactured || (vehicle.date ? vehicle.date.split('.').pop() : '') || '';
console.log(`🗓️ Год для автомобиля ${vehicle.vehicleid}:`, { year, original_year: vehicle.year, manufactured: vehicle.manufactured, date: vehicle.date });
return year || '-';
})()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
const engine = vehicle.engine || vehicle.engine_info || vehicle.engineno || '';
console.log(`🔧 Двигатель для автомобиля ${vehicle.vehicleid}:`, { engine, original_engine: vehicle.engine, engine_info: vehicle.engine_info, engineno: vehicle.engineno });
return engine || '-';
})()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
const transmission = vehicle.transmission || vehicle.bodytype || '';
console.log(`⚙️ КПП для автомобиля ${vehicle.vehicleid}:`, { transmission, original_transmission: vehicle.transmission, bodytype: vehicle.bodytype });
return transmission || '-';
})()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
const market = vehicle.market || vehicle.destinationregion || vehicle.creationregion || '';
console.log(`🌍 Рынок для автомобиля ${vehicle.vehicleid}:`, { market, original_market: vehicle.market, destinationregion: vehicle.destinationregion, creationregion: vehicle.creationregion });
return market || '-';
})()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
const releaseDate = vehicle.date || vehicle.manufactured || '';
console.log(`📅 Дата выпуска для автомобиля ${vehicle.vehicleid}:`, { releaseDate, date: vehicle.date, manufactured: vehicle.manufactured });
return releaseDate || '-';
})()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(() => {
let prodPeriod = '';
if (vehicle.prodRange) {
prodPeriod = vehicle.prodRange;
} else if (vehicle.prodPeriod) {
prodPeriod = vehicle.prodPeriod;
} else if (vehicle.datefrom && vehicle.dateto) {
prodPeriod = `${vehicle.datefrom} - ${vehicle.dateto}`;
} else if (vehicle.modelyearfrom && vehicle.modelyearto) {
prodPeriod = `${vehicle.modelyearfrom} - ${vehicle.modelyearto}`;
}
console.log(`📈 Период производства для автомобиля ${vehicle.vehicleid}:`, {
prodPeriod,
prodRange: vehicle.prodRange,
original_prodPeriod: vehicle.prodPeriod,
datefrom: vehicle.datefrom,
dateto: vehicle.dateto,
modelyearfrom: vehicle.modelyearfrom,
modelyearto: vehicle.modelyearto
});
return prodPeriod || '-';
})()}
</td>
<td className="px-6 py-4 text-sm text-gray-900">
<div className="space-y-1">
{vehicle.framecolor && (
<div className="text-xs">
<span className="font-medium">Цвет кузова:</span> {vehicle.framecolor}
</div>
)}
{vehicle.trimcolor && (
<div className="text-xs">
<span className="font-medium">Цвет салона:</span> {vehicle.trimcolor}
</div>
)}
{vehicle.engineno && (
<div className="text-xs">
<span className="font-medium">Номер двигателя:</span> {vehicle.engineno}
</div>
)}
{vehicle.engine_info && (
<div className="text-xs max-w-xs truncate" title={vehicle.engine_info}>
<span className="font-medium">Двигатель:</span> {vehicle.engine_info}
</div>
)}
{vehicle.options && (
<div className="text-xs max-w-xs truncate" title={vehicle.options}>
<span className="font-medium">Опции:</span> {vehicle.options}
</div>
)}
{vehicle.description && (
<div className="text-xs max-w-xs truncate" title={vehicle.description}>
<span className="font-medium">Описание:</span> {vehicle.description}
</div>
)}
{vehicle.modification && (
<div className="text-xs max-w-xs truncate" title={vehicle.modification}>
<span className="font-medium">Модификация:</span> {vehicle.modification}
</div>
)}
{vehicle.grade && (
<div className="text-xs">
<span className="font-medium">Класс:</span> {vehicle.grade}
</div>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* No Results */}
{!isLoading && vehicles.length === 0 && searchQuery && (
<div className="text-center py-12">
<div className="text-yellow-400 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 14.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-xl font-medium text-gray-900 mb-2">
{searchType === 'vin' ? 'VIN не найден' : 'Госномер не найден'}
</h3>
<p className="text-gray-600 mb-6">
{searchType === 'vin'
? `Автомобиль с VIN номером ${searchQuery} не найден в доступных каталогах`
: `Автомобиль с государственным номером ${searchQuery} не найден в базе данных`
}
</p>
<Link
href="/"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Вернуться на главную
</Link>
</div>
)}
{/* Invalid Search Query */}
{!isLoading && !searchQuery && (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-xl font-medium text-gray-900 mb-2">
Введите поисковый запрос
</h3>
<p className="text-gray-600 mb-6">
Используйте поле поиска в шапке сайта для поиска автомобилей по VIN номеру или государственному номеру
</p>
<Link
href="/"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Вернуться на главную
</Link>
</div>
)}
</div>
</div>
</main>
<Footer />
</>
);
};
export default VehicleSearchResultsPage;

View File

@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import VehicleSearchSection from '../../components/VehicleSearchSection';
import { GET_LAXIMO_CATALOG_INFO } from '@/lib/graphql';
import { LaximoCatalogInfo } from '@/types/laximo';
const VehicleSearchPage = () => {
const router = useRouter();
const { brand } = router.query;
const [searchType, setSearchType] = useState<'vin' | 'wizard' | 'parts' | 'plate'>('vin');
// Получаем информацию о каталоге
const { data: catalogData, loading: catalogLoading } = useQuery<{ laximoCatalogInfo: LaximoCatalogInfo }>(
GET_LAXIMO_CATALOG_INFO,
{
variables: { catalogCode: brand },
skip: !brand,
errorPolicy: 'all'
}
);
if (catalogLoading) {
return (
<>
<Head>
<title>Поиск автомобиля - {brand}</title>
</Head>
<Header />
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Загружаем информацию о каталоге...</p>
</div>
</main>
<Footer />
</>
);
}
if (!catalogData?.laximoCatalogInfo) {
return (
<>
<Head>
<title>Каталог не найден - {brand}</title>
</Head>
<Header />
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Каталог не найден</h1>
<p className="text-gray-600 mb-8">Каталог "{brand}" не существует или временно недоступен</p>
<button
onClick={() => router.push('/')}
className="bg-red-600 text-white px-6 py-3 rounded-lg hover:bg-red-700 transition-colors"
>
Вернуться на главную
</button>
</div>
</main>
<Footer />
</>
);
}
const catalogInfo = catalogData.laximoCatalogInfo;
return (
<>
<Head>
<title>Поиск автомобиля - {catalogInfo.name}</title>
<meta name="description" content={`Поиск автомобилей ${catalogInfo.name} для подбора запчастей`} />
</Head>
<Header />
<main className="min-h-screen bg-gray-50">
{/* Навигация */}
<nav className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<button
onClick={() => router.back()}
className="text-gray-500 hover:text-gray-700 flex items-center space-x-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Назад</span>
</button>
<div className="text-sm text-gray-500">
<span>Главная</span>
<span className="mx-2">/</span>
<span>Каталог</span>
<span className="mx-2">/</span>
<span className="text-gray-900 font-medium">{catalogInfo.name}</span>
</div>
</div>
</div>
</div>
</nav>
{/* Заголовок */}
<div className="bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center space-x-4">
{catalogInfo.icon && (
<img
src={`/images/brands/${catalogInfo.icon}`}
alt={catalogInfo.name}
className="w-16 h-16 object-contain"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
)}
<div>
<h1 className="text-3xl font-bold text-gray-900">{catalogInfo.name}</h1>
<p className="text-lg text-gray-600 mt-2">
Поиск автомобиля для начала подбора запчастей
</p>
<p className="text-sm text-gray-500 mt-1">
Поиск автомобиля может осуществляться по различным уникальным идентификаторам,
таким как VIN/Frame, специальным параметрам производителя или по артикулу оригинальной детали.
</p>
</div>
</div>
</div>
</div>
{/* Основное содержимое */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<VehicleSearchSection
catalogInfo={catalogInfo}
searchType={searchType}
onSearchTypeChange={setSearchType}
/>
</div>
</main>
<Footer />
</>
);
};
export default VehicleSearchPage;

View File

@ -0,0 +1,311 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
import Footer from '@/components/Footer';
import Layout from '@/components/Layout';
import VehiclePartsSearchSection from '@/components/VehiclePartsSearchSection';
import LaximoDiagnostic from '@/components/LaximoDiagnostic';
import { GET_LAXIMO_VEHICLE_INFO, GET_LAXIMO_CATALOG_INFO } from '@/lib/graphql';
import { LaximoCatalogInfo } from '@/types/laximo';
interface LaximoVehicleInfo {
vehicleid: string;
name: string;
ssd: string;
brand: string;
catalog: string;
attributes: Array<{
key: string;
name: string;
value: string;
}>;
}
const VehicleDetailsPage = () => {
const router = useRouter();
const { brand, vehicleId, oemNumber, searchType: searchTypeParam } = router.query;
// Устанавливаем тип поиска из URL или по умолчанию
// Важно: согласно документации Laximo, для групп быстрого поиска используется ListQuickGroup
// Если в URL передан searchType=categories, мы интерпретируем это как запрос на quickgroups
let defaultSearchType: 'quickgroups' | 'categories' | 'fulltext' = 'quickgroups';
if (searchTypeParam === 'categories') {
// В URL categories, но мы используем quickgroups для групп быстрого поиска
defaultSearchType = 'quickgroups';
console.log('🔄 URL содержит searchType=categories, интерпретируем как quickgroups (группы быстрого поиска)');
} else if (searchTypeParam === 'quickgroups') {
defaultSearchType = 'quickgroups';
} else if (searchTypeParam === 'fulltext') {
defaultSearchType = 'fulltext';
}
const [searchType, setSearchType] = useState<'quickgroups' | 'categories' | 'fulltext'>(defaultSearchType);
// Получаем информацию о каталоге
const { data: catalogData } = useQuery<{ laximoCatalogInfo: LaximoCatalogInfo }>(
GET_LAXIMO_CATALOG_INFO,
{
variables: { catalogCode: brand },
skip: !brand
}
);
// Получаем информацию о выбранном автомобиле
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
const useStorage = router.query.use_storage === '1';
const ssdLengthFromUrl = router.query.ssd_length ? parseInt(router.query.ssd_length as string) : 0;
// Если указано use_storage, пытаемся получить SSD из localStorage
let finalSsd = '';
if (useStorage && typeof window !== 'undefined') {
const vehicleKey = `vehicle_ssd_${brand}_${vehicleId}`;
const storedSsd = localStorage.getItem(vehicleKey);
if (storedSsd) {
finalSsd = storedSsd;
console.log('🔧 SSD получен из localStorage, длина:', storedSsd.length);
// НЕ ОЧИЩАЕМ SSD сразу, оставляем на случай перезагрузки страницы
// localStorage.removeItem(vehicleKey);
} else {
console.log('⚠️ SSD не найден в localStorage, ключ:', vehicleKey);
console.log('🔍 Все ключи localStorage:', Object.keys(localStorage));
}
} else if (ssdFromQuery && ssdFromQuery.trim() !== '') {
finalSsd = ssdFromQuery;
console.log('🔧 SSD получен из URL');
}
console.log('🔍 Vehicle page params:', {
brand,
vehicleId,
useStorage,
ssdLengthFromUrl,
ssdFromQuery: ssdFromQuery ? `${ssdFromQuery.substring(0, 50)}...` : 'отсутствует',
finalSsd: finalSsd ? `${finalSsd.substring(0, 50)}...` : 'отсутствует',
ssdLength: finalSsd.length
});
const { data: vehicleData, loading: vehicleLoading, error: vehicleError } = useQuery<{ laximoVehicleInfo: LaximoVehicleInfo }>(
GET_LAXIMO_VEHICLE_INFO,
{
variables: {
catalogCode: brand,
vehicleId: vehicleId,
...(finalSsd && { ssd: finalSsd }),
localized: true
},
skip: !brand || !vehicleId,
errorPolicy: 'all'
}
);
// Логируем ошибки
if (vehicleError) {
console.error('Vehicle GraphQL error:', vehicleError);
}
if (vehicleLoading) {
return (
<Layout>
<Head>
<title>Загрузка автомобиля...</title>
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Загружаем информацию об автомобиле...</p>
</div>
</main>
</Layout>
);
}
// Если информация о каталоге недоступна, показываем ошибку
if (!catalogData?.laximoCatalogInfo) {
return (
<Layout>
<Head>
<title>Каталог не найден</title>
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Каталог не найден</h1>
<p className="text-gray-600 mb-8">Информация о каталоге недоступна</p>
<button
onClick={() => router.back()}
className="bg-red-600 text-white px-6 py-3 rounded-lg hover:bg-red-700 transition-colors"
>
Назад к поиску
</button>
</div>
</main>
</Layout>
);
}
// Если информация об автомобиле недоступна, создаем заглушку
const vehicleInfo = vehicleData?.laximoVehicleInfo || {
vehicleid: vehicleId as string,
name: `Автомобиль ${catalogData.laximoCatalogInfo.name}`,
ssd: finalSsd,
brand: catalogData.laximoCatalogInfo.brand,
catalog: catalogData.laximoCatalogInfo.code,
attributes: []
};
// Если нет данных автомобиля и есть ошибка, показываем предупреждение
const hasError = vehicleError && !vehicleData?.laximoVehicleInfo;
const catalogInfo = catalogData.laximoCatalogInfo;
return (
<Layout>
<Head>
<title>{vehicleInfo.name} - Поиск запчастей</title>
<meta name="description" content={`Поиск запчастей для ${vehicleInfo.name} в каталоге ${catalogInfo.name}`} />
</Head>
<main className="min-h-screen bg-gray-50">
{/* Навигация */}
<nav className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<button
onClick={() => router.back()}
className="text-gray-500 hover:text-gray-700 flex items-center space-x-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Назад</span>
</button>
<div className="text-sm text-gray-500">
<span>Главная</span>
<span className="mx-2">/</span>
<span>Каталог</span>
<span className="mx-2">/</span>
<span>{catalogInfo.name}</span>
<span className="mx-2">/</span>
<span className="text-gray-900 font-medium">{vehicleInfo.name}</span>
</div>
</div>
</div>
</div>
</nav>
{/* Информация об автомобиле */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center space-x-4 mb-6">
{catalogInfo.icon && (
<img
src={`/images/brands/${catalogInfo.icon}`}
alt={catalogInfo.name}
className="w-12 h-12 object-contain"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
)}
<div>
<h1 className="text-2xl font-bold text-gray-900">{vehicleInfo.name}</h1>
<p className="text-lg text-gray-600">{catalogInfo.name}</p>
</div>
</div>
{/* Предупреждение об ошибке */}
{hasError && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Предупреждение
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>Не удалось загрузить полную информацию об автомобиле. Отображается базовая информация.</p>
</div>
</div>
</div>
</div>
)}
{/* Отладочная информация */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
<h4 className="text-sm font-medium text-gray-900 mb-3">
🔧 Отладочная информация
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Использовать localStorage:</span>
<span className="ml-2 font-medium">{useStorage ? 'Да' : 'Нет'}</span>
</div>
<div>
<span className="text-gray-500">Длина SSD из URL:</span>
<span className="ml-2 font-medium">{ssdLengthFromUrl || 'не указана'}</span>
</div>
<div>
<span className="text-gray-500">SSD получен:</span>
<span className="ml-2 font-medium">{finalSsd ? 'Да' : 'Нет'}</span>
</div>
<div>
<span className="text-gray-500">Длина SSD:</span>
<span className="ml-2 font-medium">{finalSsd.length}</span>
</div>
<div className="md:col-span-2">
<span className="text-gray-500">SSD (первые 100 символов):</span>
<span className="ml-2 font-mono text-xs break-all">
{finalSsd ? finalSsd.substring(0, 100) + '...' : 'отсутствует'}
</span>
</div>
</div>
</div>
{/* Характеристики автомобиля */}
{vehicleInfo.attributes && vehicleInfo.attributes.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{vehicleInfo.attributes.map((attr, index) => (
<div key={index} className="bg-gray-50 rounded-lg p-3">
<dt className="text-sm font-medium text-gray-500">{attr.name}</dt>
<dd className="text-sm text-gray-900 mt-1">{attr.value}</dd>
</div>
))}
</div>
)}
</div>
</div>
{/* Способы поиска запчастей */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6">
<h2 className="text-xl font-bold text-gray-900 mb-2">Поиск запчастей</h2>
<p className="text-gray-600">
Выберите способ поиска запчастей для вашего автомобиля
</p>
</div>
{/* Диагностический компонент */}
<LaximoDiagnostic
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
/>
<VehiclePartsSearchSection
catalogInfo={catalogInfo}
vehicleInfo={vehicleInfo}
searchType={searchType}
onSearchTypeChange={setSearchType}
/>
</div>
</main>
</Layout>
);
};
export default VehicleDetailsPage;

View File

@ -0,0 +1,281 @@
import React from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { GET_LAXIMO_CATALOG_INFO, SEARCH_LAXIMO_OEM } from '@/lib/graphql';
import { LaximoCatalogInfo, LaximoOEMResult } from '@/types/laximo';
const InfoPartDetail = ({ brandName, oemNumber }: { brandName: string; oemNumber: string }) => (
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="/" className="link-block w-inline-block text-[#000814] hover:text-[#EC1C24] transition-colors">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block-2 w-inline-block text-[#000814] hover:text-[#EC1C24] transition-colors">
<div>Каталог</div>
</a>
<div className="text-block-3"></div>
<div className="font-semibold text-gray-900">{brandName}</div>
<div className="text-block-3"></div>
<div className="font-semibold text-gray-900">Деталь {oemNumber}</div>
</div>
<div className="w-layout-hflex flex-block-8 mt-4">
<div className="w-layout-hflex flex-block-10 items-center gap-4">
<h1 className="heading text-2xl font-bold text-gray-900">Деталь {oemNumber}</h1>
</div>
</div>
<div className="text-lg text-gray-600 mt-2">
Подробная информация о детали {oemNumber}
</div>
</div>
</div>
</section>
);
const PartDetailPage = () => {
const router = useRouter();
const { brand, vehicleId, oemNumber } = router.query;
// Получаем SSD из localStorage или URL
const useStorage = router.query.use_storage === '1';
const ssdLengthFromUrl = router.query.ssd_length ? parseInt(router.query.ssd_length as string) : 0;
let finalSsd = '';
if (useStorage && typeof window !== 'undefined') {
const vehicleKey = `vehicle_ssd_${brand}_${vehicleId}`;
const storedSsd = localStorage.getItem(vehicleKey);
if (storedSsd) {
finalSsd = storedSsd;
}
}
// Получаем информацию о каталоге
const { data: catalogData, loading: catalogLoading } = useQuery<{ laximoCatalogInfo: LaximoCatalogInfo }>(
GET_LAXIMO_CATALOG_INFO,
{
variables: { catalogCode: brand },
skip: !brand,
errorPolicy: 'all',
}
);
// Получаем информацию о детали
const { data: oemData, loading: oemLoading, error: oemError } = useQuery<{ laximoOEMSearch: LaximoOEMResult }>(
SEARCH_LAXIMO_OEM,
{
variables: {
catalogCode: brand,
vehicleId: vehicleId,
oemNumber: oemNumber,
ssd: finalSsd
},
skip: !brand || !vehicleId || !oemNumber || !finalSsd,
errorPolicy: 'all'
}
);
if (!brand || !vehicleId || !oemNumber) {
return (
<>
<Head>
<title>Деталь не найдена</title>
</Head>
<Header />
<main style={{ minHeight: '100vh', backgroundColor: '#f9fafb' }}>
<div style={{ textAlign: 'center', padding: '4rem 1rem' }}>
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1f2937', marginBottom: '1rem' }}>
Деталь не найдена
</h1>
<p style={{ color: '#6b7280', marginBottom: '2rem' }}>
Неверные параметры для отображения детали
</p>
<button
onClick={() => router.back()}
style={{
backgroundColor: '#dc2626',
color: 'white',
padding: '0.75rem 1.5rem',
borderRadius: '0.5rem',
border: 'none',
cursor: 'pointer'
}}
>
Назад
</button>
</div>
</main>
<Footer />
</>
);
}
const catalogInfo = catalogData?.laximoCatalogInfo;
const oemResult = oemData?.laximoOEMSearch;
const totalUnits = oemResult?.categories.reduce((total, cat) => total + cat.units.length, 0) || 0;
const totalDetails = oemResult?.categories.reduce((total, cat) =>
total + cat.units.reduce((unitTotal, unit) => unitTotal + unit.details.length, 0), 0) || 0;
return (
<>
<Head>
<title>Деталь {oemNumber} - {catalogInfo?.name || 'Каталог запчастей'}</title>
<meta name="description" content={`Подробная информация о детали ${oemNumber} в каталоге ${catalogInfo?.name}`} />
</Head>
<Header />
<div className="bg-[#F5F8FB] min-h-screen w-full">
<InfoPartDetail brandName={catalogInfo?.name || String(brand)} oemNumber={String(oemNumber)} />
<div className="flex flex-col px-32 pt-10 pb-16 max-md:px-5">
<div className="flex flex-col items-center w-full">
<div className="w-full max-w-[1200px]">
{(catalogLoading || oemLoading) && (
<div className="bg-white rounded-2xl shadow p-10 flex flex-col items-center justify-center min-h-[300px]">
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-red-600 mb-6"></div>
<p className="text-lg text-gray-600">Загружаем информацию о детали {oemNumber}...</p>
</div>
)}
{oemError && !oemLoading && (
<div className="bg-red-50 border border-red-200 rounded-2xl shadow p-10 mb-6">
<div className="flex items-center">
<svg className="w-6 h-6 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="text-lg font-medium text-red-800">Ошибка загрузки</h3>
<p className="text-red-700 mt-1">Не удалось загрузить информацию о детали: {oemError.message}</p>
</div>
</div>
</div>
)}
{oemResult && oemResult.categories.length > 0 ? (
<div className="bg-white rounded-2xl shadow p-10 flex flex-col gap-8">
<div className="border-b border-gray-200 pb-4 mb-4">
<h2 className="text-xl font-semibold text-gray-900">Применимость в автомобиле</h2>
<p className="text-sm text-gray-600 mt-1">Показывает, в каких узлах и категориях используется данная деталь</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div>
<div className="text-sm font-medium text-gray-500">OEM номер</div>
<div className="text-lg font-mono font-semibold text-gray-900">{oemResult.oemNumber}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500">Категорий</div>
<div className="text-lg font-semibold text-gray-900">{oemResult.categories.length}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500">Узлов</div>
<div className="text-lg font-semibold text-gray-900">{totalUnits}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-500">Позиций</div>
<div className="text-lg font-semibold text-gray-900">{totalDetails}</div>
</div>
</div>
</div>
{oemResult.categories.map((category) => (
<div key={category.categoryid} className="bg-gray-50 border border-gray-200 rounded-lg mb-6">
<div className="bg-gray-100 border-b border-gray-200 p-4 rounded-t-lg">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
📂 {category.name}
<span className="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-medium">
{category.units.length} узл{category.units.length === 1 ? '' : category.units.length < 5 ? 'а' : 'ов'}
</span>
</h3>
</div>
<div className="p-6 flex flex-col gap-6">
{category.units.map((unit) => (
<div key={unit.unitid} className="bg-white border border-gray-200 rounded-lg p-4">
<div className="flex gap-4 mb-4">
{unit.imageurl && (
<img
src={unit.imageurl.replace('%size%', '100')}
alt={unit.name}
className="w-16 h-16 object-contain border border-gray-200 rounded bg-white"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
)}
<div>
<h4 className="text-base font-semibold text-gray-900 mb-1">🔧 {unit.name}</h4>
{unit.code && (
<p className="text-xs text-gray-500 font-mono">Код: {unit.code}</p>
)}
</div>
</div>
<div className="flex flex-col gap-4">
{unit.details.map((detail, index) => (
<div key={`${detail.detailid}-${index}`} className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h5 className="text-base font-medium text-gray-900 mb-3">📄 {detail.name}</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span className="font-medium text-gray-500">OEM номер:</span>
<span className="ml-2 font-mono font-semibold text-[#EC1C24]">{detail.oem}</span>
</div>
{detail.brand && (
<div>
<span className="font-medium text-gray-500">Бренд:</span>
<span className="ml-2 font-semibold text-blue-700">{detail.brand}</span>
</div>
)}
{detail.amount && (
<div>
<span className="font-medium text-gray-500">Количество:</span>
<span className="ml-2 bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-semibold">{detail.amount}</span>
</div>
)}
{detail.range && (
<div>
<span className="font-medium text-gray-500">Период:</span>
<span className="ml-2 text-green-700">{detail.range}</span>
</div>
)}
</div>
{detail.attributes && detail.attributes.length > 0 && (
<div className="mt-3">
<span className="text-xs font-medium text-gray-500">Характеристики:</span>
<div className="mt-1 flex flex-col gap-1">
{detail.attributes.map((attr, attrIndex) => (
<div key={attrIndex} className="text-xs text-gray-500">
<span className="font-medium">{attr.name || attr.key}:</span> <span>{attr.value}</span>
</div>
))}
</div>
</div>
)}
<div className="mt-4 flex gap-2">
<button className="bg-[#EC1C24] text-white px-4 py-2 rounded font-medium text-sm hover:bg-[#b91c1c] transition-colors">В корзину</button>
<button className="bg-white text-gray-700 border border-gray-200 px-4 py-2 rounded font-medium text-sm hover:bg-gray-100 transition-colors">Найти аналоги</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
))}
</div>
) : !oemLoading && (
<div className="bg-yellow-50 border border-yellow-200 rounded-2xl shadow p-10 text-center">
<svg className="w-16 h-16 text-yellow-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 14.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h3 className="text-xl font-semibold text-yellow-800 mb-2">Деталь не найдена</h3>
<p className="text-yellow-700 mb-4">По номеру "{oemNumber}" ничего не найдено в данном автомобиле. Проверьте правильность номера или вернитесь к поиску.</p>
</div>
)}
</div>
</div>
</div>
<Footer />
</div>
</>
);
};
export default PartDetailPage;

View File

@ -0,0 +1,275 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { FIND_LAXIMO_VEHICLES_BY_PART_NUMBER } from '@/lib/graphql';
import { LaximoVehiclesByPartResult, LaximoVehicleSearchResult } from '@/types/laximo';
const VehiclesByPartPage = () => {
const router = useRouter();
const { partNumber, catalogCode } = router.query;
const [selectedCatalog, setSelectedCatalog] = useState<string>('all');
// Отладочная информация
console.log('🔍 VehiclesByPartPage - URL параметры:', { partNumber, catalogCode });
console.log('🔍 VehiclesByPartPage - Тип partNumber:', typeof partNumber, 'Значение:', partNumber);
// Очищаем артикул от лишних символов
const cleanPartNumber = partNumber ? (partNumber as string).trim() : '';
console.log('🔍 VehiclesByPartPage - Очищенный артикул:', cleanPartNumber);
// Запрос для поиска автомобилей по артикулу
const { data, loading, error } = useQuery<{ laximoFindVehiclesByPartNumber: LaximoVehiclesByPartResult }>(
FIND_LAXIMO_VEHICLES_BY_PART_NUMBER,
{
variables: { partNumber: cleanPartNumber },
skip: !cleanPartNumber,
errorPolicy: 'all'
}
);
const handleVehicleSelect = (vehicle: LaximoVehicleSearchResult) => {
// Переходим сразу на страницу поиска результатов с артикулом
console.log('🔍 Переход на поиск результатов с артикулом:', { partNumber: cleanPartNumber, vehicle: vehicle.name });
// Определяем бренд для поиска (используем первое слово из названия автомобиля или бренд)
const vehicleBrand = vehicle.brand || vehicle.name?.split(' ')[0] || 'UNKNOWN';
// Переходим на search-result с артикулом
const url = `/search-result?article=${encodeURIComponent(cleanPartNumber)}&brand=${encodeURIComponent(vehicleBrand)}`;
router.push(url);
};
const handleBackToSearch = () => {
router.back();
};
if (loading) {
return (
<>
<Head>
<title>Поиск автомобилей по артикулу {cleanPartNumber} - Protek</title>
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Поиск автомобилей...</p>
</div>
</main>
<Footer />
</>
);
}
if (error || !data?.laximoFindVehiclesByPartNumber) {
return (
<>
<Head>
<title>Ошибка поиска - Protek</title>
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="text-red-500 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 14.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Ошибка поиска</h1>
<p className="text-gray-600 mb-8">Не удалось найти автомобили по артикулу {cleanPartNumber}</p>
<button
onClick={handleBackToSearch}
className="bg-red-600 text-white px-6 py-3 rounded-lg hover:bg-red-700 transition-colors"
>
Назад к поиску
</button>
</div>
</main>
<Footer />
</>
);
}
const result = data.laximoFindVehiclesByPartNumber;
// Фильтруем каталоги по выбранному
const filteredCatalogs = selectedCatalog === 'all'
? result.catalogs
: result.catalogs.filter(catalog => catalog.catalogCode === selectedCatalog);
return (
<>
<Head>
<title>Автомобили по артикулу {cleanPartNumber} - Protek</title>
<meta name="description" content={`Найдено ${result.totalVehicles} автомобилей по артикулу ${cleanPartNumber} в ${result.catalogs.length} каталогах`} />
</Head>
<Header />
<main className="min-h-screen bg-gray-50">
{/* Навигация */}
<nav className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<button
onClick={handleBackToSearch}
className="text-gray-500 hover:text-gray-700 flex items-center space-x-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Назад</span>
</button>
<div className="text-sm text-gray-500">
<span>Главная</span>
<span className="mx-2">/</span>
<span>Поиск</span>
<span className="mx-2">/</span>
<span className="text-gray-900 font-medium">Автомобили по артикулу {cleanPartNumber}</span>
</div>
</div>
</div>
</div>
</nav>
{/* Заголовок и статистика */}
<div className="bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">
Автомобили по артикулу {cleanPartNumber}
</h1>
<p className="text-lg text-gray-600 mt-2">
Найдено {result.totalVehicles} автомобилей в {result.catalogs.length} каталогах
</p>
</div>
{/* Фильтр по каталогам */}
{result.catalogs.length > 1 && (
<div className="flex items-center space-x-4">
<label className="text-sm font-medium text-gray-700">Каталог:</label>
<select
value={selectedCatalog}
onChange={(e) => setSelectedCatalog(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500"
>
<option value="all">Все каталоги ({result.totalVehicles})</option>
{result.catalogs.map((catalog) => (
<option key={catalog.catalogCode} value={catalog.catalogCode}>
{catalog.brand} ({catalog.vehicleCount})
</option>
))}
</select>
</div>
)}
</div>
</div>
</div>
{/* Список автомобилей */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="space-y-6">
{filteredCatalogs.map((catalog) => (
<div key={catalog.catalogCode} className="bg-white rounded-lg shadow-sm border">
{/* Заголовок каталога */}
<div className="px-6 py-4 border-b bg-gray-50">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
{catalog.brand}
</h2>
<span className="text-sm text-gray-600">
{catalog.vehicleCount} автомобилей
</span>
</div>
</div>
{/* Список автомобилей в каталоге */}
<div className="divide-y divide-gray-200">
{catalog.vehicles.map((vehicle, index) => (
<div
key={`${vehicle.vehicleid}-${index}`}
className="p-6 hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => handleVehicleSelect(vehicle)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-medium text-gray-900">
{vehicle.name || `${vehicle.brand} ${vehicle.model || 'Vehicle'}`}
</h3>
{vehicle.year && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{vehicle.year}
</span>
)}
</div>
<div className="space-y-1">
{vehicle.modification && (
<p className="text-sm text-gray-600">
<span className="font-medium">Модификация:</span> {vehicle.modification}
</p>
)}
{vehicle.engine && (
<p className="text-sm text-gray-600">
<span className="font-medium">Двигатель:</span> {vehicle.engine}
</p>
)}
{vehicle.bodytype && (
<p className="text-sm text-gray-600">
<span className="font-medium">Кузов:</span> {vehicle.bodytype}
</p>
)}
{(vehicle as any).transmission && (
<p className="text-sm text-gray-600">
<span className="font-medium">КПП:</span> {(vehicle as any).transmission}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<button className="text-red-600 hover:text-red-700 font-medium text-sm">
Выбрать автомобиль
</button>
</div>
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Пустое состояние */}
{filteredCatalogs.length === 0 && (
<div className="bg-white rounded-lg shadow-sm border p-12 text-center">
<div className="text-gray-400 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.562M15 6.306a7.962 7.962 0 00-6 0m6 0V3a1 1 0 00-1-1H8a1 1 0 00-1 1v3.306" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
Автомобили не найдены
</h3>
<p className="text-gray-600">
В выбранном каталоге нет автомобилей с артикулом {partNumber}
</p>
</div>
)}
</div>
</main>
<Footer />
</>
);
};
export default VehiclesByPartPage;

51
src/pages/wholesale.tsx Normal file
View File

@ -0,0 +1,51 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import InfoWholesale from "@/components/wholesale/InfoWholesale";
import DescWholesale from "@/components/wholesale/DescWholesale";
import WhyWholesale from "@/components/wholesale/WhyWholesale";
import ServiceWholesale from "@/components/wholesale/ServiceWholesale";
import HowToBuy from "@/components/wholesale/HowToBuy";
import Help from "@/components/Help";
export default function Wholesale() {
return (
<>
<Head>
<title>wholesale</title>
<meta content="wholesale" property="og:title" />
<meta content="wholesale" property="twitter:title" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta content="Webflow" name="generator" />
<link href="/css/normalize.css" rel="stylesheet" type="text/css" />
<link href="/css/webflow.css" rel="stylesheet" type="text/css" />
<link href="/css/protekproject.webflow.css" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoWholesale />
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-67">
<DescWholesale />
<WhyWholesale />
<ServiceWholesale />
<HowToBuy />
<Help />
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}