2 Commits

Author SHA1 Message Date
d62db55160 checkbox 2025-06-29 15:44:36 +03:00
e8f1fecb47 pravkiend cart 2025-06-29 12:36:49 +03:00
14 changed files with 516 additions and 141 deletions

View File

@ -52,7 +52,7 @@ const BrandSelectionModal: React.FC<BrandSelectionModalProps> = ({
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
className="fixed inset-0 bg-black/10 bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={handleBackdropClick}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden">

View File

@ -11,6 +11,7 @@ const CartInfo: React.FC = () => {
};
return (
<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">
@ -44,6 +45,7 @@ const CartInfo: React.FC = () => {
</div>
</div>
</div>
</section>
);
};

View File

@ -80,14 +80,49 @@ const CartItem: React.FC<CartItemProps> = ({
<div className="text-block-21-copy-copy">{deliveryDate}</div>
</div>
<div className="w-layout-hflex pcs-cart-s1">
<div className="minus-plus" onClick={() => onCountChange && onCountChange(count - 1)} style={{ cursor: 'pointer' }}>
<img loading="lazy" src="/images/minus_icon.svg" alt="-" />
<div
className="minus-plus"
onClick={() => onCountChange && onCountChange(count - 1)}
style={{ cursor: 'pointer' }}
aria-label="Уменьшить количество"
tabIndex={0}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)}
role="button"
>
<div className="pluspcs w-embed">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor"/>
</svg>
</div>
</div>
<div className="input-pcs">
<div className="text-block-26">{count}</div>
<input
type="number"
min={1}
value={count}
onChange={e => {
const value = Math.max(1, parseInt(e.target.value, 10) || 1);
onCountChange && onCountChange(value);
}}
className="text-block-26 w-full text-center outline-none"
aria-label="Количество"
style={{ width: 40 }}
/>
</div>
<div className="minus-plus" onClick={() => onCountChange && onCountChange(count + 1)} style={{ cursor: 'pointer' }}>
<img loading="lazy" src="/images/plus_icon.svg" alt="+" />
<div
className="minus-plus"
onClick={() => onCountChange && onCountChange(count + 1)}
style={{ cursor: 'pointer' }}
aria-label="Увеличить количество"
tabIndex={0}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)}
role="button"
>
<div className="pluspcs w-embed">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor"/>
</svg>
</div>
</div>
</div>
<div className="w-layout-hflex flex-block-39-copy-copy">
@ -100,7 +135,31 @@ const CartItem: React.FC<CartItemProps> = ({
<path d="M9 16.5L7.84 15.4929C3.72 11.93 1 9.57248 1 6.69619C1 4.33869 2.936 2.5 5.4 2.5C6.792 2.5 8.128 3.11798 9 4.08692C9.872 3.11798 11.208 2.5 12.6 2.5C15.064 2.5 17 4.33869 17 6.69619C17 9.57248 14.28 11.93 10.16 15.4929L9 16.5Z" fill={favorite ? "#e53935" : "currentColor"} />
</svg>
</div>
<img src="/images/delete.svg" loading="lazy" alt="" className="image-13" style={{ cursor: 'pointer' }} onClick={onRemove} />
<div
className="bdel"
role="button"
tabIndex={0}
aria-label="Удалить из корзины"
onClick={onRemove}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onRemove && onRemove()}
style={{ display: 'inline-flex', cursor: 'pointer', transition: 'color 0.2s' }}
onMouseEnter={e => {
const path = e.currentTarget.querySelector('path');
if (path) path.setAttribute('fill', '#ec1c24');
}}
onMouseLeave={e => {
const path = e.currentTarget.querySelector('path');
if (path) path.setAttribute('fill', '#D0D0D0');
}}
>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
fill="#D0D0D0"
style={{ transition: 'fill 0.2s' }}
/>
</svg>
</div>
</div>
</div>
</div>

View File

@ -90,9 +90,24 @@ const CartList: React.FC = () => {
</div>
<div className="text-block-30">Выделить всё</div>
</div>
<div className="w-layout-hflex select-all-block" onClick={handleRemoveSelected} style={{ cursor: 'pointer' }}>
<div className="w-layout-hflex select-all-block" onClick={handleRemoveSelected} style={{ cursor: 'pointer' }}
onMouseEnter={e => {
const path = (e.currentTarget.querySelector('path'));
if (path) path.setAttribute('fill', '#ec1c24');
}}
onMouseLeave={e => {
const path = (e.currentTarget.querySelector('path'));
if (path) path.setAttribute('fill', '#D0D0D0');
}}
>
<div className="text-block-30">Удалить выбранные</div>
<img src="/images/delete.svg" loading="lazy" alt="" className="image-13" />
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg" className="image-13">
<path
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
fill="#D0D0D0"
style={{ transition: 'fill 0.2s' }}
/>
</svg>
</div>
</div>
{items.length === 0 ? (

View File

@ -667,10 +667,10 @@ const CartSummary: React.FC = () => {
<button
className="submit-button fill w-button"
onClick={handleProceedToStep2}
disabled={summary.totalItems === 0}
disabled={summary.totalItems === 0 || !consent}
style={{
opacity: summary.totalItems === 0 ? 0.5 : 1,
cursor: summary.totalItems === 0 ? 'not-allowed' : 'pointer'
opacity: summary.totalItems === 0 || !consent ? 0.5 : 1,
cursor: summary.totalItems === 0 || !consent ? 'not-allowed' : 'pointer'
}}
>
Оформить заказ
@ -722,10 +722,9 @@ const CartSummary: React.FC = () => {
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #D0D0D0',
borderRadius: '4px',
fontSize: '14px',
fontFamily: 'inherit'
border: 'none',
outline: 'none',
boxShadow: 'none',
}}
/>
</div>
@ -738,10 +737,9 @@ const CartSummary: React.FC = () => {
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #D0D0D0',
borderRadius: '4px',
fontSize: '14px',
fontFamily: 'inherit'
border: 'none',
outline: 'none',
boxShadow: 'none',
}}
/>
</div>
@ -882,10 +880,10 @@ const CartSummary: React.FC = () => {
<button
className="submit-button fill w-button"
onClick={handleSubmit}
disabled={summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()}
disabled={summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent}
style={{
opacity: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()) ? 0.5 : 1,
cursor: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()) ? 'not-allowed' : 'pointer'
opacity: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent) ? 0.5 : 1,
cursor: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent) ? 'not-allowed' : 'pointer'
}}
>
{isProcessing ? 'Оформляем заказ...' :
@ -896,7 +894,7 @@ const CartSummary: React.FC = () => {
{error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>}
{/* Кнопка "Назад" */}
<button
{/* <button
onClick={handleBackToStep1}
style={{
background: 'none',
@ -910,7 +908,7 @@ const CartSummary: React.FC = () => {
}}
>
← Назад к настройкам доставки
</button>
</button> */}
<div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}>
<div

View File

@ -171,9 +171,9 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
</div>
<div className="sort-item-comments">Комментарий</div>
</div>
<div className="w-layout-hflex add-to-cart-block-copy">
{/* <div className="w-layout-hflex add-to-cart-block-copy">
<div className="text-sm font-medium text-gray-600">Действия</div>
</div>
</div> */}
{favorites.length > 0 && (
<div
className="w-layout-hflex select-all-block"
@ -233,8 +233,10 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
<div className="w-layout-hflex add-to-cart-block-copy">
<button
onClick={() => handleSearchItem(item)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors duration-200 flex items-center gap-2 text-sm font-medium"
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors duration-200 flex items-center gap-2 text-sm font-medium"
style={{color: '#fff'}}
>
<svg
width="16"
height="16"

View File

@ -2,53 +2,55 @@ import React from "react";
import Link from "next/link";
const AvailableParts = () => (
<section>
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex flex-block-5">
<div className="w-layout-hflex flex-block-31">
<h2 className="heading-4">Автозапчасти в наличии</h2>
<div className="w-layout-hflex flex-block-29">
<Link href="/catalog" className="text-block-18">
Ко всем автозапчастям
</Link>
<img src="/images/Arrow_right.svg" loading="lazy" alt="" />
</div>
</div>
<div className="w-layout-hflex flex-block-6">
<Link href="/catalog" className="div-block-12" id="w-node-bc394713-4b8e-44e3-8ddf-3edc1c31a743-3b3232bc">
<h1 className="heading-7">Аксессуары</h1>
<img src="/images/IMG_1.png" loading="lazy" alt="" className="image-22" />
<section>
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex flex-block-5">
<div className="w-layout-hflex flex-block-31">
<h2 className="heading-4">Автозапчасти в наличии</h2>
<div className="w-layout-hflex flex-block-29">
<Link href="/catalog" className="text-block-18">
Ко всем автозапчастям
</Link>
<Link href="/catalog" className="div-block-12-copy">
<h1 className="heading-7">Воздушные фильтры</h1>
<img src="/images/IMG_2.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12">
<h1 className="heading-7">Шины</h1>
<img src="/images/IMG_3.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-123">
<h1 className="heading-7-white">Аккумуляторы</h1>
<img src="/images/IMG_4.png" loading="lazy" alt="" className="image-22" />
</Link>
<div className="w-layout-hflex flex-block-35" id="w-node-_8908a890-8c8f-e12c-999f-08d5da3bcc01-3b3232bc">
<Link href="/catalog" className="div-block-12 small">
<h1 className="heading-7">Диски</h1>
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12 small">
<h1 className="heading-7">Свечи</h1>
<img src="/images/IMG_6.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-red small">
<h1 className="heading-7-white">Масла</h1>
<img src="/images/IMG_7.png" loading="lazy" alt="" className="image-22" />
</Link>
</div>
<img src="/images/Arrow_right.svg" loading="lazy" alt="" />
</div>
</div>
<div className="w-layout-hflex flex-block-6">
<Link href="/catalog" className="div-block-12">
<h1 className="heading-7">Аксессуары</h1>
<img src="/images/IMG_1.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12-copy">
<h1 className="heading-7">Воздушные фильтры</h1>
<img src="/images/IMG_2.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12">
<h1 className="heading-7">Шины</h1>
<img src="/images/IMG_3.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-123">
<h1 className="heading-7-white">Аккумуляторы</h1>
<img src="/images/IMG_4.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12 small">
<h1 className="heading-7">Диски</h1>
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12 small">
<h1 className="heading-7">Свечи</h1>
<img src="/images/IMG_6.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-red small">
<h1 className="heading-7-white">Масла</h1>
<img src="/images/IMG_7.png" loading="lazy" alt="" className="image-22" />
</Link>
<Link href="/catalog" className="div-block-12 small">
<h1 className="heading-7">Диски</h1>
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
</Link>
</div>
</div>
</section>
</div>
</section>
);
export default AvailableParts;

View File

@ -1,4 +1,27 @@
import React from "react";
import React, { useRef, useState } from "react";
import { useQuery } from '@apollo/client';
import { useRouter } from 'next/router';
import { GET_LAXIMO_UNIT_INFO, GET_LAXIMO_UNIT_IMAGE_MAP } from '@/lib/graphql';
import BrandSelectionModal from '../BrandSelectionModal';
interface KnotInProps {
catalogCode: string;
vehicleId: string;
ssd?: string;
unitId: string;
unitName?: string;
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 }>;
}>;
}
// Функция для корректного формирования URL изображения
const getImageUrl = (baseUrl: string, size: string) => {
@ -11,25 +34,138 @@ const getImageUrl = (baseUrl: string, size: string) => {
.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;
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
const imgRef = useRef<HTMLImageElement>(null);
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
const selectedImageSize = 'source';
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
const router = useRouter();
// Получаем инфо об узле (для картинки)
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery(
GET_LAXIMO_UNIT_INFO,
{
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
skip: !catalogCode || !vehicleId || !unitId,
errorPolicy: 'all',
}
);
// Получаем карту координат
const { data: imageMapData, loading: imageMapLoading, error: imageMapError } = useQuery(
GET_LAXIMO_UNIT_IMAGE_MAP,
{
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
skip: !catalogCode || !vehicleId || !unitId,
errorPolicy: 'all',
}
);
const unitInfo = unitInfoData?.laximoUnitInfo;
const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || [];
const imageUrl = unitInfo?.imageurl ? getImageUrl(unitInfo.imageurl, selectedImageSize) : '';
// Масштабируем точки после загрузки картинки
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
const img = e.currentTarget;
if (!img.naturalWidth || !img.naturalHeight) return;
setImageScale({
x: img.offsetWidth / img.naturalWidth,
y: img.offsetHeight / img.naturalHeight,
});
};
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
const handlePointClick = (codeonimage: string | number) => {
if (!parts) return;
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
const part = parts.find(
(p) =>
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
(p.detailid && p.detailid.toString() === codeonimage.toString())
);
console.log('Найдена деталь для точки:', part);
if (part?.oem) {
setSelectedDetail({ oem: part.oem, name: part.name || '' });
setIsBrandModalOpen(true);
} else {
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
}
};
// Для отладки: вывести детали и координаты
React.useEffect(() => {
console.log('KnotIn parts:', parts);
console.log('KnotIn coordinates:', coordinates);
}, [parts, coordinates]);
if (unitInfoLoading || imageMapLoading) {
return <div className="text-center py-8 text-gray-500">Загружаем схему узла...</div>;
}
if (unitInfoError) {
return <div className="text-center py-8 text-red-600">Ошибка загрузки схемы: {unitInfoError.message}</div>;
}
if (!imageUrl) {
return <div className="text-center py-8 text-gray-400">Нет изображения для этого узла</div>;
}
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 className="relative inline-block">
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
<div style={{ position: 'absolute', top: 4, left: 4, zIndex: 20, background: 'rgba(255,0,0,0.1)', color: '#c00', fontWeight: 700, fontSize: 14, padding: '2px 8px', borderRadius: 6 }}>
{coordinates.length} точек
</div>
)}
{/* <div style={{ marginTop: 8, fontWeight: 500 }}>{node.name}</div> */}
<img
ref={imgRef}
src={imageUrl}
loading="lazy"
alt={unitName || unitInfo?.name || "Изображение узла"}
onLoad={handleImageLoad}
className="max-w-full h-auto mx-auto rounded"
style={{ maxWidth: 400, display: 'block' }}
/>
{/* Точки/области */}
{coordinates.map((coord: any, idx: number) => {
const scaledX = coord.x * imageScale.x;
const scaledY = coord.y * imageScale.y;
const scaledWidth = coord.width * imageScale.x;
const scaledHeight = coord.height * imageScale.y;
return (
<div
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
tabIndex={0}
aria-label={`Деталь ${coord.codeonimage}`}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
}}
className="absolute flex items-center justify-center border-2 border-red-600 bg-white rounded-full cursor-pointer"
style={{
left: scaledX,
top: scaledY,
width: scaledWidth,
height: scaledHeight,
borderRadius: '50%',
pointerEvents: 'auto',
}}
title={coord.codeonimage}
onClick={() => handlePointClick(coord.codeonimage)}
>
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none">
{coord.codeonimage}
</span>
</div>
);
})}
</div>
{/* Модалка выбора бренда */}
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={() => setIsBrandModalOpen(false)}
articleNumber={selectedDetail?.oem || ''}
detailName={selectedDetail?.name || ''}
/>
</>
);
};

View File

@ -1,5 +1,6 @@
import React from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import BrandSelectionModal from '../BrandSelectionModal';
interface KnotPartsProps {
parts: Array<{
@ -13,39 +14,59 @@ interface KnotPartsProps {
note?: string;
attributes?: Array<{ key: string; name?: string; value: string }>;
}>;
selectedCodeOnImage?: string | number;
}
const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
const router = useRouter();
const KnotParts: React.FC<KnotPartsProps> = ({ parts, selectedCodeOnImage }) => {
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
const handlePriceClick = (part: any) => {
if (part.oem) {
setSelectedDetail({ oem: part.oem, name: part.name || '' });
setIsBrandModalOpen(true);
}
};
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`);
}
}}
<>
<div className="knot-parts">
{parts.map((part, idx) => {
const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
return (
<div
className={`w-layout-hflex knotlistitem border rounded transition-colors duration-150 ${isSelected ? 'bg-yellow-100 border-yellow-400' : 'border-transparent'}`}
key={part.detailid || idx}
>
Цена
</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 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={() => handlePriceClick(part)}
>
Цена
</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>
);
})}
</div>
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={() => setIsBrandModalOpen(false)}
articleNumber={selectedDetail?.oem || ''}
detailName={selectedDetail?.name || ''}
/>
</>
);
};

View File

@ -1,17 +1,23 @@
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';
import { SEARCH_LAXIMO_FULLTEXT, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
import VinPartCard from './VinPartCard';
interface VinLeftbarProps {
catalogCode?: string;
vehicleId?: string;
ssd?: string;
vehicleInfo: {
catalog: string;
vehicleid: string;
ssd: string;
[key: string]: any;
};
onSearchResults?: (results: any[]) => void;
onNodeSelect?: (node: any) => void;
}
const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, onSearchResults, onNodeSelect }) => {
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect }) => {
const catalogCode = vehicleInfo.catalog;
const vehicleId = vehicleInfo.vehicleid;
const ssd = vehicleInfo.ssd;
const [openIndex, setOpenIndex] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
@ -88,6 +94,92 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, on
const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0;
const showTips = isSearchAvailable && !searchQuery.trim() && !loading;
// --- QuickGroups (от производителя) ---
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId || activeTab !== 'manufacturer',
errorPolicy: 'all'
});
const quickGroups = quickGroupsData?.laximoQuickGroups || [];
const [expandedQuickGroups, setExpandedQuickGroups] = useState<Set<string>>(new Set());
const handleQuickGroupToggle = (groupId: string) => {
setExpandedQuickGroups(prev => {
const newSet = new Set(prev);
if (newSet.has(groupId)) {
newSet.delete(groupId);
} else {
newSet.add(groupId);
}
return newSet;
});
};
const handleQuickGroupClick = (group: any) => {
if (group.link) {
setSelectedQuickGroup(group);
} else {
handleQuickGroupToggle(group.quickgroupid);
}
};
// Детали выбранной группы (если link: true)
console.log('QuickDetail QUERY VARS', {
catalogCode,
vehicleId,
quickGroupId: selectedQuickGroup?.quickgroupid,
ssd
});
const skipQuickDetail =
!selectedQuickGroup ||
!catalogCode ||
!vehicleId ||
!selectedQuickGroup.quickgroupid ||
!ssd;
const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery(GET_LAXIMO_QUICK_DETAIL, {
variables: selectedQuickGroup ? {
catalogCode,
vehicleId,
quickGroupId: selectedQuickGroup.quickgroupid,
ssd
} : undefined,
skip: skipQuickDetail,
errorPolicy: 'all'
});
const quickDetail = quickDetailData?.laximoQuickDetail;
const renderQuickGroupTree = (groups: any[], level = 0): React.ReactNode => (
<div>
{groups.map(group => (
<div key={group.quickgroupid} style={{ marginLeft: level * 16, marginBottom: 8 }}>
<div
className={`flex items-center p-2 rounded cursor-pointer border ${group.link ? 'bg-white hover:bg-red-50 border-gray-200 hover:border-red-300' : 'bg-gray-50 hover:bg-gray-100 border-gray-200'}`}
onClick={() => handleQuickGroupClick(group)}
>
{group.children && group.children.length > 0 && (
<svg className={`w-4 h-4 text-gray-400 mr-2 transition-transform ${expandedQuickGroups.has(group.quickgroupid) ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
<span className={`font-medium ${group.link ? 'text-gray-900' : 'text-gray-600'}`}>{group.name}</span>
{group.link && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Доступен поиск</span>
)}
</div>
{group.children && group.children.length > 0 && expandedQuickGroups.has(group.quickgroupid) && (
<div className="mt-1">
{renderQuickGroupTree(group.children, level + 1)}
</div>
)}
</div>
))}
</div>
);
return (
<div className="w-layout-vflex vinleftbar">
<div className="div-block-2">
@ -217,8 +309,47 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, on
</>
)
) : (
// Manufacturer tab content (заглушка)
<div style={{ padding: '16px', color: '#888' }}>Здесь будет контент "От производителя"</div>
// Manufacturer tab content (QuickGroups)
<div style={{ padding: '16px' }}>
{quickGroupsLoading ? (
<div style={{ textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
) : quickGroupsError ? (
<div style={{ color: 'red' }}>Ошибка загрузки групп: {quickGroupsError.message}</div>
) : selectedQuickGroup ? (
<div>
<button onClick={() => setSelectedQuickGroup(null)} className="mb-4 px-3 py-1 bg-gray-200 rounded">Назад к группам</button>
<h3 className="text-lg font-semibold mb-2">{selectedQuickGroup.name}</h3>
{quickDetailLoading ? (
<div>Загружаем детали...</div>
) : quickDetailError ? (
<div style={{ color: 'red' }}>Ошибка загрузки деталей: {quickDetailError.message}</div>
) : quickDetail && quickDetail.units && quickDetail.units.length > 0 ? (
<div className="space-y-3">
{quickDetail.units.map((unit: any) => (
<div key={unit.unitid} className="p-3 bg-gray-50 rounded border border-gray-200">
<div className="font-medium text-gray-900">{unit.name}</div>
{unit.details && unit.details.length > 0 && (
<ul className="mt-2 text-sm text-gray-700 list-disc pl-5">
{unit.details.map((detail: any) => (
<li key={detail.detailid}>
<span className="font-medium">{detail.name}</span> <span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">OEM: {detail.oem}</span>
</li>
))}
</ul>
)}
</div>
))}
</div>
) : (
<div>Нет деталей для этой группы</div>
)}
</div>
) : quickGroups.length > 0 ? (
renderQuickGroupTree(quickGroups)
) : (
<div>Нет доступных групп быстрого поиска</div>
)}
</div>
)}
{/* Tab content end */}
</div>

View File

@ -424,12 +424,12 @@ export default function SearchResult() {
<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 className="fixed inset-0 z-50 bg-gray-50 bg-opacity-90 flex items-center justify-center min-h-screen" aria-live="polite">
<div className="flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mb-4"></div>
<p className="text-lg text-gray-600">Поиск предложений...</p>
</div>
</main>
</div>
<Footer />
</>
);

View File

@ -269,13 +269,13 @@ const VehicleDetailsPage = () => {
<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}
/>
{vehicleInfo && vehicleInfo.catalog && vehicleInfo.vehicleid && vehicleInfo.ssd && (
<VinLeftbar
vehicleInfo={vehicleInfo}
onSearchResults={setFoundParts}
onNodeSelect={setSelectedNode}
/>
)}
{/* Категории или Knot или карточки */}
{foundParts.length > 0 ? (
<div className="knot-parts">
@ -303,7 +303,14 @@ const VehicleDetailsPage = () => {
<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} />
<KnotIn
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
unitId={selectedNode.unitid}
unitName={selectedNode.name}
parts={unitDetails}
/>
{unitDetailsLoading ? (
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
) : unitDetailsError ? (

View File

@ -386,15 +386,17 @@ input.input-receiver:focus {
color: var(--_fonts---color--light-blue-grey);
}
.knotin {
/* .knotin {
max-width: 100%;
display: flex;
align-items: stretch;
}
.knotin img {
max-width: 100%;
object-fit: contain; /* или cover */
}
object-fit: contain;
} */
.tabs-menu.w-tab-menu {
scrollbar-width: none;