Compare commits
11 Commits
ed76c97915
...
1207
Author | SHA1 | Date | |
---|---|---|---|
cebe3a10ac | |||
791152a862 | |||
b11142ad0f | |||
508ad8cff3 | |||
53a398a072 | |||
268e6d3315 | |||
26e4a95ae4 | |||
9fc7d0fbf5 | |||
7abe016f0f | |||
90d1beb15e | |||
475b02ea2d |
BIN
public/images/resource2.png
Normal file
BIN
public/images/resource2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -109,7 +109,6 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
|||||||
|
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
|
@ -227,41 +227,63 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
<span>{mobileCategory.label}</span>
|
<span>{mobileCategory.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mobile-subcategories">
|
<div className="mobile-subcategories">
|
||||||
{mobileCategory.links.map((link: string, linkIndex: number) => (
|
{mobileCategory.links.length === 1 ? (
|
||||||
<div
|
<div
|
||||||
className="mobile-subcategory"
|
className="mobile-subcategory"
|
||||||
key={link}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Ищем соответствующую подгруппу по названию
|
let subcategoryId = `${mobileCategory.catalogId}_0`;
|
||||||
let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`;
|
|
||||||
|
|
||||||
if (mobileCategory.groups) {
|
if (mobileCategory.groups) {
|
||||||
for (const group of mobileCategory.groups) {
|
for (const group of mobileCategory.groups) {
|
||||||
// Проверяем в подгруппах
|
|
||||||
if (group.subgroups && group.subgroups.length > 0) {
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === mobileCategory.links[0]);
|
||||||
if (foundSubgroup) {
|
if (foundSubgroup) {
|
||||||
subcategoryId = foundSubgroup.id;
|
subcategoryId = foundSubgroup.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if (group.name === mobileCategory.links[0]) {
|
||||||
// Если нет подгрупп, проверяем саму группу
|
|
||||||
else if (group.name === link) {
|
|
||||||
subcategoryId = group.id;
|
subcategoryId = group.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем catalogId из данных
|
|
||||||
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
||||||
const catalogId = activeCatalog?.id || 'fallback';
|
const catalogId = activeCatalog?.id || 'fallback';
|
||||||
handleCategoryClick(catalogId, link, subcategoryId);
|
handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
|
||||||
}}
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
{link}
|
Показать все
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
mobileCategory.links.map((link: string, linkIndex: number) => (
|
||||||
|
<div
|
||||||
|
className="mobile-subcategory"
|
||||||
|
key={link}
|
||||||
|
onClick={() => {
|
||||||
|
let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`;
|
||||||
|
if (mobileCategory.groups) {
|
||||||
|
for (const group of mobileCategory.groups) {
|
||||||
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||||
|
if (foundSubgroup) {
|
||||||
|
subcategoryId = foundSubgroup.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (group.name === link) {
|
||||||
|
subcategoryId = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
||||||
|
const catalogId = activeCatalog?.id || 'fallback';
|
||||||
|
handleCategoryClick(catalogId, link, subcategoryId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -443,44 +465,66 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
<h3 className="heading-16">{tab.heading}</h3>
|
<h3 className="heading-16">{tab.heading}</h3>
|
||||||
<div className="w-layout-hflex flex-block-92">
|
<div className="w-layout-hflex flex-block-92">
|
||||||
<div className="w-layout-vflex flex-block-91">
|
<div className="w-layout-vflex flex-block-91">
|
||||||
{tab.links.map((link, linkIndex) => {
|
{tab.links.length === 1 ? (
|
||||||
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
<div
|
||||||
|
className="link-2"
|
||||||
// Ищем соответствующую подгруппу по названию
|
onClick={() => {
|
||||||
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||||
|
let subcategoryId = `fallback_${idx}_0`;
|
||||||
if (catalog?.groups) {
|
if (catalog?.groups) {
|
||||||
for (const group of catalog.groups) {
|
for (const group of catalog.groups) {
|
||||||
// Проверяем в подгруппах
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
if (group.subgroups && group.subgroups.length > 0) {
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === tab.links[0]);
|
||||||
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
if (foundSubgroup) {
|
||||||
if (foundSubgroup) {
|
subcategoryId = foundSubgroup.id;
|
||||||
subcategoryId = foundSubgroup.id;
|
break;
|
||||||
|
}
|
||||||
|
} else if (group.name === tab.links[0]) {
|
||||||
|
subcategoryId = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const catalogId = catalog?.id || 'fallback';
|
||||||
|
handleCategoryClick(catalogId, tab.links[0], subcategoryId);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Показать все
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tab.links.map((link: string, linkIndex: number) => {
|
||||||
|
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||||
|
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
||||||
|
if (catalog?.groups) {
|
||||||
|
for (const group of catalog.groups) {
|
||||||
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||||
|
if (foundSubgroup) {
|
||||||
|
subcategoryId = foundSubgroup.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (group.name === link) {
|
||||||
|
subcategoryId = group.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Если нет подгрупп, проверяем саму группу
|
|
||||||
else if (group.name === link) {
|
|
||||||
subcategoryId = group.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return (
|
||||||
|
<div
|
||||||
return (
|
className="link-2"
|
||||||
<div
|
key={link}
|
||||||
className="link-2"
|
onClick={() => {
|
||||||
key={link}
|
const catalogId = catalog?.id || 'fallback';
|
||||||
onClick={() => {
|
handleCategoryClick(catalogId, link, subcategoryId);
|
||||||
const catalogId = catalog?.id || 'fallback';
|
}}
|
||||||
handleCategoryClick(catalogId, link, subcategoryId);
|
style={{ cursor: "pointer" }}
|
||||||
}}
|
>
|
||||||
style={{ cursor: "pointer" }}
|
{link}
|
||||||
>
|
</div>
|
||||||
{link}
|
);
|
||||||
</div>
|
})
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-vflex flex-block-91-copy">
|
<div className="w-layout-vflex flex-block-91-copy">
|
||||||
<img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" className="image-17" />
|
<img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" className="image-17" />
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const CatalogSubscribe: React.FC = () => (
|
const CatalogSubscribe: React.FC = () => (
|
||||||
<div className="w-layout-blockcontainer container subscribe w-container">
|
<div className="w-layout-blockcontainer container subscribe w-container">
|
||||||
<div className="w-layout-hflex flex-block-18">
|
<div className="w-layout-hflex flex-block-18">
|
||||||
|
<img
|
||||||
|
src="/images/resource2.png"
|
||||||
|
alt="Ресурс 2"
|
||||||
|
className="mt-[-18px]"
|
||||||
|
/>
|
||||||
<div className="div-block-9">
|
<div className="div-block-9">
|
||||||
<h3 className="heading-3 sub">Подпишитесь на новостную рассылку</h3>
|
{/* <h3 className="heading-3 sub">Подпишитесь на новостную рассылку</h3> */}
|
||||||
|
|
||||||
<div className="text-block-14">Оставайтесь в курсе акций, <br />новинок и специальных предложений</div>
|
<div className="text-block-14">Оставайтесь в курсе акций, <br />новинок и специальных предложений</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-block-3 w-form">
|
<div className="form-block-3 w-form">
|
||||||
@ -13,6 +19,38 @@ const CatalogSubscribe: React.FC = () => (
|
|||||||
<input type="submit" className="submit-button-copy w-button" value="Подписаться" />
|
<input type="submit" className="submit-button-copy w-button" value="Подписаться" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-row items-center mt-2">
|
||||||
|
{/* Кастомный чекбокс без input/label */}
|
||||||
|
{(() => {
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`h-[24px] w-[24px] border border-[#8893A1] rounded-sm mr-4 flex-shrink-0 flex items-center justify-center cursor-pointer transition-colors duration-150 ${checked ? 'bg-[#EC1C24]' : 'bg-transparent'}`}
|
||||||
|
onClick={() => setChecked(v => !v)}
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') setChecked(v => !v); }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 text-white transition-opacity duration-150 ${checked ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-[#8893A1] text-[12px] leading-snug select-none">
|
||||||
|
Я даю свое согласие на обработку персональных данных<br />
|
||||||
|
и соглашаюсь с условиями <a href="/privacy-policy" className="underline hover:text-[#6c7684]">Политики конфиденциальности</a>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -385,7 +385,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
<section className="bottom_head">
|
<section className="bottom_head">
|
||||||
<div className="w-layout-blockcontainer container nav w-container">
|
<div className="w-layout-blockcontainer container nav w-container">
|
||||||
<div className="w-layout-hflex flex-block-93">
|
<div className="w-layout-hflex flex-block-93">
|
||||||
<Link href="/" className="code-embed-15 w-embed" style={{ display: 'block', cursor: 'pointer' }}>
|
<Link href="/" className="code-embed-15 w-embed protekauto-logo" style={{ display: 'block', cursor: 'pointer'}}>
|
||||||
<svg width="190" height="72" viewBox="0 0 190 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="190" height="72" viewBox="0 0 190 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M138.377 29.5883V23.1172H112.878V29.5883H138.377Z" fill="white"></path>
|
<path d="M138.377 29.5883V23.1172H112.878V29.5883H138.377Z" fill="white"></path>
|
||||||
<path d="M107.423 18.1195C109.21 18.1195 110.658 16.6709 110.658 14.884C110.658 13.097 109.21 11.6484 107.423 11.6484L88.395 11.6484C86.6082 11.6484 85.1596 13.097 85.1596 14.884C85.1596 16.6709 86.6082 18.1195 88.395 18.1195H107.423Z" fill="white"></path>
|
<path d="M107.423 18.1195C109.21 18.1195 110.658 16.6709 110.658 14.884C110.658 13.097 109.21 11.6484 107.423 11.6484L88.395 11.6484C86.6082 11.6484 85.1596 13.097 85.1596 14.884C85.1596 16.6709 86.6082 18.1195 88.395 18.1195H107.423Z" fill="white"></path>
|
||||||
|
@ -32,7 +32,11 @@ const InfoSearch: React.FC<InfoSearchProps> = ({
|
|||||||
<div className="w-layout-hflex flex-block-10">
|
<div className="w-layout-hflex flex-block-10">
|
||||||
<h1 className="heading">{name}</h1>
|
<h1 className="heading">{name}</h1>
|
||||||
<div className="text-block-4">
|
<div className="text-block-4">
|
||||||
Найдено {offersCount} предложений от {minPrice}
|
{offersCount > 0 ? (
|
||||||
|
<>Найдено {offersCount} предложений от {minPrice}</>
|
||||||
|
) : (
|
||||||
|
<>Ничего не найдено</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="w-layout-hflex flex-block-11">
|
{/* <div className="w-layout-hflex flex-block-11">
|
||||||
|
@ -64,7 +64,7 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
|||||||
<div
|
<div
|
||||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||||
isActive ? 'bg-slate-200' : 'bg-white'
|
isActive ? 'bg-slate-200' : 'bg-white'
|
||||||
}`}
|
} hover:bg-slate-200`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@ -93,7 +93,7 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
|||||||
<div
|
<div
|
||||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||||
isActive ? 'bg-slate-200' : 'bg-white'
|
isActive ? 'bg-slate-200' : 'bg-white'
|
||||||
}`}
|
} hover:bg-slate-200`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
@ -33,7 +33,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="pt-[62px] md:pt-[63px]">
|
<main className="pt-[62px] md:pt-[63px]">
|
||||||
<IndexTopMenuNav />
|
<IndexTopMenuNav isIndexPage={router.pathname === '/'} />
|
||||||
{children}</main>
|
{children}</main>
|
||||||
<MobileMenuBottomSection onOpenAuthModal={() => setAuthModalOpen(true)} />
|
<MobileMenuBottomSection onOpenAuthModal={() => setAuthModalOpen(true)} />
|
||||||
</>
|
</>
|
||||||
|
148
src/components/Pagination.tsx
Normal file
148
src/components/Pagination.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
className?: string;
|
||||||
|
showPageInfo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: React.FC<PaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
className = "",
|
||||||
|
showPageInfo = true
|
||||||
|
}) => {
|
||||||
|
const generatePageNumbers = () => {
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
const delta = 2; // Количество страниц вокруг текущей
|
||||||
|
|
||||||
|
if (totalPages <= 7) {
|
||||||
|
// Если страниц мало, показываем все
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Всегда показываем первую страницу
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
if (currentPage > delta + 2) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем страницы вокруг текущей
|
||||||
|
const start = Math.max(2, currentPage - delta);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + delta);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - delta - 1) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда показываем последнюю страницу
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageNumbers = generatePageNumbers();
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col items-center space-y-3 ${className}`}>
|
||||||
|
{/* Основные кнопки пагинации */}
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
{/* Предыдущая страница */}
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Номера страниц */}
|
||||||
|
{pageNumbers.map((page, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{page === '...' ? (
|
||||||
|
<span className="flex items-center justify-center w-10 h-10 text-gray-400">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<circle cx="3" cy="10" r="1.5" />
|
||||||
|
<circle cx="10" cy="10" r="1.5" />
|
||||||
|
<circle cx="17" cy="10" r="1.5" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(page as number)}
|
||||||
|
className={`flex items-center justify-center w-10 h-10 text-sm font-medium border rounded-lg transition-colors ${
|
||||||
|
currentPage === page
|
||||||
|
? 'text-white bg-[#ec1c24] border-[#ec1c24] hover:bg-[#d91920]'
|
||||||
|
: 'text-gray-500 bg-white border-gray-200 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Следующая страница */}
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
style={{ cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Информация о страницах */}
|
||||||
|
{showPageInfo && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Страница {currentPage} из {totalPages}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
@ -77,7 +77,6 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
});
|
});
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const numericPrice = parsePrice(price);
|
const numericPrice = parsePrice(price);
|
||||||
|
@ -28,6 +28,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
const [imageLoadTimeout, setImageLoadTimeout] = useState<NodeJS.Timeout | null>(null);
|
const [imageLoadTimeout, setImageLoadTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||||
const [selectedDetail, setSelectedDetail] = useState<LaximoUnitDetail | null>(null);
|
const [selectedDetail, setSelectedDetail] = useState<LaximoUnitDetail | null>(null);
|
||||||
|
const [highlightedDetailId, setHighlightedDetailId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Отладочная информация для SSD
|
// Отладочная информация для SSD
|
||||||
console.log('🔍 UnitDetailsSection получил SSD:', {
|
console.log('🔍 UnitDetailsSection получил SSD:', {
|
||||||
@ -165,11 +166,31 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
d.detailid === coord.codeonimage
|
d.detailid === coord.codeonimage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (detail) {
|
||||||
|
console.log('✅ Найдена деталь для выделения:', detail.name, 'ID:', detail.detailid);
|
||||||
|
// Выделяем деталь в списке
|
||||||
|
setHighlightedDetailId(detail.detailid);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Деталь не найдена в списке по коду:', coord.codeonimage);
|
||||||
|
setHighlightedDetailId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCoordinateDoubleClick = (coord: LaximoImageCoordinate) => {
|
||||||
|
console.log('🖱️ Двойной клик по интерактивной области:', coord.codeonimage);
|
||||||
|
|
||||||
|
// Сначала пытаемся найти деталь в списке
|
||||||
|
const detail = unitDetails.find(d =>
|
||||||
|
d.detailid === coord.detailid ||
|
||||||
|
d.codeonimage === coord.codeonimage ||
|
||||||
|
d.detailid === coord.codeonimage
|
||||||
|
);
|
||||||
|
|
||||||
if (detail && detail.oem) {
|
if (detail && detail.oem) {
|
||||||
console.log('✅ Найдена деталь для выбора бренда:', detail.name, 'OEM:', detail.oem);
|
console.log('✅ Найдена деталь для выбора бренда:', detail.name, 'OEM:', detail.oem);
|
||||||
// Показываем модал выбора бренда
|
// Переходим на страницу выбора бренда
|
||||||
setSelectedDetail(detail);
|
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${detail.oem}/brands?detailName=${encodeURIComponent(detail.name || '')}`;
|
||||||
setIsBrandModalOpen(true);
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
// Если деталь не найдена в списке, переходим к общему поиску по коду на изображении
|
// Если деталь не найдена в списке, переходим к общему поиску по коду на изображении
|
||||||
console.log('⚠️ Деталь не найдена в списке, переходим к поиску по коду:', coord.codeonimage);
|
console.log('⚠️ Деталь не найдена в списке, переходим к поиску по коду:', coord.codeonimage);
|
||||||
@ -461,7 +482,8 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
borderRadius: coord.shape === 'circle' ? '50%' : '0'
|
borderRadius: coord.shape === 'circle' ? '50%' : '0'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleCoordinateClick(coord)}
|
onClick={() => handleCoordinateClick(coord)}
|
||||||
title={detail ? `${coord.codeonimage}: ${detail.name}` : `Деталь ${coord.codeonimage}`}
|
onDoubleClick={() => handleCoordinateDoubleClick(coord)}
|
||||||
|
title={detail ? `${coord.codeonimage}: ${detail.name} (Клик - выделить, двойной клик - перейти к выбору бренда)` : `Деталь ${coord.codeonimage} (Клик - выделить, двойной клик - поиск)`}
|
||||||
>
|
>
|
||||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded font-bold">
|
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded font-bold">
|
||||||
{coord.codeonimage}
|
{coord.codeonimage}
|
||||||
@ -612,7 +634,11 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
|||||||
{unitDetails.map((detail, index) => (
|
{unitDetails.map((detail, index) => (
|
||||||
<div
|
<div
|
||||||
key={`detail-${unitId}-${index}-${detail.detailid}`}
|
key={`detail-${unitId}-${index}-${detail.detailid}`}
|
||||||
className="border border-gray-200 rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer"
|
className={`border rounded-lg p-4 hover:border-red-300 hover:shadow-md transition-all duration-200 cursor-pointer ${
|
||||||
|
highlightedDetailId === detail.detailid
|
||||||
|
? 'border-red-500 bg-red-50 shadow-md'
|
||||||
|
: 'border-gray-200'
|
||||||
|
}`}
|
||||||
onClick={() => handleDetailClick(detail)}
|
onClick={() => handleDetailClick(detail)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
@ -147,7 +147,7 @@ const BrandSelectionSection: React.FC = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
<Combobox.Options
|
<Combobox.Options
|
||||||
className="absolute left-0 top-full z-10 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-auto scrollbar-none"
|
className="absolute left-0 top-full z-100 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-auto scrollbar-none"
|
||||||
style={{ scrollbarWidth: 'none' }}
|
style={{ scrollbarWidth: 'none' }}
|
||||||
data-hide-scrollbar
|
data-hide-scrollbar
|
||||||
>
|
>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const IndexTopMenuNav = () => (
|
const IndexTopMenuNav = ({ isIndexPage = false }: { isIndexPage?: boolean }) => (
|
||||||
<section className="topmenub">
|
<section className={`topmenub${!isIndexPage ? ' topmenub-white' : ''}`} style={!isIndexPage ? { background: '#fff' } : undefined}>
|
||||||
<div className="w-layout-blockcontainer tb nav w-container">
|
<div className="w-layout-blockcontainer tb nav w-container">
|
||||||
<div className="w-layout-hflex flex-block-107">
|
<div className="w-layout-hflex flex-block-107">
|
||||||
<Link href="/about" className="link-block-8 w-inline-block">
|
<Link href="/about" className="link-block-8 w-inline-block">
|
||||||
|
@ -1,57 +1,81 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
import ArticleCard from "../ArticleCard";
|
import ArticleCard from "../ArticleCard";
|
||||||
|
import CatalogProductCardSkeleton from "../CatalogProductCardSkeleton";
|
||||||
|
import { GET_NEW_ARRIVALS } from "@/lib/graphql";
|
||||||
import { PartsAPIArticle } from "@/types/partsapi";
|
import { PartsAPIArticle } from "@/types/partsapi";
|
||||||
|
|
||||||
// Моковые данные для новых поступлений
|
|
||||||
const newArrivalsArticles: PartsAPIArticle[] = [
|
|
||||||
{
|
|
||||||
artId: "1",
|
|
||||||
artArticleNr: "6CT-60L",
|
|
||||||
artSupBrand: "TYUMEN BATTERY",
|
|
||||||
supBrand: "TYUMEN BATTERY",
|
|
||||||
supId: 1,
|
|
||||||
productGroup: "Аккумуляторная батарея",
|
|
||||||
ptId: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
artId: "2",
|
|
||||||
artArticleNr: "A0001",
|
|
||||||
artSupBrand: "Borsehung",
|
|
||||||
supBrand: "Borsehung",
|
|
||||||
supId: 2,
|
|
||||||
productGroup: "Масляный фильтр",
|
|
||||||
ptId: 2,
|
|
||||||
},
|
|
||||||
// ...добавьте еще 6 статей для примера
|
|
||||||
...Array(6).fill(0).map((_, i) => ({
|
|
||||||
artId: `${i+3}`,
|
|
||||||
artArticleNr: `ART${i+3}`,
|
|
||||||
artSupBrand: `Brand${i+3}`,
|
|
||||||
supBrand: `Brand${i+3}`,
|
|
||||||
supId: i+3,
|
|
||||||
productGroup: `Product Group ${i+3}`,
|
|
||||||
ptId: i+3,
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
|
|
||||||
const imagePath = "images/162615.webp";
|
|
||||||
|
|
||||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
|
|
||||||
|
// Интерфейс для товара из GraphQL
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
article?: string;
|
||||||
|
brand?: string;
|
||||||
|
retailPrice?: number;
|
||||||
|
wholesalePrice?: number;
|
||||||
|
createdAt: string;
|
||||||
|
images: Array<{
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
order: number;
|
||||||
|
}>;
|
||||||
|
categories: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для преобразования Product в PartsAPIArticle
|
||||||
|
const transformProductToArticle = (product: Product, index: number): PartsAPIArticle => {
|
||||||
|
return {
|
||||||
|
artId: product.id,
|
||||||
|
artArticleNr: product.article || `PROD-${product.id}`,
|
||||||
|
artSupBrand: product.brand || 'Unknown Brand',
|
||||||
|
supBrand: product.brand || 'Unknown Brand',
|
||||||
|
supId: index + 1,
|
||||||
|
productGroup: product.categories?.[0]?.name || product.name,
|
||||||
|
ptId: index + 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const NewArrivalsSection: React.FC = () => {
|
const NewArrivalsSection: React.FC = () => {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Получаем новые поступления через GraphQL
|
||||||
|
const { data, loading, error } = useQuery(GET_NEW_ARRIVALS, {
|
||||||
|
variables: { limit: 8 }
|
||||||
|
});
|
||||||
|
|
||||||
const scrollLeft = () => {
|
const scrollLeft = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollRight = () => {
|
const scrollRight = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Преобразуем данные для ArticleCard
|
||||||
|
const newArrivalsArticles = data?.newArrivals?.map((product: Product, index: number) =>
|
||||||
|
transformProductToArticle(product, index)
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
// Получаем изображения для товаров
|
||||||
|
const getProductImage = (product: Product): string => {
|
||||||
|
if (product.images && product.images.length > 0) {
|
||||||
|
return product.images[0].url;
|
||||||
|
}
|
||||||
|
return "/images/162615.webp"; // fallback изображение
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
@ -60,18 +84,71 @@ const NewArrivalsSection: React.FC = () => {
|
|||||||
<h2 className="heading-4">Новое поступление</h2>
|
<h2 className="heading-4">Новое поступление</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="carousel-row">
|
<div className="carousel-row">
|
||||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
<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">
|
<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"/>
|
<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"/>
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
{newArrivalsArticles.map((article, i) => (
|
{loading ? (
|
||||||
<ArticleCard key={article.artId || i} article={{ ...article, artId: article.artId }} index={i} image={imagePath} />
|
// Показываем скелетоны во время загрузки
|
||||||
))}
|
Array(8).fill(0).map((_, index) => (
|
||||||
|
<CatalogProductCardSkeleton key={`skeleton-${index}`} />
|
||||||
|
))
|
||||||
|
) : error ? (
|
||||||
|
// Показываем сообщение об ошибке
|
||||||
|
<div className="error-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Не удалось загрузить новые поступления</p>
|
||||||
|
<p style={{ fontSize: '14px', marginTop: '8px' }}>
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : newArrivalsArticles.length > 0 ? (
|
||||||
|
// Показываем товары
|
||||||
|
newArrivalsArticles.map((article: PartsAPIArticle, index: number) => {
|
||||||
|
const product = data.newArrivals[index];
|
||||||
|
const image = getProductImage(product);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArticleCard
|
||||||
|
key={article.artId || `article-${index}`}
|
||||||
|
article={article}
|
||||||
|
index={index}
|
||||||
|
image={image}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// Показываем сообщение о том, что товаров нет
|
||||||
|
<div className="no-products-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Пока нет новых поступлений</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
|
||||||
|
<button
|
||||||
|
className="carousel-arrow carousel-arrow-right"
|
||||||
|
onClick={scrollRight}
|
||||||
|
aria-label="Прокрутить вправо"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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"/>
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
@ -92,7 +92,7 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
|
|||||||
<div
|
<div
|
||||||
key={entity.id}
|
key={entity.id}
|
||||||
layer-name="legal"
|
layer-name="legal"
|
||||||
className="flex relative flex-col gap-8 items-start self-stretch px-5 py-3 rounded-lg bg-slate-50 max-sm:px-4 max-sm:py-2.5"
|
className="flex relative flex-col gap-8 items-start self-stretch px-5 py-3 rounded-lg bg-slate-50 max-sm:px-4 max-sm:py-2.5 hover:bg-slate-200 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="flex relative justify-between items-center self-stretch max-sm:flex-col max-sm:gap-4 max-sm:items-start">
|
<div className="flex relative justify-between items-center self-stretch max-sm:flex-col max-sm:gap-4 max-sm:items-start">
|
||||||
<div className="flex relative gap-5 items-center max-md:flex-wrap max-md:gap-4 max-sm:flex-col max-sm:gap-2.5 max-sm:items-start">
|
<div className="flex relative gap-5 items-center max-md:flex-wrap max-md:gap-4 max-sm:flex-col max-sm:gap-2.5 max-sm:items-start">
|
||||||
|
@ -78,14 +78,14 @@ const ProfileActsMain = () => {
|
|||||||
<div
|
<div
|
||||||
key={tab}
|
key={tab}
|
||||||
layer-name="Tabs_button"
|
layer-name="Tabs_button"
|
||||||
className={`flex relative gap-5 items-center self-stretch rounded-xl flex-[1_0_0] min-w-[200px] max-md:gap-4 max-md:w-full max-md:min-w-[unset] max-sm:gap-2.5 ${activeTab === tab ? "" : "bg-slate-200"}`}
|
className={`flex relative gap-5 items-center self-stretch rounded-xl flex-[1_0_0] min-w-[200px] text-[14px] max-md:gap-4 max-md:w-full max-md:min-w-[unset] max-sm:gap-2.5 ${activeTab === tab ? "" : "bg-slate-200 hover:bg-slate-200"}`}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<div className={`flex relative gap-5 justify-center items-center px-6 py-3.5 rounded-xl flex-[1_0_0] ${activeTab === tab ? "bg-red-600" : "bg-slate-200"}`}>
|
<div className={`flex relative gap-5 justify-center items-center px-6 py-3.5 rounded-xl flex-[1_0_0] ${activeTab === tab ? "bg-red-600" : "bg-slate-200"}`}>
|
||||||
<div
|
<div
|
||||||
layer-name="Курьером"
|
layer-name="Курьером"
|
||||||
className={`relative text-lg font-medium leading-5 text-center max-sm:text-base ${activeTab === tab ? "text-white" : "text-gray-950"}`}
|
className={`relative font-medium leading-5 text-center text-[14px] max-sm:text-base ${activeTab === tab ? "text-white" : "text-gray-950"}`}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,7 +179,7 @@ const ProfileGarageMain = () => {
|
|||||||
|
|
||||||
{!vehiclesLoading && filteredVehicles.map((vehicle) => (
|
{!vehiclesLoading && filteredVehicles.map((vehicle) => (
|
||||||
<div key={vehicle.id} className="mt-8">
|
<div key={vehicle.id} className="mt-8">
|
||||||
<div className="flex flex-col justify-center px-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full">
|
<div className="flex flex-col justify-center px-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full hover:bg-slate-200 transition-colors cursor-pointer">
|
||||||
<div className="flex flex-wrap gap-8 items-center w-full max-md:max-w-full">
|
<div className="flex flex-wrap gap-8 items-center w-full max-md:max-w-full">
|
||||||
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
||||||
<div className="self-stretch my-auto text-xl font-bold leading-none text-gray-950">
|
<div className="self-stretch my-auto text-xl font-bold leading-none text-gray-950">
|
||||||
@ -247,63 +247,65 @@ const ProfileGarageMain = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/* Расширенная информация об автомобиле — вложена внутрь карточки */}
|
||||||
|
<div
|
||||||
{/* Расширенная информация об автомобиле */}
|
className={
|
||||||
{expandedVehicle === vehicle.id && (
|
`overflow-hidden transition-all duration-300 rounded-lg flex flex-col gap-4` +
|
||||||
<div className="mt-4 px-5 py-4 bg-white rounded-lg border border-gray-200">
|
(expandedVehicle === vehicle.id ? ' py-4 max-h-[1000px] opacity-100 mt-4' : ' max-h-0 opacity-0 pointer-events-none')
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||||
{vehicle.brand && (
|
{vehicle.brand && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Бренд:</span>
|
<div className="font-bold text-gray-950">Бренд</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.brand}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.brand}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.model && (
|
{vehicle.model && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Модель:</span>
|
<div className="font-bold text-gray-950">Модель</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.model}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.model}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.modification && (
|
{vehicle.modification && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Модификация:</span>
|
<div className="font-bold text-gray-950">Модификация</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.modification}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.modification}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.year && (
|
{vehicle.year && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Год:</span>
|
<div className="font-bold text-gray-950">Год</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.year}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.year}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.frame && (
|
{vehicle.frame && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Номер кузова:</span>
|
<div className="font-bold text-gray-950">Номер кузова</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.frame}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.frame}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.licensePlate && (
|
{vehicle.licensePlate && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Госномер:</span>
|
<div className="font-bold text-gray-950">Госномер</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.licensePlate}</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.licensePlate}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vehicle.mileage && (
|
{vehicle.mileage && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Пробег:</span>
|
<div className="font-bold text-gray-950">Пробег</div>
|
||||||
<span className="ml-2 text-gray-900">{vehicle.mileage.toLocaleString()} км</span>
|
<div className="mt-1.5 text-gray-600">{vehicle.mileage.toLocaleString()} км</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-gray-700">Добавлен:</span>
|
<div className="font-bold text-gray-950">Добавлен</div>
|
||||||
<span className="ml-2 text-gray-900">
|
<div className="mt-1.5 text-gray-600">
|
||||||
{new Date(vehicle.createdAt).toLocaleDateString('ru-RU')}
|
{new Date(vehicle.createdAt).toLocaleDateString('ru-RU')}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!showAddCar && (
|
{!showAddCar && (
|
||||||
@ -400,10 +402,10 @@ const ProfileGarageMain = () => {
|
|||||||
{!historyLoading && searchHistory.length > 0 && (
|
{!historyLoading && searchHistory.length > 0 && (
|
||||||
<div className="flex flex-col mt-8 w-full max-md:max-w-full">
|
<div className="flex flex-col mt-8 w-full max-md:max-w-full">
|
||||||
{searchHistory.map((historyItem) => (
|
{searchHistory.map((historyItem) => (
|
||||||
<div key={historyItem.id} className="flex flex-col justify-center px-5 py-3 mb-2.5 w-full rounded-lg bg-slate-50 min-h-[44px] max-md:max-w-full">
|
<div key={historyItem.id} className="flex flex-col justify-center px-5 py-3 mb-2.5 w-full rounded-lg bg-slate-50 min-h-[44px] max-md:max-w-full hover:bg-slate-200 transition-colors cursor-pointer">
|
||||||
<div className="flex flex-wrap gap-10 justify-between items-center w-full max-md:max-w-full">
|
<div className="flex flex-wrap gap-10 justify-between items-center w-full max-md:max-w-full">
|
||||||
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
|
||||||
<div className="self-stretch my-auto text-lg font-bold leading-none text-gray-950">
|
<div className="self-stretch my-auto text-lg font-bold leading-none text-gray-950 w-[300px]">
|
||||||
{historyItem.brand && historyItem.model
|
{historyItem.brand && historyItem.model
|
||||||
? `${historyItem.brand} ${historyItem.model}`
|
? `${historyItem.brand} ${historyItem.model}`
|
||||||
: 'Автомобиль найден'}
|
: 'Автомобиль найден'}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface VehicleInfo {
|
interface VehicleInfo {
|
||||||
brand?: string;
|
brand?: string;
|
||||||
@ -15,6 +16,10 @@ interface ProfileHistoryItemProps {
|
|||||||
vehicleInfo?: VehicleInfo;
|
vehicleInfo?: VehicleInfo;
|
||||||
resultCount?: number;
|
resultCount?: number;
|
||||||
onDelete?: (id: string) => void;
|
onDelete?: (id: string) => void;
|
||||||
|
// Добавляем новые пропсы для поиска
|
||||||
|
searchType?: 'TEXT' | 'ARTICLE' | 'OEM' | 'VIN' | 'PLATE' | 'WIZARD' | 'PART_VEHICLES';
|
||||||
|
articleNumber?: string;
|
||||||
|
brand?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||||
@ -26,7 +31,12 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
vehicleInfo,
|
vehicleInfo,
|
||||||
resultCount,
|
resultCount,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
searchType,
|
||||||
|
articleNumber,
|
||||||
|
brand,
|
||||||
}) => {
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onDelete) {
|
if (onDelete) {
|
||||||
@ -34,6 +44,28 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleItemClick = () => {
|
||||||
|
// Определяем куда перенаправлять в зависимости от типа поиска
|
||||||
|
if (searchType === 'VIN' || searchType === 'PLATE') {
|
||||||
|
// Для VIN и госномера перенаправляем на vehicle-search-results
|
||||||
|
router.push(`/vehicle-search-results?q=${encodeURIComponent(name)}`);
|
||||||
|
} else if (searchType === 'ARTICLE' || searchType === 'OEM' || (searchType === 'TEXT' && articleNumber)) {
|
||||||
|
// Для поиска по артикулу/OEM или текстового поиска с артикулом
|
||||||
|
const searchBrand = brand || manufacturer || '';
|
||||||
|
const searchArticle = articleNumber || name;
|
||||||
|
router.push(`/search-result?article=${encodeURIComponent(searchArticle)}&brand=${encodeURIComponent(searchBrand)}`);
|
||||||
|
} else if (searchType === 'TEXT') {
|
||||||
|
// Для обычного текстового поиска
|
||||||
|
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||||
|
} else if (searchType === 'PART_VEHICLES') {
|
||||||
|
// Для поиска автомобилей по детали
|
||||||
|
router.push(`/vehicles-by-part?partNumber=${encodeURIComponent(name)}`);
|
||||||
|
} else {
|
||||||
|
// По умолчанию - обычный поиск
|
||||||
|
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getSearchTypeDisplay = (article: string) => {
|
const getSearchTypeDisplay = (article: string) => {
|
||||||
if (article.includes('TEXT')) return 'Текстовый поиск';
|
if (article.includes('TEXT')) return 'Текстовый поиск';
|
||||||
if (article.includes('ARTICLE')) return 'По артикулу';
|
if (article.includes('ARTICLE')) return 'По артикулу';
|
||||||
@ -48,7 +80,11 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-1.5 w-full border border-gray-200 border-solid min-h-[1px] max-md:max-w-full" />
|
<div className="mt-1.5 w-full border border-gray-200 border-solid min-h-[1px] max-md:max-w-full" />
|
||||||
<div className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors">
|
<div
|
||||||
|
className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors"
|
||||||
|
onClick={handleItemClick}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
|
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
|
||||||
<div className="self-stretch my-auto w-40 max-md:w-full text-sm">
|
<div className="self-stretch my-auto w-40 max-md:w-full text-sm">
|
||||||
<div className="font-medium text-gray-900">{date}</div>
|
<div className="font-medium text-gray-900">{date}</div>
|
||||||
|
@ -3,6 +3,7 @@ import { useQuery, useMutation } from '@apollo/client';
|
|||||||
import ProfileHistoryItem from "./ProfileHistoryItem";
|
import ProfileHistoryItem from "./ProfileHistoryItem";
|
||||||
import SearchInput from "./SearchInput";
|
import SearchInput from "./SearchInput";
|
||||||
import ProfileHistoryTabs from "./ProfileHistoryTabs";
|
import ProfileHistoryTabs from "./ProfileHistoryTabs";
|
||||||
|
import Pagination from '../Pagination';
|
||||||
import {
|
import {
|
||||||
GET_PARTS_SEARCH_HISTORY,
|
GET_PARTS_SEARCH_HISTORY,
|
||||||
DELETE_SEARCH_HISTORY_ITEM,
|
DELETE_SEARCH_HISTORY_ITEM,
|
||||||
@ -20,13 +21,20 @@ const ProfileHistoryMain = () => {
|
|||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||||
|
|
||||||
|
// Состояние пагинации
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10); // Количество элементов на странице
|
||||||
|
|
||||||
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
|
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
|
||||||
|
|
||||||
// GraphQL запросы
|
// GraphQL запросы
|
||||||
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
|
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
|
||||||
GET_PARTS_SEARCH_HISTORY,
|
GET_PARTS_SEARCH_HISTORY,
|
||||||
{
|
{
|
||||||
variables: { limit: 100, offset: 0 },
|
variables: {
|
||||||
|
limit: 1000, // Загружаем больше для клиентской пагинации с фильтрами
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
console.log('История поиска загружена:', data);
|
console.log('История поиска загружена:', data);
|
||||||
@ -161,8 +169,32 @@ const ProfileHistoryMain = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilteredItems(filtered);
|
setFilteredItems(filtered);
|
||||||
|
// Сбрасываем страницу на первую при изменении фильтров
|
||||||
|
setCurrentPage(1);
|
||||||
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||||
|
|
||||||
|
// Вычисляем элементы для текущей страницы
|
||||||
|
const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentPageItems = filteredItems.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Обработчик изменения страницы
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
// Прокручиваем к началу списка при смене страницы
|
||||||
|
const historyContainer = document.querySelector('.flex.flex-col.mt-5.w-full.text-gray-400');
|
||||||
|
if (historyContainer) {
|
||||||
|
historyContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик изменения количества элементов на странице
|
||||||
|
const handleItemsPerPageChange = (newItemsPerPage: number) => {
|
||||||
|
setItemsPerPage(newItemsPerPage);
|
||||||
|
setCurrentPage(1); // Сбрасываем на первую страницу
|
||||||
|
};
|
||||||
|
|
||||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
@ -287,6 +319,7 @@ const ProfileHistoryMain = () => {
|
|||||||
setSelectedManufacturer("Все");
|
setSelectedManufacturer("Все");
|
||||||
setSearch("");
|
setSearch("");
|
||||||
setActiveTab("Все");
|
setActiveTab("Все");
|
||||||
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
@ -424,7 +457,7 @@ const ProfileHistoryMain = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredItems.map((item) => (
|
currentPageItems.map((item) => (
|
||||||
<ProfileHistoryItem
|
<ProfileHistoryItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
id={item.id}
|
id={item.id}
|
||||||
@ -441,18 +474,58 @@ const ProfileHistoryMain = () => {
|
|||||||
vehicleInfo={item.vehicleInfo}
|
vehicleInfo={item.vehicleInfo}
|
||||||
resultCount={item.resultCount}
|
resultCount={item.resultCount}
|
||||||
onDelete={handleDeleteItem}
|
onDelete={handleDeleteItem}
|
||||||
|
searchType={item.searchType}
|
||||||
|
articleNumber={item.articleNumber}
|
||||||
|
brand={item.brand}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Пагинация */}
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<div className="mt-4 text-center text-sm text-gray-500">
|
<div className="mt-6 space-y-4">
|
||||||
Показано {filteredItems.length} из {historyItems.length} записей
|
{/* Селектор количества элементов на странице */}
|
||||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-2 sm:space-y-0">
|
||||||
<span className="ml-2 text-blue-600">
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
(применены фильтры)
|
<span>Показывать по:</span>
|
||||||
</span>
|
<select
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
||||||
|
className="px-2 py-1 border border-gray-200 rounded text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-[#ec1c24] focus:border-transparent"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
<span>записей</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500 text-center sm:text-right">
|
||||||
|
Показано {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} из {filteredItems.length} записей
|
||||||
|
{filteredItems.length !== historyItems.length && (
|
||||||
|
<span className="ml-1">
|
||||||
|
(всего {historyItems.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||||
|
<span className="ml-2 text-blue-600">
|
||||||
|
(применены фильтры)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Компонент пагинации */}
|
||||||
|
{filteredItems.length > itemsPerPage && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
showPageInfo={true}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -69,7 +69,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
|||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<div
|
<div
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`flex flex-1 shrink gap-5 items-center h-full text-center rounded-xl basis-12 min-w-[200px] ${
|
className={`flex flex-1 shrink gap-5 items-center h-full text-center rounded-xl basis-12 min-w-[160px] text-[14px] ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "text-white"
|
? "text-white"
|
||||||
: "bg-slate-200 text-gray-950"
|
: "bg-slate-200 text-gray-950"
|
||||||
@ -78,7 +78,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
|||||||
onClick={() => onTabChange(tab)}
|
onClick={() => onTabChange(tab)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 min-w-[200px] max-md:px-5 ${
|
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 min-w-[160px] text-[14px] max-md:px-5 ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "text-white bg-red-600"
|
? "text-white bg-red-600"
|
||||||
: "bg-slate-200 text-gray-950"
|
: "bg-slate-200 text-gray-950"
|
||||||
@ -94,7 +94,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center px-6 py-4 text-sm leading-snug bg-white rounded border border-solid border-stone-300 text-neutral-500 cursor-pointer select-none w-full min-w-[200px]"
|
className="flex justify-between items-center px-6 py-3 text-sm leading-snug bg-white rounded border border-solid border-stone-300 text-neutral-500 cursor-pointer select-none w-full min-w-[200px]"
|
||||||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedManufacturer}</span>
|
<span className="truncate">{selectedManufacturer}</span>
|
||||||
|
@ -172,12 +172,12 @@ const ProfileOrdersMain: React.FC<ProfileOrdersMainProps> = (props) => {
|
|||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<div
|
<div
|
||||||
key={tab.label}
|
key={tab.label}
|
||||||
className={`flex flex-1 shrink gap-5 items-center h-full rounded-xl basis-0 ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
className={`flex flex-1 shrink gap-5 items-center h-full rounded-xl basis-0 text-[14px] ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => setActiveTab(idx)}
|
onClick={() => setActiveTab(idx)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 max-md:px-5 ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
className={`flex-1 shrink gap-5 self-stretch px-6 py-3.5 my-auto w-full rounded-xl basis-0 max-md:px-5 text-[14px] ${activeTab === idx ? "bg-red-600 text-white" : "bg-slate-200 text-gray-950"}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import VehicleAttributesTooltip from './VehicleAttributesTooltip';
|
||||||
|
|
||||||
interface VehicleAttribute {
|
interface VehicleAttribute {
|
||||||
key: string;
|
key: string;
|
||||||
@ -206,42 +207,14 @@ const InfoVin: React.FC<InfoVinProps> = ({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Tooltip с фиксированным позиционированием */}
|
{/* Tooltip с фиксированным позиционированием */}
|
||||||
{showTooltip && vehicleAttributes.length > 0 && (
|
<VehicleAttributesTooltip
|
||||||
<div
|
show={showTooltip && vehicleAttributes.length > 0}
|
||||||
className="fixed w-[500px] max-w-[90vw] bg-white border border-gray-200 rounded-lg shadow-xl z-[9999] p-4 animate-in fade-in-0 zoom-in-95 duration-200"
|
position={tooltipPosition}
|
||||||
style={{
|
vehicleName={vehicleName}
|
||||||
left: `${tooltipPosition.x}px`,
|
vehicleAttributes={vehicleAttributes}
|
||||||
top: `${tooltipPosition.y}px`,
|
onMouseEnter={handleMouseEnter}
|
||||||
}}
|
onMouseLeave={handleMouseLeave}
|
||||||
onMouseEnter={handleMouseEnter}
|
/>
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{/* Заголовок */}
|
|
||||||
<div className="mb-3 pb-2 border-b border-gray-100">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900">
|
|
||||||
Полная информация об автомобиле
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-gray-600 mt-1">{vehicleName}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Атрибуты в сетке */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{vehicleAttributes.map((attr, index) => (
|
|
||||||
<div key={index} className="flex flex-col">
|
|
||||||
<dt className="text-xs font-medium text-gray-500 mb-1">{attr.name}</dt>
|
|
||||||
<dd className="text-xs text-gray-900 break-words">{attr.value}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Подвал */}
|
|
||||||
<div className="mt-3 pt-2 border-t border-gray-100">
|
|
||||||
<div className="text-xs text-gray-500 text-center">
|
|
||||||
Всего параметров: {vehicleAttributes.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,9 @@ interface KnotInProps {
|
|||||||
note?: string;
|
note?: string;
|
||||||
attributes?: Array<{ key: string; name?: string; value: string }>;
|
attributes?: Array<{ key: string; name?: string; value: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для уведомления KnotParts о выделении детали
|
||||||
|
onPartsHighlight?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||||||
|
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для корректного формирования URL изображения
|
// Функция для корректного формирования URL изображения
|
||||||
@ -34,12 +37,23 @@ const getImageUrl = (baseUrl: string, size: string) => {
|
|||||||
.replace('%size%', size);
|
.replace('%size%', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
|
const KnotIn: React.FC<KnotInProps> = ({
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
ssd,
|
||||||
|
unitId,
|
||||||
|
unitName,
|
||||||
|
parts,
|
||||||
|
onPartSelect,
|
||||||
|
onPartsHighlight,
|
||||||
|
selectedParts = new Set()
|
||||||
|
}) => {
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||||
const selectedImageSize = 'source';
|
const selectedImageSize = 'source';
|
||||||
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
|
||||||
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
|
||||||
|
const [hoveredCodeOnImage, setHoveredCodeOnImage] = useState<string | number | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Получаем инфо об узле (для картинки)
|
// Получаем инфо об узле (для картинки)
|
||||||
@ -150,21 +164,62 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
|
// Обработчик наведения на точку
|
||||||
const handlePointClick = (codeonimage: string | number) => {
|
const handlePointHover = (coord: any) => {
|
||||||
|
// Попробуем использовать разные поля для связи
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
|
||||||
|
console.log('🔍 KnotIn - hover на точку:', {
|
||||||
|
coord,
|
||||||
|
detailid: coord.detailid,
|
||||||
|
codeonimage: coord.codeonimage,
|
||||||
|
code: coord.code,
|
||||||
|
identifierToUse,
|
||||||
|
type: typeof identifierToUse,
|
||||||
|
coordinatesLength: coordinates.length,
|
||||||
|
partsLength: parts?.length || 0,
|
||||||
|
firstCoord: coordinates[0],
|
||||||
|
firstPart: parts?.[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
setHoveredCodeOnImage(identifierToUse);
|
||||||
|
if (onPartsHighlight) {
|
||||||
|
onPartsHighlight(identifierToUse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Клик по точке: выделить в списке деталей
|
||||||
|
const handlePointClick = (coord: any) => {
|
||||||
if (!parts) return;
|
if (!parts) return;
|
||||||
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
|
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
console.log('Клик по точке:', identifierToUse, 'Координата:', coord, 'Все детали:', parts);
|
||||||
|
|
||||||
|
// Уведомляем родительский компонент о выборе детали для выделения в списке
|
||||||
|
if (onPartSelect) {
|
||||||
|
onPartSelect(identifierToUse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Двойной клик по точке: переход на страницу выбора бренда
|
||||||
|
const handlePointDoubleClick = (coord: any) => {
|
||||||
|
if (!parts) return;
|
||||||
|
|
||||||
|
const identifierToUse = coord.detailid || coord.codeonimage || coord.code;
|
||||||
|
console.log('Двойной клик по точке:', identifierToUse, 'Координата:', coord);
|
||||||
|
|
||||||
const part = parts.find(
|
const part = parts.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
|
(p.detailid && p.detailid.toString() === identifierToUse?.toString()) ||
|
||||||
(p.detailid && p.detailid.toString() === codeonimage.toString())
|
(p.codeonimage && p.codeonimage.toString() === identifierToUse?.toString())
|
||||||
);
|
);
|
||||||
console.log('Найдена деталь для точки:', part);
|
|
||||||
if (part?.oem) {
|
if (part?.oem) {
|
||||||
setSelectedDetail({ oem: part.oem, name: part.name || '' });
|
// Переходим на страницу выбора бренда вместо модального окна
|
||||||
setIsBrandModalOpen(true);
|
const url = `/vehicle-search/${catalogCode}/${vehicleId}/part/${part.oem}/brands?detailName=${encodeURIComponent(part.name || '')}`;
|
||||||
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
|
console.warn('Нет артикула (oem) для выбранной точки:', identifierToUse, part);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,6 +227,40 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('KnotIn parts:', parts);
|
console.log('KnotIn parts:', parts);
|
||||||
console.log('KnotIn coordinates:', coordinates);
|
console.log('KnotIn coordinates:', coordinates);
|
||||||
|
if (coordinates.length > 0) {
|
||||||
|
console.log('🔍 Первые 5 координат:', coordinates.slice(0, 5).map((c: any) => ({
|
||||||
|
code: c.code,
|
||||||
|
codeonimage: c.codeonimage,
|
||||||
|
detailid: c.detailid,
|
||||||
|
x: c.x,
|
||||||
|
y: c.y
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
if (parts && parts.length > 0) {
|
||||||
|
console.log('🔍 Первые 5 деталей:', parts.slice(0, 5).map(p => ({
|
||||||
|
name: p.name,
|
||||||
|
codeonimage: p.codeonimage,
|
||||||
|
detailid: p.detailid,
|
||||||
|
oem: p.oem
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попытка связать координаты с деталями
|
||||||
|
if (coordinates.length > 0 && parts && parts.length > 0) {
|
||||||
|
console.log('🔗 Попытка связать координаты с деталями:');
|
||||||
|
coordinates.forEach((coord: any, idx: number) => {
|
||||||
|
const matchingPart = parts.find(part =>
|
||||||
|
part.detailid === coord.detailid ||
|
||||||
|
part.codeonimage === coord.codeonimage ||
|
||||||
|
part.codeonimage === coord.code
|
||||||
|
);
|
||||||
|
if (matchingPart) {
|
||||||
|
console.log(` ✅ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> Деталь: ${matchingPart.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Координата ${idx}: detailid=${coord.detailid}, codeonimage=${coord.codeonimage} -> НЕ НАЙДЕНА`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [parts, coordinates]);
|
}, [parts, coordinates]);
|
||||||
|
|
||||||
if (unitInfoLoading || imageMapLoading) {
|
if (unitInfoLoading || imageMapLoading) {
|
||||||
@ -222,11 +311,7 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative inline-block p-5" style={{ borderRadius: 8, background: '#fff' }}>
|
<div className="relative inline-block">
|
||||||
{/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
|
|
||||||
{/* <div style={{ position: 'absolute', top: 4, left: 4, zIndex: 20, background: 'rgba(255,0,0,0.1)', color: '#c00', fontWeight: 700, fontSize: 14, padding: '2px 8px', borderRadius: 6 }}>
|
|
||||||
{coordinates.length} точек
|
|
||||||
</div> */}
|
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
@ -242,38 +327,63 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
const size = 22;
|
const size = 22;
|
||||||
const scaledX = coord.x * imageScale.x - size / 2;
|
const scaledX = coord.x * imageScale.x - size / 2;
|
||||||
const scaledY = coord.y * imageScale.y - size / 2;
|
const scaledY = coord.y * imageScale.y - size / 2;
|
||||||
|
|
||||||
|
// Используем code или codeonimage в зависимости от структуры данных
|
||||||
|
const codeValue = coord.code || coord.codeonimage;
|
||||||
|
|
||||||
|
// Определяем состояние точки
|
||||||
|
const isSelected = selectedParts.has(codeValue);
|
||||||
|
const isHovered = hoveredCodeOnImage === codeValue;
|
||||||
|
|
||||||
|
// Определяем цвета на основе состояния
|
||||||
|
let backgroundColor = '#B7CAE2'; // Базовый цвет
|
||||||
|
let textColor = '#000';
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
backgroundColor = '#22C55E'; // Зеленый для выбранных
|
||||||
|
textColor = '#fff';
|
||||||
|
} else if (isHovered) {
|
||||||
|
backgroundColor = '#EC1C24'; // Красный при наведении
|
||||||
|
textColor = '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`Деталь ${coord.codeonimage}`}
|
aria-label={`Деталь ${codeValue}`}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord);
|
||||||
}}
|
}}
|
||||||
className="absolute flex items-center justify-center cursor-pointer transition-colors"
|
className="absolute flex items-center justify-center cursor-pointer transition-all duration-200 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
left: scaledX,
|
left: scaledX,
|
||||||
top: scaledY,
|
top: scaledY,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
background: '#B7CAE2',
|
backgroundColor,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
|
border: isSelected ? '2px solid #16A34A' : 'none',
|
||||||
|
transform: isHovered || isSelected ? 'scale(1.1)' : 'scale(1)',
|
||||||
|
zIndex: isHovered || isSelected ? 10 : 1,
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
}}
|
}}
|
||||||
title={coord.codeonimage}
|
title={`${codeValue} (Клик - выделить в списке, двойной клик - перейти к выбору бренда)`}
|
||||||
onClick={() => handlePointClick(coord.codeonimage)}
|
onClick={() => handlePointClick(coord)}
|
||||||
onMouseEnter={e => {
|
onDoubleClick={() => handlePointDoubleClick(coord)}
|
||||||
(e.currentTarget as HTMLDivElement).style.background = '#EC1C24';
|
onMouseEnter={() => handlePointHover(coord)}
|
||||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#fff';
|
onMouseLeave={() => {
|
||||||
}}
|
setHoveredCodeOnImage(null);
|
||||||
onMouseLeave={e => {
|
if (onPartsHighlight) {
|
||||||
(e.currentTarget as HTMLDivElement).style.background = '#B7CAE2';
|
onPartsHighlight(null);
|
||||||
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#000';
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none" style={{color: '#000'}}>
|
<span
|
||||||
{coord.codeonimage}
|
className="flex items-center justify-center w-full h-full text-sm font-bold select-none pointer-events-none transition-colors duration-200"
|
||||||
|
style={{ color: textColor }}
|
||||||
|
>
|
||||||
|
{codeValue}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface KnotPartsProps {
|
interface KnotPartsProps {
|
||||||
@ -16,10 +16,42 @@ interface KnotPartsProps {
|
|||||||
selectedCodeOnImage?: string | number;
|
selectedCodeOnImage?: string | number;
|
||||||
catalogCode?: string;
|
catalogCode?: string;
|
||||||
vehicleId?: string;
|
vehicleId?: string;
|
||||||
|
highlightedCodeOnImage?: string | number | null; // Деталь подсвеченная при hover на изображении
|
||||||
|
selectedParts?: Set<string | number>; // Выбранные детали (множественный выбор)
|
||||||
|
onPartSelect?: (codeOnImage: string | number | null) => void; // Коллбек для выбора детали
|
||||||
|
onPartHover?: (codeOnImage: string | number | null) => void; // Коллбек для подсветки при hover
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage, catalogCode, vehicleId }) => {
|
const KnotParts: React.FC<KnotPartsProps> = ({
|
||||||
|
parts = [],
|
||||||
|
selectedCodeOnImage,
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
highlightedCodeOnImage,
|
||||||
|
selectedParts = new Set(),
|
||||||
|
onPartSelect,
|
||||||
|
onPartHover
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Отладочные логи для проверки данных
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('🔍 KnotParts получил данные:', {
|
||||||
|
partsCount: parts.length,
|
||||||
|
firstPart: parts[0],
|
||||||
|
firstPartAttributes: parts[0]?.attributes?.length || 0,
|
||||||
|
allPartsWithAttributes: parts.map(part => ({
|
||||||
|
name: part.name,
|
||||||
|
oem: part.oem,
|
||||||
|
attributesCount: part.attributes?.length || 0,
|
||||||
|
attributes: part.attributes
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}, [parts]);
|
||||||
|
|
||||||
const handlePriceClick = (part: any) => {
|
const handlePriceClick = (part: any) => {
|
||||||
if (part.oem && catalogCode && vehicleId !== undefined) {
|
if (part.oem && catalogCode && vehicleId !== undefined) {
|
||||||
@ -29,6 +61,98 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик клика по детали в списке
|
||||||
|
const handlePartClick = (part: any) => {
|
||||||
|
if (part.codeonimage && onPartSelect) {
|
||||||
|
onPartSelect(part.codeonimage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчики наведения
|
||||||
|
const handlePartMouseEnter = (part: any) => {
|
||||||
|
if (part.codeonimage && onPartHover) {
|
||||||
|
onPartHover(part.codeonimage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePartMouseLeave = () => {
|
||||||
|
if (onPartHover) {
|
||||||
|
onPartHover(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Вычисляем позицию tooltip
|
||||||
|
const calculateTooltipPosition = (iconElement: HTMLElement) => {
|
||||||
|
if (!iconElement) {
|
||||||
|
console.error('❌ calculateTooltipPosition: элемент не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = iconElement.getBoundingClientRect();
|
||||||
|
const tooltipWidth = 400;
|
||||||
|
const tooltipHeight = 300; // примерная высота
|
||||||
|
|
||||||
|
let x = rect.left + rect.width / 2 - tooltipWidth / 2;
|
||||||
|
let y = rect.bottom + 8;
|
||||||
|
|
||||||
|
// Проверяем, не выходит ли tooltip за границы экрана
|
||||||
|
if (x < 10) x = 10;
|
||||||
|
if (x + tooltipWidth > window.innerWidth - 10) {
|
||||||
|
x = window.innerWidth - tooltipWidth - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если tooltip не помещается снизу, показываем сверху
|
||||||
|
if (y + tooltipHeight > window.innerHeight - 10) {
|
||||||
|
y = rect.top - tooltipHeight - 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTooltipPosition({ x, y });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInfoIconMouseEnter = (event: React.MouseEvent, part: any) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем ссылку на элемент до setTimeout
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
if (target && typeof target.getBoundingClientRect === 'function') {
|
||||||
|
calculateTooltipPosition(target);
|
||||||
|
setTooltipPart(part);
|
||||||
|
setShowTooltip(true);
|
||||||
|
console.log('🔍 Показываем тултип для детали:', part.name, 'Атрибуты:', part.attributes?.length || 0);
|
||||||
|
} else {
|
||||||
|
console.error('❌ handleInfoIconMouseEnter: элемент не поддерживает getBoundingClientRect:', target);
|
||||||
|
}
|
||||||
|
}, 300); // Задержка 300ms
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInfoIconMouseLeave = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setShowTooltip(false);
|
||||||
|
setTooltipPart(null);
|
||||||
|
}, 100); // Небольшая задержка перед скрытием
|
||||||
|
};
|
||||||
|
|
||||||
|
// Очищаем таймеры при размонтировании
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Если нет деталей, показываем заглушку
|
// Если нет деталей, показываем заглушку
|
||||||
if (!parts || parts.length === 0) {
|
if (!parts || parts.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -41,29 +165,107 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Эффект для отслеживания изменений подсветки
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🔍 KnotParts - подсветка изменилась:', {
|
||||||
|
highlightedCodeOnImage,
|
||||||
|
highlightedType: typeof highlightedCodeOnImage,
|
||||||
|
partsCodeOnImages: parts.map(p => p.codeonimage),
|
||||||
|
partsDetailIds: parts.map(p => p.detailid),
|
||||||
|
willHighlight: parts.some(part =>
|
||||||
|
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage?.toString()) ||
|
||||||
|
(part.detailid && part.detailid.toString() === highlightedCodeOnImage?.toString())
|
||||||
|
),
|
||||||
|
willHighlightStrict: parts.some(part =>
|
||||||
|
part.codeonimage === highlightedCodeOnImage ||
|
||||||
|
part.detailid === highlightedCodeOnImage
|
||||||
|
),
|
||||||
|
firstPartWithCodeOnImage: parts.find(p => p.codeonimage)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Детальная информация о всех деталях
|
||||||
|
console.log('📋 Все детали с их codeonimage и detailid:');
|
||||||
|
parts.forEach((part, idx) => {
|
||||||
|
console.log(` Деталь ${idx}: "${part.name}" codeonimage="${part.codeonimage}" (${typeof part.codeonimage}) detailid="${part.detailid}" (${typeof part.detailid})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🎯 Ищем подсветку для:', `"${highlightedCodeOnImage}" (${typeof highlightedCodeOnImage})`);
|
||||||
|
}, [highlightedCodeOnImage, parts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Статус выбранных деталей */}
|
||||||
|
{/* {selectedParts.size > 0 && (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-green-800 font-medium">
|
||||||
|
Выбрано деталей: {selectedParts.size}
|
||||||
|
</span>
|
||||||
|
<span className="text-green-600 text-sm ml-2">
|
||||||
|
(Кликните по детали, чтобы убрать из выбранных)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
<div className="knot-parts">
|
<div className="knot-parts">
|
||||||
{parts.map((part, idx) => {
|
{parts.map((part, idx) => {
|
||||||
const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
|
const isHighlighted = highlightedCodeOnImage !== null && highlightedCodeOnImage !== undefined && (
|
||||||
|
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage.toString()) ||
|
||||||
|
(part.detailid && part.detailid.toString() === highlightedCodeOnImage.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected = selectedParts.has(part.detailid || part.codeonimage || idx.toString());
|
||||||
|
|
||||||
|
// Создаем уникальный ключ
|
||||||
|
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-layout-hflex knotlistitem border rounded transition-colors duration-150 ${isSelected ? 'bg-yellow-100 border-yellow-400' : 'border-transparent'}`}
|
key={uniqueKey}
|
||||||
key={part.detailid || idx}
|
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-green-100 border-green-500'
|
||||||
|
: isHighlighted
|
||||||
|
? 'bg-slate-200'
|
||||||
|
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
onClick={() => handlePartClick(part)}
|
||||||
|
onMouseEnter={() => handlePartMouseEnter(part)}
|
||||||
|
onMouseLeave={handlePartMouseLeave}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<div className="w-layout-hflex flex-block-116">
|
<div className="w-layout-hflex flex-block-116">
|
||||||
<div className="nuberlist">{part.codeonimage || idx + 1}</div>
|
<div
|
||||||
<div className="oemnuber">{part.oem}</div>
|
className={`nuberlist ${isSelected ? 'text-green-700 font-bold' : isHighlighted ? ' font-bold' : ''}`}
|
||||||
|
>
|
||||||
|
{part.codeonimage || idx + 1}
|
||||||
|
</div>
|
||||||
|
<div className={`oemnuber ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>{part.oem}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`partsname ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>
|
||||||
|
{part.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="partsname">{part.name}</div>
|
|
||||||
<div className="w-layout-hflex flex-block-117">
|
<div className="w-layout-hflex flex-block-117">
|
||||||
<button
|
<button
|
||||||
className="button-3 w-button"
|
className="button-3 w-button"
|
||||||
onClick={() => handlePriceClick(part)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // Предотвращаем срабатывание onClick родительского элемента
|
||||||
|
handlePriceClick(part);
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
Цена
|
Цена
|
||||||
</button>
|
</button>
|
||||||
<div className="code-embed-16 w-embed">
|
<div
|
||||||
|
className="code-embed-16 w-embed cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
|
onMouseEnter={(e) => handleInfoIconMouseEnter(e, part)}
|
||||||
|
onMouseLeave={handleInfoIconMouseLeave}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
|
<path d="M8.1 13.5H9.89999V8.1H8.1V13.5ZM8.99999 6.3C9.25499 6.3 9.46889 6.2136 9.64169 6.0408C9.81449 5.868 9.90059 5.6544 9.89999 5.4C9.89939 5.1456 9.81299 4.932 9.64079 4.7592C9.46859 4.5864 9.25499 4.5 8.99999 4.5C8.745 4.5 8.53139 4.5864 8.35919 4.7592C8.187 4.932 8.1006 5.1456 8.1 5.4C8.0994 5.6544 8.1858 5.8683 8.35919 6.0417C8.53259 6.2151 8.74619 6.3012 8.99999 6.3ZM8.99999 18C7.755 18 6.585 17.7636 5.49 17.2908C4.395 16.818 3.4425 16.1769 2.6325 15.3675C1.8225 14.5581 1.1814 13.6056 0.709201 12.51C0.237001 11.4144 0.000601139 10.2444 1.13924e-06 9C-0.00059886 7.7556 0.235801 6.5856 0.709201 5.49C1.1826 4.3944 1.8237 3.4419 2.6325 2.6325C3.4413 1.8231 4.3938 1.182 5.49 0.7092C6.5862 0.2364 7.7562 0 8.99999 0C10.2438 0 11.4138 0.2364 12.51 0.7092C13.6062 1.182 14.5587 1.8231 15.3675 2.6325C16.1763 3.4419 16.8177 4.3944 17.2917 5.49C17.7657 6.5856 18.0018 7.7556 18 9C17.9982 10.2444 17.7618 11.4144 17.2908 12.51C16.8198 13.6056 16.1787 14.5581 15.3675 15.3675C14.5563 16.1769 13.6038 16.8183 12.51 17.2917C11.4162 17.7651 10.2462 18.0012 8.99999 18Z" fill="currentcolor" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -73,6 +275,52 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts = [], selectedCodeOnImage,
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Красивый тултип с информацией о детали */}
|
||||||
|
{showTooltip && tooltipPart && (
|
||||||
|
<div
|
||||||
|
className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[350px] min-h-[220px] max-w-full fixed z-[9999]"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: tooltipPosition.x,
|
||||||
|
top: tooltipPosition.y,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex relative flex-col w-full">
|
||||||
|
{/* Заголовок и OEM */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="font-semibold text-lg text-black mb-1 truncate">{tooltipPart.name}</div>
|
||||||
|
{tooltipPart.oem && (
|
||||||
|
<div className="inline-block bg-gray-100 text-gray-700 text-xs font-mono px-2 py-1 rounded mb-1">OEM: {tooltipPart.oem}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Характеристики */}
|
||||||
|
{tooltipPart.attributes && tooltipPart.attributes.length > 0 ? (
|
||||||
|
tooltipPart.attributes.map((attr: any, idx: number) => (
|
||||||
|
<div key={idx} className="flex gap-5 items-center mt-2 w-full whitespace-normal first:mt-0">
|
||||||
|
<div className="self-stretch my-auto text-gray-400 w-[150px] break-words">
|
||||||
|
{attr.name || attr.key}
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch my-auto font-medium text-black break-words">
|
||||||
|
{attr.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center w-full py-8">
|
||||||
|
<div className="text-gray-400 mb-2">Дополнительная информация недоступна</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tooltipPart.note && (
|
||||||
|
<div className="flex flex-col mt-6 w-full">
|
||||||
|
<div className="text-gray-400 text-xs mb-1">Примечание</div>
|
||||||
|
<div className="font-medium text-black text-sm">{tooltipPart.note}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
63
src/components/vin/VehicleAttributesTooltip.tsx
Normal file
63
src/components/vin/VehicleAttributesTooltip.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface VehicleAttribute {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VehicleAttributesTooltipProps {
|
||||||
|
show: boolean;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
vehicleName?: string;
|
||||||
|
vehicleAttributes: VehicleAttribute[];
|
||||||
|
onMouseEnter?: () => void;
|
||||||
|
onMouseLeave?: () => void;
|
||||||
|
imageUrl?: string; // опционально, для будущего
|
||||||
|
}
|
||||||
|
|
||||||
|
const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
|
||||||
|
show,
|
||||||
|
position,
|
||||||
|
vehicleAttributes,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
imageUrl,
|
||||||
|
}) => {
|
||||||
|
if (!show) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[450px] min-h-[365px] max-w-full fixed z-[9999]"
|
||||||
|
style={{
|
||||||
|
left: `${position.x + 120}px`,
|
||||||
|
top: `${position.y}px`,
|
||||||
|
}}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
>
|
||||||
|
{/* Фоновое изображение, если будет нужно */}
|
||||||
|
{imageUrl && (
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src={imageUrl}
|
||||||
|
className="object-cover absolute inset-0 size-full rounded-2xl opacity-10 pointer-events-none"
|
||||||
|
alt="vehicle background"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex relative flex-col w-full">
|
||||||
|
{vehicleAttributes.map((attr, idx) => (
|
||||||
|
<div key={idx} className="flex gap-5 items-center mt-2 w-full whitespace-nowrap first:mt-0">
|
||||||
|
<div className="self-stretch my-auto text-gray-400 w-[150px] truncate">
|
||||||
|
{attr.name}
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch my-auto font-medium text-black truncate">
|
||||||
|
{attr.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleAttributesTooltip;
|
@ -45,6 +45,8 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [shownCounts, setShownCounts] = useState<{ [unitid: string]: number }>({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* <button onClick={onBack} className="mb-4 px-4 py-2 bg-gray-200 rounded self-start">Назад</button> */}
|
{/* <button onClick={onBack} className="mb-4 px-4 py-2 bg-gray-200 rounded self-start">Назад</button> */}
|
||||||
@ -71,16 +73,48 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
|||||||
</div>
|
</div>
|
||||||
<div className="knot-img">
|
<div className="knot-img">
|
||||||
<h1 className="heading-19">{unit.name}</h1>
|
<h1 className="heading-19">{unit.name}</h1>
|
||||||
|
{(() => {
|
||||||
{unit.details && unit.details.length > 0 && unit.details.map((detail: any, index: number) => (
|
const details = unit.details || [];
|
||||||
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
const total = details.length;
|
||||||
<div className="oemnuber">{detail.oem}</div>
|
const shownCount = shownCounts[unit.unitid] ?? 3;
|
||||||
<div className="partsname">{detail.name}</div>
|
return (
|
||||||
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
<>
|
||||||
</div>
|
{details.slice(0, shownCount).map((detail: any, index: number) => (
|
||||||
))}
|
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
||||||
|
<div className="oemnuber">{detail.oem}</div>
|
||||||
<a href="#" className="showallparts w-button" onClick={e => { e.preventDefault(); handleUnitClick(unit); }}>Подробнее</a>
|
<div className="partsname">{detail.name}</div>
|
||||||
|
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{total > 3 && shownCount < total && (
|
||||||
|
<div className="flex gap-2 mt-2 w-full">
|
||||||
|
{shownCount + 3 < total && (
|
||||||
|
<button
|
||||||
|
className="expand-btn"
|
||||||
|
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: shownCount + 3 }))}
|
||||||
|
style={{ border: '1px solid #EC1C24', borderRadius: 8, background: '#fff', color: '#222', padding: '6px 18px', minWidth: 180 }}
|
||||||
|
>
|
||||||
|
Развернуть
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" style={{ display: 'inline', verticalAlign: 'middle', marginLeft: 4 }}>
|
||||||
|
<path d="M4 6l4 4 4-4" stroke="#222" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="showall-btn"
|
||||||
|
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: total }))}
|
||||||
|
style={{ background: '#e9eef5', borderRadius: 8, color: '#222', padding: '6px 18px', border: 'none'}}
|
||||||
|
>
|
||||||
|
Показать все
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shownCount >= total && (
|
||||||
|
<a href="#" className="showallparts w-button" onClick={e => { e.preventDefault(); handleUnitClick(unit); }}>Подробнее</a>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
@ -134,8 +134,13 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Товар удален из избранного', {
|
toast('Товар удален из избранного', {
|
||||||
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
||||||
|
style: {
|
||||||
|
background: '#6b7280', // Серый фон
|
||||||
|
color: '#fff', // Белый текст
|
||||||
|
},
|
||||||
|
duration: 3000,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -1679,3 +1679,30 @@ export const GET_DAILY_PRODUCTS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Запрос для получения новых поступлений
|
||||||
|
export const GET_NEW_ARRIVALS = gql`
|
||||||
|
query GetNewArrivals($limit: Int) {
|
||||||
|
newArrivals(limit: $limit) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
article
|
||||||
|
brand
|
||||||
|
retailPrice
|
||||||
|
wholesalePrice
|
||||||
|
createdAt
|
||||||
|
images {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
alt
|
||||||
|
order
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
@ -54,26 +54,40 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по цене
|
// Получаем все доступные предложения для расчета диапазонов
|
||||||
const prices: number[] = [];
|
const allAvailableOffers: any[] = [];
|
||||||
|
|
||||||
|
// Добавляем основные предложения
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
result.internalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push(offer);
|
||||||
});
|
});
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
result.externalOffers?.forEach((offer: any) => {
|
||||||
|
allAvailableOffers.push(offer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем предложения аналогов
|
||||||
|
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||||
|
analog.internalOffers?.forEach((offer: any) => {
|
||||||
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryDays
|
||||||
|
});
|
||||||
|
});
|
||||||
|
analog.externalOffers?.forEach((offer: any) => {
|
||||||
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryTime
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Фильтр по цене - только если есть предложения с разными ценами
|
||||||
|
const prices: number[] = [];
|
||||||
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
if (offer.price > 0) prices.push(offer.price);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем цены аналогов
|
if (prices.length > 1) {
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
|
||||||
});
|
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prices.length > 0) {
|
|
||||||
const minPrice = Math.min(...prices);
|
const minPrice = Math.min(...prices);
|
||||||
const maxPrice = Math.max(...prices);
|
const maxPrice = Math.max(...prices);
|
||||||
|
|
||||||
@ -87,26 +101,14 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по сроку доставки
|
// Фильтр по сроку доставки - только если есть предложения с разными сроками
|
||||||
const deliveryDays: number[] = [];
|
const deliveryDays: number[] = [];
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
const days = offer.deliveryDays || offer.deliveryTime || offer.deliveryDuration;
|
||||||
});
|
if (days && days > 0) deliveryDays.push(days);
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем сроки доставки аналогов
|
if (deliveryDays.length > 1) {
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryDays && offer.deliveryDays > 0) deliveryDays.push(offer.deliveryDays);
|
|
||||||
});
|
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.deliveryTime && offer.deliveryTime > 0) deliveryDays.push(offer.deliveryTime);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deliveryDays.length > 0) {
|
|
||||||
const minDays = Math.min(...deliveryDays);
|
const minDays = Math.min(...deliveryDays);
|
||||||
const maxDays = Math.max(...deliveryDays);
|
const maxDays = Math.max(...deliveryDays);
|
||||||
|
|
||||||
@ -120,26 +122,13 @@ const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по количеству наличия
|
// Фильтр по количеству наличия - только если есть предложения с разными количествами
|
||||||
const quantities: number[] = [];
|
const quantities: number[] = [];
|
||||||
result.internalOffers?.forEach((offer: any) => {
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
result.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем количества аналогов
|
if (quantities.length > 1) {
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
|
||||||
if (offer.quantity && offer.quantity > 0) quantities.push(offer.quantity);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (quantities.length > 0) {
|
|
||||||
const minQuantity = Math.min(...quantities);
|
const minQuantity = Math.min(...quantities);
|
||||||
const maxQuantity = Math.max(...quantities);
|
const maxQuantity = Math.max(...quantities);
|
||||||
|
|
||||||
@ -163,35 +152,24 @@ const getBestOffers = (offers: any[]) => {
|
|||||||
if (validOffers.length === 0) return [];
|
if (validOffers.length === 0) return [];
|
||||||
|
|
||||||
const result: { offer: any; type: string }[] = [];
|
const result: { offer: any; type: string }[] = [];
|
||||||
const usedOfferIds = new Set<string>();
|
|
||||||
|
|
||||||
// 1. Самая низкая цена (среди всех предложений)
|
// 1. Самая низкая цена (среди всех предложений)
|
||||||
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
|
const lowestPriceOffer = [...validOffers].sort((a, b) => a.price - b.price)[0];
|
||||||
if (lowestPriceOffer) {
|
if (lowestPriceOffer) {
|
||||||
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
|
result.push({ offer: lowestPriceOffer, type: 'Самая низкая цена' });
|
||||||
usedOfferIds.add(`${lowestPriceOffer.articleNumber}-${lowestPriceOffer.price}-${lowestPriceOffer.deliveryDuration}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Самый дешевый аналог (только среди аналогов)
|
// 2. Самый дешевый аналог (только среди аналогов) - всегда показываем если есть аналоги
|
||||||
const analogOffers = validOffers.filter(offer => offer.isAnalog);
|
const analogOffers = validOffers.filter(offer => offer.isAnalog);
|
||||||
if (analogOffers.length > 0) {
|
if (analogOffers.length > 0) {
|
||||||
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
|
const cheapestAnalogOffer = [...analogOffers].sort((a, b) => a.price - b.price)[0];
|
||||||
const analogId = `${cheapestAnalogOffer.articleNumber}-${cheapestAnalogOffer.price}-${cheapestAnalogOffer.deliveryDuration}`;
|
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
||||||
|
|
||||||
if (!usedOfferIds.has(analogId)) {
|
|
||||||
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
|
||||||
usedOfferIds.add(analogId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Самая быстрая доставка (среди всех предложений)
|
// 3. Самая быстрая доставка (среди всех предложений)
|
||||||
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
|
const fastestDeliveryOffer = [...validOffers].sort((a, b) => a.deliveryDuration - b.deliveryDuration)[0];
|
||||||
if (fastestDeliveryOffer) {
|
if (fastestDeliveryOffer) {
|
||||||
const fastestId = `${fastestDeliveryOffer.articleNumber}-${fastestDeliveryOffer.price}-${fastestDeliveryOffer.deliveryDuration}`;
|
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
||||||
|
|
||||||
if (!usedOfferIds.has(fastestId)) {
|
|
||||||
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -376,7 +354,101 @@ export default function SearchResult() {
|
|||||||
|
|
||||||
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
|
const hasOffers = result && (result.internalOffers.length > 0 || result.externalOffers.length > 0);
|
||||||
const hasAnalogs = result && result.analogs.length > 0;
|
const hasAnalogs = result && result.analogs.length > 0;
|
||||||
const searchResultFilters = createFilters(result, loadedAnalogs);
|
|
||||||
|
// Создаем динамические фильтры на основе доступных данных с учетом активных фильтров
|
||||||
|
const searchResultFilters = useMemo(() => {
|
||||||
|
const baseFilters = createFilters(result, loadedAnalogs);
|
||||||
|
|
||||||
|
// Если нет активных фильтров, возвращаем базовые фильтры
|
||||||
|
if (!filtersAreActive) {
|
||||||
|
return baseFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем динамические фильтры с учетом других активных фильтров
|
||||||
|
return baseFilters.map(filter => {
|
||||||
|
if (filter.type !== 'range') {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для каждого диапазонного фильтра пересчитываем границы на основе
|
||||||
|
// предложений, отфильтрованных другими фильтрами (исключая текущий)
|
||||||
|
let relevantOffers = allOffers;
|
||||||
|
|
||||||
|
// Применяем все фильтры кроме текущего
|
||||||
|
relevantOffers = allOffers.filter(offer => {
|
||||||
|
// Фильтр по бренду (если это не фильтр производителя)
|
||||||
|
if (filter.title !== 'Производитель' && selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Фильтр по цене (если это не фильтр цены)
|
||||||
|
if (filter.title !== 'Цена (₽)' && priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Фильтр по сроку доставки (если это не фильтр доставки)
|
||||||
|
if (filter.title !== 'Срок доставки (дни)' && deliveryRange) {
|
||||||
|
const deliveryDays = offer.deliveryDuration;
|
||||||
|
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Фильтр по количеству (если это не фильтр количества)
|
||||||
|
if (filter.title !== 'Количество (шт.)' && quantityRange) {
|
||||||
|
const quantity = offer.quantity;
|
||||||
|
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Фильтр по поисковой строке
|
||||||
|
if (filterSearchTerm) {
|
||||||
|
const searchTerm = filterSearchTerm.toLowerCase();
|
||||||
|
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
|
||||||
|
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
|
||||||
|
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
|
||||||
|
if (!brandMatch && !articleMatch && !nameMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Пересчитываем диапазон на основе отфильтрованных предложений
|
||||||
|
if (filter.title === 'Цена (₽)') {
|
||||||
|
const prices = relevantOffers.filter(o => o.price > 0).map(o => o.price);
|
||||||
|
if (prices.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.floor(Math.min(...prices)),
|
||||||
|
max: Math.ceil(Math.max(...prices))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (filter.title === 'Срок доставки (дни)') {
|
||||||
|
const deliveryDays = relevantOffers
|
||||||
|
.map(o => o.deliveryDuration)
|
||||||
|
.filter(d => d && d > 0);
|
||||||
|
if (deliveryDays.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.min(...deliveryDays),
|
||||||
|
max: Math.max(...deliveryDays)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (filter.title === 'Количество (шт.)') {
|
||||||
|
const quantities = relevantOffers
|
||||||
|
.map(o => o.quantity)
|
||||||
|
.filter(q => q && q > 0);
|
||||||
|
if (quantities.length > 0) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
min: Math.min(...quantities),
|
||||||
|
max: Math.max(...quantities)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
|
}, [result, loadedAnalogs, filtersAreActive, allOffers, selectedBrands, priceRange, deliveryRange, quantityRange, filterSearchTerm]);
|
||||||
|
|
||||||
const bestOffersData = getBestOffers(filteredOffers);
|
const bestOffersData = getBestOffers(filteredOffers);
|
||||||
|
|
||||||
|
|
||||||
@ -406,6 +478,8 @@ export default function SearchResult() {
|
|||||||
}
|
}
|
||||||
}, [q, article, router.query]);
|
}, [q, article, router.query]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
|
// Удаляем старую заглушку - теперь обрабатываем все типы поиска
|
||||||
|
|
||||||
const minPrice = useMemo(() => {
|
const minPrice = useMemo(() => {
|
||||||
@ -452,44 +526,52 @@ export default function SearchResult() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
<InfoSearch
|
{/* Показываем InfoSearch только если есть результаты */}
|
||||||
brand={result ? result.brand : brandQuery}
|
{initialOffersExist && (
|
||||||
articleNumber={result ? result.articleNumber : searchQuery}
|
<InfoSearch
|
||||||
name={result ? result.name : "деталь"}
|
brand={result ? result.brand : brandQuery}
|
||||||
offersCount={result ? result.totalOffers : 0}
|
articleNumber={result ? result.articleNumber : searchQuery}
|
||||||
minPrice={minPrice}
|
name={result ? result.name : "деталь"}
|
||||||
/>
|
offersCount={result ? result.totalOffers : 0}
|
||||||
<section className="main mobile-only">
|
minPrice={minPrice}
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
/>
|
||||||
<div className="w-layout-hflex flex-block-84">
|
)}
|
||||||
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
{/* Показываем мобильные фильтры только если есть результаты */}
|
||||||
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
{initialOffersExist && (
|
||||||
<span className="code-embed-9 w-embed">
|
<>
|
||||||
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<section className="main mobile-only">
|
||||||
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<div className="w-layout-hflex flex-block-84">
|
||||||
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
||||||
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
||||||
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<span className="code-embed-9 w-embed">
|
||||||
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</span>
|
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
<div>Фильтры</div>
|
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</div>
|
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</div>
|
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</div>
|
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</section>
|
</svg>
|
||||||
{/* Мобильная панель фильтров */}
|
</span>
|
||||||
<FiltersPanelMobile
|
<div>Фильтры</div>
|
||||||
filters={searchResultFilters}
|
</div>
|
||||||
open={showFiltersMobile}
|
</div>
|
||||||
onClose={() => setShowFiltersMobile(false)}
|
</div>
|
||||||
searchQuery={filterSearchTerm}
|
</section>
|
||||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
{/* Мобильная панель фильтров */}
|
||||||
/>
|
<FiltersPanelMobile
|
||||||
|
filters={searchResultFilters}
|
||||||
|
open={showFiltersMobile}
|
||||||
|
onClose={() => setShowFiltersMobile(false)}
|
||||||
|
searchQuery={filterSearchTerm}
|
||||||
|
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{/* Лучшие предложения */}
|
{/* Лучшие предложения */}
|
||||||
{bestOffersData.length > 0 && (
|
{bestOffersData.length > 0 && (
|
||||||
<section className="section-6">
|
<section className="section-6">
|
||||||
@ -547,24 +629,26 @@ export default function SearchResult() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<section className="main">
|
{/* Показываем основную секцию с фильтрами только если есть результаты */}
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
{initialOffersExist && (
|
||||||
<div className="w-layout-hflex flex-block-13-copy">
|
<section className="main">
|
||||||
{/* Фильтры для десктопа */}
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
|
<div className="w-layout-hflex flex-block-13-copy">
|
||||||
<Filters
|
{/* Фильтры для десктопа */}
|
||||||
filters={searchResultFilters}
|
<div style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
|
||||||
onFilterChange={handleFilterChange}
|
<Filters
|
||||||
filterValues={{
|
filters={searchResultFilters}
|
||||||
'Производитель': selectedBrands,
|
onFilterChange={handleFilterChange}
|
||||||
'Цена (₽)': priceRange,
|
filterValues={{
|
||||||
'Срок доставки (дни)': deliveryRange,
|
'Производитель': selectedBrands,
|
||||||
'Количество (шт.)': quantityRange
|
'Цена (₽)': priceRange,
|
||||||
}}
|
'Срок доставки (дни)': deliveryRange,
|
||||||
searchQuery={filterSearchTerm}
|
'Количество (шт.)': quantityRange
|
||||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
}}
|
||||||
/>
|
searchQuery={filterSearchTerm}
|
||||||
</div>
|
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Основной товар */}
|
{/* Основной товар */}
|
||||||
<div className="w-layout-vflex flex-block-14-copy">
|
<div className="w-layout-vflex flex-block-14-copy">
|
||||||
@ -736,9 +820,13 @@ export default function SearchResult() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="section-3">
|
)}
|
||||||
<CatalogSubscribe />
|
{/* Показываем CatalogSubscribe только если есть результаты */}
|
||||||
</section>
|
{initialOffersExist && (
|
||||||
|
<section className="section-3">
|
||||||
|
<CatalogSubscribe />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
<Footer />
|
<Footer />
|
||||||
<MobileMenuBottomSection />
|
<MobileMenuBottomSection />
|
||||||
</>
|
</>
|
||||||
|
@ -199,7 +199,7 @@ const SearchPage = () => {
|
|||||||
<div key={detail.detailid || index}>
|
<div key={detail.detailid || index}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePartDetail(detail)}
|
onClick={() => handlePartDetail(detail)}
|
||||||
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
className="w-full text-left p-4 hover:bg-slate-200 transition-colors block group"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>{detail.manufacturer}</div>
|
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>{detail.manufacturer}</div>
|
||||||
@ -252,7 +252,7 @@ const SearchPage = () => {
|
|||||||
{vehiclesResult!.catalogs.map((catalog) => (
|
{vehiclesResult!.catalogs.map((catalog) => (
|
||||||
<tr
|
<tr
|
||||||
key={catalog.catalogCode}
|
key={catalog.catalogCode}
|
||||||
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
className="hover:bg-slate-200 cursor-pointer transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`/search-result?article=${encodeURIComponent(searchQuery)}&brand=${encodeURIComponent(catalog.brand)}`);
|
router.push(`/search-result?article=${encodeURIComponent(searchQuery)}&brand=${encodeURIComponent(catalog.brand)}`);
|
||||||
}}
|
}}
|
||||||
|
@ -74,6 +74,8 @@ const VehicleDetailsPage = () => {
|
|||||||
});
|
});
|
||||||
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
||||||
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
||||||
|
const [selectedParts, setSelectedParts] = useState<Set<string | number>>(new Set());
|
||||||
|
const [highlightedPart, setHighlightedPart] = useState<string | number | null>(null);
|
||||||
|
|
||||||
// Получаем информацию о выбранном автомобиле
|
// Получаем информацию о выбранном автомобиле
|
||||||
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
|
const ssdFromQuery = Array.isArray(router.query.ssd) ? router.query.ssd[0] : router.query.ssd;
|
||||||
@ -138,6 +140,20 @@ const VehicleDetailsPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Получаем детали выбранного узла, если он выбран
|
// Получаем детали выбранного узла, если он выбран
|
||||||
|
console.log('🔍 [vehicleId].tsx - Проверка условий для GET_LAXIMO_UNIT_DETAILS:', {
|
||||||
|
selectedNode: selectedNode ? {
|
||||||
|
unitid: selectedNode.unitid,
|
||||||
|
name: selectedNode.name,
|
||||||
|
hasSsd: !!selectedNode.ssd
|
||||||
|
} : null,
|
||||||
|
skipCondition: !selectedNode,
|
||||||
|
catalogCode: selectedNode?.catalogCode || selectedNode?.catalog || brand,
|
||||||
|
vehicleId: selectedNode?.vehicleId || vehicleId,
|
||||||
|
unitId: selectedNode?.unitid || selectedNode?.unitId,
|
||||||
|
ssd: selectedNode?.ssd || finalSsd || '',
|
||||||
|
finalSsd: finalSsd ? `${finalSsd.substring(0, 50)}...` : 'отсутствует'
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unitDetailsData,
|
data: unitDetailsData,
|
||||||
loading: unitDetailsLoading,
|
loading: unitDetailsLoading,
|
||||||
@ -155,6 +171,23 @@ const VehicleDetailsPage = () => {
|
|||||||
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
|
: { catalogCode: '', vehicleId: '', unitId: '', ssd: '' },
|
||||||
skip: !selectedNode,
|
skip: !selectedNode,
|
||||||
errorPolicy: 'all',
|
errorPolicy: 'all',
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
notifyOnNetworkStatusChange: true,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
console.log('🔍 [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS completed:', {
|
||||||
|
detailsCount: data?.laximoUnitDetails?.length || 0,
|
||||||
|
firstDetail: data?.laximoUnitDetails?.[0],
|
||||||
|
allDetails: data?.laximoUnitDetails?.map((detail: any) => ({
|
||||||
|
name: detail.name,
|
||||||
|
oem: detail.oem,
|
||||||
|
codeonimage: detail.codeonimage,
|
||||||
|
attributesCount: detail.attributes?.length || 0
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ [vehicleId].tsx - GET_LAXIMO_UNIT_DETAILS error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -234,6 +267,22 @@ const VehicleDetailsPage = () => {
|
|||||||
|
|
||||||
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
|
const unitDetails = unitDetailsData?.laximoUnitDetails || [];
|
||||||
|
|
||||||
|
// Детальное логирование данных от API
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (unitDetailsData?.laximoUnitDetails) {
|
||||||
|
console.log('🔍 [vehicleId].tsx - Полные данные unitDetails от API:', {
|
||||||
|
totalParts: unitDetailsData.laximoUnitDetails.length,
|
||||||
|
firstPart: unitDetailsData.laximoUnitDetails[0],
|
||||||
|
allCodeOnImages: unitDetailsData.laximoUnitDetails.map((part: any) => ({
|
||||||
|
name: part.name,
|
||||||
|
codeonimage: part.codeonimage,
|
||||||
|
detailid: part.detailid,
|
||||||
|
oem: part.oem
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [unitDetailsData]);
|
||||||
|
|
||||||
// Логируем ошибки
|
// Логируем ошибки
|
||||||
if (vehicleError) {
|
if (vehicleError) {
|
||||||
console.error('Vehicle GraphQL error:', vehicleError);
|
console.error('Vehicle GraphQL error:', vehicleError);
|
||||||
@ -382,6 +431,9 @@ const VehicleDetailsPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setSelectedNode(node);
|
setSelectedNode(node);
|
||||||
|
// Сброс состояния выбранных деталей при открытии нового узла
|
||||||
|
setSelectedParts(new Set());
|
||||||
|
setHighlightedPart(null);
|
||||||
router.push(
|
router.push(
|
||||||
{ pathname: router.pathname, query: { ...router.query, unitid: node.unitid || node.id } },
|
{ pathname: router.pathname, query: { ...router.query, unitid: node.unitid || node.id } },
|
||||||
undefined,
|
undefined,
|
||||||
@ -391,6 +443,9 @@ const VehicleDetailsPage = () => {
|
|||||||
// Закрыть KnotIn и удалить unitid из URL
|
// Закрыть KnotIn и удалить unitid из URL
|
||||||
const closeKnot = () => {
|
const closeKnot = () => {
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
|
// Сброс состояния выбранных деталей при закрытии узла
|
||||||
|
setSelectedParts(new Set());
|
||||||
|
setHighlightedPart(null);
|
||||||
const { unitid, ...rest } = router.query;
|
const { unitid, ...rest } = router.query;
|
||||||
router.push(
|
router.push(
|
||||||
{ pathname: router.pathname, query: rest },
|
{ pathname: router.pathname, query: rest },
|
||||||
@ -399,6 +454,25 @@ const VehicleDetailsPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик выбора детали (множественный выбор)
|
||||||
|
const handlePartSelect = (codeOnImage: string | number | null) => {
|
||||||
|
if (codeOnImage === null) return; // Игнорируем null значения
|
||||||
|
setSelectedParts(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(codeOnImage)) {
|
||||||
|
newSet.delete(codeOnImage); // Убираем если уже выбрана
|
||||||
|
} else {
|
||||||
|
newSet.add(codeOnImage); // Добавляем если не выбрана
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик подсветки детали при наведении
|
||||||
|
const handlePartHighlight = (codeOnImage: string | number | null) => {
|
||||||
|
setHighlightedPart(codeOnImage);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
@ -551,6 +625,9 @@ const VehicleDetailsPage = () => {
|
|||||||
unitId={selectedNode.unitid}
|
unitId={selectedNode.unitid}
|
||||||
unitName={selectedNode.name}
|
unitName={selectedNode.name}
|
||||||
parts={unitDetails}
|
parts={unitDetails}
|
||||||
|
onPartSelect={handlePartSelect}
|
||||||
|
onPartsHighlight={handlePartHighlight}
|
||||||
|
selectedParts={selectedParts}
|
||||||
/>
|
/>
|
||||||
{unitDetailsLoading ? (
|
{unitDetailsLoading ? (
|
||||||
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
|
||||||
@ -561,6 +638,10 @@ const VehicleDetailsPage = () => {
|
|||||||
parts={unitDetails}
|
parts={unitDetails}
|
||||||
catalogCode={vehicleInfo.catalog}
|
catalogCode={vehicleInfo.catalog}
|
||||||
vehicleId={vehicleInfo.vehicleid}
|
vehicleId={vehicleInfo.vehicleid}
|
||||||
|
highlightedCodeOnImage={highlightedPart}
|
||||||
|
selectedParts={selectedParts}
|
||||||
|
onPartSelect={handlePartSelect}
|
||||||
|
onPartHover={handlePartHighlight}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
|
<div style={{ padding: 24, textAlign: 'center' }}>Детали не найдены</div>
|
||||||
|
@ -11,11 +11,15 @@ import MetaTags from '@/components/MetaTags';
|
|||||||
import { getMetaByPath } from '@/lib/meta-config';
|
import { getMetaByPath } from '@/lib/meta-config';
|
||||||
|
|
||||||
const InfoBrandSelection = ({
|
const InfoBrandSelection = ({
|
||||||
|
brand,
|
||||||
brandName,
|
brandName,
|
||||||
|
vehicleId,
|
||||||
oemNumber,
|
oemNumber,
|
||||||
detailName
|
detailName
|
||||||
}: {
|
}: {
|
||||||
|
brand: string;
|
||||||
brandName: string;
|
brandName: string;
|
||||||
|
vehicleId: string;
|
||||||
oemNumber: string;
|
oemNumber: string;
|
||||||
detailName?: string;
|
detailName?: string;
|
||||||
}) => (
|
}) => (
|
||||||
@ -27,20 +31,22 @@ const InfoBrandSelection = ({
|
|||||||
<div>Главная</div>
|
<div>Главная</div>
|
||||||
</a>
|
</a>
|
||||||
<div className="text-block-3">→</div>
|
<div className="text-block-3">→</div>
|
||||||
<a href="#" className="link-block-2 w-inline-block">
|
<a href="#" className="link-block w-inline-block">
|
||||||
<div>Каталог</div>
|
<div>Каталог</div>
|
||||||
</a>
|
</a>
|
||||||
<div className="text-block-3">→</div>
|
<div className="text-block-3">→</div>
|
||||||
<div>{brandName}</div>
|
<a href={`/vehicle-search/${brand}/${vehicleId}`} className="link-block w-inline-block">
|
||||||
|
<div>{brandName}</div>
|
||||||
|
</a>
|
||||||
<div className="text-block-3">→</div>
|
<div className="text-block-3">→</div>
|
||||||
<div>Деталь {oemNumber}</div>
|
<a href="#" className="link-block-2 w-inline-block">
|
||||||
<div className="text-block-3">→</div>
|
<div>Деталь {oemNumber}</div>
|
||||||
<div>Выбор производителя</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-8">
|
<div className="link-block w-inline-block">
|
||||||
<div className="w-layout-hflex flex-block-10">
|
|
||||||
<h1 className="heading">Выберите производителя для {oemNumber}</h1>
|
<div className="heading">Выберите производителя для {oemNumber}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -123,15 +129,17 @@ const BrandSelectionPage = () => {
|
|||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
<InfoBrandSelection
|
<InfoBrandSelection
|
||||||
|
brand={String(brand)}
|
||||||
brandName={catalogInfo?.name || String(brand)}
|
brandName={catalogInfo?.name || String(brand)}
|
||||||
|
vehicleId={String(vehicleId)}
|
||||||
oemNumber={String(oemNumber)}
|
oemNumber={String(oemNumber)}
|
||||||
detailName={String(detailName || '')}
|
detailName={String(detailName || '')}
|
||||||
/>
|
/>
|
||||||
<div className="page-wrapper bg-[#F5F8FB] min-h-screen">
|
<div className="page-wrapper bg-[#F5F8FB] min-h-screen">
|
||||||
<div className="w-full max-w-[1580px] mx-auto px-8 max-md:px-5 pt-10 pb-16">
|
<div className="mx-auto px-8 max-md:px-5 pt-10 pb-16 ">
|
||||||
|
|
||||||
{/* Кнопка назад */}
|
{/* Кнопка назад */}
|
||||||
<div className="mb-6">
|
{/* <div className="mb-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
@ -141,7 +149,7 @@ const BrandSelectionPage = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
Назад к деталям
|
Назад к деталям
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Обработка ошибок */}
|
{/* Обработка ошибок */}
|
||||||
{hasError && !loading && (
|
{hasError && !loading && (
|
||||||
@ -187,40 +195,40 @@ const BrandSelectionPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : brands.length > 0 && (
|
) : brands.length > 0 && (
|
||||||
<div className="bg-white rounded-2xl shadow p-10">
|
<div className="bg-white rounded-2xl shadow p-10 w-full max-w-[1580px] mx-auto min-h-[500px]">
|
||||||
<div className="border-b border-gray-200 pb-4">
|
{/* <div className="border-b border-gray-200 pb-4">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
Выбор производителя для артикула: {oemNumber}
|
Выбор производителя для артикула: {oemNumber}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
{detailName && <span>Деталь: {detailName} • </span>}
|
{detailName && <span>Деталь: {detailName} • </span>}
|
||||||
Найдено производителей: <span className="font-medium">{brands.length}</span>
|
Найдено производителей: <span className="font-medium">{brands.length}</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div> */}
|
||||||
|
<div className="divide-y divide-gray-200">
|
||||||
|
{brands.map((brandItem: any, index: number) => (
|
||||||
|
<div key={index}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleBrandSelect(brandItem.brand)}
|
||||||
|
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>
|
||||||
|
{brandItem.brand}
|
||||||
|
</div>
|
||||||
|
<div className="w-1/5 max-md:text-center max-md:w-1/3 font-bold text-left truncate group-hover:text-[#EC1C24] transition-colors">
|
||||||
|
{oemNumber}
|
||||||
|
</div>
|
||||||
|
<div className="w-3/5 max-md:w-1/3 text-left truncate">
|
||||||
|
{brandItem.name && brandItem.name !== brandItem.brand ? brandItem.name : detailName || 'Запчасть'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-gray-200">
|
)}
|
||||||
{brands.map((brandItem: any, index: number) => (
|
|
||||||
<div key={index}>
|
|
||||||
<button
|
|
||||||
onClick={() => handleBrandSelect(brandItem.brand)}
|
|
||||||
className="w-full text-left p-4 hover:bg-gray-50 transition-colors block group"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center gap-2">
|
|
||||||
<div className="w-1/5 max-md:w-1/3 font-bold text-left truncate" style={{ color: 'rgb(77, 180, 94)' }}>
|
|
||||||
{brandItem.brand}
|
|
||||||
</div>
|
|
||||||
<div className="w-1/5 max-md:text-center max-md:w-1/3 font-bold text-left truncate group-hover:text-[#EC1C24] transition-colors">
|
|
||||||
{oemNumber}
|
|
||||||
</div>
|
|
||||||
<div className="w-3/5 max-md:w-1/3 text-left truncate">
|
|
||||||
{brandItem.name && brandItem.name !== brandItem.brand ? brandItem.name : detailName || 'Запчасть'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,3 +101,280 @@ input[type=number] {
|
|||||||
.cookie-consent-enter {
|
.cookie-consent-enter {
|
||||||
animation: slideInFromBottom 0.3s ease-out;
|
animation: slideInFromBottom 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Анимации для тултипов */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-in {
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-0 {
|
||||||
|
animation-name: fadeIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-95 {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-200 {
|
||||||
|
animation-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для кнопок с курсором pointer */
|
||||||
|
button,
|
||||||
|
.cursor-pointer,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== СОВРЕМЕННЫЕ СТИЛИ ДЛЯ КРАСИВОГО ТУЛТИПА ===== */
|
||||||
|
|
||||||
|
.tooltip-detail-modern {
|
||||||
|
animation: tooltip-modern-fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content-modern {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 420px;
|
||||||
|
min-width: 280px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: none;
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern {
|
||||||
|
background: linear-gradient(135deg, #EC1C24 0%, #DC1C24 100%);
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-icon {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-oem-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-body-modern {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-section-title svg {
|
||||||
|
color: #EC1C24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attributes-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-item:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-key {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-attribute-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-note-modern {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-note-text {
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text div:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-no-data-text div:last-child {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-modern-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность для мобильных устройств */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tooltip-content-modern {
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header-modern {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-body-modern {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom_head{
|
.bottom_head{
|
||||||
z-index: 60;
|
z-index: 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top_head{
|
.top_head{
|
||||||
@ -614,6 +614,52 @@ body {
|
|||||||
font-family: Onest, sans-serif;
|
font-family: Onest, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading{
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-block-4,
|
||||||
|
.flex-block-124,
|
||||||
|
.flex-block-6-copy
|
||||||
|
{
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.link-block.w-inline-block,
|
||||||
|
a.link-block-2.w-inline-block {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-product-search.carousel-scroll {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap; /* Не переносить строки */
|
||||||
|
gap: 16px; /* Отступ между карточками, если нужно */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe{
|
||||||
|
padding-top: 10px !important;
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block-14, .div-block-9{
|
||||||
|
width: 350px !important;
|
||||||
|
max-width: 350px !important;
|
||||||
|
min-width: 350px !important;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1920px) {
|
||||||
|
.text-block-14, .div-block-9{
|
||||||
|
width: 350px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.flex-block-18{
|
||||||
|
row-gap: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.menu-button.w--open {
|
.menu-button.w--open {
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
@ -621,10 +667,9 @@ body {
|
|||||||
color: var(--white);
|
color: var(--white);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 50px;
|
width: 50px;
|
||||||
padding-top: 15px;
|
height: 44px;
|
||||||
padding-bottom: 15px;
|
padding: 13px 12px;
|
||||||
left: auto;
|
|
||||||
}
|
}
|
||||||
.heading-7 {
|
.heading-7 {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
@ -661,15 +706,25 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-block-14-copy-copy{
|
||||||
|
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showall-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.showall-btn:hover {
|
||||||
|
background: #ec1c24 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 991px) {
|
@media screen and (max-width: 991px) {
|
||||||
.flex-block-108, .flex-block-14-copy-copy {
|
.flex-block-108, .flex-block-14-copy-copy {
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
}
|
}
|
||||||
.flex-block-14-copy-copy{
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 991px) {
|
@media screen and (max-width: 991px) {
|
||||||
@ -891,6 +946,22 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topmenub[style*='#fff'] .link-block-8 {
|
||||||
|
border: 1px solid #E6EDF6 !important;
|
||||||
|
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenub-white .link-block-8 {
|
||||||
|
border: 1px solid #E6EDF6 !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.info {
|
||||||
|
padding-top: 5px !important;
|
||||||
|
padding-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel-row {
|
.carousel-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -968,3 +1039,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.protekauto-logo {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3000;
|
||||||
|
}
|
@ -1008,10 +1008,9 @@ body {
|
|||||||
color: var(--white);
|
color: var(--white);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 50px;
|
width: 50px;
|
||||||
padding-top: 15px;
|
height: 44px;
|
||||||
padding-bottom: 15px;
|
padding: 13px 12px;
|
||||||
left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-button.w--open:hover {
|
.menu-button.w--open:hover {
|
||||||
|
Reference in New Issue
Block a user