Добавлено получение баннеров для главного слайдера с использованием GraphQL. Обновлен компонент HeroSlider для отображения активных баннеров с сортировкой. Реализована логика отображения дефолтного баннера при отсутствии данных. Обновлены стили и структура компонента для улучшения пользовательского интерфейса.
This commit is contained in:
@ -3,16 +3,17 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
interface CatalogSortDropdownProps {
|
||||
active: number;
|
||||
onChange: (index: number) => void;
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
const sortOptions = [
|
||||
const defaultSortOptions = [
|
||||
'По популярности',
|
||||
'Сначала дешевле',
|
||||
'Сначала дороже',
|
||||
'Высокий рейтинг',
|
||||
];
|
||||
|
||||
const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onChange }) => {
|
||||
const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onChange, options = defaultSortOptions }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -52,7 +53,7 @@ const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onCha
|
||||
<div>Сортировка</div>
|
||||
</div>
|
||||
<nav className={`dropdown-list-2 w-dropdown-list${isOpen ? ' w--open' : ''}`} style={{ minWidth: 180, whiteSpace: 'normal' }}>
|
||||
{sortOptions.map((option, index) => (
|
||||
{options.map((option: string, index: number) => (
|
||||
<a
|
||||
key={index}
|
||||
href="#"
|
||||
|
28
src/components/CloseIcon.tsx
Normal file
28
src/components/CloseIcon.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CloseIconProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const CloseIcon: React.FC<CloseIconProps> = ({ size = 20, color = '#fff' }) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 6L6 18M6 6L18 18"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloseIcon;
|
@ -3,6 +3,7 @@ import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
import CartIcon from "./CartIcon";
|
||||
import { isDeliveryDate } from "@/lib/utils";
|
||||
|
||||
const INITIAL_OFFERS_LIMIT = 5;
|
||||
|
||||
@ -50,6 +51,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
||||
const [sortBy, setSortBy] = useState<'stock' | 'delivery' | 'price'>('price'); // Локальная сортировка для каждого товара
|
||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||
);
|
||||
@ -63,8 +65,52 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
setQuantities(offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {}));
|
||||
}, [offers.length]);
|
||||
|
||||
const displayedOffers = offers.slice(0, visibleOffersCount);
|
||||
const hasMoreOffers = visibleOffersCount < offers.length;
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
return parseFloat(cleanPrice) || 0;
|
||||
};
|
||||
|
||||
// Функция для парсинга количества в наличии
|
||||
const parseStock = (stockStr: string): number => {
|
||||
const match = stockStr.match(/\d+/);
|
||||
return match ? parseInt(match[0]) : 0;
|
||||
};
|
||||
|
||||
// Функция для парсинга времени доставки
|
||||
const parseDeliveryTime = (daysStr: string): string => {
|
||||
// Если это дата (содержит название месяца), возвращаем как есть
|
||||
if (isDeliveryDate(daysStr)) {
|
||||
return daysStr;
|
||||
}
|
||||
// Иначе парсим как количество дней (для обратной совместимости)
|
||||
const match = daysStr.match(/\d+/);
|
||||
return match ? `${match[0]} дней` : daysStr;
|
||||
};
|
||||
|
||||
// Функция сортировки предложений
|
||||
const sortOffers = (offers: CoreProductCardOffer[]) => {
|
||||
const sorted = [...offers];
|
||||
|
||||
switch (sortBy) {
|
||||
case 'stock':
|
||||
return sorted.sort((a, b) => parseStock(b.pcs) - parseStock(a.pcs));
|
||||
case 'delivery':
|
||||
return sorted.sort((a, b) => {
|
||||
const aDelivery = a.deliveryTime || 999;
|
||||
const bDelivery = b.deliveryTime || 999;
|
||||
return aDelivery - bDelivery;
|
||||
});
|
||||
case 'price':
|
||||
return sorted.sort((a, b) => parsePrice(a.price) - parsePrice(b.price));
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
};
|
||||
|
||||
const sortedOffers = sortOffers(offers);
|
||||
const displayedOffers = sortedOffers.slice(0, visibleOffersCount);
|
||||
const hasMoreOffers = visibleOffersCount < sortedOffers.length;
|
||||
|
||||
// Проверяем, есть ли товар в избранном
|
||||
const isItemFavorite = isFavorite(
|
||||
@ -74,24 +120,6 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
brand
|
||||
);
|
||||
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
return parseFloat(cleanPrice) || 0;
|
||||
};
|
||||
|
||||
// Функция для парсинга времени доставки
|
||||
const parseDeliveryTime = (daysStr: string): string => {
|
||||
const match = daysStr.match(/\d+/);
|
||||
return match ? `${match[0]} дней` : daysStr;
|
||||
};
|
||||
|
||||
// Функция для парсинга количества в наличии
|
||||
const parseStock = (stockStr: string): number => {
|
||||
const match = stockStr.match(/\d+/);
|
||||
return match ? parseInt(match[0]) : 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (idx: number, val: string) => {
|
||||
setInputValues(prev => ({ ...prev, [idx]: val }));
|
||||
if (val === "") return;
|
||||
@ -316,10 +344,28 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
</div>
|
||||
<div className="w-layout-hflex sort-list-s1">
|
||||
<div className="w-layout-hflex flex-block-49">
|
||||
<div className="sort-item first">Наличие</div>
|
||||
<div className="sort-item">Доставка</div>
|
||||
<div
|
||||
className={`sort-item first ${sortBy === 'stock' ? 'active' : ''}`}
|
||||
onClick={() => setSortBy('stock')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Наличие
|
||||
</div>
|
||||
<div
|
||||
className={`sort-item ${sortBy === 'delivery' ? 'active' : ''}`}
|
||||
onClick={() => setSortBy('delivery')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Доставим
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`sort-item price ${sortBy === 'price' ? 'active' : ''}`}
|
||||
onClick={() => setSortBy('price')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Цена
|
||||
</div>
|
||||
<div className="sort-item price">Цена</div>
|
||||
</div>
|
||||
{displayedOffers.map((offer, idx) => {
|
||||
const isLast = idx === displayedOffers.length - 1;
|
||||
@ -414,7 +460,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
className="w-layout-hflex show-more-search"
|
||||
onClick={() => {
|
||||
if (hasMoreOffers) {
|
||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
||||
setVisibleOffersCount(prev => Math.min(prev + 10, sortedOffers.length));
|
||||
} else {
|
||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||
}
|
||||
@ -422,11 +468,11 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
style={{ cursor: 'pointer' }}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
||||
aria-label={hasMoreOffers ? `Еще ${sortedOffers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
if (hasMoreOffers) {
|
||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
||||
setVisibleOffersCount(prev => Math.min(prev + 10, sortedOffers.length));
|
||||
} else {
|
||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||
}
|
||||
@ -434,7 +480,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className="text-block-27">
|
||||
{hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
||||
{hasMoreOffers ? `Еще ${sortedOffers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
||||
</div>
|
||||
<img
|
||||
src="/images/arrow_drop_down.svg"
|
||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { toast } from "react-hot-toast";
|
||||
import CartIcon from "../CartIcon";
|
||||
import { isDeliveryDate } from "@/lib/utils";
|
||||
|
||||
interface ProductBuyBlockProps {
|
||||
offer?: any;
|
||||
@ -51,7 +52,9 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
|
||||
brand: offer.brand,
|
||||
article: offer.articleNumber,
|
||||
supplier: offer.supplier || (offer.type === 'external' ? 'AutoEuro' : 'Внутренний'),
|
||||
deliveryTime: offer.deliveryTime ? String(offer.deliveryTime) + ' дней' : '1 день',
|
||||
deliveryTime: offer.deliveryTime ? (typeof offer.deliveryTime === 'string' && isDeliveryDate(offer.deliveryTime)
|
||||
? offer.deliveryTime
|
||||
: String(offer.deliveryTime) + ' дней') : '1 день',
|
||||
isExternal: offer.type === 'external'
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { isDeliveryDate } from "@/lib/utils";
|
||||
|
||||
interface ProductInfoProps {
|
||||
offer?: any;
|
||||
@ -17,6 +18,11 @@ const ProductInfo: React.FC<ProductInfoProps> = ({ offer }) => {
|
||||
|
||||
// Форматируем срок доставки
|
||||
const formatDeliveryTime = (deliveryTime: number | string) => {
|
||||
// Если это уже дата (содержит название месяца), возвращаем как есть
|
||||
if (typeof deliveryTime === 'string' && isDeliveryDate(deliveryTime)) {
|
||||
return deliveryTime;
|
||||
}
|
||||
|
||||
const days = typeof deliveryTime === 'string' ? parseInt(deliveryTime) : deliveryTime;
|
||||
|
||||
if (!days || days === 0) {
|
||||
|
@ -1,39 +1,141 @@
|
||||
import React from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const CategoryNavSection: React.FC = () => (
|
||||
<section className="catnav">
|
||||
<div className="w-layout-blockcontainer batd w-container">
|
||||
<div className="w-layout-hflex flex-block-108-copy">
|
||||
<div className="ci1">
|
||||
<div className="text-block-54-copy">Детали для ТО</div>
|
||||
interface PartsIndexCatalog {
|
||||
id: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
groups?: PartsIndexGroup[];
|
||||
}
|
||||
|
||||
interface PartsIndexGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
subgroups?: PartsIndexSubgroup[];
|
||||
entityNames?: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
interface PartsIndexSubgroup {
|
||||
id: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
entityNames?: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
const CategoryNavSection: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { data, loading, error } = useQuery<{ partsIndexCategoriesWithGroups: PartsIndexCatalog[] }>(
|
||||
GET_PARTSINDEX_CATEGORIES,
|
||||
{
|
||||
variables: {
|
||||
lang: 'ru'
|
||||
},
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-first'
|
||||
}
|
||||
);
|
||||
|
||||
// Обработчик клика по категории для перехода в каталог с товарами
|
||||
const handleCategoryClick = (catalog: PartsIndexCatalog) => {
|
||||
console.log('🔍 Клик по категории:', { catalogId: catalog.id, categoryName: catalog.name });
|
||||
|
||||
// Получаем первую группу для groupId (это правильный ID для partsIndexCategory)
|
||||
const firstGroup = catalog.groups?.[0];
|
||||
const groupId = firstGroup?.id;
|
||||
|
||||
console.log('🔍 Найденная группа:', { groupId, groupName: firstGroup?.name });
|
||||
|
||||
// Переходим на страницу каталога с параметрами PartsIndex
|
||||
router.push({
|
||||
pathname: '/catalog',
|
||||
query: {
|
||||
partsIndexCatalog: catalog.id,
|
||||
categoryName: encodeURIComponent(catalog.name),
|
||||
...(groupId && { partsIndexCategory: groupId })
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Fallback данные на случай ошибки
|
||||
const fallbackCategories = [
|
||||
{ id: '1', name: 'Двигатель', image: '/images/catalog_item.png' },
|
||||
{ id: '2', name: 'Трансмиссия', image: '/images/catalog_item2.png' },
|
||||
{ id: '3', name: 'Подвеска', image: '/images/catalog_item3.png' },
|
||||
{ id: '4', name: 'Тормоза', image: '/images/catalog_item4.png' },
|
||||
{ id: '5', name: 'Электрика', image: '/images/catalog_item5.png' },
|
||||
{ id: '6', name: 'Кузов', image: '/images/catalog_item6.png' },
|
||||
{ id: '7', name: 'Салон', image: '/images/catalog_item7.png' },
|
||||
{ id: '8', name: 'Климат', image: '/images/catalog_item8.png' },
|
||||
{ id: '9', name: 'Расходники', image: '/images/catalog_item9.png' }
|
||||
];
|
||||
|
||||
// Используем данные из API или fallback
|
||||
const categories = data?.partsIndexCategoriesWithGroups || [];
|
||||
const displayCategories = categories.length > 0 ? categories : fallbackCategories;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-layout-blockcontainer container-2 w-container">
|
||||
<div className="w-layout-hflex flex-block-6">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div key={index} className="w-layout-vflex flex-block-7">
|
||||
<div className="animate-pulse bg-gray-200 h-6 w-20 rounded mb-2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="ci2">
|
||||
<div className="text-block-54">Шины</div>
|
||||
</div>
|
||||
<div className="ci3">
|
||||
<div className="text-block-54">Диски</div>
|
||||
</div>
|
||||
<div className="ci4">
|
||||
<div className="text-block-54">Масла и жидкости</div>
|
||||
</div>
|
||||
<div className="ci5">
|
||||
<div className="text-block-54">Инструменты</div>
|
||||
</div>
|
||||
<div className="ci6">
|
||||
<div className="text-block-54">Автохимия</div>
|
||||
</div>
|
||||
<div className="ci7">
|
||||
<div className="text-block-54">Аксессуары</div>
|
||||
</div>
|
||||
<div className="ci8">
|
||||
<div className="text-block-54">Электрика</div>
|
||||
</div>
|
||||
<div className="ci9">
|
||||
<div className="text-block-54">АКБ</div>
|
||||
<div className="w-layout-hflex flex-block-5">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div key={index} className="w-layout-vflex flex-block-8">
|
||||
<div className="animate-pulse bg-gray-200 h-32 w-full rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-layout-blockcontainer container-2 w-container">
|
||||
{/* Навигационная панель с названиями категорий */}
|
||||
<div className="w-layout-hflex flex-block-6">
|
||||
{displayCategories.slice(0, 9).map((catalog) => (
|
||||
<div key={catalog.id} className="w-layout-vflex flex-block-7">
|
||||
<div
|
||||
className="text-block-10"
|
||||
onClick={() => handleCategoryClick(catalog)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{catalog.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Блок с изображениями категорий */}
|
||||
<div className="w-layout-hflex flex-block-5">
|
||||
{displayCategories.slice(0, 9).map((catalog) => (
|
||||
<div key={catalog.id} className="w-layout-vflex flex-block-8">
|
||||
<img
|
||||
src={catalog.image || '/images/catalog_item.png'}
|
||||
loading="lazy"
|
||||
alt={catalog.name}
|
||||
className="image-5"
|
||||
onClick={() => handleCategoryClick(catalog)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/catalog_item.png';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryNavSection;
|
@ -1,6 +1,23 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_HERO_BANNERS } from '@/lib/graphql';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface HeroBanner {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl: string;
|
||||
linkUrl?: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const HeroSlider = () => {
|
||||
const { data, loading, error } = useQuery(GET_HERO_BANNERS, {
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.Webflow && window.Webflow.require) {
|
||||
if (window.Webflow.destroy) {
|
||||
@ -12,118 +29,152 @@ const HeroSlider = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Фильтруем только активные баннеры и сортируем их
|
||||
const banners: HeroBanner[] = data?.heroBanners
|
||||
?.filter((banner: HeroBanner) => banner.isActive)
|
||||
?.slice()
|
||||
?.sort((a: HeroBanner, b: HeroBanner) => a.sortOrder - b.sortOrder) || [];
|
||||
|
||||
// Если нет данных или происходит загрузка, показываем дефолтный баннер
|
||||
if (loading || error || banners.length === 0) {
|
||||
return (
|
||||
<section className="section-5" style={{ overflow: 'hidden' }}>
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<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="w-layout-vflex flex-block-100">
|
||||
<div className="div-block-35">
|
||||
<img src="/images/imgfb.png" loading="lazy"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w"
|
||||
alt="Автозапчасти ProteK"
|
||||
className="image-21" />
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-99">
|
||||
<h2 className="heading-17">ШИРОКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
||||
<div className="text-block-51">
|
||||
Сотрудничаем только с проверенными поставщиками. Постоянно обновляем
|
||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-101">
|
||||
<div className="w-layout-hflex flex-block-102">
|
||||
<img src="/images/1.png" loading="lazy" alt="" className="image-20" />
|
||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102">
|
||||
<img src="/images/2.png" loading="lazy" alt="" className="image-20" />
|
||||
<div className="text-block-52">Высокое качество продукции</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102">
|
||||
<img src="/images/3.png" loading="lazy" alt="" className="image-20" />
|
||||
<div className="text-block-52">Выгодные цены</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102">
|
||||
<img src="/images/4.png" loading="lazy" alt="" className="image-20" />
|
||||
<div className="text-block-52">Профессиональная консультация</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="left-arrow w-slider-arrow-left">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-left"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-arrow w-slider-arrow-right">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSlide = (banner: HeroBanner) => {
|
||||
const slideContent = (
|
||||
<div className="w-layout-vflex flex-block-100">
|
||||
<div className="div-block-35">
|
||||
<img
|
||||
src={banner.imageUrl}
|
||||
loading="lazy"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||
alt={banner.title}
|
||||
className="image-21"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-99">
|
||||
<h2 className="heading-17">{banner.title}</h2>
|
||||
{banner.subtitle && (
|
||||
<div className="text-block-51">{banner.subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Если есть ссылка, оборачиваем в Link
|
||||
if (banner.linkUrl) {
|
||||
return (
|
||||
<Link href={banner.linkUrl} className="slide w-slide" style={{ cursor: 'pointer' }}>
|
||||
{slideContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="slide w-slide">
|
||||
{slideContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="section-5">
|
||||
<section className="section-5" style={{ overflow: 'hidden' }}>
|
||||
<div className="w-layout-blockcontainer container w-container">
|
||||
<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
|
||||
data-delay="4000"
|
||||
data-animation="slide"
|
||||
className="slider w-slider"
|
||||
data-autoplay="true"
|
||||
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="w-layout-vflex flex-block-100">
|
||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
||||
className="image-21" /></div>
|
||||
<div className="w-layout-vflex flex-block-99">
|
||||
<h2 className="heading-17">ШИРОКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-101">
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Высокое качество продукции</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Выгодные цены</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Профессиональная консультация</div>
|
||||
</div>
|
||||
{banners.map((banner) => (
|
||||
<React.Fragment key={banner.id}>
|
||||
{renderSlide(banner)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Показываем стрелки и навигацию только если баннеров больше одного */}
|
||||
{banners.length > 1 && (
|
||||
<>
|
||||
<div className="left-arrow w-slider-arrow-left">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-left"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-slide">
|
||||
<div className="w-layout-vflex flex-block-100">
|
||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
||||
className="image-21" /></div>
|
||||
<div className="w-layout-vflex flex-block-99">
|
||||
<h2 className="heading-17">УЗКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-101">
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Высокое качество продукции</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Выгодные цены</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Профессиональная консультация</div>
|
||||
</div>
|
||||
<div className="right-arrow w-slider-arrow-right">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-slide">
|
||||
<div className="w-layout-vflex flex-block-100">
|
||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
||||
className="image-21" /></div>
|
||||
<div className="w-layout-vflex flex-block-99">
|
||||
<h2 className="heading-17">ЛУЧШИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-101">
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Высокое качество продукции</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Выгодные цены</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
||||
className="image-20" />
|
||||
<div className="text-block-52">Профессиональная консультация</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="left-arrow w-slider-arrow-left">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-left"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-arrow w-slider-arrow-right">
|
||||
<div className="div-block-34">
|
||||
<div className="icon-2 w-icon-slider-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
209
src/components/index/ProductOfDayBanner.tsx
Normal file
209
src/components/index/ProductOfDayBanner.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_HERO_BANNERS } from '@/lib/graphql';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface HeroBanner {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl: string;
|
||||
linkUrl?: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const ProductOfDayBanner: React.FC = () => {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
||||
const { data, loading, error } = useQuery(GET_HERO_BANNERS, {
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
// Фильтруем только активные баннеры и сортируем их
|
||||
const banners: HeroBanner[] = data?.heroBanners
|
||||
?.filter((banner: HeroBanner) => banner.isActive)
|
||||
?.slice()
|
||||
?.sort((a: HeroBanner, b: HeroBanner) => a.sortOrder - b.sortOrder) || [];
|
||||
|
||||
// Если нет баннеров из админки, показываем дефолтный
|
||||
const allBanners = banners.length > 0 ? banners : [{
|
||||
id: 'default',
|
||||
title: 'ДОСТАВИМ БЫСТРО!',
|
||||
subtitle: 'Дополнительная скидка на товары с местного склада',
|
||||
imageUrl: '/images/imgfb.png',
|
||||
linkUrl: '',
|
||||
isActive: true,
|
||||
sortOrder: 0
|
||||
}];
|
||||
|
||||
// Автопрокрутка слайдов
|
||||
useEffect(() => {
|
||||
if (allBanners.length > 1) {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide(prev => (prev + 1) % allBanners.length);
|
||||
}, 5000); // Меняем слайд каждые 5 секунд
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [allBanners.length]);
|
||||
|
||||
// Сброс текущего слайда если он вне диапазона
|
||||
useEffect(() => {
|
||||
if (currentSlide >= allBanners.length) {
|
||||
setCurrentSlide(0);
|
||||
}
|
||||
}, [allBanners.length, currentSlide]);
|
||||
|
||||
const handlePrevSlide = () => {
|
||||
setCurrentSlide(prev => prev === 0 ? allBanners.length - 1 : prev - 1);
|
||||
};
|
||||
|
||||
const handleNextSlide = () => {
|
||||
setCurrentSlide(prev => (prev + 1) % allBanners.length);
|
||||
};
|
||||
|
||||
const handleSlideIndicator = (index: number) => {
|
||||
setCurrentSlide(index);
|
||||
};
|
||||
|
||||
const currentBanner = allBanners[currentSlide];
|
||||
|
||||
const renderBannerContent = (banner: HeroBanner) => {
|
||||
return (
|
||||
<div className="div-block-128" style={{
|
||||
backgroundImage: `url(${banner.imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '20px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{(banner.title || banner.subtitle) && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
left: '20px',
|
||||
right: '20px',
|
||||
color: 'white',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.5)'
|
||||
}}>
|
||||
{banner.title && (
|
||||
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 'bold' }}>
|
||||
{banner.title}
|
||||
</h3>
|
||||
)}
|
||||
{banner.subtitle && (
|
||||
<p style={{ margin: '5px 0 0 0', fontSize: '14px' }}>
|
||||
{banner.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const bannerContent = renderBannerContent(currentBanner);
|
||||
|
||||
const finalContent = currentBanner.linkUrl ? (
|
||||
<Link href={currentBanner.linkUrl} style={{ cursor: 'pointer', display: 'block', width: '100%', height: '100%' }}>
|
||||
{bannerContent}
|
||||
</Link>
|
||||
) : bannerContent;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
{finalContent}
|
||||
|
||||
{/* Навигация стрелками (показываем только если баннеров больше 1) */}
|
||||
{allBanners.length > 1 && (
|
||||
<>
|
||||
<div
|
||||
onClick={handlePrevSlide}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '10px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
fontSize: '18px'
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</div>
|
||||
<div
|
||||
onClick={handleNextSlide}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
fontSize: '18px'
|
||||
}}
|
||||
>
|
||||
›
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Индикаторы слайдов (показываем только если баннеров больше 1) */}
|
||||
{allBanners.length > 1 && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '10px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
zIndex: 10
|
||||
}}>
|
||||
{allBanners.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleSlideIndicator(index)}
|
||||
style={{
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
background: index === currentSlide ? 'white' : 'rgba(255,255,255,0.5)',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.3s'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductOfDayBanner;
|
@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_DAILY_PRODUCTS, PARTS_INDEX_SEARCH_BY_ARTICLE } from '@/lib/graphql';
|
||||
import Link from 'next/link';
|
||||
import ProductOfDayBanner from './ProductOfDayBanner';
|
||||
|
||||
interface DailyProduct {
|
||||
id: string;
|
||||
@ -31,7 +32,6 @@ const ProductOfDaySection: React.FC = () => {
|
||||
|
||||
// Состояние для текущего слайда
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, loading, error } = useQuery<{ dailyProducts: DailyProduct[] }>(
|
||||
GET_DAILY_PRODUCTS,
|
||||
@ -111,9 +111,9 @@ const ProductOfDaySection: React.FC = () => {
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
// Обработчики для слайдера
|
||||
// Обработчики для навигации по товарам дня
|
||||
const handlePrevSlide = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -158,63 +158,7 @@ const ProductOfDaySection: React.FC = () => {
|
||||
<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>
|
||||
|
||||
{/* Стрелки слайдера (показываем только если товаров больше 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>
|
||||
<ProductOfDayBanner />
|
||||
|
||||
<div className="div-block-129">
|
||||
<div className="w-layout-hflex flex-block-109">
|
||||
|
@ -404,16 +404,19 @@ const KnotIn: React.FC<KnotInProps> = ({
|
||||
onClick={() => setIsImageModalOpen(false)}
|
||||
style={{ cursor: 'zoom-out' }}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||
className="max-h-[90vh] max-w-[90vw] rounded shadow-lg"
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{ background: '#fff' }}
|
||||
/>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||
className="max-h-[90vh] max-w-[90vw] rounded shadow-lg"
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{ background: '#fff' }}
|
||||
/>
|
||||
{/* Убираем интерактивные точки в модальном окне */}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsImageModalOpen(false)}
|
||||
className="absolute top-4 right-4 text-white text-3xl font-bold bg-black bg-opacity-40 rounded-full w-10 h-10 flex items-center justify-center"
|
||||
className="absolute top-4 right-4 text-white text-3xl font-bold bg-black bg-opacity-40 rounded-full w-10 h-10 flex items-center justify-center hover:bg-black hover:bg-opacity-60 transition-colors"
|
||||
aria-label="Закрыть"
|
||||
style={{ zIndex: 10000 }}
|
||||
>
|
||||
|
@ -36,7 +36,9 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||||
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
||||
const [clickedPart, setClickedPart] = useState<string | number | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Отладочные логи для проверки данных
|
||||
React.useEffect(() => {
|
||||
@ -63,8 +65,31 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
|
||||
// Обработчик клика по детали в списке
|
||||
const handlePartClick = (part: any) => {
|
||||
if (part.codeonimage && onPartSelect) {
|
||||
onPartSelect(part.codeonimage);
|
||||
const codeOnImage = part.codeonimage || part.detailid;
|
||||
if (codeOnImage && onPartSelect) {
|
||||
onPartSelect(codeOnImage);
|
||||
}
|
||||
|
||||
// Также подсвечиваем деталь на схеме при клике
|
||||
if (codeOnImage && onPartHover) {
|
||||
// Очищаем предыдущий таймер, если он есть
|
||||
if (clickTimeoutRef.current) {
|
||||
clearTimeout(clickTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Устанавливаем состояние кликнутой детали
|
||||
setClickedPart(codeOnImage);
|
||||
|
||||
// Подсвечиваем на схеме
|
||||
onPartHover(codeOnImage);
|
||||
|
||||
// Убираем подсветку через интервал
|
||||
clickTimeoutRef.current = setTimeout(() => {
|
||||
setClickedPart(null);
|
||||
if (onPartHover) {
|
||||
onPartHover(null);
|
||||
}
|
||||
}, 1500); // Подсветка будет видна 1.5 секунды
|
||||
}
|
||||
};
|
||||
|
||||
@ -150,6 +175,9 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (clickTimeoutRef.current) {
|
||||
clearTimeout(clickTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -213,12 +241,17 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
|
||||
<div className="knot-parts">
|
||||
{parts.map((part, idx) => {
|
||||
const codeOnImage = part.codeonimage || part.detailid;
|
||||
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 isClicked = clickedPart !== null && (
|
||||
(part.codeonimage && part.codeonimage.toString() === clickedPart.toString()) ||
|
||||
(part.detailid && part.detailid.toString() === clickedPart.toString())
|
||||
);
|
||||
|
||||
// Создаем уникальный ключ
|
||||
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
||||
@ -226,12 +259,14 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={uniqueKey}
|
||||
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-colors ${
|
||||
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-all duration-300 ${
|
||||
isSelected
|
||||
? 'bg-green-100 border-green-500'
|
||||
: isHighlighted
|
||||
? 'bg-slate-200'
|
||||
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||
: isClicked
|
||||
? 'bg-red-100 border-red-400 shadow-md'
|
||||
: isHighlighted
|
||||
? 'bg-slate-200'
|
||||
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => handlePartClick(part)}
|
||||
onMouseEnter={() => handlePartMouseEnter(part)}
|
||||
@ -240,13 +275,37 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
||||
>
|
||||
<div className="w-layout-hflex flex-block-116">
|
||||
<div
|
||||
className={`nuberlist ${isSelected ? 'text-green-700 font-bold' : isHighlighted ? ' font-bold' : ''}`}
|
||||
className={`nuberlist ${
|
||||
isSelected
|
||||
? 'text-green-700 font-bold'
|
||||
: isClicked
|
||||
? 'text-red-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 className={`oemnuber ${
|
||||
isSelected
|
||||
? 'text-green-800 font-semibold'
|
||||
: isClicked
|
||||
? 'text-red-800 font-semibold'
|
||||
: isHighlighted
|
||||
? 'font-semibold'
|
||||
: ''
|
||||
}`}>{part.oem}</div>
|
||||
</div>
|
||||
<div className={`partsname ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>
|
||||
<div className={`partsname ${
|
||||
isSelected
|
||||
? 'text-green-800 font-semibold'
|
||||
: isClicked
|
||||
? 'text-red-800 font-semibold'
|
||||
: isHighlighted
|
||||
? 'font-semibold'
|
||||
: ''
|
||||
}`}>
|
||||
{part.name}
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-117">
|
||||
|
Reference in New Issue
Block a user