Compare commits
16 Commits
8055886082
...
1207
Author | SHA1 | Date | |
---|---|---|---|
cebe3a10ac | |||
791152a862 | |||
b11142ad0f | |||
508ad8cff3 | |||
53a398a072 | |||
268e6d3315 | |||
26e4a95ae4 | |||
9fc7d0fbf5 | |||
7abe016f0f | |||
90d1beb15e | |||
475b02ea2d | |||
ed76c97915 | |||
3b5defe3d9 | |||
c703fc839a | |||
2a983c956c | |||
96e11b0220 |
BIN
public/images/resource2.png
Normal file
BIN
public/images/resource2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -1,4 +1,7 @@
|
||||
import React from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface BestPriceItemProps {
|
||||
image: string;
|
||||
@ -7,6 +10,8 @@ interface BestPriceItemProps {
|
||||
oldPrice: string;
|
||||
title: string;
|
||||
brand: string;
|
||||
article?: string;
|
||||
productId?: string;
|
||||
onAddToCart?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
@ -17,14 +22,128 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
||||
oldPrice,
|
||||
title,
|
||||
brand,
|
||||
article,
|
||||
productId,
|
||||
onAddToCart,
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
// Проверяем, есть ли товар в избранном
|
||||
const isItemFavorite = isFavorite(productId, undefined, article, brand);
|
||||
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
return parseFloat(cleanPrice) || 0;
|
||||
};
|
||||
|
||||
// Обработчик добавления в корзину
|
||||
const handleAddToCart = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Если передан кастомный обработчик, используем его
|
||||
if (onAddToCart) {
|
||||
onAddToCart(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const numericPrice = parsePrice(price);
|
||||
|
||||
if (numericPrice <= 0) {
|
||||
toast.error('Цена товара не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем товар в корзину
|
||||
const result = await addItem({
|
||||
productId: productId,
|
||||
name: title,
|
||||
description: `${brand} - ${title}`,
|
||||
brand: brand,
|
||||
article: article,
|
||||
price: numericPrice,
|
||||
currency: 'RUB',
|
||||
quantity: 1,
|
||||
image: image,
|
||||
supplier: 'Protek',
|
||||
deliveryTime: '1 день',
|
||||
isExternal: false
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Показываем успешный тоастер
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} - ${title}`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик клика по иконке избранного
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Находим товар в избранном и удаляем
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (fav.article === article && fav.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
const numericPrice = parsePrice(price);
|
||||
addToFavorites({
|
||||
productId,
|
||||
name: title,
|
||||
brand: brand,
|
||||
article: article || '',
|
||||
price: numericPrice,
|
||||
currency: 'RUB',
|
||||
image: image
|
||||
});
|
||||
toast.success('Товар добавлен в избранное');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex bestpriceitem">
|
||||
<div className="favcardcat">
|
||||
<div
|
||||
className="favcardcat"
|
||||
onClick={handleFavoriteClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: isItemFavorite ? '#ef4444' : 'currentColor'
|
||||
}}
|
||||
title={isItemFavorite ? 'Удалить из избранного' : 'Добавить в избранное'}
|
||||
>
|
||||
<div className="icon-setting w-embed">
|
||||
<svg width="currenWidth" height="currentHeight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" stroke="currentColor"></path>
|
||||
<path
|
||||
d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z"
|
||||
fill={isItemFavorite ? 'currentColor' : 'none'}
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,14 +158,20 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
||||
/>
|
||||
<div className="saletagbp">{discount}</div>
|
||||
</div>
|
||||
<div className="div-block-3">
|
||||
<div className="div-block-3 bp-item-info">
|
||||
<div className="w-layout-hflex pricecartbp">
|
||||
<div className="actualprice">{price}</div>
|
||||
<div className="oldpricebp">{oldPrice}</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-120">
|
||||
<div className="nameitembp">{title}</div>
|
||||
<a href="#" className="button-icon w-inline-block" onClick={onAddToCart}>
|
||||
<a
|
||||
href="#"
|
||||
className="button-icon w-inline-block"
|
||||
onClick={handleAddToCart}
|
||||
style={{ cursor: 'pointer' }}
|
||||
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">
|
||||
|
@ -227,41 +227,63 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
||||
<span>{mobileCategory.label}</span>
|
||||
</div>
|
||||
<div className="mobile-subcategories">
|
||||
{mobileCategory.links.map((link: string, linkIndex: number) => (
|
||||
{mobileCategory.links.length === 1 ? (
|
||||
<div
|
||||
className="mobile-subcategory"
|
||||
key={link}
|
||||
onClick={() => {
|
||||
// Ищем соответствующую подгруппу по названию
|
||||
let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`;
|
||||
|
||||
let subcategoryId = `${mobileCategory.catalogId}_0`;
|
||||
if (mobileCategory.groups) {
|
||||
for (const group of mobileCategory.groups) {
|
||||
// Проверяем в подгруппах
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === mobileCategory.links[0]);
|
||||
if (foundSubgroup) {
|
||||
subcategoryId = foundSubgroup.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Если нет подгрупп, проверяем саму группу
|
||||
else if (group.name === link) {
|
||||
} else if (group.name === mobileCategory.links[0]) {
|
||||
subcategoryId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем catalogId из данных
|
||||
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
||||
const catalogId = activeCatalog?.id || 'fallback';
|
||||
handleCategoryClick(catalogId, link, subcategoryId);
|
||||
handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{link}
|
||||
Показать все
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
mobileCategory.links.map((link: string, linkIndex: number) => (
|
||||
<div
|
||||
className="mobile-subcategory"
|
||||
key={link}
|
||||
onClick={() => {
|
||||
let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`;
|
||||
if (mobileCategory.groups) {
|
||||
for (const group of mobileCategory.groups) {
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||
if (foundSubgroup) {
|
||||
subcategoryId = foundSubgroup.id;
|
||||
break;
|
||||
}
|
||||
} else if (group.name === link) {
|
||||
subcategoryId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
||||
const catalogId = activeCatalog?.id || 'fallback';
|
||||
handleCategoryClick(catalogId, link, subcategoryId);
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -443,44 +465,66 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
||||
<h3 className="heading-16">{tab.heading}</h3>
|
||||
<div className="w-layout-hflex flex-block-92">
|
||||
<div className="w-layout-vflex flex-block-91">
|
||||
{tab.links.map((link, linkIndex) => {
|
||||
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||
|
||||
// Ищем соответствующую подгруппу по названию
|
||||
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
||||
|
||||
if (catalog?.groups) {
|
||||
for (const group of catalog.groups) {
|
||||
// Проверяем в подгруппах
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||
if (foundSubgroup) {
|
||||
subcategoryId = foundSubgroup.id;
|
||||
{tab.links.length === 1 ? (
|
||||
<div
|
||||
className="link-2"
|
||||
onClick={() => {
|
||||
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||
let subcategoryId = `fallback_${idx}_0`;
|
||||
if (catalog?.groups) {
|
||||
for (const group of catalog.groups) {
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === tab.links[0]);
|
||||
if (foundSubgroup) {
|
||||
subcategoryId = foundSubgroup.id;
|
||||
break;
|
||||
}
|
||||
} else if (group.name === tab.links[0]) {
|
||||
subcategoryId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const catalogId = catalog?.id || 'fallback';
|
||||
handleCategoryClick(catalogId, tab.links[0], subcategoryId);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Показать все
|
||||
</div>
|
||||
) : (
|
||||
tab.links.map((link: string, linkIndex: number) => {
|
||||
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
||||
if (catalog?.groups) {
|
||||
for (const group of catalog.groups) {
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||
if (foundSubgroup) {
|
||||
subcategoryId = foundSubgroup.id;
|
||||
break;
|
||||
}
|
||||
} else if (group.name === link) {
|
||||
subcategoryId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Если нет подгрупп, проверяем саму группу
|
||||
else if (group.name === link) {
|
||||
subcategoryId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="link-2"
|
||||
key={link}
|
||||
onClick={() => {
|
||||
const catalogId = catalog?.id || 'fallback';
|
||||
handleCategoryClick(catalogId, link, subcategoryId);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div
|
||||
className="link-2"
|
||||
key={link}
|
||||
onClick={() => {
|
||||
const catalogId = catalog?.id || 'fallback';
|
||||
handleCategoryClick(catalogId, link, subcategoryId);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-91-copy">
|
||||
<img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" className="image-17" />
|
||||
|
@ -84,7 +84,7 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
|
||||
}, [state.error, clearError]);
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-48">
|
||||
<div className="w-layout-vflex flex-block-48" style={{ minHeight: '420px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{/* Отображение ошибок корзины */}
|
||||
{state.error && (
|
||||
<div className="alert alert-error mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
@ -145,9 +145,15 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
|
||||
</div>
|
||||
)}
|
||||
{displayItems.length === 0 ? (
|
||||
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
||||
<p>Ваша корзина пуста</p>
|
||||
<p>Добавьте товары из каталога</p>
|
||||
<div className="empty-cart-message" style={{ textAlign: 'center', width: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', justifyContent: 'center' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 90, height: 90, borderRadius: '50%', background: '#f3f4f6', marginBottom: 8 }}>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 36C14.3431 36 13 37.3431 13 39C13 40.6569 14.3431 42 16 42C17.6569 42 19 40.6569 19 39C19 37.3431 17.6569 36 16 36ZM6 8V12H10.68L16.44 24.016L14.16 28.08C13.7647 28.8001 13.5556 29.6352 13.5556 30.5C13.5556 32.1569 14.8987 33.5 16.5556 33.5H39V30.5H17.1756C17.0891 30.5 17.0178 30.4287 17.0178 30.3422L17.04 30.25L18.88 27H34.8C36.0212 27 37.1042 26.2627 37.6 25.16L43.048 14.352C43.1746 14.0993 43.2382 13.8132 43.2302 13.5242C43.2222 13.2352 43.1428 12.9538 42.9992 12.7087C42.8556 12.4636 42.6532 12.2632 42.4136 12.1302C42.174 11.9972 41.9062 11.9376 41.64 11.96H12.24L10.84 8H6ZM36 36C34.3431 36 33 37.3431 33 39C33 40.6569 34.3431 42 36 42C37.6569 42 39 40.6569 39 39C39 37.3431 37.6569 36 36 36Z" fill="#222"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div style={{ fontSize: '1.7rem', fontWeight: 700, color: '#222' }}>Ваша корзина пуста</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
displayItems.map((item, idx) => {
|
||||
|
@ -285,7 +285,7 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
||||
onClick={() => setShowLegalEntityDropdown(!showLegalEntityDropdown)}
|
||||
style={{ cursor: 'pointer', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<div className="text-block-31">
|
||||
<div className="text-block-31" style={{ fontSize: '14px', color: '#333' }}>
|
||||
{isIndividual ? 'Физическое лицо' : selectedLegalEntity || 'Выберите юридическое лицо'}
|
||||
</div>
|
||||
<div className="code-embed w-embed" style={{ transform: showLegalEntityDropdown ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
|
||||
@ -325,7 +325,7 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
backgroundColor: isIndividual ? '#f8f9fa' : 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: isIndividual ? 500 : 400
|
||||
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isIndividual) {
|
||||
@ -538,7 +538,9 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
backgroundColor: paymentMethod === 'yookassa' ? '#f8f9fa' : 'white',
|
||||
fontSize: '14px'
|
||||
fontSize: '14px',
|
||||
fontWeight: paymentMethod === 'yookassa' ? 500 : 400,
|
||||
color: '#222'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (paymentMethod !== 'yookassa') {
|
||||
|
@ -1,10 +1,16 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const CatalogSubscribe: React.FC = () => (
|
||||
<div className="w-layout-blockcontainer container subscribe w-container">
|
||||
<div className="w-layout-hflex flex-block-18">
|
||||
<img
|
||||
src="/images/resource2.png"
|
||||
alt="Ресурс 2"
|
||||
className="mt-[-18px]"
|
||||
/>
|
||||
<div className="div-block-9">
|
||||
<h3 className="heading-3 sub">Подпишитесь на новостную рассылку</h3>
|
||||
{/* <h3 className="heading-3 sub">Подпишитесь на новостную рассылку</h3> */}
|
||||
|
||||
<div className="text-block-14">Оставайтесь в курсе акций, <br />новинок и специальных предложений</div>
|
||||
</div>
|
||||
<div className="form-block-3 w-form">
|
||||
@ -13,6 +19,38 @@ const CatalogSubscribe: React.FC = () => (
|
||||
<input type="submit" className="submit-button-copy w-button" value="Подписаться" />
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex flex-row items-center mt-2">
|
||||
{/* Кастомный чекбокс без input/label */}
|
||||
{(() => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`h-[24px] w-[24px] border border-[#8893A1] rounded-sm mr-4 flex-shrink-0 flex items-center justify-center cursor-pointer transition-colors duration-150 ${checked ? 'bg-[#EC1C24]' : 'bg-transparent'}`}
|
||||
onClick={() => setChecked(v => !v)}
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
tabIndex={0}
|
||||
onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') setChecked(v => !v); }}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 text-white transition-opacity duration-150 ${checked ? 'opacity-100' : 'opacity-0'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[#8893A1] text-[12px] leading-snug select-none">
|
||||
Я даю свое согласие на обработку персональных данных<br />
|
||||
и соглашаюсь с условиями <a href="/privacy-policy" className="underline hover:text-[#6c7684]">Политики конфиденциальности</a>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -157,20 +157,20 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
}, []);
|
||||
|
||||
// Скрытие top_head при скролле
|
||||
useEffect(() => {
|
||||
const topHead = document.querySelector('.top_head');
|
||||
if (!topHead) return;
|
||||
const onScroll = () => {
|
||||
if (window.scrollY > 0) {
|
||||
topHead.classList.add('hide-top-head');
|
||||
} else {
|
||||
topHead.classList.remove('hide-top-head');
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', onScroll);
|
||||
onScroll();
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// const topHead = document.querySelector('.top_head');
|
||||
// if (!topHead) return;
|
||||
// const onScroll = () => {
|
||||
// if (window.scrollY > 0) {
|
||||
// topHead.classList.add('hide-top-head');
|
||||
// } else {
|
||||
// topHead.classList.remove('hide-top-head');
|
||||
// }
|
||||
// };
|
||||
// window.addEventListener('scroll', onScroll);
|
||||
// onScroll();
|
||||
// return () => window.removeEventListener('scroll', onScroll);
|
||||
// }, []);
|
||||
|
||||
// Проверяем, является ли строка VIN номером
|
||||
const isVinNumber = (query: string): boolean => {
|
||||
@ -358,7 +358,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="top_head">
|
||||
{/* <section className="top_head">
|
||||
<div className="w-layout-blockcontainer container nav w-container">
|
||||
<div data-animation="default" data-collapse="medium" data-duration="400" data-easing="ease" data-easing2="ease" role="banner" className="navbar w-nav">
|
||||
<Link href="/" className="brand w-nav-brand"><img src="/images/logo.svg" loading="lazy" alt="" className="image-24" /></Link>
|
||||
@ -381,24 +381,47 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section> */}
|
||||
<section className="bottom_head">
|
||||
<div className="w-layout-blockcontainer container nav w-container">
|
||||
<div className="w-layout-hflex flex-block-93">
|
||||
<div data-animation="default" data-collapse="all" data-duration="400" data-easing="ease-in" data-easing2="ease" role="banner" className="navbar-2 w-nav">
|
||||
<Link href="/" className="code-embed-15 w-embed protekauto-logo" style={{ display: 'block', cursor: 'pointer'}}>
|
||||
<svg width="190" height="72" viewBox="0 0 190 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M138.377 29.5883V23.1172H112.878V29.5883H138.377Z" fill="white"></path>
|
||||
<path d="M107.423 18.1195C109.21 18.1195 110.658 16.6709 110.658 14.884C110.658 13.097 109.21 11.6484 107.423 11.6484L88.395 11.6484C86.6082 11.6484 85.1596 13.097 85.1596 14.884C85.1596 16.6709 86.6082 18.1195 88.395 18.1195H107.423Z" fill="white"></path>
|
||||
<path d="M130.288 34.2491C128.773 35.3865 126.89 36.0628 124.852 36.0628C119.849 36.0628 115.791 32.0052 115.791 27.0013C115.791 21.9974 119.849 17.9399 124.852 17.9399C129.856 17.9399 133.913 21.9974 133.913 27.0013C133.913 27.9022 133.779 28.7696 133.536 29.5893H140.169C140.31 28.7481 140.384 27.8831 140.384 27.0013C140.384 18.4226 133.431 11.4688 124.852 11.4688C116.274 11.4688 109.32 18.4226 109.32 27.0013C109.32 35.5801 116.274 42.5339 124.852 42.5339C129.249 42.5339 133.218 40.7058 136.045 37.769L130.288 34.2491Z" fill="white"></path>
|
||||
<path d="M148.633 11.4531H148.631C146.629 11.4531 145.006 13.0761 145.006 15.0782V38.9075C145.006 40.9096 146.629 42.5326 148.631 42.5326H148.633C150.635 42.5326 152.258 40.9096 152.258 38.9075V15.0782C152.258 13.0761 150.635 11.4531 148.633 11.4531Z" fill="white"></path>
|
||||
<path d="M168.935 36.3511L154.515 21.9297L149.387 27.0578L163.807 41.4792C164.489 42.1603 165.411 42.5402 166.371 42.5402C169.602 42.5402 171.22 38.6356 168.935 36.3511Z" fill="white"></path>
|
||||
<path d="M168.937 17.7751L154.733 31.979L149.605 26.8509L163.809 12.6469C164.49 11.9659 165.412 11.5859 166.373 11.5859C169.603 11.5859 171.221 15.4906 168.937 17.7751Z" fill="white"></path>
|
||||
<path d="M186.029 36.3511L171.608 21.9297L166.48 27.0578L180.901 41.4792C181.582 42.1603 182.505 42.5402 183.465 42.5402C186.696 42.5402 188.314 38.6356 186.029 36.3511Z" fill="#EC1C24"></path>
|
||||
<path d="M186.029 17.7751L171.587 32.218L166.459 27.0898L180.901 12.6469C181.582 11.9659 182.505 11.5859 183.465 11.5859C186.696 11.5859 188.314 15.4906 186.029 17.7751Z" fill="#EC1C24"></path>
|
||||
<path d="M3.6249 50.4184C1.62248 50.4184 0 48.7958 0 46.7933V11.4531L7.2522 14.3207V46.7933C7.2522 48.7958 5.62971 50.4184 3.62729 50.4184H3.6249Z" fill="white"></path>
|
||||
<path d="M97.9491 42.5353C95.9467 42.5353 94.3242 40.9128 94.3242 38.9103V0L101.576 2.86755V38.9103C101.576 40.9128 99.9539 42.5353 97.9515 42.5353H97.9491Z" fill="white"></path>
|
||||
<path d="M38.578 42.5326C36.5756 42.5326 34.9531 40.91 34.9531 38.9075V11.4531L42.2053 14.3207V38.9075C42.2053 40.91 40.5828 42.5326 38.5804 42.5326H38.578Z" fill="white"></path>
|
||||
<path d="M51.334 11.4555C42.7508 11.4555 35.7949 18.4141 35.7949 26.9953H42.2705C42.2705 21.989 46.3279 17.929 51.3364 17.929C52.0102 17.929 52.6649 18.0055 53.2958 18.1441C54.2301 16.0723 55.4798 14.1749 56.9876 12.5141C55.2361 11.8307 53.3316 11.4531 51.3364 11.4531L51.334 11.4555Z" fill="white"></path>
|
||||
<path d="M70.4707 11.4531C61.8875 11.4531 54.9316 18.4117 54.9316 26.9929C54.9316 35.574 61.8899 42.5326 70.4707 42.5326C79.0515 42.5326 86.0098 35.574 86.0098 26.9929C86.0098 18.4117 79.0515 11.4531 70.4707 11.4531ZM70.4707 36.0591C65.4647 36.0591 61.4049 32.0015 61.4049 26.9929C61.4049 21.9842 65.4623 17.9266 70.4707 17.9266C75.4791 17.9266 79.5365 21.9842 79.5365 26.9929C79.5365 32.0015 75.4791 36.0591 70.4707 36.0591Z" fill="white"></path>
|
||||
<path d="M16.2309 11.4531C7.64774 11.4531 0.689453 18.4093 0.689453 26.9929C0.689453 35.5764 7.64774 42.5326 16.2285 42.5326C24.8093 42.5326 31.7676 35.574 31.7676 26.9929C31.7676 18.4117 24.8117 11.4531 16.2309 11.4531ZM16.2309 36.0591C11.2249 36.0591 7.16506 32.0015 7.16506 26.9929C7.16506 21.9842 11.2225 17.9266 16.2309 17.9266C21.2393 17.9266 25.2967 21.9842 25.2967 26.9929C25.2967 32.0015 21.2393 36.0591 16.2309 36.0591Z" fill="white"></path>
|
||||
<rect width="53.354" height="21.8647" rx="8" transform="matrix(0.991808 -0.127739 0.127728 0.991809 134.291 50.3047)" fill="#EC1C24"></rect>
|
||||
<path d="M141.15 66.1413L144.154 54.4607L146.879 54.1098L152.697 64.6542L149.925 65.0112L149.085 63.3647L144.317 63.9787L143.906 65.7864L141.15 66.1413ZM144.828 61.5681L147.98 61.1621L145.874 57.0626L144.828 61.5681Z" fill="white"></path>
|
||||
<path d="M153.767 64.5163L152.337 53.4068L157.579 52.7316C158.076 52.6677 158.536 52.6615 158.962 52.7131C159.396 52.7528 159.781 52.868 160.117 53.0587C160.462 53.2376 160.749 53.5038 160.977 53.8573C161.203 54.2003 161.353 54.6542 161.426 55.2191C161.481 55.648 161.433 56.0689 161.283 56.4818C161.132 56.8947 160.885 57.2296 160.543 57.4864C161.063 57.6108 161.499 57.8736 161.852 58.2749C162.213 58.6643 162.438 59.2043 162.527 59.8947C162.615 60.5746 162.561 61.1559 162.365 61.6383C162.179 62.109 161.886 62.5029 161.487 62.8202C161.097 63.1257 160.634 63.366 160.099 63.5414C159.563 63.7167 158.994 63.8431 158.392 63.9206L153.767 64.5163ZM156.032 61.8478L158.345 61.55C158.609 61.516 158.844 61.4645 159.049 61.3954C159.264 61.3146 159.44 61.2228 159.577 61.12C159.724 61.0055 159.824 60.8702 159.879 60.7143C159.944 60.5465 159.963 60.3632 159.937 60.1644C159.91 59.9552 159.851 59.7874 159.76 59.6609C159.679 59.5331 159.565 59.4361 159.416 59.3701C159.267 59.2937 159.095 59.252 158.901 59.2451C158.706 59.2277 158.487 59.2347 158.244 59.266L155.741 59.5883L156.032 61.8478ZM155.472 57.5013L157.516 57.2382C157.769 57.2055 157.987 57.1508 158.171 57.074C158.365 56.9959 158.519 56.9016 158.634 56.7911C158.748 56.6806 158.828 56.5533 158.874 56.4092C158.931 56.2637 158.947 56.102 158.924 55.9242C158.893 55.6836 158.811 55.5027 158.677 55.3817C158.553 55.2489 158.387 55.1692 158.18 55.1427C157.971 55.1058 157.724 55.1057 157.439 55.1424L155.206 55.43L155.472 57.5013Z" fill="white"></path>
|
||||
<path d="M166.971 62.8158L165.843 54.06L162.469 54.4945L162.166 52.1408L171.511 50.9373L171.814 53.291L168.409 53.7296L169.536 62.4854L166.971 62.8158Z" fill="white"></path>
|
||||
<path d="M178.85 61.4134C177.699 61.5617 176.671 61.4548 175.766 61.0929C174.87 60.7191 174.14 60.1378 173.577 59.3489C173.012 58.5496 172.658 57.5903 172.514 56.471C172.366 55.3203 172.469 54.2913 172.825 53.3842C173.189 52.4652 173.764 51.7159 174.548 51.1364C175.331 50.5464 176.297 50.1773 177.448 50.029C178.578 49.8835 179.591 49.9924 180.485 50.3557C181.391 50.7176 182.13 51.2924 182.704 52.0799C183.287 52.8555 183.652 53.8135 183.799 54.9537C183.943 56.073 183.839 57.0968 183.486 58.0248C183.132 58.9425 182.559 59.7022 181.767 60.304C180.984 60.894 180.012 61.2638 178.85 61.4134ZM178.588 59.0065C179.306 58.914 179.866 58.6771 180.268 58.2957C180.67 57.9143 180.939 57.4596 181.075 56.9316C181.211 56.4037 181.245 55.8782 181.178 55.3551C181.128 54.9681 181.02 54.5885 180.854 54.2164C180.698 53.843 180.478 53.5097 180.194 53.2167C179.92 52.9223 179.58 52.7003 179.174 52.5505C178.768 52.4007 178.286 52.3618 177.726 52.4339C177.018 52.525 176.464 52.7613 176.062 53.1427C175.659 53.5136 175.384 53.9637 175.238 54.4931C175.102 55.021 175.07 55.5622 175.141 56.1167C175.212 56.6711 175.386 57.1858 175.662 57.6607C175.938 58.1356 176.318 58.5014 176.801 58.7581C177.296 59.0135 177.892 59.0963 178.588 59.0065Z" fill="white"></path>
|
||||
</svg>
|
||||
</Link>
|
||||
<div data-animation="default" data-collapse="all" data-duration="400" data-easing="ease-in" data-easing2="ease" role="banner" className="topnav w-nav">
|
||||
<div
|
||||
className={`menu-button w-nav-button${menuOpen ? " w--open" : ""}`}
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className="code-embed-5 w-embed"><svg width="30" height="18" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div className="code-embed-5 w-embed"><svg width="currentwidth" height="currenthieght" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0H30V3H0V0Z" fill="currentColor"></path>
|
||||
<path d="M0 7.5H30V10.5H0V7.5Z" fill="currentColor"></path>
|
||||
<path d="M0 15H30V18H0V15Z" fill="currentColor"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-block w-form" style={{ position: 'relative' }}>
|
||||
<div className="searcj w-form" style={{ position: 'relative' }}>
|
||||
<form
|
||||
id="custom-search-form"
|
||||
name="custom-search-form"
|
||||
@ -736,7 +759,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
<div className="w-layout-hflex flex-block-76">
|
||||
<Link href="/profile-history" className="button_h w-inline-block">
|
||||
|
||||
<img src="/images/union.svg" alt="История заказов" width={22} height={10} />
|
||||
<img src="/images/union.svg" alt="История заказов" width={20} />
|
||||
|
||||
</Link>
|
||||
<Link href="/profile-gar" className="button_h w-inline-block">
|
||||
|
@ -32,7 +32,11 @@ const InfoSearch: React.FC<InfoSearchProps> = ({
|
||||
<div className="w-layout-hflex flex-block-10">
|
||||
<h1 className="heading">{name}</h1>
|
||||
<div className="text-block-4">
|
||||
Найдено {offersCount} предложений от {minPrice}
|
||||
{offersCount > 0 ? (
|
||||
<>Найдено {offersCount} предложений от {minPrice}</>
|
||||
) : (
|
||||
<>Ничего не найдено</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="w-layout-hflex flex-block-11">
|
||||
|
@ -64,7 +64,7 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
<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'
|
||||
}`}
|
||||
} hover:bg-slate-200`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
@ -93,7 +93,7 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
<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'
|
||||
}`}
|
||||
} hover:bg-slate-200`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
||||
import Header from "./Header";
|
||||
import AuthModal from "./auth/AuthModal";
|
||||
import MobileMenuBottomSection from "./MobileMenuBottomSection";
|
||||
import IndexTopMenuNav from "./index/IndexTopMenuNav";
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||
@ -30,7 +31,10 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
onSuccess={handleAuthSuccess}
|
||||
/>
|
||||
</header>
|
||||
<main className="pt-[108px] md:pt-[131px]">{children}</main>
|
||||
|
||||
<main className="pt-[62px] md:pt-[63px]">
|
||||
<IndexTopMenuNav isIndexPage={router.pathname === '/'} />
|
||||
{children}</main>
|
||||
<MobileMenuBottomSection onOpenAuthModal={() => setAuthModalOpen(true)} />
|
||||
</>
|
||||
);
|
||||
|
148
src/components/Pagination.tsx
Normal file
148
src/components/Pagination.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
className?: string;
|
||||
showPageInfo?: boolean;
|
||||
}
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
className = "",
|
||||
showPageInfo = true
|
||||
}) => {
|
||||
const generatePageNumbers = () => {
|
||||
const pages: (number | string)[] = [];
|
||||
const delta = 2; // Количество страниц вокруг текущей
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Если страниц мало, показываем все
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Всегда показываем первую страницу
|
||||
pages.push(1);
|
||||
|
||||
if (currentPage > delta + 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Показываем страницы вокруг текущей
|
||||
const start = Math.max(2, currentPage - delta);
|
||||
const end = Math.min(totalPages - 1, currentPage + delta);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - delta - 1) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Всегда показываем последнюю страницу
|
||||
if (totalPages > 1) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const pageNumbers = generatePageNumbers();
|
||||
|
||||
if (totalPages <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col items-center space-y-3 ${className}`}>
|
||||
{/* Основные кнопки пагинации */}
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
{/* Предыдущая страница */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Номера страниц */}
|
||||
{pageNumbers.map((page, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{page === '...' ? (
|
||||
<span className="flex items-center justify-center w-10 h-10 text-gray-400">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<circle cx="3" cy="10" r="1.5" />
|
||||
<circle cx="10" cy="10" r="1.5" />
|
||||
<circle cx="17" cy="10" r="1.5" />
|
||||
</svg>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onPageChange(page as number)}
|
||||
className={`flex items-center justify-center w-10 h-10 text-sm font-medium border rounded-lg transition-colors ${
|
||||
currentPage === page
|
||||
? 'text-white bg-[#ec1c24] border-[#ec1c24] hover:bg-[#d91920]'
|
||||
: 'text-gray-500 bg-white border-gray-200 hover:bg-gray-50 hover:text-gray-700'
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{/* Следующая страница */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style={{ cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Информация о страницах */}
|
||||
{showPageInfo && (
|
||||
<div className="text-sm text-gray-500">
|
||||
Страница {currentPage} из {totalPages}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
156
src/components/TopSalesItem.tsx
Normal file
156
src/components/TopSalesItem.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface TopSalesItemProps {
|
||||
image: string;
|
||||
price: string;
|
||||
title: string;
|
||||
brand: string;
|
||||
article?: string;
|
||||
productId?: string;
|
||||
onAddToCart?: (e: React.MouseEvent) => void;
|
||||
discount?: string; // Новый пропс для лейбла/скидки
|
||||
}
|
||||
|
||||
const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
||||
image,
|
||||
price,
|
||||
title,
|
||||
brand,
|
||||
article,
|
||||
productId,
|
||||
onAddToCart,
|
||||
discount = 'Топ продаж', // По умолчанию как раньше
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
const isItemFavorite = isFavorite(productId, undefined, article, brand);
|
||||
|
||||
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 (onAddToCart) {
|
||||
onAddToCart(e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!article || !brand) {
|
||||
toast.error('Недостаточно данных для добавления товара в корзину');
|
||||
return;
|
||||
}
|
||||
const numericPrice = parsePrice(price);
|
||||
addItem({
|
||||
name: title,
|
||||
brand: brand,
|
||||
article: article,
|
||||
description: title,
|
||||
price: numericPrice,
|
||||
quantity: 1,
|
||||
currency: 'RUB',
|
||||
image: image,
|
||||
isExternal: true
|
||||
});
|
||||
toast.success('Товар добавлен в корзину');
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка добавления товара в корзину');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isItemFavorite) {
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (fav.article === article && fav.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
const numericPrice = parsePrice(price);
|
||||
addToFavorites({
|
||||
productId,
|
||||
name: title,
|
||||
brand: brand,
|
||||
article: article || '',
|
||||
price: numericPrice,
|
||||
currency: 'RUB',
|
||||
image: image
|
||||
});
|
||||
toast.success('Товар добавлен в избранное');
|
||||
}
|
||||
};
|
||||
|
||||
// Ссылка на карточку товара (если нужно)
|
||||
const cardUrl = article && brand
|
||||
? `/card?article=${encodeURIComponent(article)}&brand=${encodeURIComponent(brand)}`
|
||||
: '/card';
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-15-copy">
|
||||
<div
|
||||
className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
|
||||
onClick={handleFavoriteClick}
|
||||
style={{ cursor: 'pointer', color: isItemFavorite ? '#ff4444' : '#ccc' }}
|
||||
>
|
||||
<div className="icon-setting w-embed">
|
||||
<svg width="currentwidth" height="currentheight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="div-block-4">
|
||||
<img
|
||||
src={image}
|
||||
loading="lazy"
|
||||
width="Auto"
|
||||
height="Auto"
|
||||
alt={title}
|
||||
className="image-5"
|
||||
/>
|
||||
<div className="text-block-7">{discount}</div>
|
||||
</div>
|
||||
<div className="div-block-3">
|
||||
<div className="w-layout-hflex flex-block-16">
|
||||
<div className="text-block-8">{price}</div>
|
||||
{/* <div className="text-block-9">oldPrice</div> */}
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-122">
|
||||
<div className="w-layout-vflex">
|
||||
<div className="text-block-10">{title}</div>
|
||||
<div className="text-block-11">{brand}</div>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
className="button-icon w-inline-block"
|
||||
onClick={handleAddToCart}
|
||||
style={{ cursor: 'pointer' }}
|
||||
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"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopSalesItem;
|
@ -28,6 +28,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
const [imageLoadTimeout, setImageLoadTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||
const [selectedDetail, setSelectedDetail] = useState<LaximoUnitDetail | null>(null);
|
||||
const [highlightedDetailId, setHighlightedDetailId] = useState<string | null>(null);
|
||||
|
||||
// Отладочная информация для SSD
|
||||
console.log('🔍 UnitDetailsSection получил SSD:', {
|
||||
@ -165,11 +166,31 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
d.detailid === coord.codeonimage
|
||||
);
|
||||
|
||||
if (detail) {
|
||||
console.log('✅ Найдена деталь для выделения:', detail.name, 'ID:', detail.detailid);
|
||||
// Выделяем деталь в списке
|
||||
setHighlightedDetailId(detail.detailid);
|
||||
} else {
|
||||
console.log('⚠️ Деталь не найдена в списке по коду:', coord.codeonimage);
|
||||
setHighlightedDetailId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCoordinateDoubleClick = (coord: LaximoImageCoordinate) => {
|
||||
console.log('🖱️ Двойной клик по интерактивной области:', coord.codeonimage);
|
||||
|
||||
// Сначала пытаемся найти деталь в списке
|
||||
const detail = unitDetails.find(d =>
|
||||
d.detailid === coord.detailid ||
|
||||
d.codeonimage === coord.codeonimage ||
|
||||
d.detailid === coord.codeonimage
|
||||
);
|
||||
|
||||
if (detail && detail.oem) {
|
||||
console.log('✅ Найдена деталь для выбора бренда:', detail.name, 'OEM:', detail.oem);
|
||||
// Показываем модал выбора бренда
|
||||
setSelectedDetail(detail);
|
||||
setIsBrandModalOpen(true);
|
||||
// Переходим на страницу выбора бренда
|
||||
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${detail.oem}/brands?detailName=${encodeURIComponent(detail.name || '')}`;
|
||||
router.push(url);
|
||||
} else {
|
||||
// Если деталь не найдена в списке, переходим к общему поиску по коду на изображении
|
||||
console.log('⚠️ Деталь не найдена в списке, переходим к поиску по коду:', coord.codeonimage);
|
||||
@ -461,7 +482,8 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
borderRadius: coord.shape === 'circle' ? '50%' : '0'
|
||||
}}
|
||||
onClick={() => handleCoordinateClick(coord)}
|
||||
title={detail ? `${coord.codeonimage}: ${detail.name}` : `Деталь ${coord.codeonimage}`}
|
||||
onDoubleClick={() => handleCoordinateDoubleClick(coord)}
|
||||
title={detail ? `${coord.codeonimage}: ${detail.name} (Клик - выделить, двойной клик - перейти к выбору бренда)` : `Деталь ${coord.codeonimage} (Клик - выделить, двойной клик - поиск)`}
|
||||
>
|
||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded font-bold">
|
||||
{coord.codeonimage}
|
||||
@ -612,7 +634,11 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
{unitDetails.map((detail, index) => (
|
||||
<div
|
||||
key={`detail-${unitId}-${index}-${detail.detailid}`}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer"
|
||||
className={`border rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer ${
|
||||
highlightedDetailId === detail.detailid
|
||||
? 'border-red-500 bg-red-50 shadow-md'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
onClick={() => handleDetailClick(detail)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
|
@ -1,44 +1,139 @@
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import BestPriceItem from "../BestPriceItem";
|
||||
import { GET_BEST_PRICE_PRODUCTS } from "../../lib/graphql";
|
||||
|
||||
// Моковые данные для лучших цен
|
||||
const bestPriceItems = [
|
||||
{
|
||||
image: "images/162615.webp",
|
||||
discount: "-35%",
|
||||
price: "от 17 087 ₽",
|
||||
oldPrice: "22 347 ₽",
|
||||
title: 'Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60',
|
||||
brand: "TYUMEN BATTERY",
|
||||
},
|
||||
// ...добавьте еще 7 карточек для примера
|
||||
...Array(7).fill(0).map((_, i) => ({
|
||||
image: "images/162615.webp",
|
||||
discount: "-35%",
|
||||
price: `от ${(17087 + i * 1000).toLocaleString('ru-RU')} ₽`,
|
||||
oldPrice: `${(22347 + i * 1000).toLocaleString('ru-RU')} ₽`,
|
||||
title: `Товар №${i + 2}`,
|
||||
brand: `Бренд ${i + 2}`,
|
||||
}))
|
||||
];
|
||||
interface BestPriceProductData {
|
||||
id: string;
|
||||
productId: string;
|
||||
discount: number;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article?: string;
|
||||
brand?: string;
|
||||
retailPrice?: number;
|
||||
images: { url: string; alt?: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
const BestPriceSection: React.FC = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-118">
|
||||
<div className="w-layout-vflex flex-block-119">
|
||||
<h1 className="heading-20">ЛУЧШАЯ ЦЕНА!</h1>
|
||||
<div className="text-block-58">Подборка лучших предложенийпо цене</div>
|
||||
<a href="#" className="button-24 w-button">Показать все</a>
|
||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||
|
||||
const BestPriceSection: React.FC = () => {
|
||||
const { data, loading, error } = useQuery(GET_BEST_PRICE_PRODUCTS);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const scrollRight = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-118">
|
||||
<div className="w-layout-vflex flex-block-119">
|
||||
<h1 className="heading-20">ЛУЧШАЯ ЦЕНА!</h1>
|
||||
<div className="text-block-58">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-121">
|
||||
{bestPriceItems.map((item, i) => (
|
||||
<BestPriceItem key={i} {...item} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Ошибка загрузки товаров с лучшей ценой:', error);
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-118">
|
||||
<div className="w-layout-vflex flex-block-119">
|
||||
<h1 className="heading-20">ЛУЧШАЯ ЦЕНА!</h1>
|
||||
<div className="text-block-58">Ошибка загрузки данных</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const bestPriceProducts: BestPriceProductData[] = data?.bestPriceProducts || [];
|
||||
|
||||
// Функция для форматирования цены
|
||||
const formatPrice = (price?: number) => {
|
||||
if (!price) return '—';
|
||||
return `от ${price.toLocaleString('ru-RU')} ₽`;
|
||||
};
|
||||
|
||||
// Функция для расчета цены со скидкой
|
||||
const calculateDiscountedPrice = (price?: number, discount?: number) => {
|
||||
if (!price || !discount) return price;
|
||||
return price * (1 - discount / 100);
|
||||
};
|
||||
|
||||
// Преобразование данных для компонента BestPriceItem
|
||||
const bestPriceItems = bestPriceProducts
|
||||
.filter(item => item.isActive)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.slice(0, 8) // Ограничиваем до 8 товаров
|
||||
.map(item => ({
|
||||
image: item.product.images?.[0]?.url || "images/162615.webp", // Fallback изображение
|
||||
discount: `-${item.discount}%`,
|
||||
price: formatPrice(calculateDiscountedPrice(item.product.retailPrice, item.discount)),
|
||||
oldPrice: formatPrice(item.product.retailPrice),
|
||||
title: item.product.name,
|
||||
brand: item.product.brand || "",
|
||||
article: item.product.article,
|
||||
productId: item.product.id,
|
||||
}));
|
||||
|
||||
// Если нет товаров, не показываем секцию
|
||||
if (bestPriceItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-118">
|
||||
<div className="w-layout-vflex flex-block-119">
|
||||
<h1 className="heading-20">ЛУЧШАЯ ЦЕНА!</h1>
|
||||
<div className="text-block-58">Подборка лучших предложенийпо цене</div>
|
||||
<a href="#" className="button-24 w-button">Показать все</a>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex flex-block-121 carousel-scroll" ref={scrollRef}>
|
||||
{bestPriceItems.map((item, i) => (
|
||||
<BestPriceItem key={i} {...item} />
|
||||
))}
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BestPriceSection;
|
@ -1,8 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useMemo, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_LAXIMO_BRANDS } from "@/lib/graphql";
|
||||
import { LaximoBrand } from "@/types/laximo";
|
||||
import { Combobox } from '@headlessui/react';
|
||||
|
||||
const tabs = [
|
||||
"Техническое обслуживание",
|
||||
@ -15,7 +16,8 @@ type Brand = { name: string; code?: string };
|
||||
|
||||
const BrandSelectionSection: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [selectedBrand, setSelectedBrand] = useState<string>("");
|
||||
const [selectedBrand, setSelectedBrand] = useState<Brand | null>(null);
|
||||
const [brandQuery, setBrandQuery] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const { data, loading, error } = useQuery<{ laximoBrands: LaximoBrand[] }>(GET_LAXIMO_BRANDS, {
|
||||
@ -42,6 +44,12 @@ const BrandSelectionSection: React.FC = () => {
|
||||
console.warn('Laximo API недоступен, используются статические данные:', error.message);
|
||||
}
|
||||
|
||||
// Combobox фильтрация
|
||||
const filteredBrands = useMemo(() => {
|
||||
if (!brandQuery) return brands;
|
||||
return brands.filter(b => b.name.toLowerCase().includes(brandQuery.toLowerCase()));
|
||||
}, [brands, brandQuery]);
|
||||
|
||||
const handleBrandClick = (brand: Brand) => {
|
||||
if (brand.code) {
|
||||
router.push(`/brands?selected=${brand.code}`);
|
||||
@ -53,7 +61,7 @@ const BrandSelectionSection: React.FC = () => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (selectedBrand) {
|
||||
const found = brands.find(b => b.code === selectedBrand || b.name === selectedBrand);
|
||||
const found = brands.find(b => b.code === selectedBrand.code || b.name === selectedBrand.name);
|
||||
if (found && found.code) {
|
||||
router.push(`/brands?selected=${found.code}`);
|
||||
return;
|
||||
@ -123,19 +131,44 @@ const BrandSelectionSection: React.FC = () => {
|
||||
<h1 className="heading-21">ПОДБОР АВТОЗАПЧАСТЕЙ ПО МАРКЕ АВТО</h1>
|
||||
<div className="form-block-4 w-form">
|
||||
<form id="email-form" name="email-form" data-name="Email Form" method="post" data-wf-page-id="685be6dfd87db2e01cbdb7a2" data-wf-element-id="e673036c-0caf-d251-3b66-9ba9cb85064c" onSubmit={handleSubmit}>
|
||||
<select
|
||||
id="field-7"
|
||||
name="field-7"
|
||||
data-name="Field 7"
|
||||
className="select-copy w-select"
|
||||
value={selectedBrand}
|
||||
onChange={e => setSelectedBrand(e.target.value)}
|
||||
>
|
||||
<option value="">Марка</option>
|
||||
{brands.map((brand, idx) => (
|
||||
<option value={brand.code || brand.name} key={idx}>{brand.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<div style={{ width: 180, marginBottom: 16 }}>
|
||||
<Combobox value={selectedBrand} onChange={setSelectedBrand} nullable>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className="w-full px-6 py-4 bg-white rounded border border-stone-300 text-sm text-gray-950 placeholder:text-neutral-500 outline-none focus:shadow-none focus:border-stone-300 transition-colors"
|
||||
displayValue={(brand: Brand | null) => brand?.name || ''}
|
||||
onChange={e => setBrandQuery(e.target.value)}
|
||||
placeholder="Марка"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center px-3 focus:outline-none w-12">
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 9l6 6 6-6" />
|
||||
</svg>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options
|
||||
className="absolute left-0 top-full z-100 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-auto scrollbar-none"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
data-hide-scrollbar
|
||||
>
|
||||
{filteredBrands.length === 0 && (
|
||||
<div className="px-6 py-4 text-gray-500">Бренды не найдены</div>
|
||||
)}
|
||||
{filteredBrands.map(brand => (
|
||||
<Combobox.Option
|
||||
key={brand.code || brand.name}
|
||||
value={brand}
|
||||
className={({ active, selected }) =>
|
||||
`px-6 py-4 cursor-pointer hover:!bg-[rgb(236,28,36)] hover:!text-white text-sm transition-colors ${selected ? 'bg-red-50 font-semibold text-gray-950' : 'text-neutral-500'}`
|
||||
}
|
||||
>
|
||||
{brand.name}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
<div className="div-block-10-copy">
|
||||
<input type="submit" data-wait="Please wait..." className="button-3-copy w-button" value="Далее" />
|
||||
</div>
|
||||
|
@ -1,27 +1,28 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const IndexTopMenuNav = () => (
|
||||
<section className="topmenub">
|
||||
const IndexTopMenuNav = ({ isIndexPage = false }: { isIndexPage?: boolean }) => (
|
||||
<section className={`topmenub${!isIndexPage ? ' topmenub-white' : ''}`} style={!isIndexPage ? { background: '#fff' } : undefined}>
|
||||
<div className="w-layout-blockcontainer tb nav w-container">
|
||||
<div className="w-layout-hflex flex-block-107">
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
<Link href="/about" className="link-block-8 w-inline-block">
|
||||
<div>О компании</div>
|
||||
</a>
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
</Link>
|
||||
<Link href="/payments-method" className="link-block-8 w-inline-block">
|
||||
<div>Оплата и доставка</div>
|
||||
</a>
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
</Link>
|
||||
<Link href="/" className="link-block-8 w-inline-block">
|
||||
<div>Гарантия и возврат</div>
|
||||
</a>
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
</Link>
|
||||
<Link href="/payments-method" className="link-block-8 w-inline-block">
|
||||
<div>Покупателям</div>
|
||||
</a>
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
</Link>
|
||||
<Link href="/wholesale" className="link-block-8 w-inline-block">
|
||||
<div>Оптовым клиентам</div>
|
||||
</a>
|
||||
<a href="#" className="link-block-8 w-inline-block">
|
||||
</Link>
|
||||
<Link href="/contacts" className="link-block-8 w-inline-block">
|
||||
<div>Контакты</div>
|
||||
</a>
|
||||
</Link>
|
||||
<a href="#" className="link-block-8 green w-inline-block">
|
||||
<div>Новые поступления товаров</div>
|
||||
</a>
|
||||
|
@ -1,56 +1,164 @@
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import ArticleCard from "../ArticleCard";
|
||||
import CatalogProductCardSkeleton from "../CatalogProductCardSkeleton";
|
||||
import { GET_NEW_ARRIVALS } from "@/lib/graphql";
|
||||
import { PartsAPIArticle } from "@/types/partsapi";
|
||||
|
||||
// Моковые данные для новых поступлений
|
||||
const newArrivalsArticles: PartsAPIArticle[] = [
|
||||
{
|
||||
artId: "1",
|
||||
artArticleNr: "6CT-60L",
|
||||
artSupBrand: "TYUMEN BATTERY",
|
||||
supBrand: "TYUMEN BATTERY",
|
||||
supId: 1,
|
||||
productGroup: "Аккумуляторная батарея",
|
||||
ptId: 1,
|
||||
},
|
||||
{
|
||||
artId: "2",
|
||||
artArticleNr: "A0001",
|
||||
artSupBrand: "Borsehung",
|
||||
supBrand: "Borsehung",
|
||||
supId: 2,
|
||||
productGroup: "Масляный фильтр",
|
||||
ptId: 2,
|
||||
},
|
||||
// ...добавьте еще 6 статей для примера
|
||||
...Array(6).fill(0).map((_, i) => ({
|
||||
artId: `${i+3}`,
|
||||
artArticleNr: `ART${i+3}`,
|
||||
artSupBrand: `Brand${i+3}`,
|
||||
supBrand: `Brand${i+3}`,
|
||||
supId: i+3,
|
||||
productGroup: `Product Group ${i+3}`,
|
||||
ptId: i+3,
|
||||
}))
|
||||
];
|
||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||
|
||||
const imagePath = "images/162615.webp";
|
||||
// Интерфейс для товара из GraphQL
|
||||
interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
article?: string;
|
||||
brand?: string;
|
||||
retailPrice?: number;
|
||||
wholesalePrice?: number;
|
||||
createdAt: string;
|
||||
images: Array<{
|
||||
id: string;
|
||||
url: string;
|
||||
alt?: string;
|
||||
order: number;
|
||||
}>;
|
||||
categories: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const NewArrivalsSection: React.FC = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Новое поступление</h2>
|
||||
</div>
|
||||
<div className="w-layout-hflex core-product-search">
|
||||
{newArrivalsArticles.map((article, i) => (
|
||||
<ArticleCard key={article.artId || i} article={{ ...article, artId: article.artId }} index={i} image={imagePath} />
|
||||
))}
|
||||
// Функция для преобразования Product в PartsAPIArticle
|
||||
const transformProductToArticle = (product: Product, index: number): PartsAPIArticle => {
|
||||
return {
|
||||
artId: product.id,
|
||||
artArticleNr: product.article || `PROD-${product.id}`,
|
||||
artSupBrand: product.brand || 'Unknown Brand',
|
||||
supBrand: product.brand || 'Unknown Brand',
|
||||
supId: index + 1,
|
||||
productGroup: product.categories?.[0]?.name || product.name,
|
||||
ptId: index + 1,
|
||||
};
|
||||
};
|
||||
|
||||
const NewArrivalsSection: React.FC = () => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Получаем новые поступления через GraphQL
|
||||
const { data, loading, error } = useQuery(GET_NEW_ARRIVALS, {
|
||||
variables: { limit: 8 }
|
||||
});
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const scrollRight = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
// Преобразуем данные для ArticleCard
|
||||
const newArrivalsArticles = data?.newArrivals?.map((product: Product, index: number) =>
|
||||
transformProductToArticle(product, index)
|
||||
) || [];
|
||||
|
||||
// Получаем изображения для товаров
|
||||
const getProductImage = (product: Product): string => {
|
||||
if (product.images && product.images.length > 0) {
|
||||
return product.images[0].url;
|
||||
}
|
||||
return "/images/162615.webp"; // fallback изображение
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Новое поступление</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button
|
||||
className="carousel-arrow carousel-arrow-left"
|
||||
onClick={scrollLeft}
|
||||
aria-label="Прокрутить влево"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
{loading ? (
|
||||
// Показываем скелетоны во время загрузки
|
||||
Array(8).fill(0).map((_, index) => (
|
||||
<CatalogProductCardSkeleton key={`skeleton-${index}`} />
|
||||
))
|
||||
) : error ? (
|
||||
// Показываем сообщение об ошибке
|
||||
<div className="error-message" style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
minWidth: '300px'
|
||||
}}>
|
||||
<p>Не удалось загрузить новые поступления</p>
|
||||
<p style={{ fontSize: '14px', marginTop: '8px' }}>
|
||||
{error.message}
|
||||
</p>
|
||||
</div>
|
||||
) : newArrivalsArticles.length > 0 ? (
|
||||
// Показываем товары
|
||||
newArrivalsArticles.map((article: PartsAPIArticle, index: number) => {
|
||||
const product = data.newArrivals[index];
|
||||
const image = getProductImage(product);
|
||||
|
||||
return (
|
||||
<ArticleCard
|
||||
key={article.artId || `article-${index}`}
|
||||
article={article}
|
||||
index={index}
|
||||
image={image}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Показываем сообщение о том, что товаров нет
|
||||
<div className="no-products-message" style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
minWidth: '300px'
|
||||
}}>
|
||||
<p>Пока нет новых поступлений</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="carousel-arrow carousel-arrow-right"
|
||||
onClick={scrollRight}
|
||||
aria-label="Прокрутить вправо"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewArrivalsSection;
|
@ -1,53 +1,84 @@
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import NewsCard from "@/components/news/NewsCard";
|
||||
import Link from "next/link";
|
||||
|
||||
const NewsAndPromos = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex news-index-block">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Новости и акции</h2>
|
||||
<div className="w-layout-hflex flex-block-29">
|
||||
<Link href="/news" className="text-block-18" style={{display: 'flex', alignItems: 'center'}}>
|
||||
Ко всем новостям
|
||||
<img src="/images/Arrow_right.svg" loading="lazy" alt="" style={{marginLeft: 8}} />
|
||||
</Link>
|
||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||
|
||||
const NewsAndPromos = () => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const scrollRight = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex news-index-block">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Новости и акции</h2>
|
||||
<div className="w-layout-hflex flex-block-29">
|
||||
<Link href="/news" className="text-block-18" style={{display: 'flex', alignItems: 'center'}}>
|
||||
Ко всем новостям
|
||||
<img src="/images/Arrow_right.svg" loading="lazy" alt="" style={{marginLeft: 8}} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex flex-block-6-copy-copy carousel-scroll" ref={scrollRef}>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-6-copy-copy">
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
<NewsCard
|
||||
title="Kia Syros будет выделяться необычным стилем"
|
||||
description="Компания Kia готова представить новый кроссовер Syros"
|
||||
category="Новости компании"
|
||||
date="17.12.2024"
|
||||
image="/images/news_img.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsAndPromos;
|
@ -1,67 +1,350 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_DAILY_PRODUCTS, PARTS_INDEX_SEARCH_BY_ARTICLE } from '@/lib/graphql';
|
||||
import Link from 'next/link';
|
||||
|
||||
const ProductOfDaySection: React.FC = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer batd w-container">
|
||||
<div className="w-layout-hflex flex-block-108">
|
||||
<div data-delay="4000" data-animation="slide" className="slider w-slider" data-autoplay="false" data-easing="ease" data-hide-arrows="false" data-disable-swipe="false" data-autoplay-limit="0" data-nav-spacing="3" data-duration="500" data-infinite="true">
|
||||
<div className="mask w-slider-mask">
|
||||
<div className="slide w-slide">
|
||||
<div className="div-block-128"></div>
|
||||
interface DailyProduct {
|
||||
id: string;
|
||||
discount?: number;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
article?: string;
|
||||
brand?: string;
|
||||
retailPrice?: number;
|
||||
wholesalePrice?: number;
|
||||
images: Array<{
|
||||
id: string;
|
||||
url: string;
|
||||
alt?: string;
|
||||
order: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
const ProductOfDaySection: React.FC = () => {
|
||||
// Получаем текущую дату в формате YYYY-MM-DD
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Состояние для текущего слайда
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, loading, error } = useQuery<{ dailyProducts: DailyProduct[] }>(
|
||||
GET_DAILY_PRODUCTS,
|
||||
{
|
||||
variables: { displayDate: today },
|
||||
errorPolicy: 'all'
|
||||
}
|
||||
);
|
||||
|
||||
// Фильтруем только активные товары и сортируем по sortOrder
|
||||
const activeProducts = React.useMemo(() => {
|
||||
if (!data?.dailyProducts) return [];
|
||||
return data.dailyProducts
|
||||
.filter(item => item.isActive)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
}, [data]);
|
||||
|
||||
// Получаем данные из PartsIndex для текущего товара
|
||||
const currentProduct = activeProducts[currentSlide];
|
||||
const { data: partsIndexData } = useQuery(
|
||||
PARTS_INDEX_SEARCH_BY_ARTICLE,
|
||||
{
|
||||
variables: {
|
||||
articleNumber: currentProduct?.product?.article || '',
|
||||
brandName: currentProduct?.product?.brand || '',
|
||||
lang: 'ru'
|
||||
},
|
||||
skip: !currentProduct?.product?.article || !currentProduct?.product?.brand,
|
||||
errorPolicy: 'ignore'
|
||||
}
|
||||
);
|
||||
|
||||
// Функция для расчета цены со скидкой
|
||||
const calculateDiscountedPrice = (price: number, discount?: number) => {
|
||||
if (!discount) return price;
|
||||
return price * (1 - discount / 100);
|
||||
};
|
||||
|
||||
// Функция для форматирования цены
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat('ru-RU').format(Math.round(price));
|
||||
};
|
||||
|
||||
// Функция для получения изображения товара
|
||||
const getProductImage = (product: DailyProduct['product']) => {
|
||||
// Сначала пытаемся использовать собственные изображения товара
|
||||
const productImage = product.images
|
||||
?.sort((a, b) => a.order - b.order)
|
||||
?.[0];
|
||||
|
||||
if (productImage) {
|
||||
return {
|
||||
url: productImage.url,
|
||||
alt: productImage.alt || product.name,
|
||||
source: 'internal'
|
||||
};
|
||||
}
|
||||
|
||||
// Если нет собственных изображений, используем PartsIndex
|
||||
const partsIndexImage = partsIndexData?.partsIndexSearchByArticle?.images?.[0];
|
||||
if (partsIndexImage) {
|
||||
return {
|
||||
url: partsIndexImage,
|
||||
alt: product.name,
|
||||
source: 'partsindex'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Обработчики для слайдера
|
||||
const handlePrevSlide = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCurrentSlide(prev => prev === 0 ? activeProducts.length - 1 : prev - 1);
|
||||
};
|
||||
|
||||
const handleNextSlide = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCurrentSlide(prev => prev === activeProducts.length - 1 ? 0 : prev + 1);
|
||||
};
|
||||
|
||||
const handlePrevSlideTouch = (e: React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCurrentSlide(prev => prev === 0 ? activeProducts.length - 1 : prev - 1);
|
||||
};
|
||||
|
||||
const handleNextSlideTouch = (e: React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCurrentSlide(prev => prev === activeProducts.length - 1 ? 0 : prev + 1);
|
||||
};
|
||||
|
||||
const handleSlideIndicator = (index: number) => {
|
||||
setCurrentSlide(index);
|
||||
};
|
||||
|
||||
// Сброс слайда при изменении товаров
|
||||
useEffect(() => {
|
||||
setCurrentSlide(0);
|
||||
}, [activeProducts]);
|
||||
|
||||
// Если нет активных товаров дня, не показываем секцию
|
||||
if (loading || error || activeProducts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const product = currentProduct.product;
|
||||
const productImage = getProductImage(product);
|
||||
|
||||
const originalPrice = product.retailPrice || product.wholesalePrice || 0;
|
||||
const discountedPrice = calculateDiscountedPrice(originalPrice, currentProduct.discount);
|
||||
const hasDiscount = currentProduct.discount && currentProduct.discount > 0;
|
||||
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer batd w-container">
|
||||
<div className="w-layout-hflex flex-block-108">
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className="slider w-slider"
|
||||
>
|
||||
<div className="mask w-slider-mask">
|
||||
{activeProducts.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`slide w-slide ${index === currentSlide ? 'w--current' : ''}`}
|
||||
style={{
|
||||
display: index === currentSlide ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<div className="div-block-128"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-slide"></div>
|
||||
<div className="w-slide"></div>
|
||||
</div>
|
||||
<div className="left-arrow w-slider-arrow-left">
|
||||
<div className="div-block-34">
|
||||
<div className="code-embed-14 w-embed"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-arrow w-slider-arrow-right">
|
||||
<div className="div-block-34 right">
|
||||
<div className="code-embed-14 w-embed"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||
</div>
|
||||
<div className="div-block-129">
|
||||
<div className="w-layout-hflex flex-block-109">
|
||||
<h1 className="heading-18">ТОВАРЫ ДНЯ</h1>
|
||||
<div className="saletag">-35%</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-110">
|
||||
<div className="w-layout-vflex flex-block-111">
|
||||
<div className="w-layout-hflex flex-block-16">
|
||||
<div className="text-block-8">от 17 087 ₽</div>
|
||||
<div className="text-block-9">22 347 ₽</div>
|
||||
|
||||
{/* Стрелки слайдера (показываем только если товаров больше 1) */}
|
||||
{activeProducts.length > 1 && (
|
||||
<>
|
||||
<div className="left-arrow w-slider-arrow-left">
|
||||
<div className="div-block-34">
|
||||
<div className="code-embed-14 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-arrow w-slider-arrow-right">
|
||||
<div className="div-block-34 right">
|
||||
<div className="code-embed-14 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Индикаторы слайдов */}
|
||||
{activeProducts.length > 1 && (
|
||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round">
|
||||
{activeProducts.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-slider-dot ${index === currentSlide ? 'w--current' : ''}`}
|
||||
onClick={() => handleSlideIndicator(index)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{ cursor: 'pointer', zIndex: 10 }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-block-10">Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60</div>
|
||||
</div><img width="Auto" height="Auto" alt="" src="/images/162615.webp" loading="lazy" srcSet="/images/162615-p-500.webp 500w, /images/162615.webp 600w" sizes="(max-width: 600px) 100vw, 600px" className="image-5-copy" />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-125">
|
||||
<div className="div-block-134">
|
||||
<div className="code-embed-17 w-embed"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg></div>
|
||||
|
||||
<div className="div-block-129">
|
||||
<div className="w-layout-hflex flex-block-109">
|
||||
<h1 className="heading-18">ТОВАРЫ ДНЯ</h1>
|
||||
{hasDiscount && (
|
||||
<div className="saletag">-{currentProduct.discount}%</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="div-block-134-copy">
|
||||
<div className="code-embed-17 w-embed"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg></div>
|
||||
|
||||
<div className="w-layout-hflex flex-block-110">
|
||||
<div className="w-layout-vflex flex-block-111">
|
||||
<div className="w-layout-hflex flex-block-16">
|
||||
<div className="text-block-8">
|
||||
от {formatPrice(discountedPrice)} ₽
|
||||
</div>
|
||||
{hasDiscount && (
|
||||
<div className="text-block-9">
|
||||
{formatPrice(originalPrice)} ₽
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-block-10" title={product.name}>
|
||||
{product.brand && `${product.brand} `}
|
||||
{product.name}
|
||||
</div>
|
||||
{/* Счетчик товаров если их больше одного */}
|
||||
{/* {activeProducts.length > 1 && (
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
{currentSlide + 1} из {activeProducts.length}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
{productImage && (
|
||||
<div className="relative">
|
||||
<img
|
||||
width="Auto"
|
||||
height="Auto"
|
||||
alt={productImage.alt}
|
||||
src={productImage.url}
|
||||
loading="lazy"
|
||||
className="image-5-copy"
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
{/* Метка источника изображения */}
|
||||
{productImage.source === 'partsindex' && (
|
||||
<div className="absolute bottom-0 right-0 bg-blue-600 text-white text-xs px-2 py-1 rounded-tl">
|
||||
Parts Index
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-126">
|
||||
<div className="div-block-135"></div>
|
||||
<div className="div-block-135"></div>
|
||||
|
||||
<div className="w-layout-hflex flex-block-125">
|
||||
{/* Левая стрелка - предыдущий товар */}
|
||||
{activeProducts.length > 1 ? (
|
||||
<div
|
||||
className="div-block-134"
|
||||
onClick={handlePrevSlide}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onTouchStart={handlePrevSlideTouch}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Предыдущий товар"
|
||||
>
|
||||
<div className="code-embed-17 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="div-block-134" style={{ opacity: 0.3 }}>
|
||||
<div className="code-embed-17 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Правая стрелка - следующий товар */}
|
||||
{activeProducts.length > 1 ? (
|
||||
<div
|
||||
className="div-block-134-copy"
|
||||
onClick={handleNextSlide}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onTouchStart={handleNextSlideTouch}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Следующий товар"
|
||||
>
|
||||
<div className="code-embed-17 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="div-block-134-copy" style={{ opacity: 0.3 }}>
|
||||
<div className="code-embed-17 w-embed">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentcolor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Индикаторы точки */}
|
||||
<div className="w-layout-hflex flex-block-126">
|
||||
{activeProducts.length > 1 ? (
|
||||
activeProducts.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="div-block-135"
|
||||
onClick={() => handleSlideIndicator(index)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
opacity: index === currentSlide ? 1 : 0.5,
|
||||
backgroundColor: index === currentSlide ? 'currentColor' : 'rgba(128,128,128,0.5)'
|
||||
}}
|
||||
title={`Товар ${index + 1}`}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<div className="div-block-135" style={{ backgroundColor: 'currentColor' }}></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductOfDaySection;
|
@ -3,12 +3,12 @@ import React from "react";
|
||||
const SupportVinSection: React.FC = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container-copy w-container">
|
||||
<img
|
||||
src="images/support_img.png"
|
||||
loading="lazy"
|
||||
alt="Поддержка: помощь с VIN-запросом"
|
||||
className="image-27"
|
||||
/>
|
||||
<img
|
||||
src="images/support_img.png"
|
||||
loading="lazy"
|
||||
alt=""
|
||||
className="image-27"
|
||||
/>
|
||||
<div className="div-block-11">
|
||||
<div className="w-layout-vflex flex-block-30">
|
||||
<h3 className="supportheading">МЫ ВСЕГДА РАДЫ ПОМОЧЬ</h3>
|
||||
|
@ -1,54 +1,192 @@
|
||||
import React from "react";
|
||||
import ArticleCard from "../ArticleCard";
|
||||
import { PartsAPIArticle } from "@/types/partsapi";
|
||||
import React, { useRef } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import TopSalesItem from "../TopSalesItem";
|
||||
import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql";
|
||||
|
||||
// Моковые данные для топ продаж
|
||||
const topSalesArticles: PartsAPIArticle[] = [
|
||||
{
|
||||
artId: "1",
|
||||
artArticleNr: "6CT-60L",
|
||||
artSupBrand: "TYUMEN BATTERY",
|
||||
supBrand: "TYUMEN BATTERY",
|
||||
supId: 1,
|
||||
productGroup: "Аккумуляторная батарея",
|
||||
ptId: 1,
|
||||
},
|
||||
{
|
||||
artId: "2",
|
||||
artArticleNr: "A0001",
|
||||
artSupBrand: "Borsehung",
|
||||
supBrand: "Borsehung",
|
||||
supId: 2,
|
||||
productGroup: "Масляный фильтр",
|
||||
ptId: 2,
|
||||
},
|
||||
// ...добавьте еще 6 статей для примера
|
||||
...Array(6).fill(0).map((_, i) => ({
|
||||
artId: `${i+3}`,
|
||||
artArticleNr: `ART${i+3}`,
|
||||
artSupBrand: `Brand${i+3}`,
|
||||
supBrand: `Brand${i+3}`,
|
||||
supId: i+3,
|
||||
productGroup: `Product Group ${i+3}`,
|
||||
ptId: i+3,
|
||||
}))
|
||||
];
|
||||
interface TopSalesProductData {
|
||||
id: string;
|
||||
productId: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article?: string;
|
||||
brand?: string;
|
||||
retailPrice?: number;
|
||||
images: { url: string; alt?: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
const TopSalesSection: React.FC = () => (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||
|
||||
const TopSalesSection: React.FC = () => {
|
||||
const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const scrollRight = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
<div className="text-block-58">Загрузка...</div>
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex core-product-search">
|
||||
{topSalesArticles.map((article, i) => (
|
||||
<ArticleCard key={article.artId || i} article={article} index={i} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Ошибка загрузки топ продаж:', error);
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
<div className="text-block-58">Ошибка загрузки</div>
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Фильтруем активные товары и сортируем по sortOrder
|
||||
const activeTopSalesProducts = (data?.topSalesProducts || [])
|
||||
.filter((item: TopSalesProductData) => item.isActive)
|
||||
.sort((a: TopSalesProductData, b: TopSalesProductData) => a.sortOrder - b.sortOrder)
|
||||
.slice(0, 8); // Ограничиваем до 8 товаров
|
||||
|
||||
if (activeTopSalesProducts.length === 0) {
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
<div className="text-block-58">Нет товаров в топ продаж</div>
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-vflex inbt">
|
||||
<div className="w-layout-hflex flex-block-31">
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
{activeTopSalesProducts.map((item: TopSalesProductData) => {
|
||||
const product = item.product;
|
||||
const price = product.retailPrice
|
||||
? `от ${product.retailPrice.toLocaleString('ru-RU')} ₽`
|
||||
: 'По запросу';
|
||||
|
||||
const image = product.images && product.images.length > 0
|
||||
? product.images[0].url
|
||||
: '/images/162615.webp'; // Fallback изображение
|
||||
|
||||
const title = product.name;
|
||||
const brand = product.brand || 'Неизвестный бренд';
|
||||
|
||||
return (
|
||||
<TopSalesItem
|
||||
key={item.id}
|
||||
image={image}
|
||||
price={price}
|
||||
title={title}
|
||||
brand={brand}
|
||||
article={product.article}
|
||||
productId={product.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopSalesSection;
|
@ -92,7 +92,7 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
|
||||
<div
|
||||
key={entity.id}
|
||||
layer-name="legal"
|
||||
className="flex relative flex-col gap-8 items-start self-stretch px-5 py-3 rounded-lg bg-slate-50 max-sm:px-4 max-sm:py-2.5"
|
||||
className="flex relative flex-col gap-8 items-start self-stretch px-5 py-3 rounded-lg bg-slate-50 max-sm:px-4 max-sm:py-2.5 hover:bg-slate-200 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex relative justify-between items-center self-stretch max-sm:flex-col max-sm:gap-4 max-sm:items-start">
|
||||
<div className="flex relative gap-5 items-center max-md:flex-wrap max-md:gap-4 max-sm:flex-col max-sm:gap-2.5 max-sm:items-start">
|
||||
|
@ -78,14 +78,14 @@ const ProfileActsMain = () => {
|
||||
<div
|
||||
key={tab}
|
||||
layer-name="Tabs_button"
|
||||
className={`flex relative gap-5 items-center self-stretch rounded-xl flex-[1_0_0] min-w-[200px] max-md:gap-4 max-md:w-full max-md:min-w-[unset] max-sm:gap-2.5 ${activeTab === tab ? "" : "bg-slate-200"}`}
|
||||
className={`flex relative gap-5 items-center self-stretch rounded-xl flex-[1_0_0] min-w-[200px] text-[14px] max-md:gap-4 max-md:w-full max-md:min-w-[unset] max-sm:gap-2.5 ${activeTab === tab ? "" : "bg-slate-200 hover:bg-slate-200"}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className={`flex relative gap-5 justify-center items-center px-6 py-3.5 rounded-xl flex-[1_0_0] ${activeTab === tab ? "bg-red-600" : "bg-slate-200"}`}>
|
||||
<div
|
||||
layer-name="Курьером"
|
||||
className={`relative text-lg font-medium leading-5 text-center max-sm:text-base ${activeTab === tab ? "text-white" : "text-gray-950"}`}
|
||||
className={`relative font-medium leading-5 text-center text-[14px] max-sm:text-base ${activeTab === tab ? "text-white" : "text-gray-950"}`}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
|
@ -179,7 +179,7 @@ const ProfileGarageMain = () => {
|
||||
|
||||
{!vehiclesLoading && filteredVehicles.map((vehicle) => (
|
||||
<div key={vehicle.id} className="mt-8">
|
||||
<div className="flex flex-col justify-center px-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full">
|
||||
<div className="flex flex-col justify-center px-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full hover:bg-slate-200 transition-colors cursor-pointer">
|
||||
<div className="flex flex-wrap gap-8 items-center w-full max-md:max-w-full">
|
||||
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
||||
<div className="self-stretch my-auto text-xl font-bold leading-none text-gray-950">
|
||||
@ -247,63 +247,65 @@ const ProfileGarageMain = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Расширенная информация об автомобиле */}
|
||||
{expandedVehicle === vehicle.id && (
|
||||
<div className="mt-4 px-5 py-4 bg-white rounded-lg border border-gray-200">
|
||||
{/* Расширенная информация об автомобиле — вложена внутрь карточки */}
|
||||
<div
|
||||
className={
|
||||
`overflow-hidden transition-all duration-300 rounded-lg flex flex-col gap-4` +
|
||||
(expandedVehicle === vehicle.id ? ' py-4 max-h-[1000px] opacity-100 mt-4' : ' max-h-0 opacity-0 pointer-events-none')
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||
{vehicle.brand && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Бренд:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.brand}</span>
|
||||
<div className="font-bold text-gray-950">Бренд</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.brand}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.model && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Модель:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.model}</span>
|
||||
<div className="font-bold text-gray-950">Модель</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.model}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.modification && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Модификация:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.modification}</span>
|
||||
<div className="font-bold text-gray-950">Модификация</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.modification}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.year && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Год:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.year}</span>
|
||||
<div className="font-bold text-gray-950">Год</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.year}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.frame && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Номер кузова:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.frame}</span>
|
||||
<div className="font-bold text-gray-950">Номер кузова</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.frame}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.licensePlate && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Госномер:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.licensePlate}</span>
|
||||
<div className="font-bold text-gray-950">Госномер</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.licensePlate}</div>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.mileage && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Пробег:</span>
|
||||
<span className="ml-2 text-gray-900">{vehicle.mileage.toLocaleString()} км</span>
|
||||
<div className="font-bold text-gray-950">Пробег</div>
|
||||
<div className="mt-1.5 text-gray-600">{vehicle.mileage.toLocaleString()} км</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Добавлен:</span>
|
||||
<span className="ml-2 text-gray-900">
|
||||
<div className="font-bold text-gray-950">Добавлен</div>
|
||||
<div className="mt-1.5 text-gray-600">
|
||||
{new Date(vehicle.createdAt).toLocaleDateString('ru-RU')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!showAddCar && (
|
||||
@ -400,10 +402,10 @@ const ProfileGarageMain = () => {
|
||||
{!historyLoading && searchHistory.length > 0 && (
|
||||
<div className="flex flex-col mt-8 w-full max-md:max-w-full">
|
||||
{searchHistory.map((historyItem) => (
|
||||
<div key={historyItem.id} className="flex flex-col justify-center px-5 py-3 mb-2.5 w-full rounded-lg bg-slate-50 min-h-[44px] max-md:max-w-full">
|
||||
<div key={historyItem.id} className="flex flex-col justify-center px-5 py-3 mb-2.5 w-full rounded-lg bg-slate-50 min-h-[44px] max-md:max-w-full hover:bg-slate-200 transition-colors cursor-pointer">
|
||||
<div className="flex flex-wrap gap-10 justify-between items-center w-full max-md:max-w-full">
|
||||
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
||||
<div className="self-stretch my-auto text-lg font-bold leading-none text-gray-950">
|
||||
<div className="self-stretch my-auto text-lg font-bold leading-none text-gray-950 w-[300px]">
|
||||
{historyItem.brand && historyItem.model
|
||||
? `${historyItem.brand} ${historyItem.model}`
|
||||
: 'Автомобиль найден'}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface VehicleInfo {
|
||||
brand?: string;
|
||||
@ -15,6 +16,10 @@ interface ProfileHistoryItemProps {
|
||||
vehicleInfo?: VehicleInfo;
|
||||
resultCount?: number;
|
||||
onDelete?: (id: string) => void;
|
||||
// Добавляем новые пропсы для поиска
|
||||
searchType?: 'TEXT' | 'ARTICLE' | 'OEM' | 'VIN' | 'PLATE' | 'WIZARD' | 'PART_VEHICLES';
|
||||
articleNumber?: string;
|
||||
brand?: string;
|
||||
}
|
||||
|
||||
const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||
@ -26,7 +31,12 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||
vehicleInfo,
|
||||
resultCount,
|
||||
onDelete,
|
||||
searchType,
|
||||
articleNumber,
|
||||
brand,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (onDelete) {
|
||||
@ -34,6 +44,28 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemClick = () => {
|
||||
// Определяем куда перенаправлять в зависимости от типа поиска
|
||||
if (searchType === 'VIN' || searchType === 'PLATE') {
|
||||
// Для VIN и госномера перенаправляем на vehicle-search-results
|
||||
router.push(`/vehicle-search-results?q=${encodeURIComponent(name)}`);
|
||||
} else if (searchType === 'ARTICLE' || searchType === 'OEM' || (searchType === 'TEXT' && articleNumber)) {
|
||||
// Для поиска по артикулу/OEM или текстового поиска с артикулом
|
||||
const searchBrand = brand || manufacturer || '';
|
||||
const searchArticle = articleNumber || name;
|
||||
router.push(`/search-result?article=${encodeURIComponent(searchArticle)}&brand=${encodeURIComponent(searchBrand)}`);
|
||||
} else if (searchType === 'TEXT') {
|
||||
// Для обычного текстового поиска
|
||||
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||
} else if (searchType === 'PART_VEHICLES') {
|
||||
// Для поиска автомобилей по детали
|
||||
router.push(`/vehicles-by-part?partNumber=${encodeURIComponent(name)}`);
|
||||
} else {
|
||||
// По умолчанию - обычный поиск
|
||||
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||
}
|
||||
};
|
||||
|
||||
const getSearchTypeDisplay = (article: string) => {
|
||||
if (article.includes('TEXT')) return 'Текстовый поиск';
|
||||
if (article.includes('ARTICLE')) return 'По артикулу';
|
||||
@ -48,7 +80,11 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||
return (
|
||||
<>
|
||||
<div className="mt-1.5 w-full border border-gray-200 border-solid min-h-[1px] max-md:max-w-full" />
|
||||
<div className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors">
|
||||
<div
|
||||
className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors"
|
||||
onClick={handleItemClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
|
||||
<div className="self-stretch my-auto w-40 max-md:w-full text-sm">
|
||||
<div className="font-medium text-gray-900">{date}</div>
|
||||
|
@ -3,6 +3,7 @@ import { useQuery, useMutation } from '@apollo/client';
|
||||
import ProfileHistoryItem from "./ProfileHistoryItem";
|
||||
import SearchInput from "./SearchInput";
|
||||
import ProfileHistoryTabs from "./ProfileHistoryTabs";
|
||||
import Pagination from '../Pagination';
|
||||
import {
|
||||
GET_PARTS_SEARCH_HISTORY,
|
||||
DELETE_SEARCH_HISTORY_ITEM,
|
||||
@ -19,6 +20,10 @@ const ProfileHistoryMain = () => {
|
||||
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||
|
||||
// Состояние пагинации
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10); // Количество элементов на странице
|
||||
|
||||
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
|
||||
|
||||
@ -26,7 +31,10 @@ const ProfileHistoryMain = () => {
|
||||
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
|
||||
GET_PARTS_SEARCH_HISTORY,
|
||||
{
|
||||
variables: { limit: 100, offset: 0 },
|
||||
variables: {
|
||||
limit: 1000, // Загружаем больше для клиентской пагинации с фильтрами
|
||||
offset: 0
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
onCompleted: (data) => {
|
||||
console.log('История поиска загружена:', data);
|
||||
@ -161,8 +169,32 @@ const ProfileHistoryMain = () => {
|
||||
}
|
||||
|
||||
setFilteredItems(filtered);
|
||||
// Сбрасываем страницу на первую при изменении фильтров
|
||||
setCurrentPage(1);
|
||||
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||
|
||||
// Вычисляем элементы для текущей страницы
|
||||
const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentPageItems = filteredItems.slice(startIndex, endIndex);
|
||||
|
||||
// Обработчик изменения страницы
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
// Прокручиваем к началу списка при смене страницы
|
||||
const historyContainer = document.querySelector('.flex.flex-col.mt-5.w-full.text-gray-400');
|
||||
if (historyContainer) {
|
||||
historyContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик изменения количества элементов на странице
|
||||
const handleItemsPerPageChange = (newItemsPerPage: number) => {
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1); // Сбрасываем на первую страницу
|
||||
};
|
||||
|
||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
@ -287,6 +319,7 @@ const ProfileHistoryMain = () => {
|
||||
setSelectedManufacturer("Все");
|
||||
setSearch("");
|
||||
setActiveTab("Все");
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
@ -424,7 +457,7 @@ const ProfileHistoryMain = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => (
|
||||
currentPageItems.map((item) => (
|
||||
<ProfileHistoryItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
@ -441,18 +474,58 @@ const ProfileHistoryMain = () => {
|
||||
vehicleInfo={item.vehicleInfo}
|
||||
resultCount={item.resultCount}
|
||||
onDelete={handleDeleteItem}
|
||||
searchType={item.searchType}
|
||||
articleNumber={item.articleNumber}
|
||||
brand={item.brand}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
{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 className="mt-6 space-y-4">
|
||||
{/* Селектор количества элементов на странице */}
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-2 sm:space-y-0">
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<span>Показывать по:</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
||||
className="px-2 py-1 border border-gray-200 rounded text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-[#ec1c24] focus:border-transparent"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
</select>
|
||||
<span>записей</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 text-center sm:text-right">
|
||||
Показано {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} из {filteredItems.length} записей
|
||||
{filteredItems.length !== historyItems.length && (
|
||||
<span className="ml-1">
|
||||
(всего {historyItems.length})
|
||||
</span>
|
||||
)}
|
||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
(применены фильтры)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компонент пагинации */}
|
||||
{filteredItems.length > itemsPerPage && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
showPageInfo={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
@ -69,7 +69,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab}
|
||||
className={`flex flex-1 shrink gap-5 items-center h-full text-center rounded-xl basis-12 min-w-[200px] ${
|
||||
className={`flex flex-1 shrink gap-5 items-center h-full text-center rounded-xl basis-12 min-w-[160px] text-[14px] ${
|
||||
activeTab === tab
|
||||
? "text-white"
|
||||
: "bg-slate-200 text-gray-950"
|
||||
@ -78,7 +78,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
onClick={() => onTabChange(tab)}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 min-w-[200px] max-md:px-5 ${
|
||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 min-w-[160px] text-[14px] max-md:px-5 ${
|
||||
activeTab === tab
|
||||
? "text-white bg-red-600"
|
||||
: "bg-slate-200 text-gray-950"
|
||||
@ -94,7 +94,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-center px-6 py-4 text-sm leading-snug bg-white rounded border border-solid border-stone-300 text-neutral-500 cursor-pointer select-none w-full min-w-[200px]"
|
||||
className="flex justify-between items-center px-6 py-3 text-sm leading-snug bg-white rounded border border-solid border-stone-300 text-neutral-500 cursor-pointer select-none w-full min-w-[200px]"
|
||||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
<span className="truncate">{selectedManufacturer}</span>
|
||||
|
@ -172,12 +172,12 @@ const ProfileOrdersMain: React.FC<ProfileOrdersMainProps> = (props) => {
|
||||
{tabs.map((tab, idx) => (
|
||||
<div
|
||||
key={tab.label}
|
||||
className={`flex flex-1 shrink gap-5 items-center h-full rounded-xl basis-0 ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||
className={`flex flex-1 shrink gap-5 items-center h-full rounded-xl basis-0 text-[14px] ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setActiveTab(idx)}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 max-md:px-5 ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 max-md:px-5 text-[14px] ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import VehicleAttributesTooltip from './VehicleAttributesTooltip';
|
||||
|
||||
interface VehicleAttribute {
|
||||
key: string;
|
||||
@ -206,42 +207,14 @@ const InfoVin: React.FC<InfoVinProps> = ({
|
||||
</section>
|
||||
|
||||
{/* Tooltip с фиксированным позиционированием */}
|
||||
{showTooltip && vehicleAttributes.length > 0 && (
|
||||
<div
|
||||
className="fixed w-[500px] max-w-[90vw] bg-white border border-gray-200 rounded-lg shadow-xl z-[9999] p-4 animate-in fade-in-0 zoom-in-95 duration-200"
|
||||
style={{
|
||||
left: `${tooltipPosition.x}px`,
|
||||
top: `${tooltipPosition.y}px`,
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Заголовок */}
|
||||
<div className="mb-3 pb-2 border-b border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Полная информация об автомобиле
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600 mt-1">{vehicleName}</p>
|
||||
</div>
|
||||
|
||||
{/* Атрибуты в сетке */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{vehicleAttributes.map((attr, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<dt className="text-xs font-medium text-gray-500 mb-1">{attr.name}</dt>
|
||||
<dd className="text-xs text-gray-900 break-words">{attr.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Подвал */}
|
||||
<div className="mt-3 pt-2 border-t border-gray-100">
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
Всего параметров: {vehicleAttributes.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<VehicleAttributesTooltip
|
||||
show={showTooltip && vehicleAttributes.length > 0}
|
||||
position={tooltipPosition}
|
||||
vehicleName={vehicleName}
|
||||
vehicleAttributes={vehicleAttributes}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -21,6 +21,9 @@ interface KnotInProps {
|
||||
note?: string;
|
||||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||||
}>;
|
||||
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для уведомления KnotParts о выделении детали
|
||||
onPartsHighlight?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||||
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||||
}
|
||||
|
||||
// Функция для корректного формирования URL изображения
|
||||
@ -34,12 +37,23 @@ const getImageUrl = (baseUrl: string, size: string) => {
|
||||
.replace('%size%', size);
|
||||
};
|
||||
|
||||
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
|
||||
const KnotIn: React.FC<KnotInProps> = ({
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
ssd,
|
||||
unitId,
|
||||
unitName,
|
||||
parts,
|
||||
onPartSelect,
|
||||
onPartsHighlight,
|
||||
selectedParts = new Set()
|
||||
}) => {
|
||||
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 [hoveredCodeOnImage, setHoveredCodeOnImage] = useState<string | number | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Получаем инфо об узле (для картинки)
|
||||
@ -150,21 +164,62 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
});
|
||||
};
|
||||
|
||||
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
|
||||
const handlePointClick = (codeonimage: string | number) => {
|
||||
// Обработчик наведения на точку
|
||||
const handlePointHover = (coord: any) => {
|
||||
// Попробуем использовать разные поля для связи
|
||||
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||
|
||||
console.log('🔍 KnotIn - hover на точку:', {
|
||||
coord,
|
||||
detailid: coord.detailid,
|
||||
codeonimage: coord.codeonimage,
|
||||
code: coord.code,
|
||||
identifierToUse,
|
||||
type: typeof identifierToUse,
|
||||
coordinatesLength: coordinates.length,
|
||||
partsLength: parts?.length || 0,
|
||||
firstCoord: coordinates[0],
|
||||
firstPart: parts?.[0]
|
||||
});
|
||||
|
||||
setHoveredCodeOnImage(identifierToUse);
|
||||
if (onPartsHighlight) {
|
||||
onPartsHighlight(identifierToUse);
|
||||
}
|
||||
};
|
||||
|
||||
// Клик по точке: выделить в списке деталей
|
||||
const handlePointClick = (coord: any) => {
|
||||
if (!parts) return;
|
||||
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
|
||||
|
||||
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||
console.log('Клик по точке:', identifierToUse, 'Координата:', coord, 'Все детали:', parts);
|
||||
|
||||
// Уведомляем родительский компонент о выборе детали для выделения в списке
|
||||
if (onPartSelect) {
|
||||
onPartSelect(identifierToUse);
|
||||
}
|
||||
};
|
||||
|
||||
// Двойной клик по точке: переход на страницу выбора бренда
|
||||
const handlePointDoubleClick = (coord: any) => {
|
||||
if (!parts) return;
|
||||
|
||||
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||
console.log('Двойной клик по точке:', identifierToUse, 'Координата:', coord);
|
||||
|
||||
const part = parts.find(
|
||||
(p) =>
|
||||
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
|
||||
(p.detailid && p.detailid.toString() === codeonimage.toString())
|
||||
(p.detailid && p.detailid.toString() === identifierToUse?.toString()) ||
|
||||
(p.codeonimage && p.codeonimage.toString() === identifierToUse?.toString())
|
||||
);
|
||||
console.log('Найдена деталь для точки:', part);
|
||||
|
||||
if (part?.oem) {
|
||||
setSelectedDetail({ oem: part.oem, name: part.name || '' });
|
||||
setIsBrandModalOpen(true);
|
||||
// Переходим на страницу выбора бренда вместо модального окна
|
||||
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${part.oem}/brands?detailName=${encodeURIComponent(part.name || '')}`;
|
||||
router.push(url);
|
||||
} else {
|
||||
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
|
||||
console.warn('Нет артикула (oem) для выбранной точки:', identifierToUse, part);
|
||||
}
|
||||
};
|
||||
|
||||
@ -172,6 +227,40 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
React.useEffect(() => {
|
||||
console.log('KnotIn parts:', parts);
|
||||
console.log('KnotIn coordinates:', coordinates);
|
||||
if (coordinates.length > 0) {
|
||||
console.log('🔍 Первые 5 координат:', coordinates.slice(0, 5).map((c: any) => ({
|
||||
code: c.code,
|
||||
codeonimage: c.codeonimage,
|
||||
detailid: c.detailid,
|
||||
x: c.x,
|
||||
y: c.y
|
||||
})));
|
||||
}
|
||||
if (parts && parts.length > 0) {
|
||||
console.log('🔍 Первые 5 деталей:', parts.slice(0, 5).map(p => ({
|
||||
name: p.name,
|
||||
codeonimage: p.codeonimage,
|
||||
detailid: p.detailid,
|
||||
oem: p.oem
|
||||
})));
|
||||
}
|
||||
|
||||
// Попытка связать координаты с деталями
|
||||
if (coordinates.length > 0 && parts && parts.length > 0) {
|
||||
console.log('🔗 Попытка связать координаты с деталями:');
|
||||
coordinates.forEach((coord: any, idx: number) => {
|
||||
const matchingPart = parts.find(part =>
|
||||
part.detailid === coord.detailid ||
|
||||
part.codeonimage === coord.codeonimage ||
|
||||
part.codeonimage === coord.code
|
||||
);
|
||||
if (matchingPart) {
|
||||
console.log(` ✅ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> Деталь: ${matchingPart.name}`);
|
||||
} else {
|
||||
console.log(` ❌ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> НЕ НАЙДЕНА`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [parts, coordinates]);
|
||||
|
||||
if (unitInfoLoading || imageMapLoading) {
|
||||
@ -223,10 +312,6 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
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}
|
||||
@ -242,38 +327,63 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
const size = 22;
|
||||
const scaledX = coord.x * imageScale.x - size / 2;
|
||||
const scaledY = coord.y * imageScale.y - size / 2;
|
||||
|
||||
// Используем code или codeonimage в зависимости от структуры данных
|
||||
const codeValue = coord.code || coord.codeonimage;
|
||||
|
||||
// Определяем состояние точки
|
||||
const isSelected = selectedParts.has(codeValue);
|
||||
const isHovered = hoveredCodeOnImage === codeValue;
|
||||
|
||||
// Определяем цвета на основе состояния
|
||||
let backgroundColor = '#B7CAE2'; // Базовый цвет
|
||||
let textColor = '#000';
|
||||
|
||||
if (isSelected) {
|
||||
backgroundColor = '#22C55E'; // Зеленый для выбранных
|
||||
textColor = '#fff';
|
||||
} else if (isHovered) {
|
||||
backgroundColor = '#EC1C24'; // Красный при наведении
|
||||
textColor = '#fff';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||
tabIndex={0}
|
||||
aria-label={`Деталь ${coord.codeonimage}`}
|
||||
aria-label={`Деталь ${codeValue}`}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord);
|
||||
}}
|
||||
className="absolute flex items-center justify-center cursor-pointer transition-colors"
|
||||
className="absolute flex items-center justify-center cursor-pointer transition-all duration-200 ease-in-out"
|
||||
style={{
|
||||
left: scaledX,
|
||||
top: scaledY,
|
||||
width: size,
|
||||
height: size,
|
||||
background: '#B7CAE2',
|
||||
backgroundColor,
|
||||
borderRadius: '50%',
|
||||
|
||||
border: isSelected ? '2px solid #16A34A' : 'none',
|
||||
transform: isHovered || isSelected ? 'scale(1.1)' : 'scale(1)',
|
||||
zIndex: isHovered || isSelected ? 10 : 1,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
title={coord.codeonimage}
|
||||
onClick={() => handlePointClick(coord.codeonimage)}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.background = '#EC1C24';
|
||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#fff';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.background = '#B7CAE2';
|
||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#000';
|
||||
title={`${codeValue} (Клик - выделить в списке, двойной клик - перейти к выбору бренда)`}
|
||||
onClick={() => handlePointClick(coord)}
|
||||
onDoubleClick={() => handlePointDoubleClick(coord)}
|
||||
onMouseEnter={() => handlePointHover(coord)}
|
||||
onMouseLeave={() => {
|
||||
setHoveredCodeOnImage(null);
|
||||
if (onPartsHighlight) {
|
||||
onPartsHighlight(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none" style={{color: '#000'}}>
|
||||
{coord.codeonimage}
|
||||
<span
|
||||
className="flex items-center justify-center w-full h-full text-sm font-bold select-none pointer-events-none transition-colors duration-200"
|
||||
style={{ color: textColor }}
|
||||
>
|
||||
{codeValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface KnotPartsProps {
|
||||
@ -16,10 +16,42 @@ interface KnotPartsProps {
|
||||
selectedCodeOnImage?: string | number;
|
||||
catalogCode?: string;
|
||||
vehicleId?: string;
|
||||
highlightedCodeOnImage?: string | number | null; // Деталь подсвеченная при hover на изображении
|
||||
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||||
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для выбора детали
|
||||
onPartHover?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||||
}
|
||||
|
||||
const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage, catalogCode, vehicleId }) => {
|
||||
const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
parts = [],
|
||||
selectedCodeOnImage,
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
highlightedCodeOnImage,
|
||||
selectedParts = new Set(),
|
||||
onPartSelect,
|
||||
onPartHover
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||||
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Отладочные логи для проверки данных
|
||||
React.useEffect(() => {
|
||||
console.log('🔍 KnotParts получил данные:', {
|
||||
partsCount: parts.length,
|
||||
firstPart: parts[0],
|
||||
firstPartAttributes: parts[0]?.attributes?.length || 0,
|
||||
allPartsWithAttributes: parts.map(part => ({
|
||||
name: part.name,
|
||||
oem: part.oem,
|
||||
attributesCount: part.attributes?.length || 0,
|
||||
attributes: part.attributes
|
||||
}))
|
||||
});
|
||||
}, [parts]);
|
||||
|
||||
const handlePriceClick = (part: any) => {
|
||||
if (part.oem && catalogCode && vehicleId !== undefined) {
|
||||
@ -29,6 +61,98 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик клика по детали в списке
|
||||
const handlePartClick = (part: any) => {
|
||||
if (part.codeonimage && onPartSelect) {
|
||||
onPartSelect(part.codeonimage);
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчики наведения
|
||||
const handlePartMouseEnter = (part: any) => {
|
||||
if (part.codeonimage && onPartHover) {
|
||||
onPartHover(part.codeonimage);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePartMouseLeave = () => {
|
||||
if (onPartHover) {
|
||||
onPartHover(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Вычисляем позицию tooltip
|
||||
const calculateTooltipPosition = (iconElement: HTMLElement) => {
|
||||
if (!iconElement) {
|
||||
console.error('❌ calculateTooltipPosition: элемент не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = iconElement.getBoundingClientRect();
|
||||
const tooltipWidth = 400;
|
||||
const tooltipHeight = 300; // примерная высота
|
||||
|
||||
let x = rect.left + rect.width / 2 - tooltipWidth / 2;
|
||||
let y = rect.bottom + 8;
|
||||
|
||||
// Проверяем, не выходит ли tooltip за границы экрана
|
||||
if (x < 10) x = 10;
|
||||
if (x + tooltipWidth > window.innerWidth - 10) {
|
||||
x = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
|
||||
// Если tooltip не помещается снизу, показываем сверху
|
||||
if (y + tooltipHeight > window.innerHeight - 10) {
|
||||
y = rect.top - tooltipHeight - 8;
|
||||
}
|
||||
|
||||
setTooltipPosition({ x, y });
|
||||
};
|
||||
|
||||
const handleInfoIconMouseEnter = (event: React.MouseEvent, part: any) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Сохраняем ссылку на элемент до setTimeout
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (target && typeof target.getBoundingClientRect === 'function') {
|
||||
calculateTooltipPosition(target);
|
||||
setTooltipPart(part);
|
||||
setShowTooltip(true);
|
||||
console.log('🔍 Показываем тултип для детали:', part.name, 'Атрибуты:', part.attributes?.length || 0);
|
||||
} else {
|
||||
console.error('❌ handleInfoIconMouseEnter: элемент не поддерживает getBoundingClientRect:', target);
|
||||
}
|
||||
}, 300); // Задержка 300ms
|
||||
};
|
||||
|
||||
const handleInfoIconMouseLeave = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setShowTooltip(false);
|
||||
setTooltipPart(null);
|
||||
}, 100); // Небольшая задержка перед скрытием
|
||||
};
|
||||
|
||||
// Очищаем таймеры при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Если нет деталей, показываем заглушку
|
||||
if (!parts || parts.length === 0) {
|
||||
return (
|
||||
@ -41,29 +165,107 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
||||
);
|
||||
}
|
||||
|
||||
// Эффект для отслеживания изменений подсветки
|
||||
useEffect(() => {
|
||||
console.log('🔍 KnotParts - подсветка изменилась:', {
|
||||
highlightedCodeOnImage,
|
||||
highlightedType: typeof highlightedCodeOnImage,
|
||||
partsCodeOnImages: parts.map(p => p.codeonimage),
|
||||
partsDetailIds: parts.map(p => p.detailid),
|
||||
willHighlight: parts.some(part =>
|
||||
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage?.toString()) ||
|
||||
(part.detailid && part.detailid.toString() === highlightedCodeOnImage?.toString())
|
||||
),
|
||||
willHighlightStrict: parts.some(part =>
|
||||
part.codeonimage === highlightedCodeOnImage ||
|
||||
part.detailid === highlightedCodeOnImage
|
||||
),
|
||||
firstPartWithCodeOnImage: parts.find(p => p.codeonimage)
|
||||
});
|
||||
|
||||
// Детальная информация о всех деталях
|
||||
console.log('📋 Все детали с их codeonimage и detailid:');
|
||||
parts.forEach((part, idx) => {
|
||||
console.log(` Деталь ${idx}: "${part.name}" codeonimage="${part.codeonimage}" (${typeof part.codeonimage}) detailid="${part.detailid}" (${typeof part.detailid})`);
|
||||
});
|
||||
|
||||
console.log('🎯 Ищем подсветку для:', `"${highlightedCodeOnImage}" (${typeof highlightedCodeOnImage})`);
|
||||
}, [highlightedCodeOnImage, parts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Статус выбранных деталей */}
|
||||
{/* {selectedParts.size > 0 && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-green-800 font-medium">
|
||||
Выбрано деталей: {selectedParts.size}
|
||||
</span>
|
||||
<span className="text-green-600 text-sm ml-2">
|
||||
(Кликните по детали, чтобы убрать из выбранных)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<div className="knot-parts">
|
||||
{parts.map((part, idx) => {
|
||||
const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
|
||||
const isHighlighted = highlightedCodeOnImage !== null && highlightedCodeOnImage !== undefined && (
|
||||
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage.toString()) ||
|
||||
(part.detailid && part.detailid.toString() === highlightedCodeOnImage.toString())
|
||||
);
|
||||
|
||||
const isSelected = selectedParts.has(part.detailid || part.codeonimage || idx.toString());
|
||||
|
||||
// Создаем уникальный ключ
|
||||
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
||||
|
||||
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}
|
||||
key={uniqueKey}
|
||||
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-colors ${
|
||||
isSelected
|
||||
? 'bg-green-100 border-green-500'
|
||||
: isHighlighted
|
||||
? 'bg-slate-200'
|
||||
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => handlePartClick(part)}
|
||||
onMouseEnter={() => handlePartMouseEnter(part)}
|
||||
onMouseLeave={handlePartMouseLeave}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="w-layout-hflex flex-block-116">
|
||||
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
||||
<div className="oemnuber">{part.oem}</div>
|
||||
<div
|
||||
className={`nuberlist ${isSelected ? 'text-green-700 font-bold' : isHighlighted ? ' font-bold' : ''}`}
|
||||
>
|
||||
{part.codeonimage || idx + 1}
|
||||
</div>
|
||||
<div className={`oemnuber ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>{part.oem}</div>
|
||||
</div>
|
||||
<div className={`partsname ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>
|
||||
{part.name}
|
||||
</div>
|
||||
<div className="partsname">{part.name}</div>
|
||||
<div className="w-layout-hflex flex-block-117">
|
||||
<button
|
||||
className="button-3 w-button"
|
||||
onClick={() => handlePriceClick(part)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Предотвращаем срабатывание onClick родительского элемента
|
||||
handlePriceClick(part);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Цена
|
||||
</button>
|
||||
<div className="code-embed-16 w-embed">
|
||||
<div
|
||||
className="code-embed-16 w-embed cursor-pointer hover:opacity-70 transition-opacity"
|
||||
onMouseEnter={(e) => handleInfoIconMouseEnter(e, part)}
|
||||
onMouseLeave={handleInfoIconMouseLeave}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<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>
|
||||
@ -73,6 +275,52 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Красивый тултип с информацией о детали */}
|
||||
{showTooltip && tooltipPart && (
|
||||
<div
|
||||
className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[350px] min-h-[220px] max-w-full fixed z-[9999]"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: tooltipPosition.x,
|
||||
top: tooltipPosition.y,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="flex relative flex-col w-full">
|
||||
{/* Заголовок и OEM */}
|
||||
<div className="mb-4">
|
||||
<div className="font-semibold text-lg text-black mb-1 truncate">{tooltipPart.name}</div>
|
||||
{tooltipPart.oem && (
|
||||
<div className="inline-block bg-gray-100 text-gray-700 text-xs font-mono px-2 py-1 rounded mb-1">OEM: {tooltipPart.oem}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Характеристики */}
|
||||
{tooltipPart.attributes && tooltipPart.attributes.length > 0 ? (
|
||||
tooltipPart.attributes.map((attr: any, idx: number) => (
|
||||
<div key={idx} className="flex gap-5 items-center mt-2 w-full whitespace-normal first:mt-0">
|
||||
<div className="self-stretch my-auto text-gray-400 w-[150px] break-words">
|
||||
{attr.name || attr.key}
|
||||
</div>
|
||||
<div className="self-stretch my-auto font-medium text-black break-words">
|
||||
{attr.value}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full py-8">
|
||||
<div className="text-gray-400 mb-2">Дополнительная информация недоступна</div>
|
||||
</div>
|
||||
)}
|
||||
{tooltipPart.note && (
|
||||
<div className="flex flex-col mt-6 w-full">
|
||||
<div className="text-gray-400 text-xs mb-1">Примечание</div>
|
||||
<div className="font-medium text-black text-sm">{tooltipPart.note}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
63
src/components/vin/VehicleAttributesTooltip.tsx
Normal file
63
src/components/vin/VehicleAttributesTooltip.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
interface VehicleAttribute {
|
||||
key: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface VehicleAttributesTooltipProps {
|
||||
show: boolean;
|
||||
position: { x: number; y: number };
|
||||
vehicleName?: string;
|
||||
vehicleAttributes: VehicleAttribute[];
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
imageUrl?: string; // опционально, для будущего
|
||||
}
|
||||
|
||||
const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
|
||||
show,
|
||||
position,
|
||||
vehicleAttributes,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
imageUrl,
|
||||
}) => {
|
||||
if (!show) return null;
|
||||
return (
|
||||
<div
|
||||
className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[450px] min-h-[365px] max-w-full fixed z-[9999]"
|
||||
style={{
|
||||
left: `${position.x + 120}px`,
|
||||
top: `${position.y}px`,
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{/* Фоновое изображение, если будет нужно */}
|
||||
{imageUrl && (
|
||||
<img
|
||||
loading="lazy"
|
||||
src={imageUrl}
|
||||
className="object-cover absolute inset-0 size-full rounded-2xl opacity-10 pointer-events-none"
|
||||
alt="vehicle background"
|
||||
/>
|
||||
)}
|
||||
<div className="flex relative flex-col w-full">
|
||||
{vehicleAttributes.map((attr, idx) => (
|
||||
<div key={idx} className="flex gap-5 items-center mt-2 w-full whitespace-nowrap first:mt-0">
|
||||
<div className="self-stretch my-auto text-gray-400 w-[150px] truncate">
|
||||
{attr.name}
|
||||
</div>
|
||||
<div className="self-stretch my-auto font-medium text-black truncate">
|
||||
{attr.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleAttributesTooltip;
|
@ -23,6 +23,7 @@ interface VinLeftbarProps {
|
||||
activeTab?: 'uzly' | 'manufacturer';
|
||||
openedPath?: string[];
|
||||
setOpenedPath?: (path: string[]) => void;
|
||||
onCloseQuickGroup?: () => void;
|
||||
}
|
||||
|
||||
interface QuickGroup {
|
||||
@ -32,7 +33,7 @@ interface QuickGroup {
|
||||
children?: QuickGroup[];
|
||||
}
|
||||
|
||||
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect, activeTab: activeTabProp, openedPath = [], setOpenedPath = () => {} }) => {
|
||||
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect, activeTab: activeTabProp, openedPath = [], setOpenedPath = () => {}, onCloseQuickGroup }) => {
|
||||
const router = useRouter();
|
||||
const catalogCode = vehicleInfo?.catalog || '';
|
||||
const vehicleId = vehicleInfo?.vehicleid || '';
|
||||
@ -65,6 +66,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
// Обновляем openedPath и URL
|
||||
const setOpenedPathAndUrl = (newPath: string[]) => {
|
||||
setOpenedPath(newPath);
|
||||
if (onCloseQuickGroup) onCloseQuickGroup();
|
||||
const params = new URLSearchParams(router.query as any);
|
||||
if (newPath.length > 0) {
|
||||
params.set('catpath', newPath.join(','));
|
||||
@ -320,6 +322,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
if (searchQuery) setSearchQuery('');
|
||||
if (onActiveTabChange) onActiveTabChange('uzly');
|
||||
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||
if (onCloseQuickGroup) onCloseQuickGroup();
|
||||
}}
|
||||
>
|
||||
Узлы
|
||||
@ -337,8 +340,8 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
e.preventDefault();
|
||||
if (searchQuery) setSearchQuery('');
|
||||
if (onActiveTabChange) onActiveTabChange('manufacturer');
|
||||
// Не вызываем onQuickGroupSelect с null - это вызывает ошибку
|
||||
// Просто переключаем вкладку, а обработка отображения происходит через activeTab
|
||||
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||
if (onCloseQuickGroup) onCloseQuickGroup();
|
||||
}}
|
||||
>
|
||||
От производителя
|
||||
|
@ -45,6 +45,8 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
||||
}
|
||||
};
|
||||
|
||||
const [shownCounts, setShownCounts] = useState<{ [unitid: string]: number }>({});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* <button onClick={onBack} className="mb-4 px-4 py-2 bg-gray-200 rounded self-start">Назад</button> */}
|
||||
@ -71,16 +73,48 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
||||
</div>
|
||||
<div className="knot-img">
|
||||
<h1 className="heading-19">{unit.name}</h1>
|
||||
|
||||
{unit.details && unit.details.length > 0 && unit.details.map((detail: any, index: number) => (
|
||||
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
||||
<div className="oemnuber">{detail.oem}</div>
|
||||
<div className="partsname">{detail.name}</div>
|
||||
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<a href="#" className="showallparts w-button" onClick={e => { e.preventDefault(); handleUnitClick(unit); }}>Подробнее</a>
|
||||
{(() => {
|
||||
const details = unit.details || [];
|
||||
const total = details.length;
|
||||
const shownCount = shownCounts[unit.unitid] ?? 3;
|
||||
return (
|
||||
<>
|
||||
{details.slice(0, shownCount).map((detail: any, index: number) => (
|
||||
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
||||
<div className="oemnuber">{detail.oem}</div>
|
||||
<div className="partsname">{detail.name}</div>
|
||||
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
||||
</div>
|
||||
))}
|
||||
{total > 3 && shownCount < total && (
|
||||
<div className="flex gap-2 mt-2 w-full">
|
||||
{shownCount + 3 < total && (
|
||||
<button
|
||||
className="expand-btn"
|
||||
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: shownCount + 3 }))}
|
||||
style={{ border: '1px solid #EC1C24', borderRadius: 8, background: '#fff', color: '#222', padding: '6px 18px', minWidth: 180 }}
|
||||
>
|
||||
Развернуть
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" style={{ display: 'inline', verticalAlign: 'middle', marginLeft: 4 }}>
|
||||
<path d="M4 6l4 4 4-4" stroke="#222" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="showall-btn"
|
||||
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: total }))}
|
||||
style={{ background: '#e9eef5', borderRadius: 8, color: '#222', padding: '6px 18px', border: 'none'}}
|
||||
>
|
||||
Показать все
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{shownCount >= total && (
|
||||
<a href="#" className="showallparts w-button" onClick={e => { e.preventDefault(); handleUnitClick(unit); }}>Подробнее</a>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
@ -134,8 +134,13 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
|
||||
|
||||
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||
onCompleted: () => {
|
||||
toast.success('Товар удален из избранного', {
|
||||
toast('Товар удален из избранного', {
|
||||
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
||||
style: {
|
||||
background: '#6b7280', // Серый фон
|
||||
color: '#fff', // Белый текст
|
||||
},
|
||||
duration: 3000,
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
|
@ -1,5 +1,50 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const GET_BEST_PRICE_PRODUCTS = gql`
|
||||
query GetBestPriceProducts {
|
||||
bestPriceProducts {
|
||||
id
|
||||
productId
|
||||
discount
|
||||
isActive
|
||||
sortOrder
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
brand
|
||||
retailPrice
|
||||
images {
|
||||
url
|
||||
alt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_TOP_SALES_PRODUCTS = gql`
|
||||
query GetTopSalesProducts {
|
||||
topSalesProducts {
|
||||
id
|
||||
productId
|
||||
isActive
|
||||
sortOrder
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
brand
|
||||
retailPrice
|
||||
images {
|
||||
url
|
||||
alt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CHECK_CLIENT_BY_PHONE = gql`
|
||||
mutation CheckClientByPhone($phone: String!) {
|
||||
checkClientByPhone(phone: $phone) {
|
||||
@ -1606,4 +1651,58 @@ export const GET_CATEGORY_PRODUCTS_WITH_OFFERS = gql`
|
||||
hasOffers
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Запрос для получения товаров дня
|
||||
export const GET_DAILY_PRODUCTS = gql`
|
||||
query GetDailyProducts($displayDate: String!) {
|
||||
dailyProducts(displayDate: $displayDate) {
|
||||
id
|
||||
discount
|
||||
isActive
|
||||
sortOrder
|
||||
product {
|
||||
id
|
||||
name
|
||||
slug
|
||||
article
|
||||
brand
|
||||
retailPrice
|
||||
wholesalePrice
|
||||
images {
|
||||
id
|
||||
url
|
||||
alt
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Запрос для получения новых поступлений
|
||||
export const GET_NEW_ARRIVALS = gql`
|
||||
query GetNewArrivals($limit: Int) {
|
||||
newArrivals(limit: $limit) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
article
|
||||
brand
|
||||
retailPrice
|
||||
wholesalePrice
|
||||
createdAt
|
||||
images {
|
||||
id
|
||||
url
|
||||
alt
|
||||
order
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@ -34,7 +34,7 @@ export default function Home() {
|
||||
<MetaTags {...metaData} />
|
||||
<JsonLdScript schema={organizationSchema} />
|
||||
<JsonLdScript schema={websiteSchema} />
|
||||
<IndexTopMenuNav />
|
||||
{/* <IndexTopMenuNav /> */}
|
||||
<ProductOfDaySection />
|
||||
<CategoryNavSection />
|
||||
<BrandSelectionSection />
|
||||
|
@ -54,26 +54,40 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтр по цене
|
||||
const prices: number[] = [];
|
||||
// Получаем все доступные предложения для расчета диапазонов
|
||||
const allAvailableOffers: any[] = [];
|
||||
|
||||
// Добавляем основные предложения
|
||||
result.internalOffers?.forEach((offer: any) => {
|
||||
if (offer.price > 0) prices.push(offer.price);
|
||||
allAvailableOffers.push(offer);
|
||||
});
|
||||
result.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.price > 0) prices.push(offer.price);
|
||||
allAvailableOffers.push(offer);
|
||||
});
|
||||
|
||||
// Добавляем цены аналогов
|
||||
// Добавляем предложения аналогов
|
||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||
analog.internalOffers?.forEach((offer: any) => {
|
||||
if (offer.price > 0) prices.push(offer.price);
|
||||
allAvailableOffers.push({
|
||||
...offer,
|
||||
deliveryDuration: offer.deliveryDays
|
||||
});
|
||||
});
|
||||
analog.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.price > 0) prices.push(offer.price);
|
||||
allAvailableOffers.push({
|
||||
...offer,
|
||||
deliveryDuration: offer.deliveryTime
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (prices.length > 0) {
|
||||
// Фильтр по цене - только если есть предложения с разными ценами
|
||||
const prices: number[] = [];
|
||||
allAvailableOffers.forEach((offer: any) => {
|
||||
if (offer.price > 0) prices.push(offer.price);
|
||||
});
|
||||
|
||||
if (prices.length > 1) {
|
||||
const minPrice = Math.min(...prices);
|
||||
const maxPrice = Math.max(...prices);
|
||||
|
||||
@ -87,26 +101,14 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтр по сроку доставки
|
||||
// Фильтр по сроку доставки - только если есть предложения с разными сроками
|
||||
const deliveryDays: number[] = [];
|
||||
result.internalOffers?.forEach((offer: any) => {
|
||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
||||
});
|
||||
result.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
||||
});
|
||||
|
||||
// Добавляем сроки доставки аналогов
|
||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||
analog.internalOffers?.forEach((offer: any) => {
|
||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
||||
});
|
||||
analog.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
||||
});
|
||||
allAvailableOffers.forEach((offer: any) => {
|
||||
const days = offer.deliveryDays || offer.deliveryTime || offer.deliveryDuration;
|
||||
if (days && days > 0) deliveryDays.push(days);
|
||||
});
|
||||
|
||||
if (deliveryDays.length > 0) {
|
||||
if (deliveryDays.length > 1) {
|
||||
const minDays = Math.min(...deliveryDays);
|
||||
const maxDays = Math.max(...deliveryDays);
|
||||
|
||||
@ -120,26 +122,13 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтр по количеству наличия
|
||||
// Фильтр по количеству наличия - только если есть предложения с разными количествами
|
||||
const quantities: number[] = [];
|
||||
result.internalOffers?.forEach((offer: any) => {
|
||||
allAvailableOffers.forEach((offer: any) => {
|
||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||
});
|
||||
result.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||
});
|
||||
|
||||
// Добавляем количества аналогов
|
||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||
analog.internalOffers?.forEach((offer: any) => {
|
||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||
});
|
||||
analog.externalOffers?.forEach((offer: any) => {
|
||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||
});
|
||||
});
|
||||
|
||||
if (quantities.length > 0) {
|
||||
if (quantities.length > 1) {
|
||||
const minQuantity = Math.min(...quantities);
|
||||
const maxQuantity = Math.max(...quantities);
|
||||
|
||||
@ -163,35 +152,24 @@ const getBestOffers = (offers: any[]) => {
|
||||
if (validOffers.length === 0) return [];
|
||||
|
||||
const result: { offer: any; type: string }[] = [];
|
||||
const usedOfferIds = new Set<string>();
|
||||
|
||||
// 1. Самая низкая цена (среди всех предложений)
|
||||
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
|
||||
if (lowestPriceOffer) {
|
||||
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
|
||||
usedOfferIds.add(`${lowestPriceOffer.articleNumber}-${lowestPriceOffer.price}-${lowestPriceOffer.deliveryDuration}`);
|
||||
}
|
||||
|
||||
// 2. Самый дешевый аналог (только среди аналогов)
|
||||
// 2. Самый дешевый аналог (только среди аналогов) - всегда показываем если есть аналоги
|
||||
const analogOffers = validOffers.filter(offer => offer.isAnalog);
|
||||
if (analogOffers.length > 0) {
|
||||
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
|
||||
const analogId = `${cheapestAnalogOffer.articleNumber}-${cheapestAnalogOffer.price}-${cheapestAnalogOffer.deliveryDuration}`;
|
||||
|
||||
if (!usedOfferIds.has(analogId)) {
|
||||
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
||||
usedOfferIds.add(analogId);
|
||||
}
|
||||
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
||||
}
|
||||
|
||||
// 3. Самая быстрая доставка (среди всех предложений)
|
||||
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
|
||||
if (fastestDeliveryOffer) {
|
||||
const fastestId = `${fastestDeliveryOffer.articleNumber}-${fastestDeliveryOffer.price}-${fastestDeliveryOffer.deliveryDuration}`;
|
||||
|
||||
if (!usedOfferIds.has(fastestId)) {
|
||||
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
||||
}
|
||||
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -376,7 +354,101 @@ export default function SearchResult() {
|
||||
|
||||
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
|
||||
const hasAnalogs = result && result.analogs.length > 0;
|
||||
const searchResultFilters = createFilters(result, loadedAnalogs);
|
||||
|
||||
// Создаем динамические фильтры на основе доступных данных с учетом активных фильтров
|
||||
const searchResultFilters = useMemo(() => {
|
||||
const baseFilters = createFilters(result, loadedAnalogs);
|
||||
|
||||
// Если нет активных фильтров, возвращаем базовые фильтры
|
||||
if (!filtersAreActive) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
// Создаем динамические фильтры с учетом других активных фильтров
|
||||
return baseFilters.map(filter => {
|
||||
if (filter.type !== 'range') {
|
||||
return filter;
|
||||
}
|
||||
|
||||
// Для каждого диапазонного фильтра пересчитываем границы на основе
|
||||
// предложений, отфильтрованных другими фильтрами (исключая текущий)
|
||||
let relevantOffers = allOffers;
|
||||
|
||||
// Применяем все фильтры кроме текущего
|
||||
relevantOffers = allOffers.filter(offer => {
|
||||
// Фильтр по бренду (если это не фильтр производителя)
|
||||
if (filter.title !== 'Производитель' && selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
|
||||
return false;
|
||||
}
|
||||
// Фильтр по цене (если это не фильтр цены)
|
||||
if (filter.title !== 'Цена (₽)' && priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
|
||||
return false;
|
||||
}
|
||||
// Фильтр по сроку доставки (если это не фильтр доставки)
|
||||
if (filter.title !== 'Срок доставки (дни)' && deliveryRange) {
|
||||
const deliveryDays = offer.deliveryDuration;
|
||||
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по количеству (если это не фильтр количества)
|
||||
if (filter.title !== 'Количество (шт.)' && quantityRange) {
|
||||
const quantity = offer.quantity;
|
||||
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Фильтр по поисковой строке
|
||||
if (filterSearchTerm) {
|
||||
const searchTerm = filterSearchTerm.toLowerCase();
|
||||
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
|
||||
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
|
||||
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
|
||||
if (!brandMatch && !articleMatch && !nameMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Пересчитываем диапазон на основе отфильтрованных предложений
|
||||
if (filter.title === 'Цена (₽)') {
|
||||
const prices = relevantOffers.filter(o => o.price > 0).map(o => o.price);
|
||||
if (prices.length > 0) {
|
||||
return {
|
||||
...filter,
|
||||
min: Math.floor(Math.min(...prices)),
|
||||
max: Math.ceil(Math.max(...prices))
|
||||
};
|
||||
}
|
||||
} else if (filter.title === 'Срок доставки (дни)') {
|
||||
const deliveryDays = relevantOffers
|
||||
.map(o => o.deliveryDuration)
|
||||
.filter(d => d && d > 0);
|
||||
if (deliveryDays.length > 0) {
|
||||
return {
|
||||
...filter,
|
||||
min: Math.min(...deliveryDays),
|
||||
max: Math.max(...deliveryDays)
|
||||
};
|
||||
}
|
||||
} else if (filter.title === 'Количество (шт.)') {
|
||||
const quantities = relevantOffers
|
||||
.map(o => o.quantity)
|
||||
.filter(q => q && q > 0);
|
||||
if (quantities.length > 0) {
|
||||
return {
|
||||
...filter,
|
||||
min: Math.min(...quantities),
|
||||
max: Math.max(...quantities)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
});
|
||||
}, [result, loadedAnalogs, filtersAreActive, allOffers, selectedBrands, priceRange, deliveryRange, quantityRange, filterSearchTerm]);
|
||||
|
||||
const bestOffersData = getBestOffers(filteredOffers);
|
||||
|
||||
|
||||
@ -406,6 +478,8 @@ export default function SearchResult() {
|
||||
}
|
||||
}, [q, article, router.query]);
|
||||
|
||||
|
||||
|
||||
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
|
||||
|
||||
const minPrice = useMemo(() => {
|
||||
@ -452,44 +526,52 @@ export default function SearchResult() {
|
||||
return (
|
||||
<>
|
||||
<MetaTags {...metaData} />
|
||||
<InfoSearch
|
||||
brand={result ? result.brand : brandQuery}
|
||||
articleNumber={result ? result.articleNumber : searchQuery}
|
||||
name={result ? result.name : "деталь"}
|
||||
offersCount={result ? result.totalOffers : 0}
|
||||
minPrice={minPrice}
|
||||
/>
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-84">
|
||||
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
||||
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
||||
<span className="code-embed-9 w-embed">
|
||||
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div>Фильтры</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Мобильная панель фильтров */}
|
||||
<FiltersPanelMobile
|
||||
filters={searchResultFilters}
|
||||
open={showFiltersMobile}
|
||||
onClose={() => setShowFiltersMobile(false)}
|
||||
searchQuery={filterSearchTerm}
|
||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||
/>
|
||||
{/* Показываем InfoSearch только если есть результаты */}
|
||||
{initialOffersExist && (
|
||||
<InfoSearch
|
||||
brand={result ? result.brand : brandQuery}
|
||||
articleNumber={result ? result.articleNumber : searchQuery}
|
||||
name={result ? result.name : "деталь"}
|
||||
offersCount={result ? result.totalOffers : 0}
|
||||
minPrice={minPrice}
|
||||
/>
|
||||
)}
|
||||
{/* Показываем мобильные фильтры только если есть результаты */}
|
||||
{initialOffersExist && (
|
||||
<>
|
||||
<section className="main mobile-only">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-84">
|
||||
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
||||
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
||||
<span className="code-embed-9 w-embed">
|
||||
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div>Фильтры</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Мобильная панель фильтров */}
|
||||
<FiltersPanelMobile
|
||||
filters={searchResultFilters}
|
||||
open={showFiltersMobile}
|
||||
onClose={() => setShowFiltersMobile(false)}
|
||||
searchQuery={filterSearchTerm}
|
||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* Лучшие предложения */}
|
||||
{bestOffersData.length > 0 && (
|
||||
<section className="section-6">
|
||||
@ -547,24 +629,26 @@ export default function SearchResult() {
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-13-copy">
|
||||
{/* Фильтры для десктопа */}
|
||||
<div style={{ width: '300px', marginRight: '20px' }}>
|
||||
<Filters
|
||||
filters={searchResultFilters}
|
||||
onFilterChange={handleFilterChange}
|
||||
filterValues={{
|
||||
'Производитель': selectedBrands,
|
||||
'Цена (₽)': priceRange,
|
||||
'Срок доставки (дни)': deliveryRange,
|
||||
'Количество (шт.)': quantityRange
|
||||
}}
|
||||
searchQuery={filterSearchTerm}
|
||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Показываем основную секцию с фильтрами только если есть результаты */}
|
||||
{initialOffersExist && (
|
||||
<section className="main">
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<div className="w-layout-hflex flex-block-13-copy">
|
||||
{/* Фильтры для десктопа */}
|
||||
<div style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
|
||||
<Filters
|
||||
filters={searchResultFilters}
|
||||
onFilterChange={handleFilterChange}
|
||||
filterValues={{
|
||||
'Производитель': selectedBrands,
|
||||
'Цена (₽)': priceRange,
|
||||
'Срок доставки (дни)': deliveryRange,
|
||||
'Количество (шт.)': quantityRange
|
||||
}}
|
||||
searchQuery={filterSearchTerm}
|
||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Основной товар */}
|
||||
<div className="w-layout-vflex flex-block-14-copy">
|
||||
@ -578,9 +662,8 @@ export default function SearchResult() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Используем фотографию из Parts Index, если она есть, иначе fallback на mainImageUrl
|
||||
// Используем фотографию только из Parts Index, если она есть
|
||||
const partsIndexImage = entityInfo?.images?.[0];
|
||||
const displayImage = partsIndexImage || mainImageUrl;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -588,7 +671,7 @@ export default function SearchResult() {
|
||||
brand={result.brand}
|
||||
article={result.articleNumber}
|
||||
name={result.name}
|
||||
image={displayImage}
|
||||
{...(partsIndexImage ? { image: partsIndexImage } : {})}
|
||||
offers={mainProductOffers}
|
||||
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
|
||||
partsIndexPowered={!!partsIndexImage}
|
||||
@ -737,9 +820,13 @@ export default function SearchResult() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="section-3">
|
||||
<CatalogSubscribe />
|
||||
</section>
|
||||
)}
|
||||
{/* Показываем CatalogSubscribe только если есть результаты */}
|
||||
{initialOffersExist && (
|
||||
<section className="section-3">
|
||||
<CatalogSubscribe />
|
||||
</section>
|
||||
)}
|
||||
<Footer />
|
||||
<MobileMenuBottomSection />
|
||||
</>
|
||||
|
@ -199,7 +199,7 @@ const SearchPage = () => {
|
||||
<div key={detail.detailid || index}>
|
||||
<button
|
||||
onClick={() => handlePartDetail(detail)}
|
||||
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
||||
className="w-full text-left p-4 hover:bg-slate-200 transition-colors block group"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>{detail.manufacturer}</div>
|
||||
@ -252,7 +252,7 @@ const SearchPage = () => {
|
||||
{vehiclesResult!.catalogs.map((catalog) => (
|
||||
<tr
|
||||
key={catalog.catalogCode}
|
||||
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
className="hover:bg-slate-200 cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
router.push(`/search-result?article=${encodeURIComponent(searchQuery)}&brand=${encodeURIComponent(catalog.brand)}`);
|
||||
}}
|
||||
|
@ -74,6 +74,8 @@ const VehicleDetailsPage = () => {
|
||||
});
|
||||
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
||||
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
||||
const [selectedParts, setSelectedParts] = useState<Set<string | number>>(new Set());
|
||||
const [highlightedPart, setHighlightedPart] = useState<string | number | null>(null);
|
||||
|
||||
// Получаем информацию о выбранном автомобиле
|
||||
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
|
||||
@ -138,6 +140,20 @@ const VehicleDetailsPage = () => {
|
||||
);
|
||||
|
||||
// Получаем детали выбранного узла, если он выбран
|
||||
console.log('🔍 [vehicleId].tsx - Проверка условий для GET_LAXIMO_UNIT_DETAILS:', {
|
||||
selectedNode: selectedNode ? {
|
||||
unitid: selectedNode.unitid,
|
||||
name: selectedNode.name,
|
||||
hasSsd: !!selectedNode.ssd
|
||||
} : null,
|
||||
skipCondition: !selectedNode,
|
||||
catalogCode: selectedNode?.catalogCode || selectedNode?.catalog || brand,
|
||||
vehicleId: selectedNode?.vehicleId || vehicleId,
|
||||
unitId: selectedNode?.unitid || selectedNode?.unitId,
|
||||
ssd: selectedNode?.ssd || finalSsd || '',
|
||||
finalSsd: finalSsd ? `${finalSsd.substring(0, 50)}...` : 'отсутствует'
|
||||
});
|
||||
|
||||
const {
|
||||
data: unitDetailsData,
|
||||
loading: unitDetailsLoading,
|
||||
@ -155,6 +171,23 @@ const VehicleDetailsPage = () => {
|
||||
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
|
||||
skip: !selectedNode,
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
onCompleted: (data) => {
|
||||
console.log('🔍 [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS completed:', {
|
||||
detailsCount: data?.laximoUnitDetails?.length || 0,
|
||||
firstDetail: data?.laximoUnitDetails?.[0],
|
||||
allDetails: data?.laximoUnitDetails?.map((detail: any) => ({
|
||||
name: detail.name,
|
||||
oem: detail.oem,
|
||||
codeonimage: detail.codeonimage,
|
||||
attributesCount: detail.attributes?.length || 0
|
||||
}))
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS error:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -234,6 +267,22 @@ const VehicleDetailsPage = () => {
|
||||
|
||||
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
|
||||
|
||||
// Детальное логирование данных от API
|
||||
React.useEffect(() => {
|
||||
if (unitDetailsData?.laximoUnitDetails) {
|
||||
console.log('🔍 [vehicleId].tsx - Полные данные unitDetails от API:', {
|
||||
totalParts: unitDetailsData.laximoUnitDetails.length,
|
||||
firstPart: unitDetailsData.laximoUnitDetails[0],
|
||||
allCodeOnImages: unitDetailsData.laximoUnitDetails.map((part: any) => ({
|
||||
name: part.name,
|
||||
codeonimage: part.codeonimage,
|
||||
detailid: part.detailid,
|
||||
oem: part.oem
|
||||
}))
|
||||
});
|
||||
}
|
||||
}, [unitDetailsData]);
|
||||
|
||||
// Логируем ошибки
|
||||
if (vehicleError) {
|
||||
console.error('Vehicle GraphQL error:', vehicleError);
|
||||
@ -355,15 +404,17 @@ const VehicleDetailsPage = () => {
|
||||
{ shallow: true }
|
||||
);
|
||||
};
|
||||
// Функция для закрытия VinQuick и удаления quickgroup из URL
|
||||
// --- Сброс VinQuick (selectedQuickGroup) и quickgroup в URL ---
|
||||
const closeQuickGroup = () => {
|
||||
setSelectedQuickGroup(null);
|
||||
const { quickgroup, ...rest } = router.query;
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: rest },
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
if (quickgroup) {
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: rest },
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Синхронизация selectedNode (KnotIn) с URL ---
|
||||
@ -380,6 +431,9 @@ const VehicleDetailsPage = () => {
|
||||
});
|
||||
|
||||
setSelectedNode(node);
|
||||
// Сброс состояния выбранных деталей при открытии нового узла
|
||||
setSelectedParts(new Set());
|
||||
setHighlightedPart(null);
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: { ...router.query, unitid: node.unitid || node.id } },
|
||||
undefined,
|
||||
@ -389,6 +443,9 @@ const VehicleDetailsPage = () => {
|
||||
// Закрыть KnotIn и удалить unitid из URL
|
||||
const closeKnot = () => {
|
||||
setSelectedNode(null);
|
||||
// Сброс состояния выбранных деталей при закрытии узла
|
||||
setSelectedParts(new Set());
|
||||
setHighlightedPart(null);
|
||||
const { unitid, ...rest } = router.query;
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: rest },
|
||||
@ -397,6 +454,25 @@ const VehicleDetailsPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Обработчик выбора детали (множественный выбор)
|
||||
const handlePartSelect = (codeOnImage: string | number | null) => {
|
||||
if (codeOnImage === null) return; // Игнорируем null значения
|
||||
setSelectedParts(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(codeOnImage)) {
|
||||
newSet.delete(codeOnImage); // Убираем если уже выбрана
|
||||
} else {
|
||||
newSet.add(codeOnImage); // Добавляем если не выбрана
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Обработчик подсветки детали при наведении
|
||||
const handlePartHighlight = (codeOnImage: string | number | null) => {
|
||||
setHighlightedPart(codeOnImage);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags {...metaData} />
|
||||
@ -448,6 +524,7 @@ const VehicleDetailsPage = () => {
|
||||
activeTab={activeTab}
|
||||
openedPath={openedPath}
|
||||
setOpenedPath={setOpenedPath}
|
||||
onCloseQuickGroup={closeQuickGroup}
|
||||
/>
|
||||
{searchState.isSearching ? (
|
||||
<div className="knot-parts">
|
||||
@ -548,6 +625,9 @@ const VehicleDetailsPage = () => {
|
||||
unitId={selectedNode.unitid}
|
||||
unitName={selectedNode.name}
|
||||
parts={unitDetails}
|
||||
onPartSelect={handlePartSelect}
|
||||
onPartsHighlight={handlePartHighlight}
|
||||
selectedParts={selectedParts}
|
||||
/>
|
||||
{unitDetailsLoading ? (
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
||||
@ -558,6 +638,10 @@ const VehicleDetailsPage = () => {
|
||||
parts={unitDetails}
|
||||
catalogCode={vehicleInfo.catalog}
|
||||
vehicleId={vehicleInfo.vehicleid}
|
||||
highlightedCodeOnImage={highlightedPart}
|
||||
selectedParts={selectedParts}
|
||||
onPartSelect={handlePartSelect}
|
||||
onPartHover={handlePartHighlight}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
|
||||
|
@ -11,11 +11,15 @@ import MetaTags from '@/components/MetaTags';
|
||||
import { getMetaByPath } from '@/lib/meta-config';
|
||||
|
||||
const InfoBrandSelection = ({
|
||||
brand,
|
||||
brandName,
|
||||
vehicleId,
|
||||
oemNumber,
|
||||
detailName
|
||||
}: {
|
||||
brand: string;
|
||||
brandName: string;
|
||||
vehicleId: string;
|
||||
oemNumber: string;
|
||||
detailName?: string;
|
||||
}) => (
|
||||
@ -27,20 +31,22 @@ const InfoBrandSelection = ({
|
||||
<div>Главная</div>
|
||||
</a>
|
||||
<div className="text-block-3">→</div>
|
||||
<a href="#" className="link-block-2 w-inline-block">
|
||||
<a href="#" className="link-block w-inline-block">
|
||||
<div>Каталог</div>
|
||||
</a>
|
||||
<div className="text-block-3">→</div>
|
||||
<div>{brandName}</div>
|
||||
<a href={`/vehicle-search/${brand}/${vehicleId}`} className="link-block w-inline-block">
|
||||
<div>{brandName}</div>
|
||||
</a>
|
||||
<div className="text-block-3">→</div>
|
||||
<div>Деталь {oemNumber}</div>
|
||||
<div className="text-block-3">→</div>
|
||||
<div>Выбор производителя</div>
|
||||
<a href="#" className="link-block-2 w-inline-block">
|
||||
<div>Деталь {oemNumber}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-8">
|
||||
<div className="w-layout-hflex flex-block-10">
|
||||
<h1 className="heading">Выберите производителя для {oemNumber}</h1>
|
||||
</div>
|
||||
<div className="link-block w-inline-block">
|
||||
|
||||
<div className="heading">Выберите производителя для {oemNumber}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,15 +129,17 @@ const BrandSelectionPage = () => {
|
||||
<>
|
||||
<MetaTags {...metaData} />
|
||||
<InfoBrandSelection
|
||||
brand={String(brand)}
|
||||
brandName={catalogInfo?.name || String(brand)}
|
||||
vehicleId={String(vehicleId)}
|
||||
oemNumber={String(oemNumber)}
|
||||
detailName={String(detailName || '')}
|
||||
/>
|
||||
<div className="page-wrapper bg-[#F5F8FB] min-h-screen">
|
||||
<div className="w-full max-w-[1580px] mx-auto px-8 max-md:px-5 pt-10 pb-16">
|
||||
<div className="mx-auto px-8 max-md:px-5 pt-10 pb-16 ">
|
||||
|
||||
{/* Кнопка назад */}
|
||||
<div className="mb-6">
|
||||
{/* <div className="mb-6">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
|
||||
@ -141,7 +149,7 @@ const BrandSelectionPage = () => {
|
||||
</svg>
|
||||
Назад к деталям
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Обработка ошибок */}
|
||||
{hasError && !loading && (
|
||||
@ -187,40 +195,40 @@ const BrandSelectionPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
) : brands.length > 0 && (
|
||||
<div className="bg-white rounded-2xl shadow p-10">
|
||||
<div className="border-b border-gray-200 pb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Выбор производителя для артикула: {oemNumber}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{detailName && <span>Деталь: {detailName} • </span>}
|
||||
Найдено производителей: <span className="font-medium">{brands.length}</span>
|
||||
</p>
|
||||
<div className="bg-white rounded-2xl shadow p-10 w-full max-w-[1580px] mx-auto min-h-[500px]">
|
||||
{/* <div className="border-b border-gray-200 pb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Выбор производителя для артикула: {oemNumber}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{detailName && <span>Деталь: {detailName} • </span>}
|
||||
Найдено производителей: <span className="font-medium">{brands.length}</span>
|
||||
</p>
|
||||
</div> */}
|
||||
<div className="divide-y divide-gray-200">
|
||||
{brands.map((brandItem: any, index: number) => (
|
||||
<div key={index}>
|
||||
<button
|
||||
onClick={() => handleBrandSelect(brandItem.brand)}
|
||||
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>
|
||||
{brandItem.brand}
|
||||
</div>
|
||||
<div className="w-1/5 max-md:text-center max-md:w-1/3 font-bold text-left truncate group-hover:text-[#EC1C24] transition-colors">
|
||||
{oemNumber}
|
||||
</div>
|
||||
<div className="w-3/5 max-md:w-1/3 text-left truncate">
|
||||
{brandItem.name && brandItem.name !== brandItem.brand ? brandItem.name : detailName || 'Запчасть'}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200">
|
||||
{brands.map((brandItem: any, index: number) => (
|
||||
<div key={index}>
|
||||
<button
|
||||
onClick={() => handleBrandSelect(brandItem.brand)}
|
||||
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>
|
||||
{brandItem.brand}
|
||||
</div>
|
||||
<div className="w-1/5 max-md:text-center max-md:w-1/3 font-bold text-left truncate group-hover:text-[#EC1C24] transition-colors">
|
||||
{oemNumber}
|
||||
</div>
|
||||
<div className="w-3/5 max-md:w-1/3 text-left truncate">
|
||||
{brandItem.name && brandItem.name !== brandItem.brand ? brandItem.name : detailName || 'Запчасть'}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -100,4 +100,281 @@ input[type=number] {
|
||||
|
||||
.cookie-consent-enter {
|
||||
animation: slideInFromBottom 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Анимации для тултипов */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fade-in-0 {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.zoom-in-95 {
|
||||
animation-name: zoomIn;
|
||||
}
|
||||
|
||||
.duration-200 {
|
||||
animation-duration: 200ms;
|
||||
}
|
||||
|
||||
/* Стили для кнопок с курсором pointer */
|
||||
button,
|
||||
.cursor-pointer,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== СОВРЕМЕННЫЕ СТИЛИ ДЛЯ КРАСИВОГО ТУЛТИПА ===== */
|
||||
|
||||
.tooltip-detail-modern {
|
||||
animation: tooltip-modern-fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.tooltip-content-modern {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
max-width: 420px;
|
||||
min-width: 280px;
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip-header-modern {
|
||||
background: linear-gradient(135deg, #EC1C24 0%, #DC1C24 100%);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-header-modern::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-icon {
|
||||
color: white;
|
||||
opacity: 0.9;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tooltip-title-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tooltip-oem-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.tooltip-oem-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.tooltip-oem-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.tooltip-body-modern {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tooltip-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tooltip-section-title svg {
|
||||
color: #EC1C24;
|
||||
}
|
||||
|
||||
.tooltip-attributes-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltip-attribute-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tooltip-attribute-item:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tooltip-attribute-key {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tooltip-attribute-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tooltip-note-modern {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tooltip-note-text {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fbbf24;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #92400e;
|
||||
line-height: 1.5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tooltip-no-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip-no-data-icon {
|
||||
color: #94a3b8;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tooltip-no-data-text {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tooltip-no-data-text div:first-child {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltip-no-data-text div:last-child {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes tooltip-modern-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 480px) {
|
||||
.tooltip-content-modern {
|
||||
max-width: 320px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.tooltip-header-modern {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.tooltip-body-modern {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
@ -23,14 +23,14 @@
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: -15px;
|
||||
/* margin-top: -15px; */
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bottom_head{
|
||||
z-index: 60;
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.top_head{
|
||||
@ -45,6 +45,14 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.price-in-cart-s1 {
|
||||
|
||||
max-width: 140px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
input.text-block-31 {
|
||||
background: none !important;
|
||||
}
|
||||
@ -180,7 +188,7 @@ input.text-block-31 {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.form-block, .text-field.w-input {
|
||||
.form-block {
|
||||
min-height: 44px !important;
|
||||
}
|
||||
|
||||
@ -440,6 +448,12 @@ input#VinSearchInput {
|
||||
}
|
||||
|
||||
|
||||
.w-input {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.text-block-56, .dropdown-link-3 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -600,6 +614,52 @@ body {
|
||||
font-family: Onest, sans-serif;
|
||||
}
|
||||
|
||||
.heading{
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.form-block-4,
|
||||
.flex-block-124,
|
||||
.flex-block-6-copy
|
||||
{
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
a.link-block.w-inline-block,
|
||||
a.link-block-2.w-inline-block {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.core-product-search.carousel-scroll {
|
||||
display: flex;
|
||||
flex-wrap: nowrap; /* Не переносить строки */
|
||||
gap: 16px; /* Отступ между карточками, если нужно */
|
||||
}
|
||||
|
||||
.subscribe{
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 10px !important;
|
||||
|
||||
}
|
||||
|
||||
.text-block-14, .div-block-9{
|
||||
width: 350px !important;
|
||||
max-width: 350px !important;
|
||||
min-width: 350px !important;
|
||||
}
|
||||
@media screen and (max-width: 1920px) {
|
||||
.text-block-14, .div-block-9{
|
||||
width: 350px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.flex-block-18{
|
||||
row-gap: 40px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-button.w--open {
|
||||
z-index: 2000;
|
||||
@ -607,10 +667,9 @@ body {
|
||||
color: var(--white);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
left: auto;
|
||||
width: 50px;
|
||||
height: 44px;
|
||||
padding: 13px 12px;
|
||||
}
|
||||
.heading-7 {
|
||||
z-index: 999;
|
||||
@ -643,8 +702,25 @@ body {
|
||||
}
|
||||
|
||||
|
||||
.knot-parts {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flex-block-14-copy-copy{
|
||||
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.showall-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.showall-btn:hover {
|
||||
background: #ec1c24 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.flex-block-108 {
|
||||
.flex-block-108, .flex-block-14-copy-copy {
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -805,6 +881,12 @@ body {
|
||||
min-width: 160px !important;
|
||||
padding: 15px;
|
||||
}
|
||||
.div-block-3 {
|
||||
height: 102px !important;
|
||||
}
|
||||
.div-block-3.bp-item-info {
|
||||
height: 90px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -825,6 +907,10 @@ body {
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.knot-parts {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -845,4 +931,115 @@ body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1440px) {
|
||||
.image-27 {
|
||||
margin-bottom: -212px;
|
||||
margin-left: 800px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.topnav.w-nav {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.code-embed-15.w-embed {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.topmenub[style*='#fff'] .link-block-8 {
|
||||
border: 1px solid #E6EDF6 !important;
|
||||
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.topmenub-white .link-block-8 {
|
||||
border: 1px solid #E6EDF6 !important;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.container.info {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.carousel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.carousel-arrow {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0.85;
|
||||
z-index: 2;
|
||||
}
|
||||
.carousel-arrow:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.carousel-arrow[disabled] {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.carousel-arrow-left {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.carousel-arrow-right {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.carousel-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex: 1 1 auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE и Edge */
|
||||
}
|
||||
.carousel-scroll::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.carousel-scroll {
|
||||
gap: 12px;
|
||||
}
|
||||
.carousel-row {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.carousel-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mobile-only {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.protekauto-logo {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
}
|
@ -1008,10 +1008,9 @@ body {
|
||||
color: var(--white);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
left: auto;
|
||||
width: 50px;
|
||||
height: 44px;
|
||||
padding: 13px 12px;
|
||||
}
|
||||
|
||||
.menu-button.w--open:hover {
|
||||
@ -6897,7 +6896,7 @@ body {
|
||||
}
|
||||
|
||||
.image-27 {
|
||||
margin-bottom: -218px;
|
||||
margin-bottom: -212px;
|
||||
margin-left: 800px;
|
||||
}
|
||||
}
|
||||
@ -7248,7 +7247,7 @@ body {
|
||||
.image-27 {
|
||||
margin-left: 1000px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.container, .container.nav, .container.info {
|
||||
|
Reference in New Issue
Block a user