pravki 29.06 #3

Merged
egortriston merged 1 commits from pravki2906 into main 2025-06-29 00:42:23 +03:00
10 changed files with 693 additions and 237 deletions

View File

@ -1,6 +1,11 @@
import React from "react";
const InfoVin = () => (
interface InfoVinProps {
vehicleName: string;
vehicleInfo: string;
}
const InfoVin: React.FC<InfoVinProps> = ({ vehicleName, vehicleInfo }) => (
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
@ -9,22 +14,22 @@ const InfoVin = () => (
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block w-inline-block">
<a href="/brands" 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>Audi Q7</div>
<div>{vehicleName}</div>
</a>
</div>
<div className="w-layout-hflex flex-block-8">
<div className="w-layout-hflex flex-block-10">
<h1 className="heading">Audi Q7</h1>
<h1 className="heading">{vehicleName}</h1>
</div>
</div>
</div>
<div className="w-layout-hflex flex-block-112">
<div className="text-block-55">WAUZZZ4M6JD010702 · 2018 · SUQ(8A) · CVMD · 3000CC / 249hp / 183kW TDI CR</div>
<div className="text-block-55">{vehicleInfo}</div>
<div className="w-embed">
{/* SVG icon */}
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@ -1,9 +1,36 @@
import React from "react";
const KnotIn = () => (
<div className="knotin">
<img src="/images/image-44.jpg" loading="lazy" alt="" className="image-26" />
</div>
);
// Функция для корректного формирования URL изображения
const getImageUrl = (baseUrl: string, size: string) => {
if (!baseUrl) return '';
return baseUrl
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace('%size%', size);
};
const KnotIn = ({ node }: { node: any }) => {
if (!node) return null;
let imageUrl = '';
if (node.imageurl) {
imageUrl = getImageUrl(node.imageurl, '250');
} else if (node.largeimageurl) {
imageUrl = node.largeimageurl;
}
return (
<div className="knotin">
{imageUrl ? (
<img src={imageUrl} loading="lazy" alt={node.name || "Изображение узла"} className="image-26" />
) : (
<div style={{ width: 200, height: 200, background: '#eee', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
Нет изображения
</div>
)}
{/* <div style={{ marginTop: 8, fontWeight: 500 }}>{node.name}</div> */}
</div>
);
};
export default KnotIn;

View File

@ -1,43 +1,52 @@
import React from "react";
import { useRouter } from "next/router";
const parts = [
{ n: 1, oem: "059198405B", name: "Фильтрующий элемент с проклад." },
{ n: 2, oem: "N 10196103", name: "Винт с цилиндр. скруглённой головкой Torx" },
{ n: 3, oem: "059117070J", name: "Уплотнитель" },
{ n: 4, oem: "N 10124306", name: "Винт с плоской головкой и внутренним Torx" },
{ n: 5, oem: "059117015K", name: "Масляный радиатор" },
{ n: 6, oem: "059117015K", name: "Масляный радиатор" },
{ n: 7, oem: "059117015K", name: "Масляный радиатор" },
{ n: 8, oem: "059117015K", name: "Масляный радиатор" },
{ n: 9, oem: "059117015K", name: "Масляный радиатор" },
{ n: 10, oem: "059117015K", name: "Масляный радиатор" },
{ n: 11, oem: "059117015K", name: "Масляный радиатор" },
{ n: 12, oem: "059117015K", name: "Масляный радиатор" },
{ n: 13, oem: "059117015K", name: "Масляный радиатор" },
{ n: 14, oem: "059117015K", name: "Масляный радиатор" },
{ n: 15, oem: "059117015K", name: "Масляный радиатор" },
];
interface KnotPartsProps {
parts: Array<{
detailid?: string;
codeonimage?: string | number;
oem?: string;
name?: string;
price?: string | number;
brand?: string;
availability?: string;
note?: string;
attributes?: Array<{ key: string; name?: string; value: string }>;
}>;
}
const KnotParts = () => (
<div className="knot-parts">
{parts.map((part, idx) => (
<div className="w-layout-hflex knotlistitem" key={idx}>
<div className="w-layout-hflex flex-block-116">
<div className="nuberlist">{part.n}</div>
<div className="oemnuber">{part.oem}</div>
</div>
<div className="partsname">{part.name}</div>
<div className="w-layout-hflex flex-block-117">
<a href="#" className="button-3 w-button">Цена</a>
<div className="code-embed-16 w-embed">
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
</svg>
const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
const router = useRouter();
return (
<div className="knot-parts">
{parts.map((part, idx) => (
<div className="w-layout-hflex knotlistitem" key={part.detailid || idx}>
<div className="w-layout-hflex flex-block-116">
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
<div className="oemnuber">{part.oem}</div>
</div>
<div className="partsname">{part.name}</div>
<div className="w-layout-hflex flex-block-117">
<button
className="button-3 w-button"
onClick={() => {
if (part.oem) {
router.push(`/search?q=${encodeURIComponent(part.oem)}&mode=parts`);
}
}}
>
Цена
</button>
<div className="code-embed-16 w-embed">
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
</svg>
</div>
</div>
</div>
</div>
))}
</div>
);
))}
</div>
);
};
export default KnotParts;

View File

@ -1,40 +1,128 @@
import React from "react";
const categories = [
"Детали для ТО",
"Двигатель",
"Топливная система",
"Система охлаждения",
"Система выпуска",
"Трансмиссия",
"Ходовая часть",
"Рулевое управление",
"Тормозная система",
"Электрооборудование",
"Отопление / кондиционирование",
"Детали салона",
"Детали кузова",
"Дополнительное оборудование"
];
import React, { useState, useRef } from "react";
import { useQuery, useLazyQuery } from "@apollo/client";
import { GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS } from "@/lib/graphql/laximo";
interface VinCategoryProps {
onCategoryClick?: (e: React.MouseEvent) => void;
catalogCode: string;
vehicleId: string;
ssd?: string;
onNodeSelect?: (node: any) => void;
}
const VinCategory: React.FC<VinCategoryProps> = ({ onCategoryClick }) => (
<div className="w-layout-vflex flex-block-14-copy-copy">
{categories.map((cat, idx) => (
<div className="div-block-131" key={idx} onClick={onCategoryClick} style={{ cursor: onCategoryClick ? 'pointer' : undefined }}>
<div className="text-block-57">{cat}</div>
<div className="w-embed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
</svg>
</div>
</div>
))}
</div>
);
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect }) => {
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId,
errorPolicy: "all",
});
const categories = categoriesData?.laximoCategories || [];
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
const [getUnits] = useLazyQuery(GET_LAXIMO_UNITS, {
onCompleted: (data) => {
if (data && data.laximoUnits && lastCategoryIdRef.current) {
setUnitsByCategory((prev) => ({
...prev,
[lastCategoryIdRef.current!]: data.laximoUnits || [],
}));
}
},
});
const [selectedCategory, setSelectedCategory] = useState<any | null>(null);
const lastCategoryIdRef = useRef<string | null>(null);
// Если выбрана категория — показываем подкатегории (children или units)
let subcategories: any[] = [];
if (selectedCategory) {
if (selectedCategory.children && selectedCategory.children.length > 0) {
subcategories = selectedCategory.children;
} else {
subcategories = unitsByCategory[selectedCategory.quickgroupid] || [];
}
}
const handleCategoryClick = (cat: any) => {
if (cat.children && cat.children.length > 0) {
setSelectedCategory(cat);
} else {
// Если нет children, грузим units (подкатегории)
if (!unitsByCategory[cat.quickgroupid]) {
lastCategoryIdRef.current = cat.quickgroupid;
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId: cat.quickgroupid } });
}
setSelectedCategory(cat);
}
};
const handleBack = () => {
setSelectedCategory(null);
};
const handleSubcategoryClick = (subcat: any) => {
if (onNodeSelect) {
onNodeSelect({
...subcat,
unitid: subcat.unitid || subcat.quickgroupid || subcat.id,
});
}
};
if (categoriesLoading) return <div>Загрузка категорий...</div>;
if (categoriesError) return <div style={{ color: "red" }}>Ошибка: {categoriesError.message}</div>;
return (
<div className="w-layout-vflex flex-block-14-copy-copy">
{!selectedCategory ? (
// Список категорий
categories.map((cat: any, idx: number) => (
<div
className="div-block-131"
key={cat.quickgroupid || cat.id || idx}
onClick={() => handleCategoryClick(cat)}
style={{ cursor: "pointer" }}
>
<div className="text-block-57">{cat.name}</div>
<div className="w-embed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
</svg>
</div>
</div>
))
) : (
// Список подкатегорий (children или units)
<>
<div className="div-block-131" onClick={handleBack} style={{ cursor: "pointer", fontWeight: 500 }}>
<div className="text-block-57"> Назад</div>
<div className="w-embed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
</svg>
</div>
</div>
{subcategories.length === 0 && <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>}
{subcategories.map((subcat: any, idx: number) => (
<div
className="div-block-131"
key={subcat.quickgroupid || subcat.unitid || subcat.id || idx}
onClick={() => handleSubcategoryClick(subcat)}
style={{ cursor: "pointer" }}
>
<div className="text-block-57">{subcat.name}</div>
<div className="w-embed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
</svg>
</div>
</div>
))}
</>
)}
</div>
);
};
export default VinCategory;

View File

@ -1,35 +1,86 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useLazyQuery, useQuery } from '@apollo/client';
import { SEARCH_LAXIMO_FULLTEXT, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS } from '@/lib/graphql/laximo';
const dropdownTitles = [
"Детали для ТО",
"Двигатель",
"Топливная система",
"Система охлаждения",
"Система выпуска",
"Трансмиссия",
"Ходовая часть",
"Рулевое управление",
"Тормозная система",
"Электрооборудование",
"Отопление / кондиционирование",
"Детали салона",
"Детали кузова",
"Дополнительное оборудование"
];
interface VinLeftbarProps {
catalogCode?: string;
vehicleId?: string;
ssd?: string;
onSearchResults?: (results: any[]) => void;
onNodeSelect?: (node: any) => void;
}
const VinLeftbar = () => {
const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, onSearchResults, onNodeSelect }) => {
const [openIndex, setOpenIndex] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
const [executeSearch, { data, loading, error }] = useLazyQuery(SEARCH_LAXIMO_FULLTEXT, { errorPolicy: 'all' });
const handleToggle = (idx: number) => {
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId,
errorPolicy: 'all'
});
const categories = categoriesData?.laximoCategories || [];
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
const [getUnits] = useLazyQuery(GET_LAXIMO_UNITS, {
onCompleted: (data) => {
if (data && data.laximoUnits && lastCategoryIdRef.current) {
setUnitsByCategory(prev => ({
...prev,
[lastCategoryIdRef.current!]: data.laximoUnits || []
}));
}
}
});
const lastCategoryIdRef = React.useRef<string | null>(null);
const handleToggle = (idx: number, categoryId: string) => {
setOpenIndex(openIndex === idx ? null : idx);
if (openIndex !== idx && !unitsByCategory[categoryId]) {
lastCategoryIdRef.current = categoryId;
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId } });
}
};
const handleSearch = () => {
if (!searchQuery.trim() || !catalogCode || !vehicleId || !ssd) return;
executeSearch({
variables: {
catalogCode,
vehicleId,
searchQuery: searchQuery.trim(),
ssd
}
});
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
};
const searchResults = data?.laximoFulltextSearch;
useEffect(() => {
if (searchResults && onSearchResults) {
onSearchResults(searchResults.details || []);
}
if (!searchQuery.trim() && onSearchResults) {
onSearchResults([]);
}
}, [searchResults, searchQuery, onSearchResults]);
return (
<div className="w-layout-vflex vinleftbar">
<div className="div-block-2">
<div className="form-block w-form">
<form id="wf-form-search" name="wf-form-search" data-name="search" action="http://search" method="post" className="form" data-wf-page-id="685d5478c4ebd5c8793f8c54" data-wf-element-id="14d3a852-00ba-b161-8849a97059b3785d">
<a href="#" className="link-block-3 w-inline-block">
<form id="vin-form-search" name="vin-form-search" data-name="vin-form-search" action="#" method="post" className="form">
<a href="#" className="link-block-3 w-inline-block" onClick={e => { e.preventDefault(); handleSearch(); }}>
<div className="code-embed-6 w-embed">
{/* SVG */}
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -38,48 +89,124 @@ const VinLeftbar = () => {
</svg>
</div>
</a>
<input className="text-field w-input" maxLength={256} name="Search" data-name="Search" placeholder="Поиск по названию детали" type="text" id="Search-4" required />
<input
className="text-field w-input"
maxLength={256}
name="VinSearch"
data-name="VinSearch"
placeholder="Поиск по названию детали"
type="text"
id="VinSearchInput"
required
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
disabled={!catalogCode || !vehicleId || !ssd}
/>
</form>
<div className="success-message w-form-done">
<div>Thank you! Your submission has been received!</div>
</div>
<div className="error-message w-form-fail">
<div>Oops! Something went wrong while submitting the form.</div>
</div>
{error && <div style={{ color: 'red', fontSize: 12 }}>Ошибка поиска: {error.message}</div>}
</div>
</div>
<div className="w-layout-vflex flex-block-113">
<div className="w-layout-hflex flex-block-114">
<a href="#" className="button-3 w-button">Узлы</a>
<a href="#" className="button-23 w-button">От производителя</a>
<a
href="#"
className={
searchQuery
? 'button-23 w-button'
: activeTab === 'uzly'
? 'button-3 w-button'
: 'button-23 w-button'
}
onClick={e => {
e.preventDefault();
if (searchQuery) setSearchQuery('');
setActiveTab('uzly');
}}
>
Узлы
</a>
<a
href="#"
className={
searchQuery
? 'button-23 w-button'
: activeTab === 'manufacturer'
? 'button-3 w-button'
: 'button-23 w-button'
}
onClick={e => {
e.preventDefault();
if (searchQuery) setSearchQuery('');
setActiveTab('manufacturer');
}}
>
От производителя
</a>
</div>
{/* Dropdowns start */}
{dropdownTitles.map((title, idx) => {
const isOpen = openIndex === idx;
return (
<div
key={idx}
data-hover="false"
data-delay="0"
className={`dropdown-4 w-dropdown${isOpen ? " w--open" : ""}`}
>
<div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
onClick={() => handleToggle(idx)}
style={{ cursor: "pointer" }}
>
<div className="w-icon-dropdown-toggle"></div>
<div className="text-block-56">{title}</div>
</div>
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
<a href="#" className="dropdown-link-3 w-dropdown-link">Link 1</a>
<a href="#" className="dropdown-link-3 w-dropdown-link">Link 2</a>
<a href="#" className="dropdown-link-3 w-dropdown-link">Link 3</a>
</nav>
</div>
);
})}
{/* Dropdowns end */}
{/* Tab content start */}
{activeTab === 'uzly' ? (
categoriesLoading ? (
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем категории...</div>
) : categoriesError ? (
<div style={{ color: 'red', padding: 16 }}>Ошибка загрузки категорий: {categoriesError.message}</div>
) : (
<>
{categories.map((category: any, idx: number) => {
const isOpen = openIndex === idx;
// Подкатегории: сначала children, если нет — unitsByCategory
const subcategories = category.children && category.children.length > 0
? category.children
: unitsByCategory[category.quickgroupid] || [];
return (
<div
key={category.quickgroupid}
data-hover="false"
data-delay="0"
className={`dropdown-4 w-dropdown${isOpen ? " w--open" : ""}`}
>
<div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
onClick={() => handleToggle(idx, category.quickgroupid)}
style={{ cursor: "pointer" }}
>
<div className="w-icon-dropdown-toggle"></div>
<div className="text-block-56">{category.name}</div>
</div>
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
{subcategories.length > 0 ? (
subcategories.map((subcat: any) => (
<a
href="#"
key={subcat.quickgroupid || subcat.unitid}
className="dropdown-link-3 w-dropdown-link"
onClick={e => {
e.preventDefault();
if (onNodeSelect) {
onNodeSelect({
...subcat,
unitid: subcat.unitid || subcat.quickgroupid || subcat.id
});
}
}}
>
{subcat.name}
</a>
))
) : (
<span style={{ color: '#888', padding: 8 }}>Нет подкатегорий</span>
)}
</nav>
</div>
);
})}
</>
)
) : (
// Manufacturer tab content (заглушка)
<div style={{ padding: '16px', color: '#888' }}>Здесь будет контент "От производителя"</div>
)}
{/* Tab content end */}
</div>
</div>
);

View File

@ -0,0 +1,38 @@
import React from "react";
import { useRouter } from 'next/router';
interface VinPartCardProps {
n?: number;
oem: string;
name: string;
onPriceClick?: () => void;
}
const VinPartCard: React.FC<VinPartCardProps> = ({ n, oem, name, onPriceClick }) => {
const router = useRouter();
const handlePriceClick = (e: React.MouseEvent) => {
e.preventDefault();
if (onPriceClick) onPriceClick();
if (oem) router.push(`/search?q=${encodeURIComponent(oem)}&mode=parts`);
};
return (
<div className="w-layout-hflex knotlistitem">
<div className="w-layout-hflex flex-block-116">
{n !== undefined && <div className="nuberlist">{n}</div>}
<div className="oemnuber">{oem}</div>
</div>
<div className="partsname">{name}</div>
<div className="w-layout-hflex flex-block-117">
<a href="#" className="button-3 w-button" onClick={handlePriceClick}>Цена</a>
<div className="code-embed-16 w-embed">
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
</svg>
</div>
</div>
</div>
);
};
export default VinPartCard;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import Head from 'next/head';
@ -6,8 +6,16 @@ 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 { GET_LAXIMO_VEHICLE_INFO, GET_LAXIMO_CATALOG_INFO, GET_LAXIMO_UNIT_DETAILS } from '@/lib/graphql';
import { LaximoCatalogInfo } from '@/types/laximo';
import InfoVin from '@/components/vin/InfoVin';
import VinLeftbar from '@/components/vin/VinLeftbar';
import VinKnot from '@/components/vin/VinKnot';
import VinCategory from '@/components/vin/VinCategory';
import PartDetailCard from '@/components/PartDetailCard';
import VinPartCard from '@/components/vin/VinPartCard';
import KnotIn from '@/components/vin/KnotIn';
import KnotParts from '@/components/vin/KnotParts';
interface LaximoVehicleInfo {
vehicleid: string;
@ -41,7 +49,27 @@ const VehicleDetailsPage = () => {
defaultSearchType = 'fulltext';
}
// ====== ВСЕ ХУКИ В НАЧАЛЕ КОМПОНЕНТА ======
const [searchType, setSearchType] = useState<'quickgroups' | 'categories' | 'fulltext'>(defaultSearchType);
const [showKnot, setShowKnot] = useState(false);
const [foundParts, setFoundParts] = useState<any[]>([]);
const [selectedNode, setSelectedNode] = useState<any | null>(null);
const handleCategoryClick = (e?: React.MouseEvent) => {
if (e) e.preventDefault();
setShowKnot(true);
};
useEffect(() => {
const handler = (e: Event) => {
const target = e.target as HTMLElement;
if (target.classList.contains('link-2')) {
e.preventDefault();
setShowKnot(true);
}
};
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}, []);
// ====== КОНЕЦ ХУКОВ ======
// Получаем информацию о каталоге
const { data: catalogData } = useQuery<{ laximoCatalogInfo: LaximoCatalogInfo }>(
@ -100,6 +128,28 @@ const VehicleDetailsPage = () => {
}
);
// Получаем детали выбранного узла, если он выбран
const {
data: unitDetailsData,
loading: unitDetailsLoading,
error: unitDetailsError
} = useQuery(
GET_LAXIMO_UNIT_DETAILS,
{
variables: selectedNode
? {
catalogCode: selectedNode.catalogCode || selectedNode.catalog || brand,
vehicleId: selectedNode.vehicleId || vehicleId,
unitId: selectedNode.unitid || selectedNode.unitId,
ssd: selectedNode.ssd || finalSsd || '',
}
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
skip: !selectedNode,
errorPolicy: 'all',
}
);
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
// Логируем ошибки
if (vehicleError) {
console.error('Vehicle GraphQL error:', vehicleError);
@ -107,7 +157,7 @@ const VehicleDetailsPage = () => {
if (vehicleLoading) {
return (
<Layout>
<>
<Head>
<title>Загрузка автомобиля...</title>
</Head>
@ -117,14 +167,14 @@ const VehicleDetailsPage = () => {
<p className="mt-4 text-lg text-gray-600">Загружаем информацию об автомобиле...</p>
</div>
</main>
</Layout>
</>
);
}
// Если информация о каталоге недоступна, показываем ошибку
if (!catalogData?.laximoCatalogInfo) {
return (
<Layout>
<>
<Head>
<title>Каталог не найден</title>
</Head>
@ -140,7 +190,7 @@ const VehicleDetailsPage = () => {
</button>
</div>
</main>
</Layout>
</>
);
}
@ -159,13 +209,84 @@ const VehicleDetailsPage = () => {
const catalogInfo = catalogData.laximoCatalogInfo;
return (
<Layout>
<>
<Head>
<title>{vehicleInfo.name} - Поиск запчастей</title>
<meta name="description" content={`Поиск запчастей для ${vehicleInfo.name} в каталоге ${catalogInfo.name}`} />
<title>VIN</title>
<meta content="vin" property="og:title" />
<meta content="vin" 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>
<main className="min-h-screen bg-gray-50">
{/* ====== ВРЕМЕННЫЙ МАКЕТ ДЛЯ ВЕРСТКИ (начало) ====== */}
<InfoVin
vehicleName={
vehicleInfo.brand && vehicleInfo.name && vehicleInfo.name.indexOf(vehicleInfo.brand) !== 0
? `${vehicleInfo.brand} ${vehicleInfo.name}`
: vehicleInfo.name
}
vehicleInfo={
vehicleInfo.attributes && vehicleInfo.attributes.length > 0
? vehicleInfo.attributes.map(attr => attr.value).join(' · ')
: ''
}
/>
<div className="w-layout-blockcontainer container-vin w-container">
{!selectedNode ? (
<div className="w-layout-hflex flex-block-13">
<VinLeftbar
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
onSearchResults={setFoundParts}
onNodeSelect={setSelectedNode}
/>
{/* Категории или Knot или карточки */}
{foundParts.length > 0 ? (
<div className="knot-parts">
{foundParts.map((detail, idx) => (
<VinPartCard
key={detail.oem + idx}
n={idx + 1}
name={detail.name}
oem={detail.oem}
/>
))}
</div>
) : showKnot ? (
<VinKnot />
) : (
<VinCategory
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
onNodeSelect={setSelectedNode}
/>
)}
</div>
) : (
<div className="w-layout-hflex flex-block-13">
<div className="w-layout-vflex flex-block-14-copy-copy">
<button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button>
<KnotIn node={selectedNode} />
{unitDetailsLoading ? (
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
) : unitDetailsError ? (
<div style={{ color: 'red', padding: 24 }}>Ошибка загрузки деталей: {unitDetailsError.message}</div>
) : unitDetails.length > 0 ? (
<KnotParts parts={unitDetails} />
) : (
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
)}
</div>
</div>
)}
</div>
{/* ====== ВРЕМЕННЫЙ МАКЕТ ДЛЯ ВЕРСТКИ (конец) ====== */}
{/* Навигация */}
<nav className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@ -303,8 +424,7 @@ const VehicleDetailsPage = () => {
onSearchTypeChange={setSearchType}
/>
</div>
</main>
</Layout>
</>
);
};

View File

@ -38,10 +38,6 @@ export default function Vin() {
<meta content="vin" property="og:title" />
<meta content="vin" 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" />

View File

@ -403,4 +403,20 @@ input.input-receiver:focus {
}
.tabs-menu.w-tab-menu::-webkit-scrollbar {
display: none;
}
input.text-field,
input.w-input,
input#VinSearchInput {
background: #fff !important;
}
.text-block-56, .dropdown-link-3 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
max-width: 90%;
}

View File

@ -7458,31 +7458,43 @@ body {
}
.flex-block-78 {
flex-flow: row;
justify-content: space-between;
align-items: flex-start;
display: none;
}
.flex-block-80 {
display: none;
align-items: center;
}
.flex-block-81 {
grid-column-gap: 5px;
grid-row-gap: 5px;
flex-flow: row;
justify-content: space-between;
align-self: stretch;
align-items: center;
}
.flex-block-82 {
grid-column-gap: 30px;
grid-row-gap: 30px;
.core-product-copy {
flex-flow: column;
justify-content: flex-start;
align-items: center;
align-items: flex-start;
}
.sort-item-brand-copy {
width: 50px;
.core-product-search-copy {
flex-flow: row;
align-self: stretch;
max-width: 100%;
height: 340px;
display: flex;
overflow: scroll;
}
.raiting-copy, .pcs-copy {
display: none;
}
.item-recommend-copy {
display: block;
}
.flex-block-83 {
flex-flow: column;
}
.flex-block-84 {
@ -7496,40 +7508,55 @@ body {
.flex-block-85 {
grid-column-gap: 5px;
grid-row-gap: 5px;
border-radius: var(--_round---normal);
border-radius: var(--_round---small-8);
background-color: var(--white);
justify-content: flex-start;
align-items: center;
padding: 10px 20px;
padding: 8px 12px;
display: flex;
}
.code-embed-9 {
color: var(--_button---primary);
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}
.flex-block-77-copy {
.image-15 {
max-width: 200px;
}
.code-embed-10 {
color: var(--white);
width: 12px;
height: 16px;
}
.flex-block-86 {
grid-column-gap: 5px;
grid-row-gap: 5px;
border-radius: var(--_round---small-8);
background-color: var(--_button---hover-dark_blue);
justify-content: flex-start;
align-items: center;
padding: 10px 15px;
}
.flex-block-18-copy-copy {
grid-column-gap: 40px;
grid-row-gap: 40px;
flex-flow: column;
grid-column-gap: 10px;
grid-row-gap: 10px;
}
.link-block-4-copy {
flex: 0 auto;
}
.heading-8-copy {
color: var(--_fonts---color--light-blue-grey);
font-size: var(--_fonts---font-size--small-font-size);
align-self: stretch;
padding-left: 0;
padding-right: 0;
margin-left: 0;
margin-right: 0;
font-weight: 400;
line-height: 20px;
display: block;
overflow: hidden;
}
.dropdown-2 {
@ -7537,95 +7564,98 @@ body {
margin-right: 0;
}
.dropdown-list-2 {
background-color: var(--white);
box-shadow: 0 2px 5px #0003;
}
.dropdown-list-2.w--open {
border-radius: var(--_round---small-8);
}
.heading-9-copy {
font-size: var(--_fonts---font-size--bigger);
margin-top: 0;
}
.info-block-search-copy {
grid-column-gap: 10px;
grid-row-gap: 10px;
flex-flow: column;
justify-content: space-between;
align-self: stretch;
align-items: center;
align-items: flex-start;
}
.heading-9-copy-copy {
font-size: var(--_fonts---font-size--small-font-size);
line-height: 18px;
}
.section-2 {
padding-left: 15px;
padding-right: 15px;
}
.mobile-block {
display: flex;
flex: 0 auto;
width: auto;
}
.flex-block-87 {
grid-column-gap: 10px;
grid-row-gap: 10px;
flex: 1;
grid-column-gap: 0px;
grid-row-gap: 0px;
}
.mobile-menu-bottom {
padding-top: 0;
padding-bottom: 0;
box-shadow: 0 0 5px #0003;
margin-left: 0;
margin-right: 0;
padding-left: 15px;
padding-right: 15px;
}
.mobile-menu-bottom.info {
padding-top: 20px;
padding-bottom: 20px;
.mobile-menu-bottom.nav, .mobile-menu-bottom.info {
padding-left: 15px;
padding-right: 15px;
}
.mobile-menu-buttom-section {
display: block;
.mobile-menu-bottom.subscribe, .mobile-menu-bottom.footer {
padding: 40px 15px;
}
.name-mobile-menu-item {
color: var(--black);
font-size: var(--_fonts---font-size--small-font-size);
font-weight: 400;
}
.button-for-mobile-menu-block {
grid-column-gap: 2px;
grid-row-gap: 2px;
background-color: var(--_fonts---color--white);
color: var(--_button---light-blue-grey);
flex-flow: column;
width: 70px;
}
.button-for-mobile-menu-block:hover {
background-color: var(--_button---light-blue);
}
.icon_favorite {
color: var(--_button---light-blue-grey);
}
.block-for-moble-menu-icon {
width: 30px;
height: 30px;
}
.div-block-25 {
width: 20px;
height: 20px;
display: block;
}
.info-satus {
background-color: var(--green);
color: var(--_fonts---color--white);
font-size: 10px;
font-weight: 400;
.button-for-mobile-menu-block {
grid-column-gap: 0px;
grid-row-gap: 0px;
width: 60px;
padding-bottom: 5px;
}
.flex-block-93 {
align-self: auto;
min-height: 48px;
.section-3 {
padding-left: 15px;
padding-right: 15px;
}
.sort-list-card {
padding-right: 30px;
.nav-menu-3 {
display: none;
}
.flex-block-93 {
margin-left: 0;
}
.sort-list-card {
grid-column-gap: 0px;
grid-row-gap: 0px;
padding-left: 18px;
padding-right: 18px;
}
.flex-block-49-copy {
grid-column-gap: 30px;
grid-row-gap: 30px;
grid-column-gap: 28px;
grid-row-gap: 28px;
}
.price-in-cart-s1 {