Compare commits
9 Commits
fix1607
...
1da9c6ac09
Author | SHA1 | Date | |
---|---|---|---|
1da9c6ac09 | |||
649ddbfa8a | |||
f3d21959c9 | |||
61b50d10ba | |||
ea76106caa | |||
b7edd73ce0 | |||
b6f9d017d6 | |||
27d378154f | |||
5fd2cf1b8c |
BIN
public/images/noimage.png
Normal file
BIN
public/images/noimage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
@ -312,9 +312,9 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
<img src="/images/info.svg" loading="lazy" alt="info" className="image-9" />
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-50">
|
||||
<div className="w-layout-hflex flex-block-79">
|
||||
<h3 className="heading-10 name">{brand}</h3>
|
||||
<h3 className="heading-10">{article}</h3>
|
||||
<div className="flex flex-row flex-nowrap items-center gap-2">
|
||||
<h3 className="heading-10 name" style={{marginRight: 8}}>{brand}</h3>
|
||||
<h3 className="heading-10" style={{marginRight: 8}}>{article}</h3>
|
||||
<div
|
||||
className="favorite-icon w-embed"
|
||||
onClick={handleFavoriteClick}
|
||||
@ -328,7 +328,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-block-21">{name}</div>
|
||||
<div className="text-block-21 mt-1">{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
{image && (
|
||||
|
@ -63,6 +63,7 @@ const FiltersPanelMobile: React.FC<FiltersPanelMobileProps> = ({
|
||||
setLocalFilterValues({});
|
||||
onSearchChange('');
|
||||
// Сбрасываем фильтры в родительском компоненте
|
||||
// Используем пустые массивы для правильной очистки
|
||||
Object.keys(filterValues).forEach(key => {
|
||||
onFilterChange?.(key, []);
|
||||
});
|
||||
|
@ -115,7 +115,7 @@ const Footer = () => (
|
||||
<button className="bg-[#23407A] rounded-lg py-2 px-6 font-medium mt-1 mb-2">Напиши нам</button>
|
||||
</div>
|
||||
{/* Центр: меню */}
|
||||
<div className="hidden md:flex flex-1 flex-wrap gap-10 justify-center min-w-[400px]">
|
||||
<div className="hidden md:flex flex-1 flex-wrap gap-30 justify-center min-w-[400px]">
|
||||
<div className="flex flex-col gap-3 min-w-[150px]">
|
||||
<div className="link">Подбор по марке авто</div>
|
||||
<a href="#" className="link">Поиск по VIN</a>
|
||||
@ -178,7 +178,7 @@ const Footer = () => (
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:flex-row md:items-start md:justify-center flex-1 flex-wrap gap-4 md:gap-20 md:mt-6 md:min-w-[400px]">
|
||||
<div className="flex flex-col items-center md:flex-row md:items-start md:justify-center flex-1 flex-wrap gap-4 md:gap-37 md:mt-6 md:min-w-[400px]">
|
||||
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Политика конфиденциальности</a>
|
||||
|
||||
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Согласие на обработку персональных данных</a>
|
||||
|
@ -111,12 +111,62 @@ const BestPriceSection: React.FC = () => {
|
||||
<div className="text-block-58">Подборка лучших предложенийпо цене</div>
|
||||
<a href="#" className="button-24 w-button">Показать все</a>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
<div className="carousel-row" style={{ position: 'relative' }}>
|
||||
{/* Стили для стрелок как в ProductOfDayBanner, но без абсолютного позиционирования */}
|
||||
<style>{`
|
||||
.carousel-arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.carousel-arrow-left {}
|
||||
.carousel-arrow-right {}
|
||||
.carousel-arrow .arrow-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-circle,
|
||||
.carousel-arrow:focus .arrow-circle {
|
||||
background: #ec1c24;
|
||||
}
|
||||
.carousel-arrow .arrow-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
transition: stroke 0.2s;
|
||||
stroke: #222;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-svg,
|
||||
.carousel-arrow:focus .arrow-svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
.carousel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`}</style>
|
||||
<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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div className="w-layout-hflex flex-block-121 carousel-scroll" ref={scrollRef}>
|
||||
{bestPriceItems.map((item, i) => (
|
||||
@ -124,10 +174,11 @@ const BestPriceSection: React.FC = () => {
|
||||
))}
|
||||
</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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,16 +84,66 @@ const NewArrivalsSection: React.FC = () => {
|
||||
<h2 className="heading-4">Новое поступление</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
{/* Стили для стрелок как в BestPriceSection и TopSalesSection */}
|
||||
<style>{`
|
||||
.carousel-arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.carousel-arrow-left {}
|
||||
.carousel-arrow-right {}
|
||||
.carousel-arrow .arrow-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-circle,
|
||||
.carousel-arrow:focus .arrow-circle {
|
||||
background: #ec1c24;
|
||||
}
|
||||
.carousel-arrow .arrow-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
transition: stroke 0.2s;
|
||||
stroke: #222;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-svg,
|
||||
.carousel-arrow:focus .arrow-svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
.carousel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`}</style>
|
||||
<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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
@ -149,10 +199,11 @@ const NewArrivalsSection: React.FC = () => {
|
||||
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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,11 +32,61 @@ const NewsAndPromos = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
{/* Стили для стрелок как в других секциях */}
|
||||
<style>{`
|
||||
.carousel-arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.carousel-arrow-left {}
|
||||
.carousel-arrow-right {}
|
||||
.carousel-arrow .arrow-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-circle,
|
||||
.carousel-arrow:focus .arrow-circle {
|
||||
background: #ec1c24;
|
||||
}
|
||||
.carousel-arrow .arrow-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
transition: stroke 0.2s;
|
||||
stroke: #222;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-svg,
|
||||
.carousel-arrow:focus .arrow-svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
.carousel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`}</style>
|
||||
<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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div className="w-layout-hflex flex-block-6-copy-copy carousel-scroll" ref={scrollRef}>
|
||||
<NewsCard
|
||||
@ -69,10 +119,11 @@ const NewsAndPromos = () => {
|
||||
/>
|
||||
</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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,7 +110,12 @@ const ProductOfDaySection: React.FC = () => {
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
// Если нет ни одной картинки, возвращаем noimage.png
|
||||
return {
|
||||
url: '/images/noimage.png',
|
||||
alt: product.name,
|
||||
source: 'noimage'
|
||||
};
|
||||
};
|
||||
|
||||
// Обработчики для навигации по товарам дня
|
||||
@ -209,6 +214,11 @@ const ProductOfDaySection: React.FC = () => {
|
||||
Parts Index
|
||||
</div>
|
||||
)}
|
||||
{productImage.source === 'noimage' && (
|
||||
<div className="absolute bottom-0 right-0 bg-gray-400 text-white text-xs px-2 py-1 rounded-tl">
|
||||
Нет изображения
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -143,11 +143,61 @@ const TopSalesSection: React.FC = () => {
|
||||
<h2 className="heading-4">Топ продаж</h2>
|
||||
</div>
|
||||
<div className="carousel-row">
|
||||
{/* Стили для стрелок как в BestPriceSection */}
|
||||
<style>{`
|
||||
.carousel-arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.carousel-arrow-left {}
|
||||
.carousel-arrow-right {}
|
||||
.carousel-arrow .arrow-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-circle,
|
||||
.carousel-arrow:focus .arrow-circle {
|
||||
background: #ec1c24;
|
||||
}
|
||||
.carousel-arrow .arrow-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
transition: stroke 0.2s;
|
||||
stroke: #222;
|
||||
}
|
||||
.carousel-arrow:hover .arrow-svg,
|
||||
.carousel-arrow:focus .arrow-svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
.carousel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`}</style>
|
||||
<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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||
{activeTopSalesProducts.map((item: TopSalesProductData) => {
|
||||
@ -177,10 +227,11 @@ const TopSalesSection: React.FC = () => {
|
||||
})}
|
||||
</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"/>
|
||||
<span className="arrow-circle">
|
||||
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@ interface VehicleAttributesTooltipProps {
|
||||
const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
|
||||
show,
|
||||
position,
|
||||
vehicleName,
|
||||
vehicleAttributes,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
@ -27,7 +28,7 @@ const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
|
||||
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]"
|
||||
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] max-w-full fixed z-[9999]"
|
||||
style={{
|
||||
left: `${position.x + 120}px`,
|
||||
top: `${position.y}px`,
|
||||
@ -45,16 +46,33 @@ const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
|
||||
/>
|
||||
)}
|
||||
<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">
|
||||
{/* Заголовок */}
|
||||
{vehicleName && (
|
||||
<div className="font-semibold text-lg text-black mb-3 truncate">{vehicleName}</div>
|
||||
)}
|
||||
{/* Список характеристик или сообщение */}
|
||||
{vehicleAttributes.length > 0 ? (
|
||||
vehicleAttributes.map((attr, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="grid grid-cols-[150px_1fr] gap-x-5 items-start mt-2 w-full first:mt-0"
|
||||
>
|
||||
<div className="text-gray-400 break-words whitespace-normal text-left">
|
||||
{attr.name}
|
||||
</div>
|
||||
<div className="self-stretch my-auto font-medium text-black truncate">
|
||||
<div
|
||||
className="font-medium text-black break-words whitespace-normal text-left justify-self-start"
|
||||
style={{ textAlign: 'left' }}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -88,10 +88,9 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
||||
))}
|
||||
{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 }))}
|
||||
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: total }))}
|
||||
style={{ border: '1px solid #EC1C24', borderRadius: 8, background: '#fff', color: '#222', padding: '6px 18px', minWidth: 180 }}
|
||||
>
|
||||
Развернуть
|
||||
@ -99,10 +98,9 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
||||
<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 }))}
|
||||
onClick={() => handleUnitClick(unit)}
|
||||
style={{ background: '#e9eef5', borderRadius: 8, color: '#222', padding: '6px 18px', border: 'none'}}
|
||||
>
|
||||
Показать все
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex';
|
||||
|
||||
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
|
||||
const PARTS_INDEX_API_BASE = process.env.PARTSAPI_URL+"/v1" || 'https://api.parts-index.com/v1';
|
||||
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
||||
|
||||
class PartsIndexService {
|
||||
|
@ -38,8 +38,8 @@ const mockData = Array(12).fill({
|
||||
brand: "Borsehung",
|
||||
});
|
||||
|
||||
const ITEMS_PER_PAGE = 50; // Целевое количество товаров на странице
|
||||
const PARTSINDEX_PAGE_SIZE = 25; // Размер страницы PartsIndex API (фиксированный)
|
||||
const ITEMS_PER_PAGE = 50; // Уменьшено для быстрой загрузки и лучшего UX
|
||||
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
|
||||
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
|
||||
|
||||
export default function Catalog() {
|
||||
@ -56,6 +56,36 @@ export default function Catalog() {
|
||||
const [showSortMobile, setShowSortMobile] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({});
|
||||
|
||||
// Инициализация фильтров из URL при загрузке
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
const urlFilters: {[key: string]: string[]} = {};
|
||||
const urlSearchQuery = router.query.q as string || '';
|
||||
|
||||
// Восстанавливаем фильтры из URL
|
||||
Object.keys(router.query).forEach(key => {
|
||||
if (key.startsWith('filter_')) {
|
||||
const filterName = key.replace('filter_', '');
|
||||
const filterValue = router.query[key];
|
||||
if (typeof filterValue === 'string') {
|
||||
urlFilters[filterName] = [filterValue];
|
||||
} else if (Array.isArray(filterValue)) {
|
||||
urlFilters[filterName] = filterValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔗 Восстанавливаем фильтры из URL:', { urlFilters, urlSearchQuery });
|
||||
|
||||
if (Object.keys(urlFilters).length > 0) {
|
||||
setSelectedFilters(urlFilters);
|
||||
}
|
||||
if (urlSearchQuery) {
|
||||
setSearchQuery(urlSearchQuery);
|
||||
}
|
||||
}
|
||||
}, [router.isReady]);
|
||||
const [visibleArticles, setVisibleArticles] = useState<PartsAPIArticle[]>([]);
|
||||
const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@ -80,6 +110,7 @@ export default function Catalog() {
|
||||
const [isAutoLoading, setIsAutoLoading] = useState(false); // Автоматическая подгрузка в процессе
|
||||
const [currentUserPage, setCurrentUserPage] = useState(1); // Текущая пользовательская страница
|
||||
const [entitiesCache, setEntitiesCache] = useState<Map<number, PartsIndexEntity[]>>(new Map()); // Кэш страниц
|
||||
const [isFilterChanging, setIsFilterChanging] = useState(false); // Флаг изменения фильтров
|
||||
|
||||
// Карта видимости товаров по индексу
|
||||
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
|
||||
@ -175,16 +206,17 @@ export default function Catalog() {
|
||||
// Хук для загрузки цен товаров PartsIndex
|
||||
const { getPrice, isLoadingPrice, ensurePriceLoaded } = useProductPrices();
|
||||
|
||||
// Загружаем цены для видимых товаров PartsIndex
|
||||
// Загружаем цены для видимых товаров PartsIndex (для отображения конкретных цен)
|
||||
useEffect(() => {
|
||||
if (isPartsIndexMode && visibleEntities.length > 0) {
|
||||
// Загружаем цены только для видимых товаров для отображения точных цен
|
||||
visibleEntities.forEach((entity, index) => {
|
||||
const productForPrice = {
|
||||
id: entity.id,
|
||||
code: entity.code,
|
||||
brand: entity.brand.name
|
||||
};
|
||||
// Загружаем с небольшой задержкой чтобы не перегружать сервер
|
||||
// Загружаем с небольшой задержкой
|
||||
setTimeout(() => {
|
||||
ensurePriceLoaded(productForPrice);
|
||||
}, index * 50);
|
||||
@ -208,9 +240,16 @@ export default function Catalog() {
|
||||
console.log('📊 Обновляем entitiesData:', {
|
||||
listLength: entitiesData.partsIndexCatalogEntities.list.length,
|
||||
pagination: entitiesData.partsIndexCatalogEntities.pagination,
|
||||
currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1
|
||||
currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1,
|
||||
isFilterChanging
|
||||
});
|
||||
|
||||
// Если изменяются фильтры, сбрасываем флаг после получения новых данных
|
||||
if (isFilterChanging) {
|
||||
setIsFilterChanging(false);
|
||||
console.log('🔄 Сброшен флаг isFilterChanging - получены новые отфильтрованные данные');
|
||||
}
|
||||
|
||||
const newEntities = entitiesData.partsIndexCatalogEntities.list;
|
||||
const pagination = entitiesData.partsIndexCatalogEntities.pagination;
|
||||
|
||||
@ -228,9 +267,13 @@ export default function Catalog() {
|
||||
// Если это первая страница или сброс, заменяем накопленные товары
|
||||
if (currentPage === 1) {
|
||||
setAccumulatedEntities(newEntities);
|
||||
// Устанавливаем visibleEntities сразу, не дожидаясь проверки цен
|
||||
// Устанавливаем visibleEntities сразу, только если не идет изменение фильтров
|
||||
if (!isFilterChanging) {
|
||||
setVisibleEntities(newEntities);
|
||||
console.log('✅ Установлены visibleEntities для первой страницы:', newEntities.length);
|
||||
} else {
|
||||
console.log('🔄 Пропускаем установку visibleEntities - фильтры изменяются');
|
||||
}
|
||||
} else {
|
||||
// Добавляем к накопленным товарам
|
||||
setAccumulatedEntities(prev => [...prev, ...newEntities]);
|
||||
@ -245,7 +288,7 @@ export default function Catalog() {
|
||||
|
||||
console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev });
|
||||
}
|
||||
}, [entitiesData]);
|
||||
}, [entitiesData, isFilterChanging]);
|
||||
|
||||
// Преобразование выбранных фильтров в формат PartsIndex API
|
||||
const convertFiltersToPartsIndexParams = useMemo((): Record<string, any> => {
|
||||
@ -284,25 +327,18 @@ export default function Catalog() {
|
||||
// Восстанавливаем автоподгрузку
|
||||
console.log('🔄 Автоподгрузка активна');
|
||||
|
||||
// Подсчитываем текущее количество товаров с предложениями
|
||||
const currentEntitiesWithOffers = accumulatedEntities.filter(entity => {
|
||||
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||
const priceData = getPrice(productForPrice);
|
||||
const isLoadingPriceData = isLoadingPrice(productForPrice);
|
||||
// Товар считается "с предложениями" если у него есть реальная цена (не null и не undefined)
|
||||
return (priceData && priceData.price && priceData.price > 0) || isLoadingPriceData;
|
||||
});
|
||||
// Подсчитываем текущее количество товаров (все уже отфильтрованы на сервере)
|
||||
const currentEntitiesCount = accumulatedEntities.length;
|
||||
|
||||
console.log('📊 Автоподгрузка: текущее состояние:', {
|
||||
накопленоТоваров: accumulatedEntities.length,
|
||||
сПредложениями: currentEntitiesWithOffers.length,
|
||||
накопленоТоваров: currentEntitiesCount,
|
||||
целевоеКоличество: ITEMS_PER_PAGE,
|
||||
естьЕщеТовары: hasMoreEntities
|
||||
});
|
||||
|
||||
// Если у нас уже достаточно товаров с предложениями, не загружаем
|
||||
if (currentEntitiesWithOffers.length >= ITEMS_PER_PAGE) {
|
||||
console.log('✅ Автоподгрузка: достаточно товаров с предложениями');
|
||||
// Если у нас уже достаточно товаров, не загружаем
|
||||
if (currentEntitiesCount >= ITEMS_PER_PAGE) {
|
||||
console.log('✅ Автоподгрузка: достаточно товаров');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -444,27 +480,26 @@ export default function Catalog() {
|
||||
}
|
||||
}, [isPartsIndexMode, entitiesWithOffers.length, hasMoreEntities, isAutoLoading]);
|
||||
|
||||
// Обновляем список товаров с предложениями при изменении накопленных товаров или цен
|
||||
// Обновляем список товаров при изменении накопленных товаров (серверная фильтрация)
|
||||
useEffect(() => {
|
||||
if (!isPartsIndexMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем все товары, но отдельно считаем те, у которых есть цены
|
||||
// Если фильтры изменяются, не обновляем отображение старых данных
|
||||
if (isFilterChanging) {
|
||||
console.log('🔄 Пропускаем обновление entitiesWithOffers - фильтры изменяются');
|
||||
return;
|
||||
}
|
||||
|
||||
// Все товары уже отфильтрованы на сервере - показываем все накопленные
|
||||
const entitiesWithOffers = accumulatedEntities;
|
||||
|
||||
// Подсчитываем количество товаров с реальными ценами для автоподгрузки
|
||||
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
|
||||
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||
const priceData = getPrice(productForPrice);
|
||||
return priceData && priceData.price && priceData.price > 0;
|
||||
});
|
||||
|
||||
console.log('📊 Обновляем entitiesWithOffers:', {
|
||||
console.log('📊 Обновляем entitiesWithOffers (серверная фильтрация):', {
|
||||
накопленоТоваров: accumulatedEntities.length,
|
||||
отображаемыхТоваров: entitiesWithOffers.length,
|
||||
сРеальнымиЦенами: entitiesWithRealPrices.length,
|
||||
целевоеКоличество: ITEMS_PER_PAGE
|
||||
целевоеКоличество: ITEMS_PER_PAGE,
|
||||
isFilterChanging
|
||||
});
|
||||
|
||||
setEntitiesWithOffers(entitiesWithOffers);
|
||||
@ -484,31 +519,9 @@ export default function Catalog() {
|
||||
|
||||
setVisibleEntities(visibleForCurrentPage);
|
||||
|
||||
}, [isPartsIndexMode, accumulatedEntities, currentUserPage]);
|
||||
}, [isPartsIndexMode, accumulatedEntities, currentUserPage, isFilterChanging]);
|
||||
|
||||
// Отдельный useEffect для обновления статистики цен (без влияния на visibleEntities)
|
||||
useEffect(() => {
|
||||
if (!isPartsIndexMode || accumulatedEntities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем статистику каждые 2 секунды
|
||||
const timer = setTimeout(() => {
|
||||
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
|
||||
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||
const priceData = getPrice(productForPrice);
|
||||
return priceData && priceData.price && priceData.price > 0;
|
||||
});
|
||||
|
||||
console.log('💰 Обновление статистики цен:', {
|
||||
накопленоТоваров: accumulatedEntities.length,
|
||||
сРеальнымиЦенами: entitiesWithRealPrices.length,
|
||||
процентЗагрузки: Math.round((entitiesWithRealPrices.length / accumulatedEntities.length) * 100)
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isPartsIndexMode, accumulatedEntities.length, getPrice]);
|
||||
|
||||
// Генерируем динамические фильтры для PartsAPI
|
||||
const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
|
||||
@ -595,27 +608,94 @@ export default function Catalog() {
|
||||
|
||||
|
||||
|
||||
// Функция для обновления URL с фильтрами
|
||||
const updateUrlWithFilters = useCallback((filters: {[key: string]: string[]}, search: string) => {
|
||||
const query: any = { ...router.query };
|
||||
|
||||
// Удаляем старые фильтры из URL
|
||||
Object.keys(query).forEach(key => {
|
||||
if (key.startsWith('filter_') || key === 'q') {
|
||||
delete query[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем новые фильтры
|
||||
Object.entries(filters).forEach(([filterName, values]) => {
|
||||
if (values.length > 0) {
|
||||
query[`filter_${filterName}`] = values.length === 1 ? values[0] : values;
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем поисковый запрос
|
||||
if (search.trim()) {
|
||||
query.q = search;
|
||||
}
|
||||
|
||||
// Обновляем URL без перезагрузки страницы
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query
|
||||
}, undefined, { shallow: true });
|
||||
}, [router]);
|
||||
|
||||
const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => {
|
||||
setSelectedFilters(prev => ({
|
||||
...prev,
|
||||
[filterTitle]: Array.isArray(value) ? value : [value]
|
||||
}));
|
||||
setSelectedFilters(prev => {
|
||||
const newFilters = { ...prev };
|
||||
|
||||
// Если значение пустое (пустой массив или пустая строка), удаляем фильтр
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
delete newFilters[filterTitle];
|
||||
} else if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
delete newFilters[filterTitle];
|
||||
} else {
|
||||
// Иначе устанавливаем значение
|
||||
newFilters[filterTitle] = Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
// Обновляем URL
|
||||
updateUrlWithFilters(newFilters, searchQuery);
|
||||
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
const handleMobileFilterChange = (type: string, value: any) => {
|
||||
setSelectedFilters(prev => ({
|
||||
...prev,
|
||||
[type]: Array.isArray(value) ? value : [value]
|
||||
}));
|
||||
setSelectedFilters(prev => {
|
||||
const newFilters = { ...prev };
|
||||
|
||||
// Если значение пустое (пустой массив или пустая строка), удаляем фильтр
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
delete newFilters[type];
|
||||
} else if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
delete newFilters[type];
|
||||
} else {
|
||||
// Иначе устанавливаем значение
|
||||
newFilters[type] = Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
// Обновляем URL
|
||||
updateUrlWithFilters(newFilters, searchQuery);
|
||||
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
// Обработчик изменения поискового запроса
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchQuery(value);
|
||||
updateUrlWithFilters(selectedFilters, value);
|
||||
}, [selectedFilters, updateUrlWithFilters]);
|
||||
|
||||
// Функция для сброса всех фильтров
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setSearchQuery('');
|
||||
setSelectedFilters({});
|
||||
setShowAllBrands(false);
|
||||
setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую
|
||||
}, []);
|
||||
|
||||
// Очищаем URL от фильтров
|
||||
updateUrlWithFilters({}, '');
|
||||
}, [updateUrlWithFilters]);
|
||||
|
||||
// Фильтрация по поиску и фильтрам для PartsAPI
|
||||
const filteredArticles = useMemo(() => {
|
||||
@ -672,6 +752,10 @@ export default function Catalog() {
|
||||
// Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера
|
||||
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
||||
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
||||
|
||||
// Устанавливаем флаг изменения фильтров
|
||||
setIsFilterChanging(true);
|
||||
|
||||
setPartsIndexPage(1);
|
||||
setCurrentUserPage(1);
|
||||
setHasMoreEntities(true);
|
||||
@ -679,10 +763,36 @@ export default function Catalog() {
|
||||
setEntitiesWithOffers([]);
|
||||
setEntitiesCache(new Map());
|
||||
|
||||
// Перезагружаем данные с новыми параметрами фильтрации
|
||||
const apiParams = convertFiltersToPartsIndexParams;
|
||||
// Вычисляем параметры фильтрации прямо здесь, чтобы избежать зависимости от useMemo
|
||||
let apiParams: Record<string, any> = {};
|
||||
if (paramsData?.partsIndexCatalogParams?.list && Object.keys(selectedFilters).length > 0) {
|
||||
paramsData.partsIndexCatalogParams.list.forEach((param: any) => {
|
||||
const selectedValues = selectedFilters[param.name];
|
||||
if (selectedValues && selectedValues.length > 0) {
|
||||
// Находим соответствующие значения из API данных
|
||||
const matchingValues = param.values.filter((value: any) =>
|
||||
selectedValues.includes(value.title || value.value)
|
||||
);
|
||||
|
||||
if (matchingValues.length > 0) {
|
||||
// Используем ID параметра из API и значения
|
||||
apiParams[param.id] = matchingValues.map((v: any) => v.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
||||
|
||||
console.log('🔄 Запуск refetch с новыми фильтрами:', {
|
||||
searchQuery,
|
||||
selectedFilters,
|
||||
apiParams,
|
||||
paramsString,
|
||||
catalogId,
|
||||
groupId
|
||||
});
|
||||
|
||||
// Также обновляем параметры фильтрации
|
||||
refetchParams({
|
||||
catalogId: catalogId as string,
|
||||
@ -690,6 +800,10 @@ export default function Catalog() {
|
||||
lang: 'ru',
|
||||
q: searchQuery || undefined,
|
||||
params: paramsString
|
||||
}).then(result => {
|
||||
console.log('✅ refetchParams результат:', result);
|
||||
}).catch(error => {
|
||||
console.error('❌ refetchParams ошибка:', error);
|
||||
});
|
||||
|
||||
refetchEntities({
|
||||
@ -700,11 +814,20 @@ export default function Catalog() {
|
||||
page: 1,
|
||||
q: searchQuery || undefined,
|
||||
params: paramsString
|
||||
}).then(result => {
|
||||
console.log('✅ refetchEntities результат:', result.data?.partsIndexCatalogEntities?.list?.length || 0, 'товаров');
|
||||
}).catch(error => {
|
||||
console.error('❌ refetchEntities ошибка:', error);
|
||||
});
|
||||
} else {
|
||||
// Если нет активных фильтров, сбрасываем флаг
|
||||
if (isFilterChanging) {
|
||||
setIsFilterChanging(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]);
|
||||
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), paramsData]);
|
||||
|
||||
// Управляем показом пустого состояния с задержкой
|
||||
useEffect(() => {
|
||||
@ -724,12 +847,18 @@ export default function Catalog() {
|
||||
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
|
||||
// Для PartsIndex показываем пустое состояние если нет товаров И данные уже загружены
|
||||
const hasLoadedData = accumulatedEntities.length > 0 || Boolean(entitiesData?.partsIndexCatalogEntities?.list);
|
||||
setShowEmptyState(hasLoadedData && visibleEntities.length === 0);
|
||||
console.log('📊 Определяем showEmptyState для PartsIndex:', {
|
||||
|
||||
// Показываем пустое состояние если данные загружены и нет видимых товаров
|
||||
// (товары уже отфильтрованы на сервере, поэтому не нужно ждать загрузки цен)
|
||||
const shouldShowEmpty = hasLoadedData && visibleEntities.length === 0;
|
||||
setShowEmptyState(shouldShowEmpty);
|
||||
|
||||
console.log('📊 Определяем showEmptyState для PartsIndex (серверная фильтрация):', {
|
||||
hasLoadedData,
|
||||
visibleEntitiesLength: visibleEntities.length,
|
||||
accumulatedEntitiesLength: accumulatedEntities.length,
|
||||
showEmptyState: hasLoadedData && visibleEntities.length === 0
|
||||
shouldShowEmpty,
|
||||
showEmptyState: shouldShowEmpty
|
||||
});
|
||||
} else {
|
||||
setShowEmptyState(false);
|
||||
@ -884,7 +1013,7 @@ export default function Catalog() {
|
||||
onFilterChange={handleDesktopFilterChange}
|
||||
filterValues={selectedFilters}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
isLoading={filtersGenerating}
|
||||
/>
|
||||
</div>
|
||||
@ -895,7 +1024,7 @@ export default function Catalog() {
|
||||
onFilterChange={handleDesktopFilterChange}
|
||||
filterValues={selectedFilters}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
isLoading={filtersLoading}
|
||||
/>
|
||||
</div>
|
||||
@ -906,7 +1035,7 @@ export default function Catalog() {
|
||||
onFilterChange={handleDesktopFilterChange}
|
||||
filterValues={selectedFilters}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
isLoading={filtersLoading}
|
||||
/>
|
||||
</div>
|
||||
@ -916,7 +1045,7 @@ export default function Catalog() {
|
||||
onClose={() => setShowFiltersMobile(false)}
|
||||
filters={isPartsAPIMode ? dynamicFilters : catalogFilters}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
filterValues={selectedFilters}
|
||||
onFilterChange={handleMobileFilterChange}
|
||||
/>
|
||||
@ -936,6 +1065,8 @@ export default function Catalog() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Сообщение об ошибке */}
|
||||
{isPartsAPIMode && articlesError && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
@ -985,12 +1116,21 @@ export default function Catalog() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Показываем индикатор загрузки при изменении фильтров */}
|
||||
{isPartsIndexMode && isFilterChanging && (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<LoadingSpinner />
|
||||
<div className="text-gray-500 text-lg mt-4">Применяем фильтры...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Отображение товаров PartsIndex */}
|
||||
{isPartsIndexMode && (() => {
|
||||
{isPartsIndexMode && !isFilterChanging && (() => {
|
||||
console.log('🎯 Проверяем отображение PartsIndex товаров:', {
|
||||
isPartsIndexMode,
|
||||
visibleEntitiesLength: visibleEntities.length,
|
||||
visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name }))
|
||||
visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name })),
|
||||
isFilterChanging
|
||||
});
|
||||
return visibleEntities.length > 0;
|
||||
})() && (
|
||||
@ -1001,16 +1141,20 @@ export default function Catalog() {
|
||||
const priceData = getPrice(productForPrice);
|
||||
const isLoadingPriceData = isLoadingPrice(productForPrice);
|
||||
|
||||
// Определяем цену для отображения
|
||||
let displayPrice = "Цена по запросу";
|
||||
// Определяем цену для отображения (все товары уже отфильтрованы на сервере)
|
||||
let displayPrice = "";
|
||||
let displayCurrency = "RUB";
|
||||
let priceElement;
|
||||
|
||||
if (isLoadingPriceData) {
|
||||
// Показываем скелетон загрузки вместо текста
|
||||
priceElement = <PriceSkeleton />;
|
||||
} else if (priceData && priceData.price) {
|
||||
displayPrice = `${priceData.price.toLocaleString('ru-RU')} ₽`;
|
||||
displayCurrency = priceData.currency || "RUB";
|
||||
} else {
|
||||
// Если нет данных о цене, показываем скелетон (товар должен загрузиться)
|
||||
priceElement = <PriceSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -1021,7 +1165,7 @@ export default function Catalog() {
|
||||
articleNumber={entity.code}
|
||||
brandName={entity.brand.name}
|
||||
image={entity.images?.[0] || ''}
|
||||
price={isLoadingPriceData ? "" : displayPrice}
|
||||
price={priceElement ? "" : displayPrice}
|
||||
priceElement={priceElement}
|
||||
oldPrice=""
|
||||
discount=""
|
||||
|
146
src/pages/confidentiality.tsx
Normal file
146
src/pages/confidentiality.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import CatalogSubscribe from "@/components/CatalogSubscribe";
|
||||
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
|
||||
import NewsAndPromos from "@/components/index/NewsAndPromos";
|
||||
import Footer from "@/components/Footer";
|
||||
import IndexTopMenuNav from "@/components/index/IndexTopMenuNav";
|
||||
import MetaTags from "@/components/MetaTags";
|
||||
import { getMetaByPath } from "@/lib/meta-config";
|
||||
import JsonLdScript from "@/components/JsonLdScript";
|
||||
import { generateOrganizationSchema, generateWebSiteSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
|
||||
|
||||
|
||||
export default function Confidentiality() {
|
||||
const metaData = getMetaByPath('/');
|
||||
|
||||
// Добавьте эти строки:
|
||||
const organizationSchema = generateOrganizationSchema(PROTEK_ORGANIZATION);
|
||||
const websiteSchema = generateWebSiteSchema(
|
||||
"Protek - Автозапчасти и аксессуары",
|
||||
"https://protek.ru",
|
||||
"https://protek.ru/search"
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags {...metaData} />
|
||||
<JsonLdScript schema={organizationSchema} />
|
||||
<JsonLdScript schema={websiteSchema} />
|
||||
<section className="section-info">
|
||||
<div className="w-layout-blockcontainer container info w-container">
|
||||
<div className="w-layout-vflex flex-block-9">
|
||||
<div className="w-layout-hflex flex-block-7">
|
||||
<a href="#" className="link-block w-inline-block">
|
||||
<div>Главная</div>
|
||||
</a>
|
||||
<div className="text-block-3">→</div>
|
||||
<a href="#" className="link-block-2 w-inline-block">
|
||||
<div>Политика конфиденциальности</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-8">
|
||||
<div className="w-layout-hflex flex-block-10">
|
||||
<h1 className="heading">Политика конфиденциальности</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex relative gap-8 items-start self-stretch pt-10 pb-20 max-md:p-8 max-sm:gap-5 max-sm:p-5">
|
||||
<div className="flex relative flex-col gap-8 items-start p-10 bg-white rounded-3xl flex-[1_0_0] max-w-[1580px] mx-auto max-md:p-8 max-sm:gap-5 max-sm:p-5">
|
||||
<div className="flex relative flex-col gap-5 items-start self-stretch max-sm:gap-4">
|
||||
<div
|
||||
layer-name="Объявлен старт продаж электрических насосов"
|
||||
className="relative self-stretch text-3xl font-bold leading-9 text-gray-950"
|
||||
>
|
||||
Объявлен старт продаж электрических насосов
|
||||
</div>
|
||||
<div
|
||||
layer-name="Бренд вывел на рынок сразу широкий ассортимент, уже на старте продаж - более 100 артикулов и включает в себя позиции для брендов-лидеров автомобильного рынка, например: артикул 77WPE080 для Mercedes-Benz S-CLASS (W221, C216), артикул 77WPE096 – Land Rover DISCOVERY V (L462) / Jaguar F-PACE (X761), артикул 77WPE014 – Audi Q5 (8RB) / Volkswagen TOUAREG (7P5, 7P6)."
|
||||
className="relative self-stretch text-base leading-6 text-gray-600 max-sm:text-sm"
|
||||
>
|
||||
Бренд вывел на рынок сразу широкий ассортимент, уже на старте
|
||||
продаж - более 100 артикулов и включает в себя позиции для
|
||||
брендов-лидеров автомобильного рынка, например: артикул 77WPE080
|
||||
для Mercedes-Benz S-CLASS (W221, C216), артикул 77WPE096 – Land
|
||||
Rover DISCOVERY V (L462) / Jaguar F-PACE (X761), артикул 77WPE014
|
||||
– Audi Q5 (8RB) / Volkswagen TOUAREG (7P5, 7P6).
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative flex-col gap-8 items-start self-stretch max-sm:gap-5">
|
||||
<div
|
||||
layer-name="Преимущества электрических насосов охлаждающей жидкости MasterKit Electro:"
|
||||
className="relative self-stretch text-3xl font-medium leading-9 text-gray-950"
|
||||
>
|
||||
Преимущества электрических насосов охлаждающей жидкости MasterKit
|
||||
Electro:
|
||||
</div>
|
||||
<div className="flex relative flex-col gap-3.5 items-start self-stretch">
|
||||
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
|
||||
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
|
||||
<div
|
||||
layer-name="Отличная производительность за счёт применения компонентов известных мировых брендов."
|
||||
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
|
||||
>
|
||||
Отличная производительность за счёт применения компонентов
|
||||
известных мировых брендов.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
|
||||
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
|
||||
<div
|
||||
layer-name="Герметичность и устойчивость к коррозии"
|
||||
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
|
||||
>
|
||||
Герметичность и устойчивость к коррозии
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
|
||||
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
|
||||
<div
|
||||
layer-name="Высококачественные материалы компонентов, обеспечивающие долгий срок службы"
|
||||
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
|
||||
>
|
||||
Высококачественные материалы компонентов, обеспечивающие
|
||||
долгий срок службы
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
|
||||
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
|
||||
<div
|
||||
layer-name="Широкий ассортимент – более 100 артикулов"
|
||||
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
|
||||
>
|
||||
Широкий ассортимент – более 100 артикулов
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
layer-name="На электрические насосы системы охлаждения MasterKit Electro предоставляется гарантия 1 год или 30.000 км пробега, в зависимости от того, что наступит раньше. Все новинки уже внесены в каталог подбора продукции и доступны для заказа."
|
||||
className="relative self-stretch text-base leading-6 text-gray-600 max-sm:text-sm"
|
||||
>
|
||||
На электрические насосы системы охлаждения MasterKit Electro
|
||||
предоставляется гарантия 1 год или 30.000 км пробега, в
|
||||
зависимости от того, что наступит раньше. Все новинки уже внесены
|
||||
в каталог подбора продукции и доступны для заказа.
|
||||
</div>
|
||||
<div
|
||||
layer-name="ABig_Button"
|
||||
data-component-name="ABig_Button"
|
||||
data-variant-name="Button big=Default"
|
||||
className="relative gap-2.5 px-10 py-6 text-lg font-medium leading-5 text-center text-white no-underline bg-red-600 rounded-xl transition-all cursor-pointer border-[none] duration-[0.2s] ease-[ease] w-fit max-sm:px-8 max-sm:py-5 max-sm:w-full hover:bg-red-700"
|
||||
>
|
||||
Перейти к товару
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="section-3">
|
||||
<CatalogSubscribe />
|
||||
</section>
|
||||
<Footer />
|
||||
<MobileMenuBottomSection />
|
||||
</>
|
||||
);
|
||||
}
|
@ -491,7 +491,6 @@ input#VinSearchInput {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.heading-9-copy,
|
||||
.text-block-21-copy {
|
||||
width: 250px;
|
||||
overflow: hidden;
|
||||
@ -939,12 +938,11 @@ a.link-block-2.w-inline-block {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.heading-9-copy {
|
||||
min-width: 100px;
|
||||
|
||||
.flex-block-36 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.flex-block-15-copy {
|
||||
width: 232px!important;
|
||||
min-width: 232px!important;
|
||||
@ -1194,3 +1192,84 @@ a.link-block-2.w-inline-block {
|
||||
box-shadow: 0 8px 32px rgba(44,62,80,0.10), 0 1.5px 4px rgba(44,62,80,0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pricecartbp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px; /* или другой нужный вам отступ */
|
||||
}
|
||||
|
||||
.bestpriceitem {
|
||||
height: 279px;
|
||||
}
|
||||
|
||||
|
||||
.flex-block-49 {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.pcs-search-s1,
|
||||
.sort-item.first {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.pcs-search-s1,
|
||||
.sort-item.first {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 479px) {
|
||||
.pcs-search-s1,
|
||||
.sort-item.first {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.w-layout-vflex.flex-block-36 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.w-layout-vflex.flex-block-44 {
|
||||
flex: 1 1 calc(33.333% - 16px);
|
||||
max-width: calc(33.333% - 16px);
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.w-layout-vflex.flex-block-44 {
|
||||
flex: 1 1 calc(50% - 12px);
|
||||
max-width: calc(50% - 12px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.w-layout-vflex.flex-block-44 {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.w-layout-vflex.flex-block-36 {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.w-layout-vflex.flex-block-44 {
|
||||
min-width: 160px;
|
||||
max-width: 160px;
|
||||
flex: 0 0 160px;
|
||||
}
|
||||
.heading-9-copy {
|
||||
text-align: left !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ async function testPartsIndexAPI() {
|
||||
|
||||
// Получаем каталоги
|
||||
console.log('\n📦 Получаем список каталогов...');
|
||||
const catalogsResponse = await fetch('https://api.parts-index.com/v1/catalogs?lang=ru', {
|
||||
const catalogsResponse = await fetch(process.env.PARTSAPI_URL+"/v1/catalogs?lang=ru", {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
@ -31,7 +31,7 @@ async function testPartsIndexAPI() {
|
||||
console.log(`\n🎯 Получаем группы для каталога "${firstCatalog.name}"...`);
|
||||
|
||||
const groupsResponse = await fetch(
|
||||
`https://api.parts-index.com/v1/catalogs/${firstCatalog.id}/groups?lang=ru`,
|
||||
`${process.env.PARTSAPI_URL}/v1/catalogs/${firstCatalog.id}/groups?lang=ru`,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
21
user_input.py
Normal file
21
user_input.py
Normal file
@ -0,0 +1,21 @@
|
||||
def main():
|
||||
while True:
|
||||
print("\n" + "="*50)
|
||||
user_input = input("Please provide feedback or next task (type 'stop' to exit): ").strip()
|
||||
|
||||
if user_input.lower() == 'stop':
|
||||
print("Exiting task loop. Thank you!")
|
||||
break
|
||||
elif user_input.lower() == '':
|
||||
print("Please provide some input or type 'stop' to exit.")
|
||||
continue
|
||||
else:
|
||||
print(f"\nReceived input: {user_input}")
|
||||
print("Processing your request...")
|
||||
# Here the main process would handle the user's input
|
||||
return user_input
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = main()
|
||||
if result and result.lower() != 'stop':
|
||||
print(f"Next task received: {result}")
|
Reference in New Issue
Block a user