Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
d62db55160 | |||
e8f1fecb47 | |||
7f91da525f | |||
d268bb3359 | |||
d4ba549a81 | |||
936a08aa11 | |||
f6cc95e714 | |||
1b0bbb2992 | |||
dcd47e9139 |
@ -1,117 +0,0 @@
|
||||
# Интеграция Parts Index API
|
||||
|
||||
## Описание
|
||||
|
||||
В проект добавлена интеграция с Parts Index API для отображения детальной информации о деталях, включая:
|
||||
- Главную фотографию детали
|
||||
- Штрих-коды
|
||||
- Технические характеристики
|
||||
- Категории
|
||||
- Дополнительные изображения
|
||||
- Логотип "powered by Parts Index"
|
||||
|
||||
## Реализованные компоненты
|
||||
|
||||
### 1. Типы (`src/types/partsindex.ts`)
|
||||
- `PartsIndexEntityInfo` - информация о детали
|
||||
- `PartsIndexEntityInfoResponse` - ответ API
|
||||
- `PartsIndexEntityInfoVariables` - параметры запроса
|
||||
|
||||
### 2. Сервис (`src/lib/partsindex-service.ts`)
|
||||
- `getEntityInfo(code, brand?, lang?)` - получение информации о детали
|
||||
|
||||
### 3. Хук (`src/hooks/usePartsIndex.ts`)
|
||||
- `usePartsIndexEntityInfo(code, brand)` - хук для получения данных
|
||||
|
||||
### 4. Компонент (`src/components/PartsIndexCard.tsx`)
|
||||
- Отображение карточки с информацией о детали
|
||||
- Поддержка состояния загрузки
|
||||
- Адаптивный дизайн
|
||||
|
||||
## Интеграция в страницу поиска
|
||||
|
||||
В файле `src/pages/search-result.tsx` добавлено:
|
||||
|
||||
```tsx
|
||||
import PartsIndexCard from "@/components/PartsIndexCard";
|
||||
import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex";
|
||||
|
||||
// В компоненте:
|
||||
const { entityInfo, loading: partsIndexLoading } = usePartsIndexEntityInfo(
|
||||
searchQuery || null,
|
||||
brandQuery || null
|
||||
);
|
||||
|
||||
// В JSX:
|
||||
{partsIndexLoading && (
|
||||
<PartsIndexCard
|
||||
entityInfo={null as any}
|
||||
loading={true}
|
||||
/>
|
||||
)}
|
||||
{entityInfo && !partsIndexLoading && (
|
||||
<PartsIndexCard
|
||||
entityInfo={entityInfo}
|
||||
loading={false}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
## API Parts Index
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
GET https://api.parts-index.com/v1/entities
|
||||
```
|
||||
|
||||
### Параметры
|
||||
- `code` (обязательный) - артикул детали
|
||||
- `brand` (опциональный) - бренд
|
||||
- `lang` (опциональный) - язык (по умолчанию 'ru')
|
||||
|
||||
### Заголовки
|
||||
```
|
||||
Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Пример запроса
|
||||
```bash
|
||||
curl -H "Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE" \
|
||||
"https://api.parts-index.com/v1/entities?code=059198405B&brand=VAG&lang=ru"
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### URL для тестирования
|
||||
```
|
||||
http://localhost:3002/search-result?article=059198405B&brand=VAG
|
||||
```
|
||||
|
||||
### Тестовая HTML страница
|
||||
Создана страница `test-parts-index.html` для демонстрации работы API без React.
|
||||
|
||||
## Функциональность
|
||||
|
||||
1. **Автоматическая загрузка** - при переходе на страницу результатов поиска
|
||||
2. **Главная фотография** - отображается первое изображение из массива
|
||||
3. **Логотип Parts Index** - в правом верхнем углу карточки
|
||||
4. **Характеристики** - первые 6 параметров из API
|
||||
5. **Штрих-коды** - все доступные штрих-коды
|
||||
6. **Дополнительные изображения** - до 4 дополнительных фото
|
||||
7. **Обработка ошибок** - скрытие изображений при ошибке загрузки
|
||||
|
||||
## Стили
|
||||
|
||||
Компонент использует Tailwind CSS классы для стилизации:
|
||||
- Адаптивная сетка для характеристик
|
||||
- Скроллинг для дополнительных изображений
|
||||
- Состояние загрузки с анимацией
|
||||
- Обработка ошибок изображений
|
||||
|
||||
## Производительность
|
||||
|
||||
- Загрузка данных только при наличии артикула
|
||||
- Кэширование на уровне React Query (через Apollo Client)
|
||||
- Ленивая загрузка изображений
|
||||
- Обработка ошибок сети
|
@ -1,4 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface BestPriceCardProps {
|
||||
bestOfferType: string;
|
||||
@ -7,9 +9,20 @@ interface BestPriceCardProps {
|
||||
price: string;
|
||||
delivery: string;
|
||||
stock: string;
|
||||
offer?: any; // Добавляем полный объект предложения для корзины
|
||||
}
|
||||
|
||||
const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, description, price, delivery, stock }) => {
|
||||
const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
bestOfferType,
|
||||
title,
|
||||
description,
|
||||
price,
|
||||
delivery,
|
||||
stock,
|
||||
offer
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
|
||||
// Парсим stock в число, если возможно
|
||||
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
||||
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
||||
@ -28,12 +41,75 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, des
|
||||
let value = parseInt(e.target.value, 10);
|
||||
if (isNaN(value) || value < 1) value = 1;
|
||||
if (maxCount !== undefined && value > maxCount) {
|
||||
window.alert(`Максимум ${maxCount} шт.`);
|
||||
toast.error(`Максимум ${maxCount} шт.`);
|
||||
return;
|
||||
}
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
return parseFloat(cleanPrice) || 0;
|
||||
};
|
||||
|
||||
// Обработчик добавления в корзину
|
||||
const handleAddToCart = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!offer) {
|
||||
toast.error('Информация о товаре недоступна');
|
||||
return;
|
||||
}
|
||||
|
||||
const numericPrice = parsePrice(price);
|
||||
if (numericPrice <= 0) {
|
||||
toast.error('Цена товара не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем наличие
|
||||
if (maxCount !== undefined && count > maxCount) {
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${maxCount} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
addItem({
|
||||
productId: offer.productId,
|
||||
offerKey: offer.offerKey,
|
||||
name: description,
|
||||
description: `${offer.brand} ${offer.articleNumber} - ${description}`,
|
||||
brand: offer.brand,
|
||||
article: offer.articleNumber,
|
||||
price: numericPrice,
|
||||
currency: offer.currency || 'RUB',
|
||||
quantity: count,
|
||||
deliveryTime: delivery,
|
||||
warehouse: offer.warehouse || 'Склад',
|
||||
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
|
||||
isExternal: offer.isExternal || false,
|
||||
image: offer.image,
|
||||
});
|
||||
|
||||
// Показываем тоастер об успешном добавлении
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка добавления товара в корзину');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-44">
|
||||
<h3 className="heading-8-copy line-clamp-2 md:line-clamp-1 min-h-[2.5em] md:min-h-0">{bestOfferType}</h3>
|
||||
@ -80,11 +156,17 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, des
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-42">
|
||||
<a href="#" className="button-icon w-inline-block">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddToCart}
|
||||
className="button-icon w-inline-block"
|
||||
style={{ cursor: 'pointer', textDecoration: 'none' }}
|
||||
aria-label="Добавить в корзину"
|
||||
>
|
||||
<div className="div-block-26">
|
||||
<div className="icon-setting w-embed"><svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"/></svg></div>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -5,7 +5,7 @@ import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
|
||||
const CartList: React.FC = () => {
|
||||
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const { items } = state;
|
||||
|
||||
const allSelected = items.length > 0 && items.every((item) => item.selected);
|
||||
@ -29,9 +29,18 @@ const CartList: React.FC = () => {
|
||||
const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand);
|
||||
|
||||
if (isInFavorites) {
|
||||
// Удаляем из избранного
|
||||
const favoriteId = `${item.productId || item.offerKey || ''}:${item.article}:${item.brand}`;
|
||||
removeFromFavorites(favoriteId);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (item.productId && fav.productId === item.productId) return true;
|
||||
if (item.offerKey && fav.offerKey === item.offerKey) return true;
|
||||
if (fav.article === item.article && fav.brand === item.brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
@ -81,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 ? (
|
||||
|
@ -1,88 +1,79 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const initialItems = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Ganz GIE37312",
|
||||
description: "Ролик ремня ГРМ VW AD GANZ GIE37312",
|
||||
delivery: "Послезавтра, курьером",
|
||||
deliveryDate: "пт, 7 февраля",
|
||||
price: "18 763 ₽",
|
||||
pricePerItem: "18 763 ₽/шт",
|
||||
count: 1,
|
||||
comment: "",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Ganz GIE37312",
|
||||
description: "Ролик ремня ГРМ VW AD GANZ GIE37312",
|
||||
delivery: "Послезавтра, курьером",
|
||||
deliveryDate: "пт, 7 февраля",
|
||||
price: "18 763 ₽",
|
||||
pricePerItem: "18 763 ₽/шт",
|
||||
count: 1,
|
||||
comment: "",
|
||||
},
|
||||
// ...ещё товары
|
||||
];
|
||||
import React from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
|
||||
const CartList2: React.FC = () => {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
const { state, updateComment } = useCart();
|
||||
const { items } = state;
|
||||
|
||||
const handleComment = (id: number, comment: string) => {
|
||||
setItems((prev) => prev.map((item) => item.id === id ? { ...item, comment } : item));
|
||||
const handleComment = (id: string, comment: string) => {
|
||||
updateComment(id, comment);
|
||||
};
|
||||
|
||||
// Функция для форматирования цены
|
||||
const formatPrice = (price: number, currency: string = 'RUB') => {
|
||||
return `${price.toLocaleString('ru-RU')} ${currency === 'RUB' ? '₽' : currency}`;
|
||||
};
|
||||
|
||||
// Показываем только выбранные товары на втором этапе
|
||||
const selectedItems = items.filter(item => item.selected);
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-48">
|
||||
<div className="w-layout-vflex product-list-cart-check">
|
||||
{items.map((item) => (
|
||||
<div className="div-block-21-copy" key={item.id}>
|
||||
<div className="w-layout-hflex cart-item-check">
|
||||
<div className="w-layout-hflex info-block-search">
|
||||
<div className="text-block-35">{item.count}</div>
|
||||
<div className="w-layout-hflex block-name">
|
||||
<h4 className="heading-9-copy">{item.name}</h4>
|
||||
<div className="text-block-21-copy">{item.description}</div>
|
||||
</div>
|
||||
<div className="form-block-copy w-form">
|
||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
||||
<input
|
||||
className="text-field-copy w-input"
|
||||
maxLength={256}
|
||||
name="Search-5"
|
||||
data-name="Search 5"
|
||||
placeholder="Комментарий"
|
||||
type="text"
|
||||
id="Search-5"
|
||||
value={item.comment}
|
||||
onChange={e => handleComment(item.id, e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
<div className="success-message w-form-done">
|
||||
<div>Thank you! Your submission has been received!</div>
|
||||
{selectedItems.length === 0 ? (
|
||||
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
||||
<p>Не выбрано товаров для заказа</p>
|
||||
<p>Вернитесь на предыдущий шаг и выберите товары</p>
|
||||
</div>
|
||||
) : (
|
||||
selectedItems.map((item) => (
|
||||
<div className="div-block-21-copy" key={item.id}>
|
||||
<div className="w-layout-hflex cart-item-check">
|
||||
<div className="w-layout-hflex info-block-search">
|
||||
<div className="text-block-35">{item.quantity}</div>
|
||||
<div className="w-layout-hflex block-name">
|
||||
<h4 className="heading-9-copy">{item.name}</h4>
|
||||
<div className="text-block-21-copy">{item.description}</div>
|
||||
</div>
|
||||
<div className="error-message w-form-fail">
|
||||
<div>Oops! Something went wrong while submitting the form.</div>
|
||||
<div className="form-block-copy w-form">
|
||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
||||
<input
|
||||
className="text-field-copy w-input"
|
||||
maxLength={256}
|
||||
name="Search-5"
|
||||
data-name="Search 5"
|
||||
placeholder="Комментарий"
|
||||
type="text"
|
||||
id={`Search-${item.id}`}
|
||||
value={item.comment || ''}
|
||||
onChange={e => handleComment(item.id, e.target.value)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex add-to-cart-block">
|
||||
<div className="w-layout-hflex flex-block-39-copy">
|
||||
<h4 className="heading-9-copy">{item.delivery}</h4>
|
||||
<div className="text-block-21-copy">{item.deliveryDate}</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex pcs">
|
||||
<div className="pcs-text">{item.count} шт.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||
<h4 className="heading-9-copy-copy">{item.price}</h4>
|
||||
<div className="text-block-21-copy-copy">{item.pricePerItem}</div>
|
||||
<div className="w-layout-hflex add-to-cart-block">
|
||||
<div className="w-layout-hflex flex-block-39-copy">
|
||||
<h4 className="heading-9-copy">{item.deliveryTime || 'Уточняется'}</h4>
|
||||
<div className="text-block-21-copy">{item.deliveryDate || ''}</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex pcs">
|
||||
<div className="pcs-text">{item.quantity} шт.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||
<h4 className="heading-9-copy-copy">{formatPrice(item.price * item.quantity, item.currency)}</h4>
|
||||
<div className="text-block-21-copy-copy">{formatPrice(item.price, item.currency)}/шт</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES, GET_DELIVERY_OFFERS } from "@/lib/graphql";
|
||||
import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES } from "@/lib/graphql";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const CartSummary: React.FC = () => {
|
||||
const { state, updateDelivery, updateOrderComment, clearCart } = useCart();
|
||||
@ -15,7 +16,7 @@ const CartSummary: React.FC = () => {
|
||||
const [error, setError] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [showAuthWarning, setShowAuthWarning] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(1); // 1 - первый шаг, 2 - второй шаг
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// Новые состояния для первого шага
|
||||
const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>("");
|
||||
@ -32,22 +33,17 @@ const CartSummary: React.FC = () => {
|
||||
const [paymentMethod, setPaymentMethod] = useState<string>("yookassa");
|
||||
const [showPaymentDropdown, setShowPaymentDropdown] = useState(false);
|
||||
|
||||
// Состояния для офферов доставки
|
||||
const [deliveryOffers, setDeliveryOffers] = useState<any[]>([]);
|
||||
const [selectedDeliveryOffer, setSelectedDeliveryOffer] = useState<any>(null);
|
||||
const [loadingOffers, setLoadingOffers] = useState(false);
|
||||
const [offersError, setOffersError] = useState<string>("");
|
||||
|
||||
// Упрощенный тип доставки - только курьер или самовывоз
|
||||
// const [deliveryType, setDeliveryType] = useState<'courier' | 'pickup'>('courier');
|
||||
|
||||
const [createOrder] = useMutation(CREATE_ORDER);
|
||||
const [createPayment] = useMutation(CREATE_PAYMENT);
|
||||
const [getDeliveryOffers] = useMutation(GET_DELIVERY_OFFERS);
|
||||
// Убираем useMutation для GET_DELIVERY_OFFERS
|
||||
|
||||
// Получаем данные клиента
|
||||
const { data: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME);
|
||||
const { data: addressesData, loading: addressesLoading } = useQuery(GET_CLIENT_DELIVERY_ADDRESSES);
|
||||
|
||||
|
||||
|
||||
// Получаем пользователя из localStorage для проверки авторизации
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
|
||||
@ -67,7 +63,7 @@ const CartSummary: React.FC = () => {
|
||||
if (savedCartSummaryState) {
|
||||
try {
|
||||
const state = JSON.parse(savedCartSummaryState);
|
||||
setCurrentStep(state.currentStep || 1);
|
||||
setStep(state.step || 1);
|
||||
setSelectedLegalEntity(state.selectedLegalEntity || '');
|
||||
setSelectedLegalEntityId(state.selectedLegalEntityId || '');
|
||||
setIsIndividual(state.isIndividual ?? true);
|
||||
@ -87,7 +83,7 @@ const CartSummary: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stateToSave = {
|
||||
currentStep,
|
||||
step,
|
||||
selectedLegalEntity,
|
||||
selectedLegalEntityId,
|
||||
isIndividual,
|
||||
@ -99,7 +95,7 @@ const CartSummary: React.FC = () => {
|
||||
};
|
||||
localStorage.setItem('cartSummaryState', JSON.stringify(stateToSave));
|
||||
}
|
||||
}, [currentStep, selectedLegalEntity, selectedLegalEntityId, isIndividual, selectedDeliveryAddress, recipientName, recipientPhone, paymentMethod, consent]);
|
||||
}, [step, selectedLegalEntity, selectedLegalEntityId, isIndividual, selectedDeliveryAddress, recipientName, recipientPhone, paymentMethod, consent]);
|
||||
|
||||
// Инициализация данных получателя
|
||||
useEffect(() => {
|
||||
@ -134,176 +130,35 @@ const CartSummary: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Функция для загрузки офферов доставки
|
||||
const loadDeliveryOffers = async () => {
|
||||
if (!selectedDeliveryAddress || !recipientName || !recipientPhone || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingOffers(true);
|
||||
setOffersError("");
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// Подготавливаем данные для API
|
||||
const deliveryOffersInput = {
|
||||
items: items.map(item => {
|
||||
// Извлекаем срок поставки из deliveryTime товара
|
||||
let deliveryDays = 0;
|
||||
if (item.deliveryTime) {
|
||||
const match = item.deliveryTime.match(/(\d+)/);
|
||||
if (match) {
|
||||
deliveryDays = parseInt(match[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: item.name,
|
||||
article: item.article || '',
|
||||
brand: item.brand || '',
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
weight: item.weight || 500, // Примерный вес в граммах
|
||||
dimensions: "10x10x5", // Примерные размеры
|
||||
deliveryTime: deliveryDays, // Срок поставки товара в днях
|
||||
offerKey: item.offerKey,
|
||||
isExternal: item.isExternal
|
||||
};
|
||||
}),
|
||||
deliveryAddress: selectedDeliveryAddress,
|
||||
recipientName,
|
||||
recipientPhone
|
||||
};
|
||||
|
||||
const { data } = await getDeliveryOffers({
|
||||
variables: { input: deliveryOffersInput }
|
||||
});
|
||||
|
||||
if (data?.getDeliveryOffers?.success && data.getDeliveryOffers.offers && Array.isArray(data.getDeliveryOffers.offers) && data.getDeliveryOffers.offers.length > 0) {
|
||||
setDeliveryOffers(data.getDeliveryOffers.offers);
|
||||
setOffersError('');
|
||||
|
||||
// Автоматически выбираем первый оффер
|
||||
const firstOffer = data.getDeliveryOffers.offers[0];
|
||||
setSelectedDeliveryOffer(firstOffer);
|
||||
|
||||
// Обновляем стоимость доставки в корзине
|
||||
updateDelivery({
|
||||
address: selectedDeliveryAddress,
|
||||
cost: firstOffer.cost,
|
||||
date: firstOffer.deliveryDate,
|
||||
time: firstOffer.deliveryTime
|
||||
});
|
||||
} else {
|
||||
const errorMessage = data?.getDeliveryOffers?.error || 'Не удалось получить варианты доставки';
|
||||
setOffersError(errorMessage);
|
||||
|
||||
// Добавляем стандартные варианты доставки как fallback
|
||||
const standardOffers = data?.getDeliveryOffers?.offers || [
|
||||
{
|
||||
id: 'standard',
|
||||
name: 'Стандартная доставка',
|
||||
description: 'Доставка в течение 3-5 рабочих дней',
|
||||
deliveryDate: 'в течение 3-5 рабочих дней',
|
||||
deliveryTime: '',
|
||||
cost: 500
|
||||
},
|
||||
{
|
||||
id: 'express',
|
||||
name: 'Экспресс доставка',
|
||||
description: 'Доставка на следующий день',
|
||||
deliveryDate: 'завтра',
|
||||
deliveryTime: '10:00-18:00',
|
||||
cost: 1000
|
||||
}
|
||||
];
|
||||
setDeliveryOffers(standardOffers);
|
||||
setSelectedDeliveryOffer(standardOffers[0]);
|
||||
updateDelivery({
|
||||
address: selectedDeliveryAddress,
|
||||
cost: standardOffers[0].cost,
|
||||
date: standardOffers[0].deliveryDate,
|
||||
time: standardOffers[0].deliveryTime
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setOffersError('Ошибка загрузки вариантов доставки');
|
||||
|
||||
// Добавляем стандартные варианты доставки как fallback при ошибке
|
||||
const standardOffers = [
|
||||
{
|
||||
id: 'standard',
|
||||
name: 'Стандартная доставка',
|
||||
description: 'Доставка в течение 3-5 рабочих дней',
|
||||
deliveryDate: 'в течение 3-5 рабочих дней',
|
||||
deliveryTime: '',
|
||||
cost: 500
|
||||
}
|
||||
];
|
||||
setDeliveryOffers(standardOffers);
|
||||
setSelectedDeliveryOffer(standardOffers[0]);
|
||||
updateDelivery({
|
||||
address: selectedDeliveryAddress,
|
||||
cost: standardOffers[0].cost,
|
||||
date: standardOffers[0].deliveryDate,
|
||||
time: standardOffers[0].deliveryTime
|
||||
});
|
||||
} finally {
|
||||
setLoadingOffers(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Автоматическая загрузка офферов при изменении ключевых данных
|
||||
useEffect(() => {
|
||||
if (selectedDeliveryAddress && recipientName && recipientPhone && items.length > 0) {
|
||||
// Загружаем офферы с небольшой задержкой для избежания множественных запросов
|
||||
const timeoutId = setTimeout(() => {
|
||||
loadDeliveryOffers();
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [selectedDeliveryAddress, recipientName, recipientPhone, items.length]);
|
||||
|
||||
const handleProceedToStep2 = () => {
|
||||
if (!selectedDeliveryAddress) {
|
||||
setError("Пожалуйста, выберите адрес доставки.");
|
||||
if (!recipientName.trim()) {
|
||||
toast.error('Пожалуйста, введите имя получателя');
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.totalItems === 0) {
|
||||
setError("Корзина пуста. Добавьте товары для оформления заказа.");
|
||||
if (!recipientPhone.trim()) {
|
||||
toast.error('Пожалуйста, введите телефон получателя');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedDeliveryAddress.trim()) {
|
||||
toast.error('Пожалуйста, выберите адрес доставки');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedDeliveryOffer) {
|
||||
setError("Пожалуйста, выберите способ доставки.");
|
||||
return;
|
||||
}
|
||||
// Обновляем данные доставки без стоимости
|
||||
updateDelivery({
|
||||
address: selectedDeliveryAddress,
|
||||
cost: 0, // Стоимость включена в товары
|
||||
date: 'Включена в стоимость товаров',
|
||||
time: 'Способ доставки указан в адресе'
|
||||
});
|
||||
|
||||
// Проверяем достаточность средств для оплаты с баланса
|
||||
if (paymentMethod === 'balance' && !isIndividual) {
|
||||
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
|
||||
const finalAmount = summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice);
|
||||
const availableBalance = (defaultContract?.balance || 0) + (defaultContract?.creditLimit || 0);
|
||||
|
||||
if (availableBalance < finalAmount) {
|
||||
setError("Недостаточно средств на балансе для оплаты заказа. Выберите другой способ оплаты.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setError("");
|
||||
setCurrentStep(2);
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleBackToStep1 = () => {
|
||||
setCurrentStep(1);
|
||||
setStep(1);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -339,7 +194,7 @@ const CartSummary: React.FC = () => {
|
||||
deliveryAddress: selectedDeliveryAddress || delivery.address,
|
||||
legalEntityId: !isIndividual ? selectedLegalEntityId : null,
|
||||
paymentMethod: paymentMethod,
|
||||
comment: orderComment || `Адрес доставки: ${selectedDeliveryAddress}. ${!isIndividual && selectedLegalEntity ? `Юридическое лицо: ${selectedLegalEntity}.` : 'Физическое лицо.'} Способ оплаты: ${getPaymentMethodName(paymentMethod)}. Доставка: ${selectedDeliveryOffer?.name || 'Стандартная доставка'} (${selectedDeliveryOffer?.deliveryDate || ''} ${selectedDeliveryOffer?.deliveryTime || ''}).`,
|
||||
comment: orderComment || `Адрес доставки: ${selectedDeliveryAddress}. ${!isIndividual && selectedLegalEntity ? `Юридическое лицо: ${selectedLegalEntity}.` : 'Физическое лицо.'} Способ оплаты: ${getPaymentMethodName(paymentMethod)}. Доставка: ${selectedDeliveryAddress}.`,
|
||||
items: selectedItems.map(item => ({
|
||||
productId: item.productId,
|
||||
externalId: item.offerKey,
|
||||
@ -367,14 +222,6 @@ const CartSummary: React.FC = () => {
|
||||
localStorage.removeItem('cartSummaryState');
|
||||
}
|
||||
window.location.href = `/payment/success?orderId=${order.id}&orderNumber=${order.orderNumber}&paymentMethod=balance`;
|
||||
} else if (paymentMethod === 'invoice') {
|
||||
// Для оплаты по реквизитам - переходим на страницу с реквизитами
|
||||
clearCart();
|
||||
// Очищаем сохраненное состояние оформления заказа
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('cartSummaryState');
|
||||
}
|
||||
window.location.href = `/payment/invoice?orderId=${order.id}&orderNumber=${order.orderNumber}`;
|
||||
} else {
|
||||
// Для ЮКассы - создаем платеж и переходим на оплату
|
||||
const paymentResult = await createPayment({
|
||||
@ -421,14 +268,12 @@ const CartSummary: React.FC = () => {
|
||||
return 'ЮКасса (банковские карты)';
|
||||
case 'balance':
|
||||
return 'Оплата с баланса';
|
||||
case 'invoice':
|
||||
return 'Оплата по реквизитам';
|
||||
default:
|
||||
return 'Выберите способ оплаты';
|
||||
return 'ЮКасса (банковские карты)';
|
||||
}
|
||||
};
|
||||
|
||||
if (currentStep === 1) {
|
||||
if (step === 1) {
|
||||
// Первый шаг - настройка доставки
|
||||
return (
|
||||
<div className="w-layout-vflex cart-ditail">
|
||||
@ -650,107 +495,6 @@ const CartSummary: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Варианты доставки */}
|
||||
<div className="w-layout-vflex flex-block-66">
|
||||
<div className="text-block-31" style={{ marginBottom: '12px' }}>Варианты доставки</div>
|
||||
|
||||
{loadingOffers && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}>
|
||||
Загружаем варианты доставки...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{offersError && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: '#FEF3C7',
|
||||
border: '1px solid #F59E0B',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#92400E',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{offersError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deliveryOffers.length > 0 && !loadingOffers && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{deliveryOffers.map((offer, index) => (
|
||||
<div
|
||||
key={offer.id}
|
||||
onClick={() => {
|
||||
setSelectedDeliveryOffer(offer);
|
||||
updateDelivery({
|
||||
address: selectedDeliveryAddress,
|
||||
cost: offer.cost,
|
||||
date: offer.deliveryDate,
|
||||
time: offer.deliveryTime
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: selectedDeliveryOffer?.id === offer.id ? '2px solid #007bff' : '1px solid #dee2e6',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedDeliveryOffer?.id === offer.id ? '#f8f9fa' : 'white',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedDeliveryOffer?.id !== offer.id) {
|
||||
e.currentTarget.style.backgroundColor = '#f8f9fa';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedDeliveryOffer?.id !== offer.id) {
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500, fontSize: '14px', marginBottom: '4px' }}>
|
||||
{offer.name}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
|
||||
{offer.description}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#007bff' }}>
|
||||
{offer.deliveryDate} • {offer.deliveryTime}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
fontWeight: 500,
|
||||
fontSize: '14px',
|
||||
color: offer.cost === 0 ? '#28a745' : '#333'
|
||||
}}>
|
||||
{offer.cost === 0 ? 'Бесплатно' : `${offer.cost} ₽`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deliveryOffers.length === 0 && !loadingOffers && selectedDeliveryAddress && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
border: '1px dashed #dee2e6',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
Выберите адрес доставки для просмотра вариантов
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Способ оплаты */}
|
||||
<div className="w-layout-vflex flex-block-58" style={{ position: 'relative' }} ref={paymentDropdownRef}>
|
||||
<div className="text-block-31">Способ оплаты</div>
|
||||
@ -856,24 +600,19 @@ const CartSummary: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const contracts = clientData?.clientMe?.contracts || [];
|
||||
const defaultContract = contracts.find((contract: any) => contract.isDefault && contract.isActive);
|
||||
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
|
||||
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
|
||||
|
||||
if (!defaultContract) {
|
||||
const anyActiveContract = contracts.find((contract: any) => contract.isActive);
|
||||
|
||||
if (!anyActiveContract) {
|
||||
return (
|
||||
<span style={{ fontWeight: 500, color: '#e74c3c' }}>
|
||||
Нет активных контрактов
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span style={{ color: '#EF4444', fontWeight: 500 }}>
|
||||
Активный договор не найден
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contract = defaultContract || contracts.find((contract: any) => contract.isActive);
|
||||
const balance = contract?.balance || 0;
|
||||
const creditLimit = contract?.creditLimit || 0;
|
||||
const balance = defaultContract.balance || 0;
|
||||
const creditLimit = defaultContract.creditLimit || 0;
|
||||
const totalAvailable = balance + creditLimit;
|
||||
|
||||
return (
|
||||
@ -884,59 +623,10 @@ const CartSummary: React.FC = () => {
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
setPaymentMethod('invoice');
|
||||
setShowPaymentDropdown(false);
|
||||
}}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: paymentMethod === 'invoice' ? '#f8f9fa' : 'white',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (paymentMethod !== 'invoice') {
|
||||
e.currentTarget.style.backgroundColor = '#f8f9fa';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (paymentMethod !== 'invoice') {
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Оплата по реквизитам
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Показываем предупреждение для оплаты с баланса если недостаточно средств */}
|
||||
{paymentMethod === 'balance' && !isIndividual && (
|
||||
(() => {
|
||||
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
|
||||
const availableBalance = (defaultContract?.balance || 0) + (defaultContract?.creditLimit || 0);
|
||||
const finalAmount = summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice);
|
||||
const isInsufficientFunds = availableBalance < finalAmount;
|
||||
|
||||
return isInsufficientFunds ? (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#FEF3C7',
|
||||
border: '1px solid #F59E0B',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#92400E'
|
||||
}}>
|
||||
Недостаточно средств на балансе для оплаты заказа
|
||||
</div>
|
||||
) : null;
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-line"></div>
|
||||
@ -958,10 +648,7 @@ const CartSummary: React.FC = () => {
|
||||
<div className="w-layout-hflex flex-block-59">
|
||||
<div className="text-block-21-copy-copy">Доставка</div>
|
||||
<div className="text-block-33">
|
||||
{selectedDeliveryOffer?.cost === 0
|
||||
? 'Бесплатно'
|
||||
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
||||
}
|
||||
Включена в стоимость товаров
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -972,7 +659,7 @@ const CartSummary: React.FC = () => {
|
||||
<div className="text-block-32">Итого</div>
|
||||
<h4 className="heading-9-copy-copy">
|
||||
{formatPrice(
|
||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
@ -980,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'
|
||||
}}
|
||||
>
|
||||
Оформить заказ
|
||||
@ -1035,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>
|
||||
@ -1051,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>
|
||||
@ -1075,9 +760,19 @@ const CartSummary: React.FC = () => {
|
||||
{paymentMethod === 'balance' && !isIndividual && (
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
{(() => {
|
||||
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
|
||||
const balance = defaultContract?.balance || 0;
|
||||
const creditLimit = defaultContract?.creditLimit || 0;
|
||||
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
|
||||
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
|
||||
|
||||
if (!defaultContract) {
|
||||
return (
|
||||
<span style={{ color: '#EF4444', fontWeight: 500 }}>
|
||||
Активный договор не найден
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const balance = defaultContract.balance || 0;
|
||||
const creditLimit = defaultContract.creditLimit || 0;
|
||||
const totalAvailable = balance + creditLimit;
|
||||
|
||||
return (
|
||||
@ -1131,10 +826,7 @@ const CartSummary: React.FC = () => {
|
||||
<div className="w-layout-hflex flex-block-59">
|
||||
<div className="text-block-21-copy-copy">Доставка</div>
|
||||
<div className="text-block-33">
|
||||
{selectedDeliveryOffer?.cost === 0
|
||||
? 'Бесплатно'
|
||||
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
||||
}
|
||||
Включена в стоимость товаров
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1145,7 +837,7 @@ const CartSummary: React.FC = () => {
|
||||
<div className="text-block-32">Итого</div>
|
||||
<h4 className="heading-9-copy-copy">
|
||||
{formatPrice(
|
||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
@ -1188,22 +880,21 @@ 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 ? 'Оформляем заказ...' :
|
||||
paymentMethod === 'balance' ? 'Оплатить с баланса' :
|
||||
paymentMethod === 'invoice' ? 'Выставить счёт' :
|
||||
'Оплатить'}
|
||||
</button>
|
||||
|
||||
{error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>}
|
||||
|
||||
{/* Кнопка "Назад" */}
|
||||
<button
|
||||
{/* <button
|
||||
onClick={handleBackToStep1}
|
||||
style={{
|
||||
background: 'none',
|
||||
@ -1217,7 +908,7 @@ const CartSummary: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
← Назад к настройкам доставки
|
||||
</button>
|
||||
</button> */}
|
||||
|
||||
<div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}>
|
||||
<div
|
||||
|
@ -35,7 +35,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
priceElement,
|
||||
onAddToCart,
|
||||
}) => {
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
|
||||
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
|
||||
@ -57,9 +57,18 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0;
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brandName || brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (offerKey && fav.offerKey === offerKey) return true;
|
||||
if (fav.article === articleNumber && fav.brand === (brandName || brand)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const INITIAL_OFFERS_LIMIT = 5;
|
||||
|
||||
@ -46,7 +47,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
partsIndexPowered = false
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||
@ -88,7 +89,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num) || num < 1) num = 1;
|
||||
if (num > availableStock) {
|
||||
window.alert(`Максимум ${availableStock} шт.`);
|
||||
toast.error(`Максимум ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
||||
@ -100,7 +101,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
|
||||
// Проверяем наличие
|
||||
if (quantity > availableStock) {
|
||||
alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -123,8 +124,17 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
image: image,
|
||||
});
|
||||
|
||||
// Показываем уведомление о добавлении
|
||||
alert(`Товар "${brand} ${article}" добавлен в корзину (${quantity} шт.)`);
|
||||
// Показываем тоастер вместо alert
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${brand} ${article} (${quantity} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Обработчик клика по сердечку
|
||||
@ -133,9 +143,18 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${offers[0]?.productId || offers[0]?.offerKey || ''}:${article}:${brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном и удаляем по правильному ID
|
||||
const favoriteItem = favorites.find((item: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (offers[0]?.productId && item.productId === offers[0].productId) return true;
|
||||
if (offers[0]?.offerKey && item.offerKey === offers[0].offerKey) return true;
|
||||
if (item.article === article && item.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
const bestOffer = offers[0]; // Берем первое предложение как лучшее
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Filters, { FilterConfig } from "./Filters";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
|
||||
@ -8,9 +9,9 @@ interface FavoriteListProps {
|
||||
onFilterChange?: (type: string, value: any) => void;
|
||||
searchQuery?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
sortBy?: 'name' | 'brand' | 'price' | 'date';
|
||||
sortBy?: 'name' | 'brand' | 'date';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
onSortChange?: (sortBy: 'name' | 'brand' | 'price' | 'date') => void;
|
||||
onSortChange?: (sortBy: 'name' | 'brand' | 'date') => void;
|
||||
onSortOrderChange?: (sortOrder: 'asc' | 'desc') => void;
|
||||
}
|
||||
|
||||
@ -25,6 +26,7 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
onSortChange,
|
||||
onSortOrderChange
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { favorites, removeFromFavorites, clearFavorites } = useFavorites();
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
@ -35,6 +37,14 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
clearFavorites();
|
||||
};
|
||||
|
||||
// Функция для поиска товара
|
||||
const handleSearchItem = (item: any) => {
|
||||
// Формируем поисковый запрос из бренда и артикула
|
||||
const searchQuery = `${item.brand} ${item.article}`;
|
||||
// Переходим на страницу поиска с результатами
|
||||
router.push(`/search-result?article=${encodeURIComponent(item.article)}&brand=${encodeURIComponent(item.brand)}`);
|
||||
};
|
||||
|
||||
// Состояние для hover на иконке удаления всех
|
||||
const [removeAllHover, setRemoveAllHover] = useState(false);
|
||||
// Состояние для hover на корзине отдельного товара
|
||||
@ -82,11 +92,6 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
case 'brand':
|
||||
comparison = a.brand.localeCompare(b.brand);
|
||||
break;
|
||||
case 'price':
|
||||
const priceA = a.price || 0;
|
||||
const priceB = b.price || 0;
|
||||
comparison = priceA - priceB;
|
||||
break;
|
||||
case 'date':
|
||||
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
@ -97,29 +102,19 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
return sortOrder === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
|
||||
const formatPrice = (price?: number, currency?: string) => {
|
||||
if (!price) {
|
||||
return 'Цена не указана';
|
||||
}
|
||||
if (currency === 'RUB') {
|
||||
return `от ${price.toLocaleString('ru-RU')} ₽`;
|
||||
}
|
||||
return `от ${price} ${currency || ''}`;
|
||||
};
|
||||
|
||||
const handleSortClick = (newSortBy: 'name' | 'brand' | 'price' | 'date') => {
|
||||
const handleSortClick = (newSortBy: 'name' | 'brand' | 'date') => {
|
||||
if (sortBy === newSortBy) {
|
||||
// Если тот же столбец, меняем порядок
|
||||
onSortOrderChange?.(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
// Если новый столбец, устанавливаем его и порядок по умолчанию
|
||||
onSortChange?.(newSortBy);
|
||||
onSortOrderChange?.(newSortBy === 'price' ? 'asc' : 'desc');
|
||||
onSortOrderChange?.(newSortBy === 'date' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// SVG-галочки для сортировки — всегда видны у всех колонок
|
||||
const getSortIcon = (columnSort: 'name' | 'brand' | 'price' | 'date') => {
|
||||
const getSortIcon = (columnSort: 'name' | 'brand' | 'date') => {
|
||||
const isActive = sortBy === columnSort;
|
||||
const isAsc = sortOrder === 'asc';
|
||||
const color = isActive ? 'var(--_button---primary)' : '#94a3b8';
|
||||
@ -176,6 +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="text-sm font-medium text-gray-600">Действия</div>
|
||||
</div> */}
|
||||
{favorites.length > 0 && (
|
||||
<div
|
||||
className="w-layout-hflex select-all-block"
|
||||
@ -233,12 +231,24 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex add-to-cart-block-copy">
|
||||
<h4
|
||||
className="heading-9-copy-copy cursor-pointer hover:text-blue-600 flex items-center gap-1"
|
||||
onClick={() => handleSortClick('price')}
|
||||
>
|
||||
{formatPrice(item.price, item.currency)} {getSortIcon('price')}
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => handleSearchItem(item)}
|
||||
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"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="m21 21-4.35-4.35" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
Поиск
|
||||
</button>
|
||||
<div className="w-layout-hflex control-element-copy">
|
||||
{/* Корзина с hover-эффектом для удаления товара */}
|
||||
<span
|
||||
|
@ -31,6 +31,8 @@ const FulltextSearchSection: React.FC<FulltextSearchSectionProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('SEARCH PARAMS', { catalogCode, vehicleId, searchQuery: searchQuery.trim(), ssd });
|
||||
|
||||
executeSearch({
|
||||
variables: {
|
||||
catalogCode,
|
||||
@ -199,6 +201,4 @@ const FulltextSearchSection: React.FC<FulltextSearchSectionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default FulltextSearchSection;
|
@ -58,6 +58,15 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
setSearchQuery(q);
|
||||
}
|
||||
}
|
||||
// Если мы находимся на странице деталей автомобиля, восстанавливаем VIN из URL
|
||||
else if (router.pathname === '/vehicle-search/[brand]/[vehicleId]') {
|
||||
const { vin } = router.query;
|
||||
if (vin && typeof vin === 'string') {
|
||||
setSearchQuery(vin);
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
}
|
||||
}
|
||||
// Для других страниц очищаем поисковый запрос
|
||||
else {
|
||||
setSearchQuery('');
|
||||
@ -321,11 +330,31 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
const catalogCode = (vehicle as any).catalog || vehicle.brand.toLowerCase();
|
||||
console.log('🚗 Переход на страницу автомобиля:', { catalogCode, vehicleId: vehicle.vehicleid, ssd: vehicle.ssd });
|
||||
|
||||
// Создаем параметры URL
|
||||
const urlParams = new URLSearchParams();
|
||||
|
||||
// Добавляем SSD если есть
|
||||
if (vehicle.ssd) {
|
||||
urlParams.set('ssd', vehicle.ssd);
|
||||
}
|
||||
|
||||
// Добавляем VIN-номер в URL, если поиск был по VIN
|
||||
if (searchType === 'vin' && searchQuery) {
|
||||
urlParams.set('vin', searchQuery);
|
||||
}
|
||||
|
||||
// Если переход происходит из поиска автомобилей по артикулу, передаем артикул для автоматического поиска
|
||||
const currentOEMNumber = oemSearchMode === 'vehicles' ? searchQuery.trim().toUpperCase() : '';
|
||||
const url = `/vehicle-search/${catalogCode}/${vehicle.vehicleid}?ssd=${vehicle.ssd || ''}${currentOEMNumber ? `&oemNumber=${encodeURIComponent(currentOEMNumber)}` : ''}`;
|
||||
if (currentOEMNumber) {
|
||||
urlParams.set('oemNumber', currentOEMNumber);
|
||||
}
|
||||
|
||||
setSearchQuery('');
|
||||
// Формируем URL
|
||||
const baseUrl = `/vehicle-search/${catalogCode}/${vehicle.vehicleid}`;
|
||||
const url = urlParams.toString() ? `${baseUrl}?${urlParams.toString()}` : baseUrl;
|
||||
|
||||
// НЕ очищаем поисковый запрос, чтобы он остался в строке поиска
|
||||
// setSearchQuery('');
|
||||
router.push(url);
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import * as React from "react";
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
|
||||
const menuItems = [
|
||||
{ label: 'Заказы', href: '/profile-orders', icon: 'https://cdn.builder.io/api/v1/image/assets/TEMP/22ecd7e6251abe04521d03f0ac09f73018a8c2c8?placeholderIfAbsent=true&apiKey=f5bc5a2dc9b841d0aba1cc6c74a35920' },
|
||||
@ -28,6 +30,15 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
const router = useRouter();
|
||||
const isClient = useIsClient();
|
||||
|
||||
// Получаем данные клиента для проверки наличия юридических лиц
|
||||
const { data: clientData } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isClient,
|
||||
errorPolicy: 'ignore' // Игнорируем ошибки, чтобы не ломать интерфейс
|
||||
});
|
||||
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
const hasLegalEntities = clientData?.clientMe?.legalEntities && clientData.clientMe.legalEntities.length > 0;
|
||||
|
||||
const handleLogout = () => {
|
||||
if (isClient) {
|
||||
localStorage.removeItem('authToken');
|
||||
@ -66,31 +77,37 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
||||
Финансы
|
||||
</div>
|
||||
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
||||
{financeItems.map((item) => {
|
||||
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
||||
return (
|
||||
<Link href={item.href} key={item.href}>
|
||||
<div
|
||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||
isActive ? 'bg-slate-200' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.icon}
|
||||
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
||||
alt={item.label}
|
||||
/>
|
||||
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||
{hasLegalEntities && (
|
||||
<>
|
||||
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
||||
Финансы
|
||||
</div>
|
||||
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
||||
{financeItems.map((item) => {
|
||||
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
||||
return (
|
||||
<Link href={item.href} key={item.href}>
|
||||
<div
|
||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||
isActive ? 'bg-slate-200' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.icon}
|
||||
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
||||
alt={item.label}
|
||||
/>
|
||||
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Кнопка выхода */}
|
||||
<div className="mt-3">
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
|
||||
interface ProfileSidebarProps {
|
||||
activeItem: string;
|
||||
@ -8,6 +10,15 @@ interface ProfileSidebarProps {
|
||||
const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
||||
const isClient = useIsClient();
|
||||
|
||||
// Получаем данные клиента для проверки наличия юридических лиц
|
||||
const { data: clientData } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isClient,
|
||||
errorPolicy: 'ignore' // Игнорируем ошибки, чтобы не ломать интерфейс
|
||||
});
|
||||
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
const hasLegalEntities = clientData?.clientMe?.legalEntities && clientData.clientMe.legalEntities.length > 0;
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'orders', icon: 'order', label: 'Заказы', href: '/profile-orders' },
|
||||
{ id: 'history', icon: 'history', label: 'История поиска', href: '/profile-history' },
|
||||
@ -133,25 +144,28 @@ const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header">
|
||||
<h3 className="sidebar-title">Финансы</h3>
|
||||
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||
{hasLegalEntities && (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header">
|
||||
<h3 className="sidebar-title">Финансы</h3>
|
||||
</div>
|
||||
<div className="sidebar-menu">
|
||||
{financeItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className={`sidebar-item ${activeItem === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<div className="sidebar-icon">
|
||||
{renderIcon(item.icon, activeItem === item.id)}
|
||||
</div>
|
||||
<span className="sidebar-label">{item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-menu">
|
||||
{financeItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className={`sidebar-item ${activeItem === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<div className="sidebar-icon">
|
||||
{renderIcon(item.icon, activeItem === item.id)}
|
||||
</div>
|
||||
<span className="sidebar-label">{item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка выхода */}
|
||||
<div className="logout-section">
|
||||
|
@ -210,7 +210,7 @@ const VehiclePartsSearchSection: React.FC<VehiclePartsSearchSectionProps> = ({
|
||||
<div className="min-h-[400px]">
|
||||
{searchType === 'quickgroups' && supportsQuickGroups && (
|
||||
<QuickGroupsSection
|
||||
catalogCode={vehicleInfo.catalog}
|
||||
catalogCode={vehicleInfo.catalog}
|
||||
vehicleId={vehicleInfo.vehicleid}
|
||||
ssd={vehicleInfo.ssd}
|
||||
/>
|
||||
|
@ -23,7 +23,7 @@ export default function InfoCard({
|
||||
currency = 'RUB',
|
||||
image
|
||||
}: InfoCardProps) {
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
// Проверяем, есть ли товар в избранном
|
||||
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brand);
|
||||
@ -34,9 +34,18 @@ export default function InfoCard({
|
||||
e.stopPropagation();
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (offerKey && fav.offerKey === offerKey) return true;
|
||||
if (fav.article === articleNumber && fav.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
|
@ -1,96 +1,47 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { PARTS_INDEX_SEARCH_BY_ARTICLE } from "@/lib/graphql";
|
||||
|
||||
interface ProductCharacteristicsProps {
|
||||
result?: any;
|
||||
}
|
||||
|
||||
const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
||||
const [partsIndexData, setPartsIndexData] = useState<any>(null);
|
||||
|
||||
// Функция для рендеринга характеристик из нашей базы данных
|
||||
const renderInternalCharacteristics = () => {
|
||||
if (!result?.characteristics || result.characteristics.length === 0) return null;
|
||||
// Запрос к Parts Index для получения дополнительных характеристик
|
||||
const { data: partsIndexResult, loading: partsIndexLoading } = useQuery(PARTS_INDEX_SEARCH_BY_ARTICLE, {
|
||||
variables: {
|
||||
articleNumber: result?.articleNumber || '',
|
||||
brandName: result?.brand || '',
|
||||
lang: 'ru'
|
||||
},
|
||||
skip: !result?.articleNumber || !result?.brand,
|
||||
errorPolicy: 'ignore'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Характеристики товара:
|
||||
</span>
|
||||
</div>
|
||||
{result.characteristics.map((char: any, index: number) => (
|
||||
<div key={index} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{char.characteristic.name}:</span>
|
||||
<span className="text-block-28">{char.value}</span>
|
||||
useEffect(() => {
|
||||
if (partsIndexResult?.partsIndexSearchByArticle) {
|
||||
setPartsIndexData(partsIndexResult.partsIndexSearchByArticle);
|
||||
}
|
||||
}, [partsIndexResult]);
|
||||
|
||||
// Функция для рендеринга параметров из Parts Index
|
||||
const renderPartsIndexParameters = () => {
|
||||
if (!partsIndexData?.parameters) return null;
|
||||
|
||||
return partsIndexData.parameters.map((paramGroup: any, groupIndex: number) => (
|
||||
<div key={groupIndex} className="w-layout-vflex flex-block-53">
|
||||
{paramGroup.params?.map((param: any, paramIndex: number) => (
|
||||
<div key={paramIndex} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{param.title}:</span>
|
||||
<span className="text-block-28">
|
||||
{param.values?.map((value: any) => value.value).join(', ') || 'Нет данных'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Функция для рендеринга характеристик из Parts Index
|
||||
const renderPartsIndexCharacteristics = () => {
|
||||
if (!result?.partsIndexCharacteristics || result.partsIndexCharacteristics.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Дополнительные характеристики:
|
||||
</span>
|
||||
</div>
|
||||
{result.partsIndexCharacteristics.map((char: any, index: number) => (
|
||||
<div key={index} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{char.name}:</span>
|
||||
<span className="text-block-28">{char.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Функция для рендеринга изображений из нашей базы данных
|
||||
const renderInternalImages = () => {
|
||||
if (!result?.images || result.images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53" style={{ marginTop: '20px' }}>
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Изображения товара:
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-layout-hflex" style={{ flexWrap: 'wrap', gap: '10px', marginTop: '10px' }}>
|
||||
{result.images.slice(0, 6).map((image: any, index: number) => (
|
||||
<div key={image.id || index} style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || `${result?.brand} ${result?.articleNumber} - изображение ${index + 1}`}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
onClick={() => window.open(image.url, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -111,27 +62,33 @@ const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
||||
<span className="text-block-29">Название:</span>
|
||||
<span className="text-block-28">{result.name}</span>
|
||||
</div>
|
||||
{result?.description && (
|
||||
{partsIndexData?.originalName && (
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Оригинальное название:</span>
|
||||
<span className="text-block-28">{partsIndexData.originalName}</span>
|
||||
</div>
|
||||
)}
|
||||
{partsIndexData?.description && (
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Описание:</span>
|
||||
<span className="text-block-28" style={{ maxWidth: '400px', wordWrap: 'break-word' }}>
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="text-block-28">{partsIndexData.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Характеристики из нашей базы данных */}
|
||||
{renderInternalCharacteristics()}
|
||||
|
||||
{/* Дополнительные характеристики из Parts Index */}
|
||||
{renderPartsIndexCharacteristics()}
|
||||
{partsIndexLoading ? (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Загрузка характеристик...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderPartsIndexParameters()
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Изображения из нашей базы данных */}
|
||||
{renderInternalImages()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -15,6 +15,7 @@ import {
|
||||
const ProfileHistoryMain = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("Все");
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState("Все");
|
||||
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||
@ -105,6 +106,14 @@ const ProfileHistoryMain = () => {
|
||||
useEffect(() => {
|
||||
let filtered = [...getFilteredByTime(historyItems, activeTab)];
|
||||
|
||||
// Фильтрация по производителю
|
||||
if (selectedManufacturer !== "Все") {
|
||||
filtered = filtered.filter(item =>
|
||||
item.brand === selectedManufacturer ||
|
||||
item.vehicleInfo?.brand === selectedManufacturer
|
||||
);
|
||||
}
|
||||
|
||||
// Поиск
|
||||
if (search.trim()) {
|
||||
const searchLower = search.toLowerCase();
|
||||
@ -152,7 +161,7 @@ const ProfileHistoryMain = () => {
|
||||
}
|
||||
|
||||
setFilteredItems(filtered);
|
||||
}, [historyItems, search, activeTab, sortField, sortOrder]);
|
||||
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||
|
||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||
if (sortField === field) {
|
||||
@ -272,6 +281,18 @@ const ProfileHistoryMain = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedManufacturer("Все");
|
||||
setSearch("");
|
||||
setActiveTab("Все");
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
)}
|
||||
{historyItems.length === 0 && (
|
||||
<button
|
||||
onClick={handleCreateTestData}
|
||||
@ -297,7 +318,14 @@ const ProfileHistoryMain = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-5 w-full text-lg font-medium leading-tight whitespace-nowrap text-gray-950 max-md:max-w-full">
|
||||
<ProfileHistoryTabs tabs={tabOptions} activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
<ProfileHistoryTabs
|
||||
tabs={tabOptions}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
historyItems={historyItems}
|
||||
selectedManufacturer={selectedManufacturer}
|
||||
onManufacturerChange={setSelectedManufacturer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-5 w-full text-gray-400 max-md:max-w-full flex-1 h-full">
|
||||
@ -421,6 +449,11 @@ const ProfileHistoryMain = () => {
|
||||
{filteredItems.length > 0 && (
|
||||
<div className="mt-4 text-center text-sm text-gray-500">
|
||||
Показано {filteredItems.length} из {historyItems.length} записей
|
||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
(применены фильтры)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,22 +1,47 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { PartsSearchHistoryItem } from '@/lib/graphql/search-history';
|
||||
|
||||
interface ProfileHistoryTabsProps {
|
||||
tabs: string[];
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
historyItems: PartsSearchHistoryItem[];
|
||||
selectedManufacturer: string;
|
||||
onManufacturerChange: (manufacturer: string) => void;
|
||||
}
|
||||
|
||||
const manufacturers = ["Все", "VAG", "Toyota", "Ford", "BMW"];
|
||||
|
||||
const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
historyItems,
|
||||
selectedManufacturer,
|
||||
onManufacturerChange,
|
||||
}) => {
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState(manufacturers[0]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Получаем уникальных производителей из истории поиска
|
||||
const getUniqueManufacturers = () => {
|
||||
const manufacturersSet = new Set<string>();
|
||||
|
||||
historyItems.forEach(item => {
|
||||
// Добавляем бренд из поля brand
|
||||
if (item.brand) {
|
||||
manufacturersSet.add(item.brand);
|
||||
}
|
||||
// Добавляем бренд из информации об автомобиле
|
||||
if (item.vehicleInfo?.brand) {
|
||||
manufacturersSet.add(item.vehicleInfo.brand);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueManufacturers = Array.from(manufacturersSet).sort();
|
||||
return ["Все", ...uniqueManufacturers];
|
||||
};
|
||||
|
||||
const manufacturers = getUniqueManufacturers();
|
||||
|
||||
// Закрытие дропдауна при клике вне
|
||||
React.useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@ -34,6 +59,11 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
};
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
const handleManufacturerSelect = (manufacturer: string) => {
|
||||
onManufacturerChange(manufacturer);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-5 w-full max-md:max-w-full">
|
||||
{tabs.map((tab) => (
|
||||
@ -69,20 +99,41 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
>
|
||||
<span className="truncate">{selectedManufacturer}</span>
|
||||
<span className="ml-2 flex-shrink-0 flex items-center">
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 20 20"><path d="M6 8l4 4 4-4" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
className={`transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||
>
|
||||
<path d="M6 8l4 4 4-4" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{isDropdownOpen && (
|
||||
<ul className="absolute left-0 top-full z-10 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full">
|
||||
{manufacturers.map((option) => (
|
||||
<li
|
||||
key={option}
|
||||
className={`px-6 py-4 cursor-pointer hover:bg-blue-100 ${option === selectedManufacturer ? 'bg-blue-50 font-semibold' : ''}`}
|
||||
onMouseDown={() => { setSelectedManufacturer(option); setIsDropdownOpen(false); }}
|
||||
>
|
||||
{option}
|
||||
<ul className="absolute left-0 top-full z-10 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-y-auto">
|
||||
{manufacturers.length === 0 ? (
|
||||
<li className="px-6 py-4 text-gray-400 text-center">
|
||||
Нет данных
|
||||
</li>
|
||||
))}
|
||||
) : (
|
||||
manufacturers.map((manufacturer) => (
|
||||
<li
|
||||
key={manufacturer}
|
||||
className={`px-6 py-4 cursor-pointer hover:bg-blue-100 transition-colors ${manufacturer === selectedManufacturer ? 'bg-blue-50 font-semibold text-blue-600' : ''}`}
|
||||
onMouseDown={() => handleManufacturerSelect(manufacturer)}
|
||||
>
|
||||
{manufacturer}
|
||||
{manufacturer !== "Все" && (
|
||||
<span className="ml-2 text-xs text-gray-400">
|
||||
({historyItems.filter(item =>
|
||||
item.brand === manufacturer || item.vehicleInfo?.brand === manufacturer
|
||||
).length})
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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">
|
||||
|
@ -1,9 +1,172 @@
|
||||
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';
|
||||
|
||||
const KnotIn = () => (
|
||||
<div className="knotin">
|
||||
<img src="/images/image-44.jpg" loading="lazy" alt="" className="image-26" />
|
||||
</div>
|
||||
);
|
||||
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) => {
|
||||
if (!baseUrl) return '';
|
||||
return baseUrl
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace('%size%', size);
|
||||
};
|
||||
|
||||
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="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>
|
||||
<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 || ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnotIn;
|
@ -1,43 +1,73 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import BrandSelectionModal from '../BrandSelectionModal';
|
||||
|
||||
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 }>;
|
||||
}>;
|
||||
selectedCodeOnImage?: string | number;
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
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) => {
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
<BrandSelectionModal
|
||||
isOpen={isBrandModalOpen}
|
||||
onClose={() => setIsBrandModalOpen(false)}
|
||||
articleNumber={selectedDetail?.oem || ''}
|
||||
detailName={selectedDetail?.name || ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnotParts;
|
@ -1,31 +1,116 @@
|
||||
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 }) => (
|
||||
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">
|
||||
{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>
|
||||
{!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>
|
||||
@ -34,7 +119,10 @@ const VinCategory: React.FC<VinCategoryProps> = ({ onCategoryClick }) => (
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VinCategory;
|
@ -1,35 +1,191 @@
|
||||
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, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
|
||||
import VinPartCard from './VinPartCard';
|
||||
|
||||
const dropdownTitles = [
|
||||
"Детали для ТО",
|
||||
"Двигатель",
|
||||
"Топливная система",
|
||||
"Система охлаждения",
|
||||
"Система выпуска",
|
||||
"Трансмиссия",
|
||||
"Ходовая часть",
|
||||
"Рулевое управление",
|
||||
"Тормозная система",
|
||||
"Электрооборудование",
|
||||
"Отопление / кондиционирование",
|
||||
"Детали салона",
|
||||
"Детали кузова",
|
||||
"Дополнительное оборудование"
|
||||
];
|
||||
|
||||
const VinLeftbar = () => {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
|
||||
const handleToggle = (idx: number) => {
|
||||
setOpenIndex(openIndex === idx ? null : idx);
|
||||
interface VinLeftbarProps {
|
||||
vehicleInfo: {
|
||||
catalog: string;
|
||||
vehicleid: string;
|
||||
ssd: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
onSearchResults?: (results: any[]) => void;
|
||||
onNodeSelect?: (node: any) => void;
|
||||
}
|
||||
|
||||
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');
|
||||
const [executeSearch, { data, loading, error }] = useLazyQuery(SEARCH_LAXIMO_FULLTEXT, { errorPolicy: 'all' });
|
||||
|
||||
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()) return;
|
||||
if (!ssd || ssd.trim() === '') {
|
||||
console.error('SSD обязателен для поиска по названию');
|
||||
return;
|
||||
}
|
||||
console.log('SEARCH PARAMS', { catalogCode, vehicleId, searchQuery: searchQuery.trim(), ssd });
|
||||
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]);
|
||||
|
||||
// --- Новый блок: вычисляем доступность поиска ---
|
||||
const isSearchAvailable = !!catalogCode && !!vehicleId && !!ssd && ssd.trim() !== '';
|
||||
const showWarning = !isSearchAvailable;
|
||||
const showError = !!error && isSearchAvailable && searchQuery.trim();
|
||||
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">
|
||||
<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(); if (!ssd || ssd.trim() === '') { return; } 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 +194,164 @@ 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={loading}
|
||||
/>
|
||||
</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>
|
||||
{/* Варианты отображения: предупреждение, ошибка, подсказки, результаты */}
|
||||
|
||||
</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>
|
||||
{/* 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 (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>
|
||||
<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 */}
|
||||
) : quickGroups.length > 0 ? (
|
||||
renderQuickGroupTree(quickGroups)
|
||||
) : (
|
||||
<div>Нет доступных групп быстрого поиска</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Tab content end */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
38
src/components/vin/VinPartCard.tsx
Normal file
38
src/components/vin/VinPartCard.tsx
Normal 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;
|
@ -18,7 +18,7 @@ export const useArticleImage = (
|
||||
artId: string | undefined | null,
|
||||
options: UseArticleImageOptions = {}
|
||||
): UseArticleImageReturn => {
|
||||
const { enabled = true, fallbackImage = '' } = options;
|
||||
const { enabled = true, fallbackImage = '/images/image-10.png' } = options;
|
||||
const [imageUrl, setImageUrl] = useState<string>(fallbackImage);
|
||||
|
||||
// Проверяем что artId валидный
|
||||
|
@ -7,7 +7,6 @@ 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() {
|
||||
|
||||
@ -39,7 +38,6 @@ export default function CartPage() {
|
||||
</section>
|
||||
<Footer />
|
||||
<MobileMenuBottomSection />
|
||||
<CartDebug />
|
||||
</>
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ export default function Favorite() {
|
||||
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 [sortBy, setSortBy] = useState<'name' | 'brand' | 'date'>('date');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
// Создаем динамические фильтры на основе данных избранного
|
||||
|
@ -1,11 +1,11 @@
|
||||
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";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const CONFIRM_PAYMENT = gql`
|
||||
mutation ConfirmPayment($orderId: ID!) {
|
||||
@ -51,8 +51,15 @@ function PaymentSuccessContent() {
|
||||
}
|
||||
}).then(() => {
|
||||
console.log('Оплата заказа подтверждена');
|
||||
toast.success('Оплата успешно подтверждена!', {
|
||||
duration: 3000,
|
||||
icon: '✅',
|
||||
});
|
||||
}).catch((error: any) => {
|
||||
console.error('Ошибка подтверждения оплаты:', error);
|
||||
toast.error('Ошибка подтверждения оплаты: ' + error.message, {
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [router.query, confirmPayment]);
|
||||
@ -76,7 +83,7 @@ function PaymentSuccessContent() {
|
||||
<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">
|
||||
|
@ -1,4 +1,8 @@
|
||||
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 ProfileSidebar from '@/components/ProfileSidebar';
|
||||
@ -11,6 +15,48 @@ import NotificationMane from "@/components/profile/NotificationMane";
|
||||
import Head from "next/head";
|
||||
|
||||
const ProfileActsPage = () => {
|
||||
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>
|
||||
|
@ -1,4 +1,8 @@
|
||||
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';
|
||||
@ -8,9 +12,49 @@ import ProfileRequisitiesMain from '@/components/profile/ProfileRequisitiesMain'
|
||||
import ProfileInfo from '@/components/profile/ProfileInfo';
|
||||
import Head from "next/head";
|
||||
|
||||
|
||||
|
||||
const ProfileRequisitiesPage = () => {
|
||||
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>
|
||||
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
@ -502,6 +502,7 @@ export default function SearchResult() {
|
||||
price={`${offer.price.toLocaleString()} ₽`}
|
||||
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
|
||||
stock={`${offer.quantity} шт.`}
|
||||
offer={offer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -105,6 +105,14 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
||||
ssdLength: ssd.length
|
||||
});
|
||||
|
||||
// Создаем базовые параметры URL
|
||||
const urlParams = new URLSearchParams();
|
||||
|
||||
// Добавляем VIN-номер в URL, если он есть
|
||||
if (searchQuery && searchType === 'vin') {
|
||||
urlParams.set('vin', searchQuery);
|
||||
}
|
||||
|
||||
// Если есть SSD, сохраняем его в localStorage для безопасной передачи
|
||||
if (ssd && ssd.trim() !== '') {
|
||||
const vehicleKey = `vehicle_ssd_${catalogCode}_${vehicleId}`;
|
||||
@ -121,25 +129,22 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
||||
|
||||
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);
|
||||
urlParams.set('use_storage', '1');
|
||||
urlParams.set('ssd_length', ssd.length.toString());
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
if (skipToCategories) {
|
||||
urlParams.set('searchType', 'categories');
|
||||
}
|
||||
|
||||
// Формируем URL с параметрами
|
||||
const baseUrl = `/vehicle-search/${catalogCode}/${vehicleId}`;
|
||||
const url = urlParams.toString() ? `${baseUrl}?${urlParams.toString()}` : baseUrl;
|
||||
|
||||
console.log('🔗 Переходим на URL:', url);
|
||||
// Используем replace вместо push для моментального перехода
|
||||
router.replace(url);
|
||||
}, [router, searchQuery, searchType]);
|
||||
|
||||
// Предзагрузка и автоматический переход при поиске по VIN, если найден только один автомобиль
|
||||
useEffect(() => {
|
||||
|
@ -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,24 +157,24 @@ const VehicleDetailsPage = () => {
|
||||
|
||||
if (vehicleLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<>
|
||||
<Head>
|
||||
<title>Загрузка автомобиля...</title>
|
||||
</Head>
|
||||
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f9fafb' }}>
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Если информация о каталоге недоступна, показываем ошибку
|
||||
if (!catalogData?.laximoCatalogInfo) {
|
||||
return (
|
||||
<Layout>
|
||||
<>
|
||||
<Head>
|
||||
<title>Каталог не найден</title>
|
||||
</Head>
|
||||
@ -140,32 +190,143 @@ const VehicleDetailsPage = () => {
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Если информация об автомобиле недоступна, создаем заглушку
|
||||
const vehicleInfo = vehicleData?.laximoVehicleInfo || {
|
||||
vehicleid: vehicleId as string,
|
||||
// Если vehicleId невалидный (например, '0'), показываем предупреждение и не рендерим поиск
|
||||
if (!vehicleId || vehicleId === '0') {
|
||||
return (
|
||||
<main className="min-h-screen bg-yellow-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-yellow-900 mb-4">Автомобиль не выбран</h1>
|
||||
<p className="text-yellow-700 mb-8">Для поиска по деталям необходимо выбрать конкретный автомобиль через VIN или мастер подбора.</p>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="bg-yellow-600 text-white px-6 py-3 rounded-lg hover:bg-yellow-700 transition-colors"
|
||||
>
|
||||
Назад к поиску
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Гарантируем, что vehicleId — строка
|
||||
const vehicleIdStr = Array.isArray(vehicleId) ? (vehicleId[0] || '') : (vehicleId || '');
|
||||
const fallbackVehicleId = (vehicleIdStr !== '0' ? vehicleIdStr : '');
|
||||
|
||||
let vehicleInfo = vehicleData?.laximoVehicleInfo || {
|
||||
vehicleid: fallbackVehicleId,
|
||||
name: `Автомобиль ${catalogData.laximoCatalogInfo.name}`,
|
||||
ssd: finalSsd,
|
||||
brand: catalogData.laximoCatalogInfo.brand,
|
||||
catalog: catalogData.laximoCatalogInfo.code,
|
||||
attributes: []
|
||||
attributes: [] as never[]
|
||||
};
|
||||
|
||||
// Если вдруг с сервера пришёл vehicleid: '0', подменяем на корректный
|
||||
if (vehicleInfo.vehicleid === '0' && fallbackVehicleId) {
|
||||
vehicleInfo = { ...vehicleInfo, vehicleid: fallbackVehicleId };
|
||||
}
|
||||
|
||||
// Логируем, что реально передаём в VinLeftbar
|
||||
console.log('Передаём в VinLeftbar:', {
|
||||
catalog: vehicleInfo.catalog,
|
||||
vehicleid: vehicleInfo.vehicleid,
|
||||
ssd: vehicleInfo.ssd
|
||||
});
|
||||
|
||||
// Если нет данных автомобиля и есть ошибка, показываем предупреждение
|
||||
const hasError = vehicleError && !vehicleData?.laximoVehicleInfo;
|
||||
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">
|
||||
{vehicleInfo && vehicleInfo.catalog && vehicleInfo.vehicleid && vehicleInfo.ssd && (
|
||||
<VinLeftbar
|
||||
vehicleInfo={vehicleInfo}
|
||||
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
|
||||
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 ? (
|
||||
<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 +464,7 @@ const VehicleDetailsPage = () => {
|
||||
onSearchTypeChange={setSearchType}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -386,16 +386,17 @@ input.input-receiver:focus {
|
||||
color: var(--_fonts---color--light-blue-grey);
|
||||
}
|
||||
|
||||
.knotin {
|
||||
height: 100%;
|
||||
/* .knotin {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.knotin img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
object-fit: contain; /* или cover */
|
||||
}
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
} */
|
||||
|
||||
|
||||
|
||||
.tabs-menu.w-tab-menu {
|
||||
scrollbar-width: none;
|
||||
@ -403,4 +404,30 @@ 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%;
|
||||
}
|
||||
|
||||
.text-block-55 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 2.8em;
|
||||
line-height: 1.4em;
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user