Compare commits
23 Commits
c703fc839a
...
fix1407
Author | SHA1 | Date | |
---|---|---|---|
47844749eb | |||
d95d008c0c | |||
657016731c | |||
87339d577e | |||
ad5dcc03e3 | |||
132e39b87e | |||
aef3915dde | |||
e22828039f | |||
d25970946c | |||
320b7500e0 | |||
cebe3a10ac | |||
791152a862 | |||
b11142ad0f | |||
508ad8cff3 | |||
53a398a072 | |||
268e6d3315 | |||
26e4a95ae4 | |||
9fc7d0fbf5 | |||
7abe016f0f | |||
90d1beb15e | |||
475b02ea2d | |||
ed76c97915 | |||
3b5defe3d9 |
BIN
public/images/resource2.png
Normal file
BIN
public/images/resource2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
4
public/images/tg.svg
Normal file
4
public/images/tg.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" stroke="#3666AF"/>
|
||||||
|
<path d="M34.2793 16.5068C34.3984 16.5224 34.4333 16.557 34.4463 16.5742C34.4631 16.5968 34.5228 16.6979 34.4912 16.9902V16.9912C34.4154 17.6921 33.7439 21.3424 33.0635 24.957C32.3861 28.5553 31.7089 32.0707 31.6309 32.4668L31.626 32.4932L31.623 32.5195C31.6053 32.7215 31.53 32.9106 31.4102 33.0635C31.2905 33.216 31.1329 33.3249 30.959 33.3789C30.7854 33.4328 30.6006 33.4312 30.4277 33.374C30.2548 33.3168 30.0986 33.2054 29.9814 33.0508L29.9463 33.0049L29.9023 32.9678L29.2803 32.458C28.6312 31.931 27.9193 31.3671 27.3184 30.8965C26.9173 30.5824 26.5647 30.3094 26.3125 30.1143C26.1864 30.0167 26.0852 29.9384 26.0156 29.8848C25.9808 29.858 25.9538 29.8373 25.9355 29.8232C25.9266 29.8164 25.9197 29.8112 25.915 29.8076C25.9127 29.8058 25.9104 29.8037 25.9092 29.8027L25.9082 29.8018L25.4434 29.4453L25.1641 29.96L25.1631 29.9619C25.1623 29.9634 25.1608 29.9658 25.1592 29.9688C25.1559 29.9748 25.1509 29.9843 25.1445 29.9961C25.1317 30.0197 25.1129 30.0545 25.0889 30.0986C25.0405 30.1873 24.9701 30.3143 24.8857 30.4678C24.7169 30.775 24.4887 31.1878 24.251 31.6104C24.0128 32.0336 23.7668 32.4646 23.5615 32.8086C23.4587 32.9808 23.3679 33.1279 23.2949 33.2402C23.2584 33.2964 23.228 33.3408 23.2041 33.374C23.1962 33.385 23.1886 33.3929 23.1836 33.3994C23.1637 33.4152 23.1446 33.4313 23.123 33.4434L23.0352 33.4814C22.9745 33.4999 22.9106 33.5047 22.8486 33.4961C22.7869 33.4875 22.7264 33.4651 22.6709 33.4297C22.635 33.4068 22.6029 33.3768 22.5732 33.3438L23.499 28.5059L30.4111 21.4072L29.7969 20.6299L20.418 26.2236C20.3006 26.1868 20.1399 26.1365 19.9482 26.0762C19.5336 25.9455 18.9738 25.769 18.3945 25.584C17.2605 25.2217 16.0903 24.8387 15.7441 24.7031C15.673 24.6568 15.611 24.5917 15.5674 24.5098C15.519 24.4187 15.4953 24.3133 15.501 24.2061C15.5067 24.0988 15.5416 23.9979 15.5986 23.9141C15.6556 23.8303 15.7317 23.7678 15.8164 23.7314L15.8359 23.7227L15.8545 23.7129C16.0536 23.6069 17.1957 23.1279 18.8613 22.4463C20.5063 21.7731 22.6204 20.9185 24.7236 20.0742C26.8267 19.23 28.9184 18.396 30.5176 17.7627C31.317 17.4461 31.9927 17.1794 32.4854 16.9873C32.7318 16.8912 32.9321 16.8144 33.0781 16.7588C33.1512 16.731 33.2102 16.7094 33.2539 16.6934C33.2757 16.6853 33.2928 16.6783 33.3057 16.6738C33.3155 16.6704 33.3206 16.6685 33.3223 16.668C33.6166 16.5781 34.0014 16.4706 34.2793 16.5068Z" fill="white" stroke="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
16
public/images/tg2.svg
Normal file
16
public/images/tg2.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_124_6073)">
|
||||||
|
<circle opacity="0.5" cx="9.16667" cy="9.16667" r="8.41667" transform="matrix(-1 0 0 1 19.1665 0.832031)" stroke="white" stroke-width="1.5"/>
|
||||||
|
<g clip-path="url(#clip1_124_6073)">
|
||||||
|
<path d="M11.9858 12.9316L10.1548 11.582L9.81592 11.332L9.51221 11.624L9.12451 11.9961L9.17041 11.3467L12.5698 8.27344L12.5708 8.27441C12.6421 8.21083 12.7861 8.06052 12.7944 7.8291C12.8031 7.58109 12.6598 7.42067 12.519 7.33691L13.231 7.0625L11.9858 12.9316ZM7.53564 10.043L6.40967 9.69043L10.7515 8.01758L7.53564 10.043Z" fill="white" stroke="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_124_6073">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_124_6073">
|
||||||
|
<rect width="8.33333" height="8.33333" fill="white" transform="translate(5.4165 5.83203)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 920 B |
4
public/images/vk.svg
Normal file
4
public/images/vk.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" stroke="#3666AF"/>
|
||||||
|
<path d="M23.4375 20.917L23.3975 20.8223C23.1956 20.3506 22.8792 19.9429 22.4844 19.6279C22.6135 19.568 22.7523 19.5305 22.8945 19.5205C23.2821 19.5044 23.9699 19.497 24.6172 19.501C25.2655 19.5049 25.816 19.5212 26.0049 19.5459C26.1226 19.5701 26.2295 19.6331 26.3076 19.7256C26.3848 19.8171 26.4293 19.9319 26.4346 20.0518V24.3838H26.4365C26.4322 24.5232 26.4562 24.6623 26.5117 24.791C26.5715 24.9296 26.6639 25.0523 26.7803 25.1484C26.8965 25.2444 27.0341 25.3113 27.1816 25.3438C27.3129 25.3726 27.4481 25.3711 27.5791 25.3438C27.7616 25.3168 27.8822 25.2101 27.9355 25.1562C28.0039 25.0873 28.0583 25.008 28.0996 24.9395C28.1838 24.7998 28.2655 24.6207 28.3418 24.4316C28.4964 24.0483 28.6638 23.5411 28.8262 23.0264C28.994 22.4944 29.1477 21.9811 29.2969 21.5127C29.4092 21.1601 29.5047 20.8789 29.5791 20.6924L29.6465 20.5381L29.6514 20.5273C29.8222 20.161 30.097 19.8537 30.4404 19.6426L30.4795 19.6182L30.5146 19.5859C30.5643 19.541 30.6272 19.5147 30.6934 19.5088H34.4189L34.4561 19.5254C34.4712 19.5376 34.4835 19.5536 34.4912 19.5723C34.4989 19.5911 34.5013 19.6118 34.499 19.6318C34.4966 19.6521 34.489 19.6715 34.4775 19.6875L34.4492 19.7275L34.4287 19.7734C34.3677 19.9094 34.1674 20.2666 33.8779 20.7598C33.5957 21.2408 33.2449 21.8232 32.8984 22.3916C32.5521 22.9599 32.2108 23.513 31.9482 23.9336C31.8171 24.1436 31.706 24.3198 31.624 24.4482C31.5412 24.578 31.5033 24.6348 31.499 24.6416L31.4932 24.6484L31.4883 24.6553C31.2828 24.9337 31.1446 25.256 31.084 25.5967L31.0693 25.6816L31.083 25.7666C31.1505 26.1721 31.3358 26.5486 31.6152 26.8496V26.8506C31.8697 27.139 33.9028 29.3836 34.2236 29.7168H34.2246C34.3319 29.8404 34.4083 29.987 34.4512 30.1445C34.4172 30.2326 34.3603 30.3104 34.2852 30.3682H34.2842C34.1854 30.444 34.0625 30.4811 33.9385 30.4727L33.9219 30.4717H30.8867C30.5116 30.3295 30.1654 30.1187 29.8672 29.8486L29.8643 29.8467L29.7363 29.7207C29.5756 29.5559 29.3261 29.2837 29.0674 28.9971C28.7448 28.6397 28.4081 28.2609 28.3135 28.166H28.3125C28.2096 28.0527 28.0842 27.9621 27.9424 27.9033C27.8249 27.8546 27.6996 27.8277 27.5732 27.8242L27.4941 27.8262C27.3925 27.8262 27.077 27.8223 26.8086 28.0508C26.5266 28.291 26.4234 28.668 26.4336 29.1162L26.4346 29.1328L26.4355 29.1504C26.4716 29.551 26.3934 29.9522 26.2139 30.3105C26.1467 30.3642 26.0718 30.4073 25.9912 30.4375C25.8869 30.4765 25.7752 30.4939 25.6641 30.4883L25.6367 30.4873L25.6094 30.4883C24.354 30.5646 23.1049 30.2526 22.0322 29.5938C21.0667 28.9919 19.9953 27.8569 19.0127 26.5635C18.0362 25.278 17.1816 23.8807 16.6426 22.8008L16.6338 22.7842L16.624 22.7676L16.4014 22.3721C15.9326 21.4819 15.6341 20.5114 15.5166 19.5098H18.6152C18.6843 19.5593 18.7426 19.6231 18.7861 19.6973L18.791 19.7061C18.7975 19.7204 18.8074 19.7426 18.8203 19.7744C18.8482 19.8433 18.8848 19.9395 18.9287 20.0576C19.0166 20.2938 19.1266 20.6002 19.2432 20.9209C19.3588 21.2391 19.4804 21.5702 19.5879 21.8477C19.6893 22.1094 19.7944 22.367 19.876 22.5078C19.9784 22.6845 20.3421 23.3664 20.7451 23.9805C20.9472 24.2884 21.1742 24.6028 21.4014 24.8457C21.5147 24.9669 21.6411 25.0846 21.7764 25.1748C21.9051 25.2606 22.0897 25.3535 22.3086 25.3535H22.3574L22.4053 25.3447C22.7412 25.2785 23.0369 25.0814 23.2285 24.7979C23.4082 24.5317 23.4811 24.2091 23.4375 23.8926V20.917Z" fill="white" stroke="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
4
public/images/whatsapp.svg
Normal file
4
public/images/whatsapp.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.5" d="M10.2144 1.73047C12.4066 1.73142 14.4613 2.58407 16.0083 4.13281C17.5553 5.68162 18.4056 7.73816 18.4048 9.92773C18.4026 14.4434 14.7277 18.1182 10.2144 18.1182H10.2114C8.84031 18.1177 7.49315 17.7733 6.29736 17.1211L6.12256 17.0264L5.93115 17.0762L2.16162 18.0645L3.16553 14.3975L3.22119 14.1963L3.1167 14.0156C2.4002 12.774 2.02332 11.3651 2.02393 9.9209C2.02563 5.54635 5.47414 1.96113 9.79346 1.74121L10.2144 1.73047ZM10.2173 2.19824C5.95688 2.19825 2.49238 5.6622 2.49072 9.9209C2.49017 11.2885 2.84942 12.6235 3.53174 13.7988L3.67236 14.0312L3.72607 14.1172L3.05518 16.5723L2.82666 17.4072L3.66455 17.1875L6.1958 16.5234L6.27979 16.5732C7.39217 17.2333 8.6565 17.6016 9.95166 17.6455L10.2114 17.6504H10.2144C14.472 17.6504 17.9363 14.1856 17.938 9.92676C17.9387 7.86425 17.1356 5.92198 15.6782 4.46289C14.2207 3.00372 12.2793 2.19888 10.2173 2.19824Z" fill="white" stroke="white"/>
|
||||||
|
<path d="M7.13721 6.40625C7.28076 6.40625 7.41557 6.40743 7.52881 6.41309C7.53489 6.41339 7.54078 6.41291 7.54639 6.41309C7.55519 6.42998 7.56882 6.45033 7.58057 6.47852C7.67057 6.69484 7.82362 7.06815 7.9624 7.40527C8.08594 7.70537 8.21122 8.007 8.25732 8.10547C8.19617 8.22907 8.18583 8.25503 8.1333 8.31641C7.99284 8.48046 7.91484 8.58784 7.83447 8.66797C7.79349 8.70879 7.65299 8.83899 7.58252 9.02832C7.49435 9.2655 7.53677 9.5058 7.65967 9.7168C7.79467 9.94847 8.26243 10.7141 8.96729 11.3428C9.86049 12.1394 10.6526 12.4097 10.8218 12.4941C10.9634 12.5651 11.1564 12.6379 11.3755 12.6113C11.6149 12.5822 11.782 12.4493 11.8931 12.3223C11.9998 12.2001 12.353 11.7824 12.5493 11.5146C12.6501 11.5567 12.9118 11.6791 13.2104 11.8232L13.9526 12.1865C14.0105 12.2155 14.0651 12.2414 14.1069 12.2617C14.1112 12.2638 14.1156 12.2666 14.1196 12.2686C14.1189 12.2815 14.1209 12.2959 14.1196 12.3105C14.1074 12.4519 14.0704 12.645 13.9946 12.8574C13.962 12.9488 13.821 13.1158 13.5522 13.2881C13.2955 13.4526 13.0412 13.5463 12.9263 13.5635C12.5937 13.6132 12.2043 13.6297 11.7837 13.4961V13.4951C11.4758 13.3974 11.0892 13.2699 10.5933 13.0557C8.56391 12.1794 7.22183 10.119 7.08545 9.93652V9.93555L6.91846 9.69922C6.83849 9.57792 6.7412 9.418 6.64697 9.23242C6.45516 8.85459 6.29834 8.4111 6.29834 7.98926C6.29842 7.1292 6.73054 6.7305 6.92725 6.51562C7.01528 6.41949 7.10687 6.40632 7.13721 6.40625Z" fill="white" stroke="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
11
public/images/ws.svg
Normal file
11
public/images/ws.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="49" height="49" rx="24.5" stroke="#3666AF"/>
|
||||||
|
<g clip-path="url(#clip0_124_6100)">
|
||||||
|
<path d="M24.959 15.5H25.04C28.9933 15.5 31.1211 15.9124 32.5967 17.3398C34.1048 18.8793 34.5 20.9914 34.5 24.9443V25.0557C34.5 29.0116 34.1024 31.1363 32.5977 32.6582C31.1219 34.0869 29.0112 34.5 25.04 34.5H24.96C20.9866 34.5 18.8612 34.0884 17.4033 32.6611C15.8948 31.1216 15.5 29.0089 15.5 25.0557V24.9443C15.5 20.9895 15.8954 18.8639 17.4014 17.3408C18.8758 15.9121 20.9887 15.5 24.959 15.5ZM24.8389 16.7891C24.4867 16.7891 24.2102 17.0698 24.1943 17.4053V17.4102C24.0896 20.1796 23.7351 21.6827 22.7041 22.7139C21.7385 23.6795 20.3601 24.0516 17.9141 24.1807L17.4102 24.2031C17.0654 24.2155 16.7891 24.4995 16.7891 24.8477V25.1709C16.7893 25.4787 17.0039 25.7286 17.2822 25.7969L17.4053 25.8145H17.4102C20.1743 25.9192 21.6753 26.2758 22.7041 27.3047C23.6684 28.2693 24.0399 29.6454 24.1699 32.0869L24.1934 32.5898C24.2058 32.9343 24.489 33.2107 24.8369 33.2109H25.1611C25.5131 33.2109 25.7898 32.9303 25.8057 32.5947V32.5898C25.9116 29.8307 26.2663 28.3336 27.2949 27.3047C28.3249 26.2746 29.8246 25.9192 32.5889 25.8145L32.5879 25.8135C32.8717 25.8035 33.1074 25.6103 33.1826 25.3477H33.2109V24.8477C33.2109 24.4954 32.9302 24.22 32.5947 24.2041L32.5898 24.2031C29.8258 24.0984 28.3247 23.7428 27.2959 22.7139C26.3293 21.7471 25.9572 20.3666 25.8281 17.915L25.8057 17.4102C25.7932 17.0656 25.5094 16.7891 25.1611 16.7891H24.8389Z" fill="white" stroke="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_124_6100">
|
||||||
|
<rect width="20" height="20" fill="white" transform="translate(15 15)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -109,7 +109,6 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
|||||||
|
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
|
@ -2,8 +2,9 @@ import React, { useState, useEffect } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql';
|
||||||
import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex';
|
import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex';
|
||||||
|
import { NavigationCategory } from '@/types';
|
||||||
|
|
||||||
function useIsMobile(breakpoint = 767) {
|
function useIsMobile(breakpoint = 767) {
|
||||||
const [isMobile, setIsMobile] = React.useState(false);
|
const [isMobile, setIsMobile] = React.useState(false);
|
||||||
@ -111,6 +112,22 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
|
|||||||
return transformed;
|
return transformed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция для поиска иконки для категории
|
||||||
|
const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => {
|
||||||
|
console.log('🔍 Ищем иконку для catalogId:', catalogId);
|
||||||
|
console.log('📋 Доступные навигационные категории:', navigationCategories);
|
||||||
|
|
||||||
|
// Ищем навигационную категорию для данного каталога (без группы)
|
||||||
|
const categoryIcon = navigationCategories.find(
|
||||||
|
nav => nav.partsIndexCatalogId === catalogId && (!nav.partsIndexGroupId || nav.partsIndexGroupId === '')
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('🎯 Найденная категория:', categoryIcon);
|
||||||
|
console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null);
|
||||||
|
|
||||||
|
return categoryIcon?.icon || null;
|
||||||
|
};
|
||||||
|
|
||||||
const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -155,6 +172,20 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Получаем навигационные категории с иконками
|
||||||
|
const { data: navigationData, loading: navigationLoading, error: navigationError } = useQuery<{ navigationCategories: NavigationCategory[] }>(
|
||||||
|
GET_NAVIGATION_CATEGORIES,
|
||||||
|
{
|
||||||
|
errorPolicy: 'all',
|
||||||
|
onCompleted: (data) => {
|
||||||
|
console.log('🎉 Навигационные категории получены:', data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ Ошибка загрузки навигационных категорий:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Обновляем данные табов когда получаем данные от API
|
// Обновляем данные табов когда получаем данные от API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
||||||
@ -227,33 +258,54 @@ 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
|
||||||
|
className="mobile-subcategory"
|
||||||
|
onClick={() => {
|
||||||
|
let subcategoryId = `${mobileCategory.catalogId}_0`;
|
||||||
|
if (mobileCategory.groups) {
|
||||||
|
for (const group of mobileCategory.groups) {
|
||||||
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === mobileCategory.links[0]);
|
||||||
|
if (foundSubgroup) {
|
||||||
|
subcategoryId = foundSubgroup.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (group.name === mobileCategory.links[0]) {
|
||||||
|
subcategoryId = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
|
||||||
|
const catalogId = activeCatalog?.id || 'fallback';
|
||||||
|
handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Показать все
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
mobileCategory.links.map((link: string, linkIndex: number) => (
|
||||||
<div
|
<div
|
||||||
className="mobile-subcategory"
|
className="mobile-subcategory"
|
||||||
key={link}
|
key={link}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Ищем соответствующую подгруппу по названию
|
|
||||||
let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`;
|
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 === link);
|
||||||
if (foundSubgroup) {
|
if (foundSubgroup) {
|
||||||
subcategoryId = foundSubgroup.id;
|
subcategoryId = foundSubgroup.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if (group.name === link) {
|
||||||
// Если нет подгрупп, проверяем саму группу
|
|
||||||
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, link, subcategoryId);
|
||||||
@ -261,7 +313,8 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
>
|
>
|
||||||
{link}
|
{link}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -334,7 +387,14 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
<div className="w-layout-hflex flex-block-90">
|
<div className="w-layout-hflex flex-block-90">
|
||||||
<div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}>
|
<div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}>
|
||||||
{/* Меню с иконками - показываем все категории из API */}
|
{/* Меню с иконками - показываем все категории из API */}
|
||||||
{tabData.map((tab, idx) => (
|
{tabData.map((tab, idx) => {
|
||||||
|
// Получаем catalogId для поиска иконки
|
||||||
|
const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[idx]?.id || `fallback_${idx}`;
|
||||||
|
console.log(`🏷️ Обрабатываем категорию ${idx}: "${tab.label}" с catalogId: "${catalogId}"`);
|
||||||
|
const icon = navigationData?.navigationCategories ? findCategoryIcon(catalogId, navigationData.navigationCategories) : null;
|
||||||
|
console.log(`🎨 Для категории "${tab.label}" будет показана ${icon ? 'иконка: ' + icon : 'звездочка (fallback)'}`);
|
||||||
|
|
||||||
|
return (
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||||
@ -346,15 +406,30 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
>
|
>
|
||||||
<div className="div-block-29">
|
<div className="div-block-29">
|
||||||
<div className="code-embed-12 w-embed">
|
<div className="code-embed-12 w-embed">
|
||||||
{/* SVG-звезда */}
|
{icon ? (
|
||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt={tab.label}
|
||||||
|
width="21"
|
||||||
|
height="20"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="21"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 21 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path d="M10.3158 0.643914C10.4674 0.365938 10.8666 0.365938 11.0182 0.643914L14.0029 6.11673C14.0604 6.22222 14.1623 6.29626 14.2804 6.31838L20.4077 7.46581C20.7189 7.52409 20.8423 7.9037 20.6247 8.13378L16.3421 12.6636C16.2595 12.7509 16.2206 12.8707 16.2361 12.9899L17.0382 19.1718C17.079 19.4858 16.7561 19.7204 16.47 19.5847L10.8385 16.9114C10.73 16.8599 10.604 16.8599 10.4955 16.9114L4.86394 19.5847C4.5779 19.7204 4.25499 19.4858 4.29573 19.1718L5.0979 12.9899C5.11336 12.8707 5.07444 12.7509 4.99189 12.6636L0.709252 8.13378C0.491728 7.9037 0.615069 7.52409 0.926288 7.46581L7.05357 6.31838C7.17168 6.29626 7.27358 6.22222 7.33112 6.11673L10.3158 0.643914Z" fill="CurrentColor"></path>
|
<path d="M10.3158 0.643914C10.4674 0.365938 10.8666 0.365938 11.0182 0.643914L14.0029 6.11673C14.0604 6.22222 14.1623 6.29626 14.2804 6.31838L20.4077 7.46581C20.7189 7.52409 20.8423 7.9037 20.6247 8.13378L16.3421 12.6636C16.2595 12.7509 16.2206 12.8707 16.2361 12.9899L17.0382 19.1718C17.079 19.4858 16.7561 19.7204 16.47 19.5847L10.8385 16.9114C10.73 16.8599 10.604 16.8599 10.4955 16.9114L4.86394 19.5847C4.5779 19.7204 4.25499 19.4858 4.29573 19.1718L5.0979 12.9899C5.11336 12.8707 5.07444 12.7509 4.99189 12.6636L0.709252 8.13378C0.491728 7.9037 0.615069 7.52409 0.926288 7.46581L7.05357 6.31838C7.17168 6.29626 7.27358 6.22222 7.33112 6.11673L10.3158 0.643914Z" fill="CurrentColor"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-block-47">{tab.label}</div>
|
<div className="text-block-47">{tab.label}</div>
|
||||||
</a>
|
</a>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Правая часть меню с подкатегориями и картинками */}
|
{/* Правая часть меню с подкатегориями и картинками */}
|
||||||
<div className="w-layout-vflex flex-block-89">
|
<div className="w-layout-vflex flex-block-89">
|
||||||
@ -443,30 +518,51 @@ 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 ? (
|
||||||
|
<div
|
||||||
|
className="link-2"
|
||||||
|
onClick={() => {
|
||||||
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||||
|
let subcategoryId = `fallback_${idx}_0`;
|
||||||
// Ищем соответствующую подгруппу по названию
|
if (catalog?.groups) {
|
||||||
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
for (const group of catalog.groups) {
|
||||||
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === tab.links[0]);
|
||||||
|
if (foundSubgroup) {
|
||||||
|
subcategoryId = foundSubgroup.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (group.name === tab.links[0]) {
|
||||||
|
subcategoryId = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const catalogId = catalog?.id || 'fallback';
|
||||||
|
handleCategoryClick(catalogId, tab.links[0], subcategoryId);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Показать все
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tab.links.map((link: string, linkIndex: number) => {
|
||||||
|
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
|
||||||
|
let subcategoryId = `fallback_${idx}_${linkIndex}`;
|
||||||
if (catalog?.groups) {
|
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 === link);
|
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
|
||||||
if (foundSubgroup) {
|
if (foundSubgroup) {
|
||||||
subcategoryId = foundSubgroup.id;
|
subcategoryId = foundSubgroup.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if (group.name === link) {
|
||||||
// Если нет подгрупп, проверяем саму группу
|
|
||||||
else if (group.name === link) {
|
|
||||||
subcategoryId = group.id;
|
subcategoryId = group.id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="link-2"
|
className="link-2"
|
||||||
@ -480,7 +576,8 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
{link}
|
{link}
|
||||||
</div>
|
</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,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useQuery, useLazyQuery } from '@apollo/client';
|
||||||
|
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
||||||
|
|
||||||
function useIsMobile(breakpoint = 767) {
|
function useIsMobile(breakpoint = 767) {
|
||||||
const [isMobile, setIsMobile] = React.useState(false);
|
const [isMobile, setIsMobile] = React.useState(false);
|
||||||
@ -13,35 +14,38 @@ function useIsMobile(breakpoint = 767) {
|
|||||||
return isMobile;
|
return isMobile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Типы для Parts Index API
|
// Типы данных
|
||||||
interface PartsIndexCatalog {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PartsIndexEntityName {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PartsIndexGroup {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
lang: string;
|
|
||||||
image: string;
|
|
||||||
lft: number;
|
|
||||||
rgt: number;
|
|
||||||
entityNames: PartsIndexEntityName[];
|
|
||||||
subgroups: PartsIndexGroup[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PartsIndexTabData {
|
interface PartsIndexTabData {
|
||||||
label: string;
|
label: string;
|
||||||
heading: string;
|
heading: string;
|
||||||
links: string[];
|
links: string[];
|
||||||
catalogId: string;
|
catalogId: string;
|
||||||
group?: PartsIndexGroup;
|
group?: any;
|
||||||
|
groupsLoaded?: boolean; // флаг что группы загружены
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PartsIndexCatalog {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image?: string;
|
||||||
|
groups?: PartsIndexGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PartsIndexGroup {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image?: string;
|
||||||
|
entityNames?: { id: string; name: string }[];
|
||||||
|
subgroups?: { id: string; name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQL типы
|
||||||
|
interface PartsIndexCatalogsData {
|
||||||
|
partsIndexCategoriesWithGroups: PartsIndexCatalog[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PartsIndexCatalogsVariables {
|
||||||
|
lang?: 'ru' | 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback статичные данные
|
// Fallback статичные данные
|
||||||
@ -51,65 +55,74 @@ const fallbackTabData: PartsIndexTabData[] = [
|
|||||||
heading: "Детали ТО",
|
heading: "Детали ТО",
|
||||||
catalogId: "parts_to",
|
catalogId: "parts_to",
|
||||||
links: ["Детали ТО"],
|
links: ["Детали ТО"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Масла",
|
label: "Масла",
|
||||||
heading: "Масла",
|
heading: "Масла",
|
||||||
catalogId: "oils",
|
catalogId: "oils",
|
||||||
links: ["Масла"],
|
links: ["Масла"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Шины",
|
label: "Шины",
|
||||||
heading: "Шины",
|
heading: "Шины",
|
||||||
catalogId: "tyres",
|
catalogId: "tyres",
|
||||||
links: ["Шины"],
|
links: ["Шины"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Сервис для работы с Parts Index API
|
// Создаем базовые табы только с названиями каталогов
|
||||||
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
|
const createBaseTabData = (catalogs: PartsIndexCatalog[]): PartsIndexTabData[] => {
|
||||||
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
console.log('🔄 Создаем базовые табы из каталогов:', catalogs.length, 'элементов');
|
||||||
|
|
||||||
async function fetchCatalogs(): Promise<PartsIndexCatalog[]> {
|
return catalogs.map(catalog => ({
|
||||||
try {
|
label: catalog.name,
|
||||||
const response = await fetch(`${PARTS_INDEX_API_BASE}/v1/catalogs?lang=ru`, {
|
heading: catalog.name,
|
||||||
headers: { 'Accept': 'application/json' },
|
links: [catalog.name], // Изначально показываем только название каталога
|
||||||
|
catalogId: catalog.id,
|
||||||
|
groupsLoaded: false, // Группы еще не загружены
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Преобразуем данные PartsIndex в формат нашего меню с группами
|
||||||
|
const transformPartsIndexToTabData = (catalog: PartsIndexCatalog): string[] => {
|
||||||
|
console.log(`📝 Обрабатываем группы каталога: "${catalog.name}"`);
|
||||||
|
|
||||||
|
let links: string[] = [];
|
||||||
|
|
||||||
|
if (catalog.groups && catalog.groups.length > 0) {
|
||||||
|
// Для каждой группы проверяем есть ли подгруппы
|
||||||
|
catalog.groups.forEach(group => {
|
||||||
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
|
// Если есть подгруппы, добавляем их названия
|
||||||
|
links.push(...group.subgroups.slice(0, 9 - links.length).map(subgroup => subgroup.name));
|
||||||
|
} else {
|
||||||
|
// Если подгрупп нет, добавляем название самой группы
|
||||||
|
if (links.length < 9) {
|
||||||
|
links.push(group.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
const data = await response.json();
|
|
||||||
return data.list;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения каталогов Parts Index:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCatalogGroup(catalogId: string): Promise<PartsIndexGroup | null> {
|
// Если подкатегорий нет, показываем название категории
|
||||||
try {
|
if (links.length === 0) {
|
||||||
const response = await fetch(
|
links = [catalog.name];
|
||||||
`${PARTS_INDEX_API_BASE}/v1/catalogs/${catalogId}/groups?lang=ru`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': API_KEY,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Ошибка получения группы каталога ${catalogId}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🔗 Подкатегории для "${catalog.name}":`, links);
|
||||||
|
return links.slice(0, 9); // Ограничиваем максимум 9 элементов
|
||||||
|
};
|
||||||
|
|
||||||
const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
||||||
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loadingGroups, setLoadingGroups] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Пагинация категорий
|
// Пагинация категорий
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
@ -126,52 +139,116 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
}
|
}
|
||||||
}, [menuOpen]);
|
}, [menuOpen]);
|
||||||
|
|
||||||
// Загрузка каталогов и их групп
|
// Получаем только каталоги PartsIndex (без групп для начальной загрузки)
|
||||||
|
const { data: catalogsData, loading, error } = useQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
|
||||||
|
GET_PARTSINDEX_CATEGORIES,
|
||||||
|
{
|
||||||
|
variables: {
|
||||||
|
lang: 'ru'
|
||||||
|
},
|
||||||
|
errorPolicy: 'all',
|
||||||
|
fetchPolicy: 'cache-first', // Используем кэширование агрессивно
|
||||||
|
nextFetchPolicy: 'cache-first', // Продолжаем использовать кэш
|
||||||
|
notifyOnNetworkStatusChange: false,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
console.log('🎉 PartsIndex каталоги получены через GraphQL (базовые):', data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ Ошибка загрузки PartsIndex каталогов:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ленивый запрос для загрузки групп конкретного каталога
|
||||||
|
const [loadCatalogGroups, { loading: groupsLoading }] = useLazyQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
|
||||||
|
GET_PARTSINDEX_CATEGORIES,
|
||||||
|
{
|
||||||
|
errorPolicy: 'all',
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
nextFetchPolicy: 'cache-first',
|
||||||
|
notifyOnNetworkStatusChange: false,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
console.log('🎉 Группы каталога загружены:', data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ Ошибка загрузки групп каталога:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обновляем базовые данные табов когда получаем каталоги
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
||||||
if (tabData === fallbackTabData) { // Загружаем только если еще не загружали
|
console.log('✅ Обновляем базовое меню PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов');
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
console.log('🔄 Загружаем каталоги Parts Index...');
|
|
||||||
const catalogs = await fetchCatalogs();
|
|
||||||
|
|
||||||
if (catalogs.length > 0) {
|
const baseTabData = createBaseTabData(catalogsData.partsIndexCategoriesWithGroups);
|
||||||
console.log(`✅ Получено ${catalogs.length} каталогов`);
|
setTabData(baseTabData);
|
||||||
|
setActiveTabIndex(0);
|
||||||
// Загружаем группы для первых нескольких каталогов
|
} else if (error) {
|
||||||
const catalogsToLoad = catalogs.slice(0, 10);
|
console.warn('⚠️ Используем fallback данные из-за ошибки PartsIndex:', error);
|
||||||
const tabDataPromises = catalogsToLoad.map(async (catalog) => {
|
setTabData(fallbackTabData);
|
||||||
const group = await fetchCatalogGroup(catalog.id);
|
|
||||||
|
|
||||||
// Получаем подкатегории из entityNames или повторяем название категории
|
|
||||||
const links = group?.entityNames && group.entityNames.length > 0
|
|
||||||
? group.entityNames.slice(0, 9).map(entity => entity.name)
|
|
||||||
: [catalog.name]; // Если нет подкатегорий, повторяем название категории
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: catalog.name,
|
|
||||||
heading: catalog.name,
|
|
||||||
links,
|
|
||||||
catalogId: catalog.id,
|
|
||||||
group
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiTabData = await Promise.all(tabDataPromises);
|
|
||||||
console.log('✅ Данные обновлены:', apiTabData.length, 'категорий');
|
|
||||||
setTabData(apiTabData as PartsIndexTabData[]);
|
|
||||||
setActiveTabIndex(0);
|
setActiveTabIndex(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, [catalogsData, error]);
|
||||||
console.error('Ошибка загрузки данных Parts Index:', error);
|
|
||||||
} finally {
|
// Функция для ленивой загрузки групп при наведении на таб
|
||||||
setLoading(false);
|
const loadGroupsForTab = async (tabIndex: number) => {
|
||||||
|
const tab = tabData[tabIndex];
|
||||||
|
if (!tab || tab.groupsLoaded || loadingGroups.has(tabIndex)) {
|
||||||
|
return; // Группы уже загружены или загружаются
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Загружаем группы для каталога:', tab.catalogId);
|
||||||
|
setLoadingGroups(prev => new Set([...prev, tabIndex]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await loadCatalogGroups({
|
||||||
|
variables: {
|
||||||
|
lang: 'ru'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.data?.partsIndexCategoriesWithGroups) {
|
||||||
|
const catalog = result.data.partsIndexCategoriesWithGroups.find(c => c.id === tab.catalogId);
|
||||||
|
if (catalog) {
|
||||||
|
const links = transformPartsIndexToTabData(catalog);
|
||||||
|
|
||||||
|
// Обновляем конкретный таб с загруженными группами
|
||||||
|
setTabData(prevTabs => {
|
||||||
|
const newTabs = [...prevTabs];
|
||||||
|
newTabs[tabIndex] = {
|
||||||
|
...newTabs[tabIndex],
|
||||||
|
links,
|
||||||
|
group: catalog.groups?.[0],
|
||||||
|
groupsLoaded: true
|
||||||
|
};
|
||||||
|
return newTabs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки групп для каталога:', tab.catalogId, error);
|
||||||
|
} finally {
|
||||||
|
setLoadingGroups(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(tabIndex);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
// Обработчик наведения на таб - загружаем группы
|
||||||
}, []);
|
const handleTabHover = (tabIndex: number) => {
|
||||||
|
loadGroupsForTab(tabIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик клика на таб
|
||||||
|
const handleTabClick = (tabIndex: number) => {
|
||||||
|
setActiveTabIndex(tabIndex);
|
||||||
|
|
||||||
|
// Загружаем группы если еще не загружены
|
||||||
|
loadGroupsForTab(tabIndex);
|
||||||
|
};
|
||||||
|
|
||||||
// Обработка клика по категории для перехода в каталог
|
// Обработка клика по категории для перехода в каталог
|
||||||
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
|
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
|
||||||
@ -184,7 +261,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
query: {
|
query: {
|
||||||
partsIndexCatalog: catalogId,
|
partsIndexCatalog: catalogId,
|
||||||
categoryName: encodeURIComponent(categoryName),
|
categoryName: encodeURIComponent(categoryName),
|
||||||
...(entityId && { entityId })
|
...(entityId && { partsIndexCategory: entityId })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -294,6 +371,12 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
className="mobile-subcategory"
|
className="mobile-subcategory"
|
||||||
key={cat.catalogId}
|
key={cat.catalogId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Загружаем группы для категории если нужно
|
||||||
|
const catIndex = tabData.findIndex(tab => tab.catalogId === cat.catalogId);
|
||||||
|
if (catIndex !== -1) {
|
||||||
|
loadGroupsForTab(catIndex);
|
||||||
|
}
|
||||||
|
|
||||||
const categoryWithData = {
|
const categoryWithData = {
|
||||||
...cat,
|
...cat,
|
||||||
catalogId: cat.catalogId,
|
catalogId: cat.catalogId,
|
||||||
@ -304,6 +387,9 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
{cat.label}
|
{cat.label}
|
||||||
|
{loadingGroups.has(tabData.findIndex(tab => tab.catalogId === cat.catalogId)) && (
|
||||||
|
<span className="text-xs text-gray-500 ml-2">(загрузка...)</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -367,9 +453,8 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
href="#"
|
href="#"
|
||||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||||
key={tab.catalogId}
|
key={tab.catalogId}
|
||||||
onClick={() => {
|
onClick={() => handleTabClick(idx)}
|
||||||
setActiveTabIndex(idx);
|
onMouseEnter={() => handleTabHover(idx)}
|
||||||
}}
|
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="div-block-29">
|
<div className="div-block-29">
|
||||||
@ -388,6 +473,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
<h3 className="heading-16">
|
<h3 className="heading-16">
|
||||||
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
||||||
{loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>}
|
{loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>}
|
||||||
|
{loadingGroups.has(activeTabIndex) && <span className="text-sm text-gray-500 ml-2">(загрузка групп...)</span>}
|
||||||
</h3>
|
</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">
|
||||||
|
@ -84,7 +84,7 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
|
|||||||
}, [state.error, clearError]);
|
}, [state.error, clearError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-layout-vflex flex-block-48">
|
<div className="w-layout-vflex flex-block-48" style={{ minHeight: '420px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
{/* Отображение ошибок корзины */}
|
{/* Отображение ошибок корзины */}
|
||||||
{state.error && (
|
{state.error && (
|
||||||
<div className="alert alert-error mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
<div className="alert alert-error mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||||
@ -145,9 +145,15 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{displayItems.length === 0 ? (
|
{displayItems.length === 0 ? (
|
||||||
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
<div className="empty-cart-message" style={{ textAlign: 'center', width: '100%' }}>
|
||||||
<p>Ваша корзина пуста</p>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', justifyContent: 'center' }}>
|
||||||
<p>Добавьте товары из каталога</p>
|
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 90, height: 90, borderRadius: '50%', background: '#f3f4f6', marginBottom: 8 }}>
|
||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16 36C14.3431 36 13 37.3431 13 39C13 40.6569 14.3431 42 16 42C17.6569 42 19 40.6569 19 39C19 37.3431 17.6569 36 16 36ZM6 8V12H10.68L16.44 24.016L14.16 28.08C13.7647 28.8001 13.5556 29.6352 13.5556 30.5C13.5556 32.1569 14.8987 33.5 16.5556 33.5H39V30.5H17.1756C17.0891 30.5 17.0178 30.4287 17.0178 30.3422L17.04 30.25L18.88 27H34.8C36.0212 27 37.1042 26.2627 37.6 25.16L43.048 14.352C43.1746 14.0993 43.2382 13.8132 43.2302 13.5242C43.2222 13.2352 43.1428 12.9538 42.9992 12.7087C42.8556 12.4636 42.6532 12.2632 42.4136 12.1302C42.174 11.9972 41.9062 11.9376 41.64 11.96H12.24L10.84 8H6ZM36 36C34.3431 36 33 37.3431 33 39C33 40.6569 34.3431 42 36 42C37.6569 42 39 40.6569 39 39C39 37.3431 37.6569 36 36 36Z" fill="#222"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div style={{ fontSize: '1.7rem', fontWeight: 700, color: '#222' }}>Ваша корзина пуста</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
displayItems.map((item, idx) => {
|
displayItems.map((item, idx) => {
|
||||||
|
@ -285,7 +285,7 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
|||||||
onClick={() => setShowLegalEntityDropdown(!showLegalEntityDropdown)}
|
onClick={() => setShowLegalEntityDropdown(!showLegalEntityDropdown)}
|
||||||
style={{ cursor: 'pointer', justifyContent: 'space-between', alignItems: 'center' }}
|
style={{ cursor: 'pointer', justifyContent: 'space-between', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<div className="text-block-31">
|
<div className="text-block-31" style={{ fontSize: '14px', color: '#333' }}>
|
||||||
{isIndividual ? 'Физическое лицо' : selectedLegalEntity || 'Выберите юридическое лицо'}
|
{isIndividual ? 'Физическое лицо' : selectedLegalEntity || 'Выберите юридическое лицо'}
|
||||||
</div>
|
</div>
|
||||||
<div className="code-embed w-embed" style={{ transform: showLegalEntityDropdown ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
|
<div className="code-embed w-embed" style={{ transform: showLegalEntityDropdown ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
|
||||||
@ -325,7 +325,7 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
|||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: '1px solid #f0f0f0',
|
||||||
backgroundColor: isIndividual ? '#f8f9fa' : 'white',
|
backgroundColor: isIndividual ? '#f8f9fa' : 'white',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: isIndividual ? 500 : 400
|
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isIndividual) {
|
if (!isIndividual) {
|
||||||
@ -538,7 +538,9 @@ const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: '1px solid #f0f0f0',
|
||||||
backgroundColor: paymentMethod === 'yookassa' ? '#f8f9fa' : 'white',
|
backgroundColor: paymentMethod === 'yookassa' ? '#f8f9fa' : 'white',
|
||||||
fontSize: '14px'
|
fontSize: '14px',
|
||||||
|
fontWeight: paymentMethod === 'yookassa' ? 500 : 400,
|
||||||
|
color: '#222'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (paymentMethod !== 'yookassa') {
|
if (paymentMethod !== 'yookassa') {
|
||||||
|
@ -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] hide-on-991"
|
||||||
|
/>
|
||||||
<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 pl-0 justify-start">
|
||||||
|
{/* Кастомный чекбокс без input/label */}
|
||||||
|
{(() => {
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="text-[#8893A1] text-[12px] leading-snug select-none mr-4">
|
||||||
|
Я даю свое согласие на обработку персональных данных<br />
|
||||||
|
и соглашаюсь с условиями <a href="/privacy-policy" className="underline hover:text-[#6c7684]">Политики конфиденциальности</a>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={`h-[24px] w-[24px] border border-[#8893A1] rounded-sm 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,270 +1,64 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import * as React from "react";
|
||||||
import { CookiePreferences, initializeAnalytics, initializeMarketing } from '@/lib/cookie-utils';
|
|
||||||
|
|
||||||
interface CookieConsentProps {
|
const CookieConsent: React.FC = () => {
|
||||||
onAccept?: () => void;
|
const [isVisible, setIsVisible] = React.useState(false);
|
||||||
onDecline?: () => void;
|
|
||||||
onConfigure?: (preferences: CookiePreferences) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CookieConsent: React.FC<CookieConsentProps> = ({ onAccept, onDecline, onConfigure }) => {
|
React.useEffect(() => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
|
||||||
const [preferences, setPreferences] = useState<CookiePreferences>({
|
|
||||||
necessary: true, // Всегда включены
|
|
||||||
analytics: false,
|
|
||||||
marketing: false,
|
|
||||||
functional: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Проверяем, есть ли уже согласие в localStorage
|
|
||||||
const cookieConsent = localStorage.getItem('cookieConsent');
|
const cookieConsent = localStorage.getItem('cookieConsent');
|
||||||
if (!cookieConsent) {
|
if (!cookieConsent) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAcceptAll = () => {
|
const handleAccept = () => {
|
||||||
const allAccepted = {
|
|
||||||
necessary: true,
|
|
||||||
analytics: true,
|
|
||||||
marketing: true,
|
|
||||||
functional: true,
|
|
||||||
};
|
|
||||||
localStorage.setItem('cookieConsent', 'accepted');
|
localStorage.setItem('cookieConsent', 'accepted');
|
||||||
localStorage.setItem('cookiePreferences', JSON.stringify(allAccepted));
|
|
||||||
|
|
||||||
// Инициализируем сервисы после согласия
|
|
||||||
initializeAnalytics();
|
|
||||||
initializeMarketing();
|
|
||||||
|
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
onAccept?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeclineAll = () => {
|
|
||||||
const onlyNecessary = {
|
|
||||||
necessary: true,
|
|
||||||
analytics: false,
|
|
||||||
marketing: false,
|
|
||||||
functional: false,
|
|
||||||
};
|
|
||||||
localStorage.setItem('cookieConsent', 'declined');
|
|
||||||
localStorage.setItem('cookiePreferences', JSON.stringify(onlyNecessary));
|
|
||||||
setIsVisible(false);
|
|
||||||
onDecline?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSavePreferences = () => {
|
|
||||||
localStorage.setItem('cookieConsent', 'configured');
|
|
||||||
localStorage.setItem('cookiePreferences', JSON.stringify(preferences));
|
|
||||||
|
|
||||||
// Инициализируем сервисы согласно настройкам
|
|
||||||
if (preferences.analytics) {
|
|
||||||
initializeAnalytics();
|
|
||||||
}
|
|
||||||
if (preferences.marketing) {
|
|
||||||
initializeMarketing();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsVisible(false);
|
|
||||||
onConfigure?.(preferences);
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePreference = (key: keyof CookiePreferences) => {
|
|
||||||
if (key === 'necessary') return; // Необходимые cookies нельзя отключить
|
|
||||||
setPreferences(prev => ({
|
|
||||||
...prev,
|
|
||||||
[key]: !prev[key]
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-lg cookie-consent-enter">
|
<>
|
||||||
<div className="max-w-7xl mx-auto p-6 max-md:p-4">
|
<link
|
||||||
{!showDetails ? (
|
href="https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700&display=swap"
|
||||||
// Основной вид
|
rel="stylesheet"
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
/>
|
||||||
{/* Текст согласия */}
|
<div
|
||||||
<div className="flex-1">
|
layer-name="cookie"
|
||||||
<div className="flex items-start gap-3">
|
className="box-border flex gap-16 justify-between items-center px-16 py-10 mx-auto my-0 w-full bg-white rounded-3xl shadow-sm max-w-[1240px] max-md:flex-col max-md:gap-10 max-md:px-10 max-md:py-8 max-md:text-center max-sm:gap-5 max-sm:p-5 max-sm:rounded-2xl fixed bottom-6 left-1/2 -translate-x-1/2 z-5000"
|
||||||
{/* Иконка cookie */}
|
>
|
||||||
<div className="flex-shrink-0 mt-1">
|
<div
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className="text-gray-600">
|
layer-name="Мы используем cookie-файлы, чтобы получить статистику, которая помогает нам улучшать сайт для Вас. Нажимая Принять, вы даёте согласие на использование ваших cookie-файлов. Подробнее о том, как мы используем ваши персональные данные, в нашей Политике обработки персональных данных."
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1.5 3.5c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm3 2c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-6 1c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm2.5 3c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm4.5-1c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-2 4c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-3.5-2c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1z" fill="currentColor"/>
|
className="flex-1 text-base font-medium leading-5 text-red-600 max-w-[933px] max-md:max-w-full max-sm:text-sm"
|
||||||
</svg>
|
>
|
||||||
</div>
|
<span className="text-base text-gray-600">
|
||||||
|
Мы используем cookie-файлы, чтобы получить статистику, которая
|
||||||
<div>
|
помогает нам улучшать сайт для Вас. Нажимая Принять, вы даёте
|
||||||
<h3 className="text-lg font-semibold text-gray-950 mb-2">
|
согласие на использование ваших cookie-файлов. Подробнее о том, как
|
||||||
Мы используем файлы cookie
|
мы используем ваши персональные данные, в нашей{' '}
|
||||||
</h3>
|
</span>
|
||||||
<p className="text-sm text-gray-600 leading-relaxed">
|
|
||||||
Наш сайт использует файлы cookie для улучшения работы сайта, персонализации контента и анализа трафика.
|
|
||||||
Продолжая использовать сайт, вы соглашаетесь с нашей{' '}
|
|
||||||
<a
|
<a
|
||||||
href="/privacy-policy"
|
href="/privacy-policy"
|
||||||
className="text-red-600 hover:text-red-700 underline"
|
className="text-base text-red-600 underline hover:text-red-700"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
политикой конфиденциальности
|
Политике обработки персональных данных.
|
||||||
</a>
|
</a>
|
||||||
{' '}и использованием файлов cookie.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Кнопки */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 md:flex-shrink-0">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDetails(true)}
|
onClick={handleAccept}
|
||||||
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 min-w-[120px]"
|
className="box-border flex gap-5 justify-center items-center px-8 py-4 bg-red-600 hover:bg-red-700 rounded-xl h-[51px] min-w-[126px] max-md:w-full max-md:max-w-[200px] max-sm:px-5 max-sm:py-3.5 max-sm:w-full max-sm:h-auto focus:outline-none focus:ring-2 focus:ring-red-400 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
Настроить
|
<span
|
||||||
</button>
|
layer-name="Принять"
|
||||||
<button
|
className="text-base font-semibold leading-5 text-center text-white max-sm:text-sm"
|
||||||
onClick={handleDeclineAll}
|
|
||||||
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 min-w-[120px]"
|
|
||||||
>
|
>
|
||||||
Отклонить
|
Принять
|
||||||
</button>
|
</span>
|
||||||
<button
|
|
||||||
onClick={handleAcceptAll}
|
|
||||||
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 min-w-[120px]"
|
|
||||||
>
|
|
||||||
Принять все
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : (
|
|
||||||
// Детальный вид с настройками
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Заголовок */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-950">
|
|
||||||
Настройки файлов cookie
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDetails(false)}
|
|
||||||
className="text-gray-500 hover:text-gray-700 p-1"
|
|
||||||
>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
||||||
<path d="M15 5L5 15M5 5l10 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Настройки cookies */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Необходимые cookies */}
|
|
||||||
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h4 className="font-medium text-gray-950">Необходимые cookies</h4>
|
|
||||||
<span className="text-xs px-2 py-1 bg-gray-200 text-gray-600 rounded">Обязательные</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Эти файлы cookie необходимы для работы сайта и не могут быть отключены.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 ml-4">
|
|
||||||
<div className="w-12 h-6 bg-red-600 rounded-full flex items-center justify-end px-1">
|
|
||||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Аналитические cookies */}
|
|
||||||
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-medium text-gray-950 mb-2">Аналитические cookies</h4>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Помогают нам понять, как посетители взаимодействуют с сайтом.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePreference('analytics')}
|
|
||||||
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
|
|
||||||
preferences.analytics ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
|
|
||||||
} px-1`}
|
|
||||||
>
|
|
||||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Маркетинговые cookies */}
|
|
||||||
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-medium text-gray-950 mb-2">Маркетинговые cookies</h4>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Используются для отслеживания посетителей и показа релевантной рекламы.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePreference('marketing')}
|
|
||||||
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
|
|
||||||
preferences.marketing ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
|
|
||||||
} px-1`}
|
|
||||||
>
|
|
||||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Функциональные cookies */}
|
|
||||||
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-medium text-gray-950 mb-2">Функциональные cookies</h4>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Обеспечивают расширенную функциональность и персонализацию.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePreference('functional')}
|
|
||||||
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
|
|
||||||
preferences.functional ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
|
|
||||||
} px-1`}
|
|
||||||
>
|
|
||||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Кнопки действий */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-gray-200">
|
|
||||||
<button
|
|
||||||
onClick={handleDeclineAll}
|
|
||||||
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
|
|
||||||
>
|
|
||||||
Только необходимые
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSavePreferences}
|
|
||||||
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
|
|
||||||
>
|
|
||||||
Сохранить настройки
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleAcceptAll}
|
|
||||||
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
|
|
||||||
>
|
|
||||||
Принять все
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -272,6 +272,12 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-layout-hflex core-product-search-s1">
|
<div className="w-layout-hflex core-product-search-s1">
|
||||||
|
<div className="w-layout-vflex flex-block-48-copy">
|
||||||
|
<div className="w-layout-vflex product-list-search-s1">
|
||||||
|
|
||||||
|
<div className="w-layout-vflex flex-block-48-copy">
|
||||||
|
|
||||||
|
<div className="w-layout-vflex product-list-search-s1">
|
||||||
<div className="w-layout-vflex core-product-s1">
|
<div className="w-layout-vflex core-product-s1">
|
||||||
<div className="w-layout-vflex flex-block-47">
|
<div className="w-layout-vflex flex-block-47">
|
||||||
<div className="div-block-19">
|
<div className="div-block-19">
|
||||||
@ -308,7 +314,6 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-vflex flex-block-48-copy">
|
|
||||||
<div className="w-layout-hflex sort-list-s1">
|
<div className="w-layout-hflex sort-list-s1">
|
||||||
<div className="w-layout-hflex flex-block-49">
|
<div className="w-layout-hflex flex-block-49">
|
||||||
<div className="sort-item first">Наличие</div>
|
<div className="sort-item first">Наличие</div>
|
||||||
@ -316,7 +321,6 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="sort-item price">Цена</div>
|
<div className="sort-item price">Цена</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-vflex product-list-search-s1">
|
|
||||||
{displayedOffers.map((offer, idx) => {
|
{displayedOffers.map((offer, idx) => {
|
||||||
const isLast = idx === displayedOffers.length - 1;
|
const isLast = idx === displayedOffers.length - 1;
|
||||||
const maxCount = parseStock(offer.pcs);
|
const maxCount = parseStock(offer.pcs);
|
||||||
@ -405,7 +409,6 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
{hasMoreOffers || visibleOffersCount > INITIAL_OFFERS_LIMIT ? (
|
{hasMoreOffers || visibleOffersCount > INITIAL_OFFERS_LIMIT ? (
|
||||||
<div
|
<div
|
||||||
className="w-layout-hflex show-more-search"
|
className="w-layout-hflex show-more-search"
|
||||||
@ -443,6 +446,9 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -88,7 +88,7 @@ const Filters: React.FC<FiltersProps> = ({
|
|||||||
if (filter.type === "range") {
|
if (filter.type === "range") {
|
||||||
return (
|
return (
|
||||||
<FilterRange
|
<FilterRange
|
||||||
key={filter.title + idx}
|
key={filter.title + idx + JSON.stringify((filterValues && filterValues[filter.title]) || null)}
|
||||||
title={filter.title}
|
title={filter.title}
|
||||||
min={filter.min}
|
min={filter.min}
|
||||||
max={filter.max}
|
max={filter.max}
|
||||||
|
@ -134,7 +134,7 @@ const FiltersPanelMobile: React.FC<FiltersPanelMobileProps> = ({
|
|||||||
if (filter.type === "range") {
|
if (filter.type === "range") {
|
||||||
return (
|
return (
|
||||||
<FilterRange
|
<FilterRange
|
||||||
key={filter.title + idx}
|
key={filter.title + idx + JSON.stringify(localFilterValues[filter.title] || null)}
|
||||||
title={filter.title}
|
title={filter.title}
|
||||||
min={filter.min}
|
min={filter.min}
|
||||||
max={filter.max}
|
max={filter.max}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Footer = () => (
|
const Footer = () => (
|
||||||
<footer className="section-2">
|
<>
|
||||||
|
{/* <footer className="section-2">
|
||||||
<div className="w-layout-blockcontainer container footer w-container">
|
<div className="w-layout-blockcontainer container footer w-container">
|
||||||
<div className="w-layout-vflex flex-block-20">
|
<div className="w-layout-vflex flex-block-20">
|
||||||
<div className="w-layout-hflex flex-block-18-copy-copy">
|
<div className="w-layout-hflex flex-block-18-copy-copy">
|
||||||
@ -98,7 +99,96 @@ const Footer = () => (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer> */}
|
||||||
|
{/* Новый футер под основным */}
|
||||||
|
<footer className="section-2 text-white h-full">
|
||||||
|
<div className="w-layout-blockcontainer container footer w-container h-full">
|
||||||
|
<div className="mx-auto flex flex-col md:flex-row items-center md:items-end justify-between gap-2 md:gap-4 mt-2 h-full w-full">
|
||||||
|
{/* Левая часть: логотип и контакты */}
|
||||||
|
<div className="flex flex-col gap-4 min-w-[260px] items-center md:items-start mx-auto md:mx-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img src="/images/logo_gor.svg" alt="Protek Авто" className="h-10" />
|
||||||
|
{/* <span className="bg-[#EC1C24] text-white font-bold rounded px-2 py-1 ml-2 text-sm">АВТО</span> */}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs opacity-80 leading-tight">ООО «Протек» ИНН 5007117840<br />ОРГН 1225000146282</div>
|
||||||
|
<div className="font-semibold mt-2">Есть вопросы или предложения?</div>
|
||||||
|
<button className="bg-[#23407A] rounded-lg py-2 px-6 font-medium mt-1 mb-2">Напиши нам</button>
|
||||||
|
</div>
|
||||||
|
{/* Центр: меню */}
|
||||||
|
<div className="hidden md:flex flex-1 flex-wrap gap-10 justify-center min-w-[400px]">
|
||||||
|
<div className="flex flex-col gap-3 min-w-[150px]">
|
||||||
|
<div className="link">Подбор по марке авто</div>
|
||||||
|
<a href="#" className="link">Поиск по VIN</a>
|
||||||
|
<a href="#" className="link">Добавить авто в гараж</a>
|
||||||
|
<a href="#" className="link">Личный кабинет</a>
|
||||||
|
<a href="#" className="link">История поиска</a>
|
||||||
|
<a href="#" className="link">Избранное</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 min-w-[150px]">
|
||||||
|
<div className="link">Детали для ТО</div>
|
||||||
|
<a href="#" className="link">Шины</a>
|
||||||
|
<a href="#" className="link">Диски</a>
|
||||||
|
<a href="#" className="link">Масла и жидкости</a>
|
||||||
|
<a href="#" className="link">Инструменты</a>
|
||||||
|
<a href="#" className="link">Все категории</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 min-w-[150px]">
|
||||||
|
<div className="link">О компании</div>
|
||||||
|
<a href="#" className="link">Оплата и доставка</a>
|
||||||
|
<a href="#" className="link">Гарантии и возврат</a>
|
||||||
|
<a href="#" className="link">Оптовым клиентам</a>
|
||||||
|
<a href="#" className="link">Покупателям</a>
|
||||||
|
<a href="#" className="link">Контакты</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Правая часть: контакты и платежи */}
|
||||||
|
<div className="flex flex-col gap-3 min-w-[220px] items-center md:items-end mx-auto md:mx-0">
|
||||||
|
<div className="text-lg font-bold">+7 (495) 260-20-60</div>
|
||||||
|
<div className="text-xs opacity-80">Ежедневно 9:00 – 21:00</div>
|
||||||
|
<div className="text-sm font-medium">info@protekauto.ru</div>
|
||||||
|
<div className="flex gap-3 mt-2">
|
||||||
|
<a href="#" className="hover:opacity-80 flex items-center gap-1">
|
||||||
|
<img src="/images/whatsapp.svg" alt="Whatsapp" className="" />
|
||||||
|
<span className="font-medium text-sm">WhatsApp</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:opacity-80 flex items-center gap-1">
|
||||||
|
<img src="/images/tg2.svg" alt="Telegram" className="" />
|
||||||
|
<span className="font-medium text-sm">Telegram</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-4 items-center">
|
||||||
|
<img src="/images/mastercard.svg" alt="Mastercard" className="h-6" />
|
||||||
|
<img src="/images/visa.svg" alt="Visa" className="h-3" />
|
||||||
|
<img src="/images/mir.svg" alt="Mir" className="h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto flex flex-col md:flex-row items-center md:items-start justify-between gap-2 md:gap-4 mt-5 h-full w-full">
|
||||||
|
<div className="hidden md:flex gap-4 min-w-[260px]">
|
||||||
|
<a href="#" className="hover:opacity-80 flex items-center gap-1">
|
||||||
|
<img src="/images/vk.svg" alt="VK" />
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:opacity-80 flex items-center gap-1">
|
||||||
|
<img src="/images/tg.svg" alt="Telegram" />
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<a href="#" className="hover:opacity-80 flex items-center gap-1">
|
||||||
|
<img src="/images/ws.svg" alt="Support" />
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center md:flex-row md:items-start md:justify-center flex-1 flex-wrap gap-4 md:gap-20 md:mt-6 md:min-w-[400px]">
|
||||||
|
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Политика конфиденциальности</a>
|
||||||
|
|
||||||
|
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Согласие на обработку персональных данных</a>
|
||||||
|
<span className="text-xs opacity-70">© 2025 Protek. Все права защищены.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
@ -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>
|
||||||
@ -414,7 +414,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
onClick={() => setMenuOpen((open) => !open)}
|
onClick={() => setMenuOpen((open) => !open)}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="code-embed-5 w-embed"><svg width="30" height="18" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div className="code-embed-5 w-embed"><svg width="currentwidth" height="currenthieght" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0H30V3H0V0Z" fill="currentColor"></path>
|
<path d="M0 0H30V3H0V0Z" fill="currentColor"></path>
|
||||||
<path d="M0 7.5H30V10.5H0V7.5Z" fill="currentColor"></path>
|
<path d="M0 7.5H30V10.5H0V7.5Z" fill="currentColor"></path>
|
||||||
<path d="M0 15H30V18H0V15Z" fill="currentColor"></path>
|
<path d="M0 15H30V18H0V15Z" fill="currentColor"></path>
|
||||||
@ -759,7 +759,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
<div className="w-layout-hflex flex-block-76">
|
<div className="w-layout-hflex flex-block-76">
|
||||||
<Link href="/profile-history" className="button_h w-inline-block">
|
<Link href="/profile-history" className="button_h w-inline-block">
|
||||||
|
|
||||||
<img src="/images/union.svg" alt="История заказов" width={22} height={10} />
|
<img src="/images/union.svg" alt="История заказов" width={20} />
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/profile-gar" className="button_h w-inline-block">
|
<Link href="/profile-gar" className="button_h w-inline-block">
|
||||||
|
@ -32,7 +32,11 @@ const InfoSearch: React.FC<InfoSearchProps> = ({
|
|||||||
<div className="w-layout-hflex flex-block-10">
|
<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;
|
@ -1,3 +1,4 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCart } from "@/contexts/CartContext";
|
import { useCart } from "@/contexts/CartContext";
|
||||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||||
@ -11,6 +12,7 @@ interface TopSalesItemProps {
|
|||||||
article?: string;
|
article?: string;
|
||||||
productId?: string;
|
productId?: string;
|
||||||
onAddToCart?: (e: React.MouseEvent) => void;
|
onAddToCart?: (e: React.MouseEvent) => void;
|
||||||
|
discount?: string; // Новый пропс для лейбла/скидки
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
||||||
@ -21,37 +23,31 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
article,
|
article,
|
||||||
productId,
|
productId,
|
||||||
onAddToCart,
|
onAddToCart,
|
||||||
|
discount = 'Топ продаж', // По умолчанию как раньше
|
||||||
}) => {
|
}) => {
|
||||||
const { addItem } = useCart();
|
const { addItem } = useCart();
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
|
|
||||||
// Проверяем, есть ли товар в избранном
|
|
||||||
const isItemFavorite = isFavorite(productId, undefined, article, brand);
|
const isItemFavorite = isFavorite(productId, undefined, article, brand);
|
||||||
|
|
||||||
// Функция для парсинга цены из строки
|
|
||||||
const parsePrice = (priceStr: string): number => {
|
const parsePrice = (priceStr: string): number => {
|
||||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||||
return parseFloat(cleanPrice) || 0;
|
return parseFloat(cleanPrice) || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик клика по корзине
|
|
||||||
const handleAddToCart = (e: React.MouseEvent) => {
|
const handleAddToCart = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (onAddToCart) {
|
if (onAddToCart) {
|
||||||
onAddToCart(e);
|
onAddToCart(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!article || !brand) {
|
if (!article || !brand) {
|
||||||
toast.error('Недостаточно данных для добавления товара в корзину');
|
toast.error('Недостаточно данных для добавления товара в корзину');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numericPrice = parsePrice(price);
|
const numericPrice = parsePrice(price);
|
||||||
|
|
||||||
addItem({
|
addItem({
|
||||||
name: title,
|
name: title,
|
||||||
brand: brand,
|
brand: brand,
|
||||||
@ -63,7 +59,6 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
image: image,
|
image: image,
|
||||||
isExternal: true
|
isExternal: true
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Товар добавлен в корзину');
|
toast.success('Товар добавлен в корзину');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка добавления в корзину:', error);
|
console.error('Ошибка добавления в корзину:', error);
|
||||||
@ -71,25 +66,19 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик клика по иконке избранного
|
|
||||||
const handleFavoriteClick = (e: React.MouseEvent) => {
|
const handleFavoriteClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isItemFavorite) {
|
if (isItemFavorite) {
|
||||||
// Находим товар в избранном и удаляем
|
|
||||||
const favoriteItem = favorites.find((fav: any) => {
|
const favoriteItem = favorites.find((fav: any) => {
|
||||||
if (productId && fav.productId === productId) return true;
|
if (productId && fav.productId === productId) return true;
|
||||||
if (fav.article === article && fav.brand === brand) return true;
|
if (fav.article === article && fav.brand === brand) return true;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем в избранное
|
|
||||||
const numericPrice = parsePrice(price);
|
const numericPrice = parsePrice(price);
|
||||||
addToFavorites({
|
addToFavorites({
|
||||||
productId,
|
productId,
|
||||||
@ -104,23 +93,24 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ссылка на карточку товара (если нужно)
|
||||||
|
const cardUrl = article && brand
|
||||||
|
? `/card?article=${encodeURIComponent(article)}&brand=${encodeURIComponent(brand)}`
|
||||||
|
: '/card';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-layout-vflex flex-block-15">
|
<div className="w-layout-vflex flex-block-15-copy">
|
||||||
<div
|
<div
|
||||||
className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
|
className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
|
||||||
onClick={handleFavoriteClick}
|
onClick={handleFavoriteClick}
|
||||||
style={{
|
style={{ cursor: 'pointer', color: isItemFavorite ? '#ff4444' : '#ccc' }}
|
||||||
cursor: 'pointer',
|
|
||||||
color: isItemFavorite ? '#ff4444' : '#ccc'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="icon-setting w-embed">
|
<div className="icon-setting w-embed">
|
||||||
<svg width="currenWidth" height="currentHeight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="currentwidth" height="currentheight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" stroke="currentColor"></path>
|
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" ></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="div-block-4">
|
<div className="div-block-4">
|
||||||
<img
|
<img
|
||||||
src={image}
|
src={image}
|
||||||
@ -130,17 +120,18 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
alt={title}
|
alt={title}
|
||||||
className="image-5"
|
className="image-5"
|
||||||
/>
|
/>
|
||||||
<div className="text-block-7">Топ продаж</div>
|
<div className="text-block-7">{discount}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="div-block-3">
|
<div className="div-block-3">
|
||||||
<div className="w-layout-hflex flex-block-16">
|
<div className="w-layout-hflex flex-block-16">
|
||||||
<div className="text-block-8">{price}</div>
|
<div className="text-block-8">{price}</div>
|
||||||
|
{/* <div className="text-block-9">oldPrice</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-122">
|
||||||
|
<div className="w-layout-vflex">
|
||||||
<div className="text-block-10">{title}</div>
|
<div className="text-block-10">{title}</div>
|
||||||
<div className="text-block-11">{brand}</div>
|
<div className="text-block-11">{brand}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="button-icon w-inline-block"
|
className="button-icon w-inline-block"
|
||||||
@ -148,16 +139,17 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
aria-label="Добавить в корзину"
|
aria-label="Добавить в корзину"
|
||||||
>
|
>
|
||||||
<div className="div-block-25">
|
<div className="div-block-26">
|
||||||
<div className="icon-setting w-embed">
|
<div className="icon-setting w-embed">
|
||||||
<svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"></path>
|
<path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-block-6">Купить</div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -24,20 +24,24 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Обновляем локальное состояние при изменении внешнего значения
|
// Обновляем локальное состояние при изменении внешнего значения или границ
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
let nextFrom = value ? value[0] : min;
|
||||||
setFrom(String(value[0]));
|
let nextTo = value ? value[1] : max;
|
||||||
setTo(String(value[1]));
|
let changed = false;
|
||||||
setConfirmedFrom(value[0]);
|
// Корректируем значения, если они вне новых границ
|
||||||
setConfirmedTo(value[1]);
|
if (nextFrom < min) { nextFrom = min; changed = true; }
|
||||||
} else {
|
if (nextTo > max) { nextTo = max; changed = true; }
|
||||||
setFrom(String(min));
|
if (nextFrom > nextTo) { nextFrom = nextTo; changed = true; }
|
||||||
setTo(String(max));
|
setFrom(String(nextFrom));
|
||||||
setConfirmedFrom(min);
|
setTo(String(nextTo));
|
||||||
setConfirmedTo(max);
|
setConfirmedFrom(nextFrom);
|
||||||
|
setConfirmedTo(nextTo);
|
||||||
|
// Если значения были скорректированы, уведомляем родителя
|
||||||
|
if (changed && onChange) {
|
||||||
|
onChange([nextFrom, nextTo]);
|
||||||
}
|
}
|
||||||
}, [value, min, max]);
|
}, [value, min, max, onChange]);
|
||||||
|
|
||||||
// Обновляем ширину полосы при монтировании и ресайзе
|
// Обновляем ширину полосы при монтировании и ресайзе
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import BestPriceItem from "../BestPriceItem";
|
import BestPriceItem from "../BestPriceItem";
|
||||||
import { GET_BEST_PRICE_PRODUCTS } from "../../lib/graphql";
|
import { GET_BEST_PRICE_PRODUCTS } from "../../lib/graphql";
|
||||||
@ -19,8 +19,22 @@ interface BestPriceProductData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
|
|
||||||
const BestPriceSection: React.FC = () => {
|
const BestPriceSection: React.FC = () => {
|
||||||
const { data, loading, error } = useQuery(GET_BEST_PRICE_PRODUCTS);
|
const { data, loading, error } = useQuery(GET_BEST_PRICE_PRODUCTS);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollLeft = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scrollRight = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -97,11 +111,25 @@ const BestPriceSection: React.FC = () => {
|
|||||||
<div className="text-block-58">Подборка лучших предложенийпо цене</div>
|
<div className="text-block-58">Подборка лучших предложенийпо цене</div>
|
||||||
<a href="#" className="button-24 w-button">Показать все</a>
|
<a href="#" className="button-24 w-button">Показать все</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-121">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex flex-block-121 carousel-scroll" ref={scrollRef}>
|
||||||
{bestPriceItems.map((item, i) => (
|
{bestPriceItems.map((item, i) => (
|
||||||
<BestPriceItem key={i} {...item} />
|
<BestPriceItem key={i} {...item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useMemo, useRef } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { GET_LAXIMO_BRANDS } from "@/lib/graphql";
|
import { GET_LAXIMO_BRANDS } from "@/lib/graphql";
|
||||||
import { LaximoBrand } from "@/types/laximo";
|
import { LaximoBrand } from "@/types/laximo";
|
||||||
|
import { Combobox } from '@headlessui/react';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
"Техническое обслуживание",
|
"Техническое обслуживание",
|
||||||
@ -15,7 +16,8 @@ type Brand = { name: string; code?: string };
|
|||||||
|
|
||||||
const BrandSelectionSection: React.FC = () => {
|
const BrandSelectionSection: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [selectedBrand, setSelectedBrand] = useState<string>("");
|
const [selectedBrand, setSelectedBrand] = useState<Brand | null>(null);
|
||||||
|
const [brandQuery, setBrandQuery] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data, loading, error } = useQuery<{ laximoBrands: LaximoBrand[] }>(GET_LAXIMO_BRANDS, {
|
const { data, loading, error } = useQuery<{ laximoBrands: LaximoBrand[] }>(GET_LAXIMO_BRANDS, {
|
||||||
@ -42,6 +44,12 @@ const BrandSelectionSection: React.FC = () => {
|
|||||||
console.warn('Laximo API недоступен, используются статические данные:', error.message);
|
console.warn('Laximo API недоступен, используются статические данные:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combobox фильтрация
|
||||||
|
const filteredBrands = useMemo(() => {
|
||||||
|
if (!brandQuery) return brands;
|
||||||
|
return brands.filter(b => b.name.toLowerCase().includes(brandQuery.toLowerCase()));
|
||||||
|
}, [brands, brandQuery]);
|
||||||
|
|
||||||
const handleBrandClick = (brand: Brand) => {
|
const handleBrandClick = (brand: Brand) => {
|
||||||
if (brand.code) {
|
if (brand.code) {
|
||||||
router.push(`/brands?selected=${brand.code}`);
|
router.push(`/brands?selected=${brand.code}`);
|
||||||
@ -53,7 +61,7 @@ const BrandSelectionSection: React.FC = () => {
|
|||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (selectedBrand) {
|
if (selectedBrand) {
|
||||||
const found = brands.find(b => b.code === selectedBrand || b.name === selectedBrand);
|
const found = brands.find(b => b.code === selectedBrand.code || b.name === selectedBrand.name);
|
||||||
if (found && found.code) {
|
if (found && found.code) {
|
||||||
router.push(`/brands?selected=${found.code}`);
|
router.push(`/brands?selected=${found.code}`);
|
||||||
return;
|
return;
|
||||||
@ -123,19 +131,44 @@ const BrandSelectionSection: React.FC = () => {
|
|||||||
<h1 className="heading-21">ПОДБОР АВТОЗАПЧАСТЕЙ ПО МАРКЕ АВТО</h1>
|
<h1 className="heading-21">ПОДБОР АВТОЗАПЧАСТЕЙ ПО МАРКЕ АВТО</h1>
|
||||||
<div className="form-block-4 w-form">
|
<div className="form-block-4 w-form">
|
||||||
<form id="email-form" name="email-form" data-name="Email Form" method="post" data-wf-page-id="685be6dfd87db2e01cbdb7a2" data-wf-element-id="e673036c-0caf-d251-3b66-9ba9cb85064c" onSubmit={handleSubmit}>
|
<form id="email-form" name="email-form" data-name="Email Form" method="post" data-wf-page-id="685be6dfd87db2e01cbdb7a2" data-wf-element-id="e673036c-0caf-d251-3b66-9ba9cb85064c" onSubmit={handleSubmit}>
|
||||||
<select
|
<div style={{ width: 180, marginBottom: 16 }}>
|
||||||
id="field-7"
|
<Combobox value={selectedBrand} onChange={setSelectedBrand} nullable>
|
||||||
name="field-7"
|
<div className="relative">
|
||||||
data-name="Field 7"
|
<Combobox.Input
|
||||||
className="select-copy w-select"
|
className="w-full px-6 py-4 bg-white rounded border border-stone-300 text-sm text-gray-950 placeholder:text-neutral-500 outline-none focus:shadow-none focus:border-stone-300 transition-colors"
|
||||||
value={selectedBrand}
|
displayValue={(brand: Brand | null) => brand?.name || ''}
|
||||||
onChange={e => setSelectedBrand(e.target.value)}
|
onChange={e => setBrandQuery(e.target.value)}
|
||||||
|
placeholder="Марка"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center px-3 focus:outline-none w-12">
|
||||||
|
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</Combobox.Button>
|
||||||
|
<Combobox.Options
|
||||||
|
className="absolute left-0 top-full z-100 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-auto scrollbar-none"
|
||||||
|
style={{ scrollbarWidth: 'none' }}
|
||||||
|
data-hide-scrollbar
|
||||||
>
|
>
|
||||||
<option value="">Марка</option>
|
{filteredBrands.length === 0 && (
|
||||||
{brands.map((brand, idx) => (
|
<div className="px-6 py-4 text-gray-500">Бренды не найдены</div>
|
||||||
<option value={brand.code || brand.name} key={idx}>{brand.name}</option>
|
)}
|
||||||
|
{filteredBrands.map(brand => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={brand.code || brand.name}
|
||||||
|
value={brand}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`px-6 py-4 cursor-pointer hover:!bg-[rgb(236,28,36)] hover:!text-white text-sm transition-colors ${selected ? 'bg-red-50 font-semibold text-gray-950' : 'text-neutral-500'}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{brand.name}
|
||||||
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Combobox.Options>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
<div className="div-block-10-copy">
|
<div className="div-block-10-copy">
|
||||||
<input type="submit" data-wait="Please wait..." className="button-3-copy w-button" value="Далее" />
|
<input type="submit" data-wait="Please wait..." className="button-3-copy w-button" value="Далее" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -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,56 +1,164 @@
|
|||||||
import React 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 SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
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";
|
// Интерфейс для товара из GraphQL
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
article?: string;
|
||||||
|
brand?: string;
|
||||||
|
retailPrice?: number;
|
||||||
|
wholesalePrice?: number;
|
||||||
|
createdAt: string;
|
||||||
|
images: Array<{
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
order: number;
|
||||||
|
}>;
|
||||||
|
categories: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
const NewArrivalsSection: React.FC = () => (
|
// Функция для преобразования Product в PartsAPIArticle
|
||||||
|
const transformProductToArticle = (product: Product, index: number): PartsAPIArticle => {
|
||||||
|
return {
|
||||||
|
artId: product.id,
|
||||||
|
artArticleNr: product.article || `PROD-${product.id}`,
|
||||||
|
artSupBrand: product.brand || 'Unknown Brand',
|
||||||
|
supBrand: product.brand || 'Unknown Brand',
|
||||||
|
supId: index + 1,
|
||||||
|
productGroup: product.categories?.[0]?.name || product.name,
|
||||||
|
ptId: index + 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewArrivalsSection: React.FC = () => {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Получаем новые поступления через GraphQL
|
||||||
|
const { data, loading, error } = useQuery(GET_NEW_ARRIVALS, {
|
||||||
|
variables: { limit: 8 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollLeft = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollRight = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Преобразуем данные для ArticleCard
|
||||||
|
const newArrivalsArticles = data?.newArrivals?.map((product: Product, index: number) =>
|
||||||
|
transformProductToArticle(product, index)
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
// Получаем изображения для товаров
|
||||||
|
const getProductImage = (product: Product): string => {
|
||||||
|
if (product.images && product.images.length > 0) {
|
||||||
|
return product.images[0].url;
|
||||||
|
}
|
||||||
|
return "/images/162615.webp"; // fallback изображение
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-vflex inbt">
|
<div className="w-layout-vflex inbt">
|
||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Новое поступление</h2>
|
<h2 className="heading-4">Новое поступление</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex core-product-search">
|
<div className="carousel-row">
|
||||||
{newArrivalsArticles.map((article, i) => (
|
<button
|
||||||
<ArticleCard key={article.artId || i} article={{ ...article, artId: article.artId }} index={i} image={imagePath} />
|
className="carousel-arrow carousel-arrow-left"
|
||||||
))}
|
onClick={scrollLeft}
|
||||||
|
aria-label="Прокрутить влево"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
|
{loading ? (
|
||||||
|
// Показываем скелетоны во время загрузки
|
||||||
|
Array(8).fill(0).map((_, index) => (
|
||||||
|
<CatalogProductCardSkeleton key={`skeleton-${index}`} />
|
||||||
|
))
|
||||||
|
) : error ? (
|
||||||
|
// Показываем сообщение об ошибке
|
||||||
|
<div className="error-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Не удалось загрузить новые поступления</p>
|
||||||
|
<p style={{ fontSize: '14px', marginTop: '8px' }}>
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : newArrivalsArticles.length > 0 ? (
|
||||||
|
// Показываем товары
|
||||||
|
newArrivalsArticles.map((article: PartsAPIArticle, index: number) => {
|
||||||
|
const product = data.newArrivals[index];
|
||||||
|
const image = getProductImage(product);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArticleCard
|
||||||
|
key={article.artId || `article-${index}`}
|
||||||
|
article={article}
|
||||||
|
index={index}
|
||||||
|
image={image}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// Показываем сообщение о том, что товаров нет
|
||||||
|
<div className="no-products-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Пока нет новых поступлений</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="carousel-arrow carousel-arrow-right"
|
||||||
|
onClick={scrollRight}
|
||||||
|
aria-label="Прокрутить вправо"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default NewArrivalsSection;
|
export default NewArrivalsSection;
|
@ -1,8 +1,24 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import NewsCard from "@/components/news/NewsCard";
|
import NewsCard from "@/components/news/NewsCard";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const NewsAndPromos = () => (
|
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
|
|
||||||
|
const NewsAndPromos = () => {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollLeft = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scrollRight = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-vflex news-index-block">
|
<div className="w-layout-vflex news-index-block">
|
||||||
@ -15,7 +31,14 @@ const NewsAndPromos = () => (
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-6-copy-copy">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex flex-block-6-copy-copy carousel-scroll" ref={scrollRef}>
|
||||||
<NewsCard
|
<NewsCard
|
||||||
title="Kia Syros будет выделяться необычным стилем"
|
title="Kia Syros будет выделяться необычным стилем"
|
||||||
description="Компания Kia готова представить новый кроссовер Syros"
|
description="Компания Kia готова представить новый кроссовер Syros"
|
||||||
@ -45,9 +68,17 @@ const NewsAndPromos = () => (
|
|||||||
image="/images/news_img.png"
|
image="/images/news_img.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default NewsAndPromos;
|
export default NewsAndPromos;
|
@ -49,6 +49,16 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Корректный сброс currentSlide только если индекс вне диапазона
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSlide > activeProducts.length - 1) {
|
||||||
|
setCurrentSlide(activeProducts.length > 0 ? activeProducts.length - 1 : 0);
|
||||||
|
}
|
||||||
|
// Если товаров стало больше и текущий слайд = 0, ничего не делаем
|
||||||
|
// Если товаров стало меньше и текущий слайд в диапазоне, ничего не делаем
|
||||||
|
// Если товаров стало меньше и текущий слайд вне диапазона, сбрасываем на последний
|
||||||
|
}, [activeProducts.length]);
|
||||||
|
|
||||||
// Получаем данные из PartsIndex для текущего товара
|
// Получаем данные из PartsIndex для текущего товара
|
||||||
const currentProduct = activeProducts[currentSlide];
|
const currentProduct = activeProducts[currentSlide];
|
||||||
const { data: partsIndexData } = useQuery(
|
const { data: partsIndexData } = useQuery(
|
||||||
@ -132,11 +142,6 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
setCurrentSlide(index);
|
setCurrentSlide(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сброс слайда при изменении товаров
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentSlide(0);
|
|
||||||
}, [activeProducts]);
|
|
||||||
|
|
||||||
// Если нет активных товаров дня, не показываем секцию
|
// Если нет активных товаров дня, не показываем секцию
|
||||||
if (loading || error || activeProducts.length === 0) {
|
if (loading || error || activeProducts.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -236,15 +241,15 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
{product.name}
|
{product.name}
|
||||||
</div>
|
</div>
|
||||||
{/* Счетчик товаров если их больше одного */}
|
{/* Счетчик товаров если их больше одного */}
|
||||||
{activeProducts.length > 1 && (
|
{/* {activeProducts.length > 1 && (
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
{currentSlide + 1} из {activeProducts.length}
|
{currentSlide + 1} из {activeProducts.length}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{productImage && (
|
{productImage && (
|
||||||
<div className="relative">
|
<div className="">
|
||||||
<img
|
<img
|
||||||
width="Auto"
|
width="Auto"
|
||||||
height="Auto"
|
height="Auto"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import TopSalesItem from "../TopSalesItem";
|
import TopSalesItem from "../TopSalesItem";
|
||||||
import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql";
|
import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql";
|
||||||
@ -18,8 +18,22 @@ interface TopSalesProductData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
|
|
||||||
const TopSalesSection: React.FC = () => {
|
const TopSalesSection: React.FC = () => {
|
||||||
const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS);
|
const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollLeft = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scrollRight = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -29,9 +43,23 @@ const TopSalesSection: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Топ продаж</h2>
|
<h2 className="heading-4">Топ продаж</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex core-product-search">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
<div className="text-block-58">Загрузка...</div>
|
<div className="text-block-58">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -47,9 +75,23 @@ const TopSalesSection: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Топ продаж</h2>
|
<h2 className="heading-4">Топ продаж</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex core-product-search">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
<div className="text-block-58">Ошибка загрузки</div>
|
<div className="text-block-58">Ошибка загрузки</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -70,9 +112,23 @@ const TopSalesSection: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Топ продаж</h2>
|
<h2 className="heading-4">Топ продаж</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex core-product-search">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
<div className="text-block-58">Нет товаров в топ продаж</div>
|
<div className="text-block-58">Нет товаров в топ продаж</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -86,7 +142,14 @@ const TopSalesSection: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Топ продаж</h2>
|
<h2 className="heading-4">Топ продаж</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex core-product-search">
|
<div className="carousel-row">
|
||||||
|
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
{activeTopSalesProducts.map((item: TopSalesProductData) => {
|
{activeTopSalesProducts.map((item: TopSalesProductData) => {
|
||||||
const product = item.product;
|
const product = item.product;
|
||||||
const price = product.retailPrice
|
const price = product.retailPrice
|
||||||
@ -113,6 +176,13 @@ const TopSalesSection: React.FC = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -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,20 +474,60 @@ 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} записей
|
{/* Селектор количества элементов на странице */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-2 sm:space-y-0">
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
|
<span>Показывать по:</span>
|
||||||
|
<select
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
||||||
|
className="px-2 py-1 border border-gray-200 rounded text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-[#ec1c24] focus:border-transparent"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
<span>записей</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500 text-center sm:text-right">
|
||||||
|
Показано {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} из {filteredItems.length} записей
|
||||||
|
{filteredItems.length !== historyItems.length && (
|
||||||
|
<span className="ml-1">
|
||||||
|
(всего {historyItems.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||||
<span className="ml-2 text-blue-600">
|
<span className="ml-2 text-blue-600">
|
||||||
(применены фильтры)
|
(применены фильтры)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Компонент пагинации */}
|
||||||
|
{filteredItems.length > itemsPerPage && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
showPageInfo={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
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,13 +37,25 @@ 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();
|
||||||
|
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
||||||
|
|
||||||
// Получаем инфо об узле (для картинки)
|
// Получаем инфо об узле (для картинки)
|
||||||
console.log('🔍 KnotIn - GET_LAXIMO_UNIT_INFO запрос:', {
|
console.log('🔍 KnotIn - GET_LAXIMO_UNIT_INFO запрос:', {
|
||||||
@ -150,21 +165,68 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
|
// Обработчик клика по картинке (zoom)
|
||||||
const handlePointClick = (codeonimage: string | number) => {
|
const handleImageClick = (e: React.MouseEvent<HTMLImageElement>) => {
|
||||||
|
// Если клик был по точке, не открываем модалку (точки выше по z-index)
|
||||||
|
setIsImageModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик наведения на точку
|
||||||
|
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 +234,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) {
|
||||||
@ -223,18 +319,15 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative inline-block">
|
<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}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt={unitName || unitInfo?.name || "Изображение узла"}
|
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
className="max-w-full h-auto mx-auto rounded"
|
className="max-w-full h-auto mx-auto rounded cursor-zoom-in"
|
||||||
style={{ maxWidth: 400, display: 'block' }}
|
style={{ maxWidth: 400, display: 'block' }}
|
||||||
|
onClick={handleImageClick}
|
||||||
/>
|
/>
|
||||||
{/* Точки/области */}
|
{/* Точки/области */}
|
||||||
{coordinates.map((coord: any, idx: number) => {
|
{coordinates.map((coord: any, idx: number) => {
|
||||||
@ -242,43 +335,92 @@ 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={e => { e.stopPropagation(); handlePointClick(coord); }}
|
||||||
onMouseEnter={e => {
|
onDoubleClick={e => { e.stopPropagation(); 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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Модалка увеличенного изображения */}
|
||||||
|
{isImageModalOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/20 bg-opacity-70"
|
||||||
|
onClick={() => setIsImageModalOpen(false)}
|
||||||
|
style={{ cursor: 'zoom-out' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||||
|
className="max-h-[90vh] max-w-[90vw] rounded shadow-lg"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{ background: '#fff' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsImageModalOpen(false)}
|
||||||
|
className="absolute top-4 right-4 text-white text-3xl font-bold bg-black bg-opacity-40 rounded-full w-10 h-10 flex items-center justify-center"
|
||||||
|
aria-label="Закрыть"
|
||||||
|
style={{ zIndex: 10000 }}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Модалка выбора бренда */}
|
{/* Модалка выбора бренда */}
|
||||||
<BrandSelectionModal
|
<BrandSelectionModal
|
||||||
isOpen={isBrandModalOpen}
|
isOpen={isBrandModalOpen}
|
||||||
|
@ -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 || [];
|
||||||
|
const total = details.length;
|
||||||
|
const shownCount = shownCounts[unit.unitid] ?? 3;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{details.slice(0, shownCount).map((detail: any, index: number) => (
|
||||||
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
||||||
<div className="oemnuber">{detail.oem}</div>
|
<div className="oemnuber">{detail.oem}</div>
|
||||||
<div className="partsname">{detail.name}</div>
|
<div className="partsname">{detail.name}</div>
|
||||||
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
||||||
</div>
|
</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>
|
<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) => {
|
||||||
|
@ -373,6 +373,8 @@ export const CREATE_PAYMENT = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const GET_ORDERS = gql`
|
export const GET_ORDERS = gql`
|
||||||
query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) {
|
query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) {
|
||||||
orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) {
|
orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) {
|
||||||
@ -1367,6 +1369,23 @@ export const GET_PARTSINDEX_CATEGORIES = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Навигационные категории с иконками
|
||||||
|
export const GET_NAVIGATION_CATEGORIES = gql`
|
||||||
|
query GetNavigationCategories {
|
||||||
|
navigationCategories {
|
||||||
|
id
|
||||||
|
partsIndexCatalogId
|
||||||
|
partsIndexGroupId
|
||||||
|
name
|
||||||
|
catalogName
|
||||||
|
groupName
|
||||||
|
icon
|
||||||
|
sortOrder
|
||||||
|
isHidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// Новый запрос для получения товаров каталога PartsIndex
|
// Новый запрос для получения товаров каталога PartsIndex
|
||||||
export const GET_PARTSINDEX_CATALOG_ENTITIES = gql`
|
export const GET_PARTSINDEX_CATALOG_ENTITIES = gql`
|
||||||
query GetPartsIndexCatalogEntities(
|
query GetPartsIndexCatalogEntities(
|
||||||
@ -1679,3 +1698,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
@ -138,7 +138,7 @@ export default function Catalog() {
|
|||||||
limit: ITEMS_PER_PAGE,
|
limit: ITEMS_PER_PAGE,
|
||||||
page: partsIndexPage,
|
page: partsIndexPage,
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
|
params: undefined // Будем обновлять через refetch
|
||||||
},
|
},
|
||||||
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
||||||
fetchPolicy: 'cache-and-network'
|
fetchPolicy: 'cache-and-network'
|
||||||
@ -146,7 +146,7 @@ export default function Catalog() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Загружаем параметры фильтрации для PartsIndex
|
// Загружаем параметры фильтрации для PartsIndex
|
||||||
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
|
const { data: paramsData, loading: paramsLoading, error: paramsError, refetch: refetchParams } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
|
||||||
GET_PARTSINDEX_CATALOG_PARAMS,
|
GET_PARTSINDEX_CATALOG_PARAMS,
|
||||||
{
|
{
|
||||||
variables: {
|
variables: {
|
||||||
@ -154,7 +154,7 @@ export default function Catalog() {
|
|||||||
groupId: groupId as string,
|
groupId: groupId as string,
|
||||||
lang: 'ru',
|
lang: 'ru',
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
|
params: undefined // Будем обновлять через refetch
|
||||||
},
|
},
|
||||||
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
||||||
fetchPolicy: 'cache-first'
|
fetchPolicy: 'cache-first'
|
||||||
@ -215,18 +215,44 @@ export default function Catalog() {
|
|||||||
}
|
}
|
||||||
}, [entitiesData]);
|
}, [entitiesData]);
|
||||||
|
|
||||||
|
// Преобразование выбранных фильтров в формат PartsIndex API
|
||||||
|
const convertFiltersToPartsIndexParams = useCallback((): Record<string, any> => {
|
||||||
|
if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiParams: Record<string, any> = {};
|
||||||
|
|
||||||
|
paramsData.partsIndexCatalogParams.list.forEach((param: any) => {
|
||||||
|
const selectedValues = selectedFilters[param.name];
|
||||||
|
if (selectedValues && selectedValues.length > 0) {
|
||||||
|
// Находим соответствующие значения из API данных
|
||||||
|
const matchingValues = param.values.filter((value: any) =>
|
||||||
|
selectedValues.includes(value.title || value.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingValues.length > 0) {
|
||||||
|
// Используем ID параметра из API и значения
|
||||||
|
apiParams[param.id] = matchingValues.map((v: any) => v.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return apiParams;
|
||||||
|
}, [paramsData, selectedFilters]);
|
||||||
|
|
||||||
// Генерация фильтров для PartsIndex на основе параметров API
|
// Генерация фильтров для PartsIndex на основе параметров API
|
||||||
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
||||||
if (!paramsData?.partsIndexCatalogParams?.list) {
|
if (!paramsData?.partsIndexCatalogParams?.list) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return paramsData.partsIndexCatalogParams.list.map(param => {
|
return paramsData.partsIndexCatalogParams.list.map((param: any) => {
|
||||||
if (param.type === 'range') {
|
if (param.type === 'range') {
|
||||||
// Для range фильтров ищем min и max значения
|
// Для range фильтров ищем min и max значения
|
||||||
const numericValues = param.values
|
const numericValues = param.values
|
||||||
.map(v => parseFloat(v.value))
|
.map((v: any) => parseFloat(v.value))
|
||||||
.filter(v => !isNaN(v));
|
.filter((v: number) => !isNaN(v));
|
||||||
|
|
||||||
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
|
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
|
||||||
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
|
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
|
||||||
@ -243,8 +269,8 @@ export default function Catalog() {
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
title: param.name,
|
title: param.name,
|
||||||
options: param.values
|
options: param.values
|
||||||
.filter(value => value.available) // Показываем только доступные
|
.filter((value: any) => value.available) // Показываем только доступные
|
||||||
.map(value => value.title || value.value),
|
.map((value: any) => value.title || value.value),
|
||||||
multi: true,
|
multi: true,
|
||||||
showAll: true
|
showAll: true
|
||||||
};
|
};
|
||||||
@ -252,6 +278,8 @@ export default function Catalog() {
|
|||||||
});
|
});
|
||||||
}, [paramsData]);
|
}, [paramsData]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPartsIndexMode) {
|
if (isPartsIndexMode) {
|
||||||
// Для PartsIndex генерируем фильтры на основе параметров API
|
// Для PartsIndex генерируем фильтры на основе параметров API
|
||||||
@ -426,16 +454,38 @@ export default function Catalog() {
|
|||||||
// При изменении поиска или фильтров сбрасываем пагинацию
|
// При изменении поиска или фильтров сбрасываем пагинацию
|
||||||
setShowEmptyState(false);
|
setShowEmptyState(false);
|
||||||
|
|
||||||
// Если изменился поисковый запрос, нужно перезагрузить данные с сервера
|
// Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера
|
||||||
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
||||||
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
||||||
setPartsIndexPage(1);
|
setPartsIndexPage(1);
|
||||||
setHasMoreEntities(true);
|
setHasMoreEntities(true);
|
||||||
// refetch будет автоматически вызван при изменении partsIndexPage
|
|
||||||
|
// Перезагружаем данные с новыми параметрами фильтрации
|
||||||
|
const apiParams = convertFiltersToPartsIndexParams();
|
||||||
|
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
||||||
|
|
||||||
|
// Также обновляем параметры фильтрации
|
||||||
|
refetchParams({
|
||||||
|
catalogId: catalogId as string,
|
||||||
|
groupId: groupId as string,
|
||||||
|
lang: 'ru',
|
||||||
|
q: searchQuery || undefined,
|
||||||
|
params: paramsString
|
||||||
|
});
|
||||||
|
|
||||||
|
refetchEntities({
|
||||||
|
catalogId: catalogId as string,
|
||||||
|
groupId: groupId as string,
|
||||||
|
lang: 'ru',
|
||||||
|
limit: ITEMS_PER_PAGE,
|
||||||
|
page: 1,
|
||||||
|
q: searchQuery || undefined,
|
||||||
|
params: paramsString
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]);
|
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]);
|
||||||
|
|
||||||
// Управляем показом пустого состояния с задержкой
|
// Управляем показом пустого состояния с задержкой
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -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) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push(offer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем цены аналогов
|
// Добавляем предложения аналогов
|
||||||
Object.values(loadedAnalogs).forEach((analog: any) => {
|
Object.values(loadedAnalogs).forEach((analog: any) => {
|
||||||
analog.internalOffers?.forEach((offer: any) => {
|
analog.internalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryDays
|
||||||
|
});
|
||||||
});
|
});
|
||||||
analog.externalOffers?.forEach((offer: any) => {
|
analog.externalOffers?.forEach((offer: any) => {
|
||||||
if (offer.price > 0) prices.push(offer.price);
|
allAvailableOffers.push({
|
||||||
|
...offer,
|
||||||
|
deliveryDuration: offer.deliveryTime
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (prices.length > 0) {
|
// Фильтр по цене - только если есть предложения с разными ценами
|
||||||
|
const prices: number[] = [];
|
||||||
|
allAvailableOffers.forEach((offer: any) => {
|
||||||
|
if (offer.price > 0) prices.push(offer.price);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prices.length > 1) {
|
||||||
const minPrice = Math.min(...prices);
|
const 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,36 +152,25 @@ 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}`;
|
|
||||||
|
|
||||||
if (!usedOfferIds.has(analogId)) {
|
|
||||||
result.push({ offer: cheapestAnalogOffer, type: 'Самый дешевый аналог' });
|
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}`;
|
|
||||||
|
|
||||||
if (!usedOfferIds.has(fastestId)) {
|
|
||||||
result.push({ offer: fastestDeliveryOffer, type: 'Самая быстрая доставка' });
|
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,6 +526,8 @@ export default function SearchResult() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
|
{/* Показываем InfoSearch только если есть результаты */}
|
||||||
|
{initialOffersExist && (
|
||||||
<InfoSearch
|
<InfoSearch
|
||||||
brand={result ? result.brand : brandQuery}
|
brand={result ? result.brand : brandQuery}
|
||||||
articleNumber={result ? result.articleNumber : searchQuery}
|
articleNumber={result ? result.articleNumber : searchQuery}
|
||||||
@ -459,7 +535,11 @@ export default function SearchResult() {
|
|||||||
offersCount={result ? result.totalOffers : 0}
|
offersCount={result ? result.totalOffers : 0}
|
||||||
minPrice={minPrice}
|
minPrice={minPrice}
|
||||||
/>
|
/>
|
||||||
<section className="main">
|
)}
|
||||||
|
{/* Показываем мобильные фильтры только если есть результаты */}
|
||||||
|
{initialOffersExist && (
|
||||||
|
<>
|
||||||
|
<section className="main mobile-only">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-hflex flex-block-84">
|
<div className="w-layout-hflex flex-block-84">
|
||||||
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
{/* <CatalogSortDropdown active={sortActive} onChange={setSortActive} /> */}
|
||||||
@ -490,6 +570,8 @@ export default function SearchResult() {
|
|||||||
searchQuery={filterSearchTerm}
|
searchQuery={filterSearchTerm}
|
||||||
onSearchChange={(value) => handleFilterChange('search', value)}
|
onSearchChange={(value) => handleFilterChange('search', value)}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{/* Лучшие предложения */}
|
{/* Лучшие предложения */}
|
||||||
{bestOffersData.length > 0 && (
|
{bestOffersData.length > 0 && (
|
||||||
<section className="section-6">
|
<section className="section-6">
|
||||||
@ -547,11 +629,13 @@ export default function SearchResult() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
{/* Показываем основную секцию с фильтрами только если есть результаты */}
|
||||||
|
{initialOffersExist && (
|
||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-hflex flex-block-13-copy">
|
<div className="w-layout-hflex flex-block-13-copy">
|
||||||
{/* Фильтры для десктопа */}
|
{/* Фильтры для десктопа */}
|
||||||
<div style={{ width: '300px', marginRight: '20px' }}>
|
<div style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
|
||||||
<Filters
|
<Filters
|
||||||
filters={searchResultFilters}
|
filters={searchResultFilters}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
@ -578,9 +662,8 @@ export default function SearchResult() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Используем фотографию из Parts Index, если она есть, иначе fallback на mainImageUrl
|
// Используем фотографию только из Parts Index, если она есть
|
||||||
const partsIndexImage = entityInfo?.images?.[0];
|
const partsIndexImage = entityInfo?.images?.[0];
|
||||||
const displayImage = partsIndexImage || mainImageUrl;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -588,7 +671,7 @@ export default function SearchResult() {
|
|||||||
brand={result.brand}
|
brand={result.brand}
|
||||||
article={result.articleNumber}
|
article={result.articleNumber}
|
||||||
name={result.name}
|
name={result.name}
|
||||||
image={displayImage}
|
{...(partsIndexImage ? { image: partsIndexImage } : {})}
|
||||||
offers={mainProductOffers}
|
offers={mainProductOffers}
|
||||||
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
|
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
|
||||||
partsIndexPowered={!!partsIndexImage}
|
partsIndexPowered={!!partsIndexImage}
|
||||||
@ -737,9 +820,13 @@ export default function SearchResult() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
{/* Показываем CatalogSubscribe только если есть результаты */}
|
||||||
|
{initialOffersExist && (
|
||||||
<section className="section-3">
|
<section className="section-3">
|
||||||
<CatalogSubscribe />
|
<CatalogSubscribe />
|
||||||
</section>
|
</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)}`);
|
||||||
}}
|
}}
|
||||||
|
@ -175,39 +175,33 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
|||||||
<>
|
<>
|
||||||
|
|
||||||
|
|
||||||
<main className="bg-gray-50 min-h-screen">
|
<main className="bg-[#F5F8FB] min-h-screen">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb (InfoSearch style) */}
|
||||||
<div className="bg-white border-b">
|
<section className="section-info">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="w-layout-blockcontainer container info w-container">
|
||||||
<nav className="flex" aria-label="Breadcrumb">
|
<div className="w-layout-vflex flex-block-9">
|
||||||
<ol className="flex items-center space-x-4">
|
<div className="w-layout-hflex flex-block-7">
|
||||||
<li>
|
<a href="/" className="link-block w-inline-block">
|
||||||
<Link href="/" className="text-gray-400 hover:text-gray-500">
|
<div>Главная</div>
|
||||||
Главная
|
</a>
|
||||||
</Link>
|
<div className="text-block-3">→</div>
|
||||||
</li>
|
<a href="#" className="link-block-2 w-inline-block">
|
||||||
<li>
|
<div>Найденные автомобили</div>
|
||||||
<div className="flex items-center">
|
</a>
|
||||||
<svg className="flex-shrink-0 h-5 w-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
|
||||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
|
||||||
</svg>
|
|
||||||
<span className="ml-4 text-sm font-medium text-red-600">
|
|
||||||
{searchType === 'vin' ? 'Найденные автомобили' : 'Найденные автомобили'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
<div className="w-layout-hflex flex-block-8">
|
||||||
</ol>
|
<div className="w-layout-hflex flex-block-10">
|
||||||
</nav>
|
<h1 className="heading">{searchType === 'vin' ? 'Поиск по VIN номеру' : 'Поиск по государственному номеру'}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Search Results Header */}
|
{/* Search Results Header */}
|
||||||
<div className="bg-white">
|
<div className="flex flex-col items-center pt-10 pb-16 max-md:px-5">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="w-full max-w-[1580px]">
|
||||||
<div className="mb-6">
|
{/* <div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
||||||
{searchType === 'vin' ? 'Поиск по VIN номеру' : 'Поиск по государственному номеру'}
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-gray-600">
|
<p className="text-lg text-gray-600">
|
||||||
Запрос: <span className="font-mono font-bold">{searchQuery}</span>
|
Запрос: <span className="font-mono font-bold">{searchQuery}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -216,18 +210,13 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
|||||||
Найдено {vehicles.length} автомобилей
|
Найдено {vehicles.length} автомобилей
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading State */}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="bg-white rounded-2xl shadow p-10 flex flex-col items-center justify-center min-h-[300px]">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-red-600 mb-6"></div>
|
||||||
<svg className="animate-spin h-8 w-8 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<p className="text-lg text-gray-600">Поиск автомобилей...</p>
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span className="text-lg text-gray-600">Поиск автомобилей...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -262,209 +251,58 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results Table */}
|
{/* Results List (Search-like style, not table) */}
|
||||||
{!isLoading && vehicles.length > 0 && !isRedirecting && (
|
{!isLoading && vehicles.length > 0 && !isRedirecting && (
|
||||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
<div className="bg-white rounded-2xl shadow p-10">
|
||||||
<div className="overflow-x-auto">
|
<div className="flex flex-wrap items-center gap-6 font-bold text-gray-900 text-base mb-2 px-2">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<div className="min-w-[100px] flex-1 break-words">Бренд</div>
|
||||||
<thead className="bg-gray-50">
|
<div className="min-w-[120px] flex-1 break-words">Название</div>
|
||||||
<tr>
|
<div className="min-w-[120px] flex-1 break-words">Модель</div>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<div className="min-w-[60px] flex-1 break-words">Год</div>
|
||||||
Бренд
|
<div className="min-w-[120px] flex-1 break-words">Двигатель</div>
|
||||||
</th>
|
<div className="min-w-[80px] flex-1 break-words">КПП</div>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<div className="min-w-[80px] flex-1 break-words">Рынок</div>
|
||||||
Название
|
<div className="min-w-[100px] flex-1 break-words">Дата выпуска</div>
|
||||||
</th>
|
<div className="min-w-[140px] flex-1 break-words">Период производства</div>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
</div>
|
||||||
Модель
|
<div className="space-y-0">
|
||||||
</th>
|
{vehicles.map((vehicle, index) => (
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<div
|
||||||
Год
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Двигатель
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
КПП
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Рынок
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Дата выпуска
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Период производства
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Дополнительно
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{vehicles.map((vehicle, index) => {
|
|
||||||
console.log('🔍 Отображаем автомобиль в таблице:', {
|
|
||||||
index,
|
|
||||||
vehicleid: vehicle.vehicleid,
|
|
||||||
name: vehicle.name,
|
|
||||||
brand: vehicle.brand,
|
|
||||||
catalog: vehicle.catalog,
|
|
||||||
model: vehicle.model,
|
|
||||||
year: vehicle.year,
|
|
||||||
engine: vehicle.engine,
|
|
||||||
ssd: vehicle.ssd ? vehicle.ssd.substring(0, 30) + '...' : 'отсутствует'
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={vehicle.vehicleid || index}
|
key={vehicle.vehicleid || index}
|
||||||
|
className="flex flex-wrap items-center gap-6 bg-white border-b border-gray-200 px-6 py-3 cursor-pointer hover:bg-slate-100 transition-colors max-w-full"
|
||||||
onClick={() => handleVehicleSelect(vehicle)}
|
onClick={() => handleVehicleSelect(vehicle)}
|
||||||
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
style={{ minWidth: 0 }}
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<div className="font-bold text-gray-900 text-base min-w-[100px] flex-1 break-words">{vehicle.brand}</div>
|
||||||
{vehicle.brand}
|
<div className="text-gray-900 text-base min-w-[120px] flex-1 break-words">{vehicle.name}</div>
|
||||||
</td>
|
<div className="text-gray-900 text-base min-w-[120px] flex-1 break-words">{vehicle.model}</div>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<div className="text-gray-900 text-base min-w-[60px] flex-1 break-words">{vehicle.year || '-'}</div>
|
||||||
{vehicle.name}
|
<div className="text-gray-900 text-base min-w-[120px] flex-1 break-words">{vehicle.engine || '-'}</div>
|
||||||
</td>
|
<div className="text-gray-900 text-base min-w-[80px] flex-1 break-words">{vehicle.transmission || '-'}</div>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<div className="text-gray-900 text-base min-w-[80px] flex-1 break-words">{vehicle.market || '-'}</div>
|
||||||
{vehicle.model}
|
<div className="text-gray-900 text-base min-w-[100px] flex-1 break-words">{vehicle.date || vehicle.manufactured || '-'}</div>
|
||||||
</td>
|
<div className="text-gray-900 text-base min-w-[140px] flex-1 break-words">{vehicle.prodRange || vehicle.prodPeriod || ((vehicle.datefrom && vehicle.dateto) ? `${vehicle.datefrom} - ${vehicle.dateto}` : (vehicle.modelyearfrom && vehicle.modelyearto) ? `${vehicle.modelyearfrom} - ${vehicle.modelyearto}` : '-')}</div>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
const year = vehicle.year || vehicle.manufactured || (vehicle.date ? vehicle.date.split('.').pop() : '') || '';
|
|
||||||
console.log(`🗓️ Год для автомобиля ${vehicle.vehicleid}:`, { year, original_year: vehicle.year, manufactured: vehicle.manufactured, date: vehicle.date });
|
|
||||||
return year || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
const engine = vehicle.engine || vehicle.engine_info || vehicle.engineno || '';
|
|
||||||
console.log(`🔧 Двигатель для автомобиля ${vehicle.vehicleid}:`, { engine, original_engine: vehicle.engine, engine_info: vehicle.engine_info, engineno: vehicle.engineno });
|
|
||||||
return engine || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
const transmission = vehicle.transmission || vehicle.bodytype || '';
|
|
||||||
console.log(`⚙️ КПП для автомобиля ${vehicle.vehicleid}:`, { transmission, original_transmission: vehicle.transmission, bodytype: vehicle.bodytype });
|
|
||||||
return transmission || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
const market = vehicle.market || vehicle.destinationregion || vehicle.creationregion || '';
|
|
||||||
console.log(`🌍 Рынок для автомобиля ${vehicle.vehicleid}:`, { market, original_market: vehicle.market, destinationregion: vehicle.destinationregion, creationregion: vehicle.creationregion });
|
|
||||||
return market || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
const releaseDate = vehicle.date || vehicle.manufactured || '';
|
|
||||||
console.log(`📅 Дата выпуска для автомобиля ${vehicle.vehicleid}:`, { releaseDate, date: vehicle.date, manufactured: vehicle.manufactured });
|
|
||||||
return releaseDate || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
||||||
{(() => {
|
|
||||||
let prodPeriod = '';
|
|
||||||
if (vehicle.prodRange) {
|
|
||||||
prodPeriod = vehicle.prodRange;
|
|
||||||
} else if (vehicle.prodPeriod) {
|
|
||||||
prodPeriod = vehicle.prodPeriod;
|
|
||||||
} else if (vehicle.datefrom && vehicle.dateto) {
|
|
||||||
prodPeriod = `${vehicle.datefrom} - ${vehicle.dateto}`;
|
|
||||||
} else if (vehicle.modelyearfrom && vehicle.modelyearto) {
|
|
||||||
prodPeriod = `${vehicle.modelyearfrom} - ${vehicle.modelyearto}`;
|
|
||||||
}
|
|
||||||
console.log(`📈 Период производства для автомобиля ${vehicle.vehicleid}:`, {
|
|
||||||
prodPeriod,
|
|
||||||
prodRange: vehicle.prodRange,
|
|
||||||
original_prodPeriod: vehicle.prodPeriod,
|
|
||||||
datefrom: vehicle.datefrom,
|
|
||||||
dateto: vehicle.dateto,
|
|
||||||
modelyearfrom: vehicle.modelyearfrom,
|
|
||||||
modelyearto: vehicle.modelyearto
|
|
||||||
});
|
|
||||||
return prodPeriod || '-';
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{vehicle.framecolor && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Цвет кузова:</span> {vehicle.framecolor}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
{vehicle.trimcolor && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Цвет салона:</span> {vehicle.trimcolor}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.engineno && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Номер двигателя:</span> {vehicle.engineno}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.engine_info && (
|
|
||||||
<div className="text-xs max-w-xs truncate" title={vehicle.engine_info}>
|
|
||||||
<span className="font-medium">Двигатель:</span> {vehicle.engine_info}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.options && (
|
|
||||||
<div className="text-xs max-w-xs truncate" title={vehicle.options}>
|
|
||||||
<span className="font-medium">Опции:</span> {vehicle.options}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.description && (
|
|
||||||
<div className="text-xs max-w-xs truncate" title={vehicle.description}>
|
|
||||||
<span className="font-medium">Описание:</span> {vehicle.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.modification && (
|
|
||||||
<div className="text-xs max-w-xs truncate" title={vehicle.modification}>
|
|
||||||
<span className="font-medium">Модификация:</span> {vehicle.modification}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vehicle.grade && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Класс:</span> {vehicle.grade}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No Results */}
|
{/* No Results */}
|
||||||
{!isLoading && vehicles.length === 0 && searchQuery && (
|
{!isLoading && vehicles.length === 0 && searchQuery && (
|
||||||
<div className="text-center py-12">
|
<div className="bg-[#eaf0fa] border border-[#b3c6e6] rounded-2xl shadow p-10 text-center">
|
||||||
<div className="text-yellow-400 mb-4">
|
<svg className="w-16 h-16 mx-auto mb-4" style={{ color: '#0d336c' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.562M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 14.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
<h3 className="text-xl font-semibold mb-2" style={{ color: '#0d336c' }}>
|
||||||
<h3 className="text-xl font-medium text-gray-900 mb-2">
|
Автомобили не найдены
|
||||||
{searchType === 'vin' ? 'VIN не найден' : 'Госномер не найден'}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="mb-4" style={{ color: '#0d336c' }}>
|
||||||
{searchType === 'vin'
|
По запросу <span className="font-mono font-semibold">{searchQuery}</span> автомобили не найдены.
|
||||||
? `Автомобиль с VIN номером ${searchQuery} не найден в доступных каталогах`
|
</p>
|
||||||
: `Автомобиль с государственным номером ${searchQuery} не найден в базе данных`
|
<p className="text-sm" style={{ color: '#3b5a99' }}>
|
||||||
}
|
Попробуйте изменить запрос или проверьте правильность написания.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
|
||||||
>
|
|
||||||
Вернуться на главную
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
<a href={`/vehicle-search/${brand}/${vehicleId}`} className="link-block w-inline-block">
|
||||||
<div>{brandName}</div>
|
<div>{brandName}</div>
|
||||||
|
</a>
|
||||||
<div className="text-block-3">→</div>
|
<div className="text-block-3">→</div>
|
||||||
|
<a href="#" className="link-block-2 w-inline-block">
|
||||||
<div>Деталь {oemNumber}</div>
|
<div>Деталь {oemNumber}</div>
|
||||||
<div className="text-block-3">→</div>
|
</a>
|
||||||
<div>Выбор производителя</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-8">
|
|
||||||
<div className="w-layout-hflex flex-block-10">
|
|
||||||
<h1 className="heading">Выберите производителя для {oemNumber}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="link-block w-inline-block">
|
||||||
|
|
||||||
|
<div className="heading">Выберите производителя для {oemNumber}</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,8 +195,8 @@ 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>
|
||||||
@ -196,7 +204,7 @@ const BrandSelectionPage = () => {
|
|||||||
{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> */}
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{brands.map((brandItem: any, index: number) => (
|
{brands.map((brandItem: any, index: number) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
|
@ -101,3 +101,281 @@ 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 {
|
||||||
@ -45,6 +45,17 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.price-in-cart-s1 {
|
||||||
|
|
||||||
|
max-width: 140px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block-40 {
|
||||||
|
background-color: #fff;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
input.text-block-31 {
|
input.text-block-31 {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
}
|
}
|
||||||
@ -354,6 +365,15 @@ input.input-receiver:focus {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-5-copy {
|
||||||
|
width: 97px !important;
|
||||||
|
height: 97px !important;
|
||||||
|
}
|
||||||
|
.flex-block-111 {
|
||||||
|
width: 172px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.show-more-btn {
|
.show-more-btn {
|
||||||
background-color: #ec1c24;
|
background-color: #ec1c24;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -399,7 +419,14 @@ input.input-receiver:focus {
|
|||||||
|
|
||||||
|
|
||||||
.core-product-s1{
|
.core-product-s1{
|
||||||
max-width: 320px ;
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-10 {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
|
||||||
|
white-space: nowrap; /* если хотите, чтобы текст не переносился */
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-112 {
|
.flex-block-112 {
|
||||||
@ -440,6 +467,12 @@ input#VinSearchInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.w-input {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.text-block-56, .dropdown-link-3 {
|
.text-block-56, .dropdown-link-3 {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -493,6 +526,9 @@ input#VinSearchInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.div-block-19{
|
||||||
|
padding-left: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-toggle-card {
|
.dropdown-toggle-card {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@ -600,6 +636,47 @@ 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{
|
||||||
|
max-width: 350px !important;
|
||||||
|
min-width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.flex-block-18{
|
||||||
|
row-gap: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.menu-button.w--open {
|
.menu-button.w--open {
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
@ -607,10 +684,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;
|
||||||
@ -647,15 +723,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) {
|
||||||
@ -679,7 +765,7 @@ body {
|
|||||||
height: 140px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.div-block-128 {
|
.div-block-128 {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
@ -772,10 +858,57 @@ body {
|
|||||||
max-width: 33%;
|
max-width: 33%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-block-44 {
|
||||||
|
grid-column-gap: 0px;
|
||||||
|
grid-row-gap: 0px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-list-s1 {
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.show-more-search {
|
||||||
|
padding: 6px 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.flex-block-37 {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
.w-layout-vflex.flex-block-40 {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block-47 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important; /* по центру по горизонтали */
|
||||||
|
gap: 16px !important;
|
||||||
|
}
|
||||||
|
.flex-block-50 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 16px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.flex-block-79 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.text-block-21 {
|
.text-block-21 {
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-45 {
|
.flex-block-45 {
|
||||||
@ -793,8 +926,8 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
.flex-block-15-copy {
|
.flex-block-15-copy {
|
||||||
width: 235px!important;
|
width: 232px!important;
|
||||||
min-width: 235px!important;
|
min-width: 232px!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nameitembp {
|
.nameitembp {
|
||||||
@ -846,7 +979,16 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.flex-block-110 {
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
.image-5-copy {
|
||||||
|
width: 75px !important;
|
||||||
|
height: 75px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.topmenub {
|
.topmenub {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@ -877,3 +1019,132 @@ 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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-arrow {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
opacity: 0.85;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.carousel-arrow:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.carousel-arrow[disabled] {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-arrow-left {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.carousel-arrow-right {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE и Edge */
|
||||||
|
}
|
||||||
|
.carousel-scroll::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.carousel-scroll {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.carousel-row {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.carousel-arrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.protekauto-logo {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-991 {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 991px) {
|
||||||
|
.hide-on-991 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.flex-block-50 {
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.div-block-19 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.core-product-s1 {
|
||||||
|
flex-direction: row !important; /* или column, если нужно вертикально */
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
@ -2325,8 +2324,7 @@ body {
|
|||||||
.text-block-21 {
|
.text-block-21 {
|
||||||
color: var(--_fonts---color--light-blue-grey);
|
color: var(--_fonts---color--light-blue-grey);
|
||||||
font-size: var(--_fonts---font-size--small-font-size);
|
font-size: var(--_fonts---font-size--small-font-size);
|
||||||
align-self: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-block-22 {
|
.text-block-22 {
|
||||||
@ -2397,13 +2395,7 @@ body {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-47 {
|
|
||||||
grid-column-gap: 15px;
|
|
||||||
grid-row-gap: 15px;
|
|
||||||
flex-flow: row;
|
|
||||||
flex: 1;
|
|
||||||
align-self: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-10 {
|
.image-10 {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@ -3717,9 +3709,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-79 {
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-80 {
|
.flex-block-80 {
|
||||||
grid-column-gap: 20px;
|
grid-column-gap: 20px;
|
||||||
@ -4492,16 +4482,7 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
grid-row-gap: 10px;
|
|
||||||
flex-flow: row-reverse;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-self: stretch;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-48-copy {
|
.flex-block-48-copy {
|
||||||
grid-column-gap: 16px;
|
grid-column-gap: 16px;
|
||||||
@ -6768,14 +6749,7 @@ body {
|
|||||||
flex: 0 auto;
|
flex: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: column;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 270px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-product-search-s2 {
|
.core-product-search-s2 {
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
@ -7038,10 +7012,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: column;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-product-search-s2 {
|
.core-product-search-s2 {
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
@ -7870,9 +7841,7 @@ body {
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-list-s1 {
|
.sort-list-s1 {
|
||||||
padding-right: 210px;
|
padding-right: 210px;
|
||||||
@ -9499,9 +9468,9 @@ body {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-block-21 {
|
/* .text-block-21 {
|
||||||
line-height: 140%;
|
line-height: 140%;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.flex-block-45 {
|
.flex-block-45 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -10058,13 +10027,7 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
grid-row-gap: 10px;
|
|
||||||
flex-flow: column-reverse wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-48-copy {
|
.flex-block-48-copy {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
12
src/types/index.ts
Normal file
12
src/types/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Навигационные категории
|
||||||
|
export interface NavigationCategory {
|
||||||
|
id: string
|
||||||
|
partsIndexCatalogId: string
|
||||||
|
partsIndexGroupId: string | null
|
||||||
|
name: string
|
||||||
|
catalogName: string
|
||||||
|
groupName: string | null
|
||||||
|
icon: string | null
|
||||||
|
sortOrder: number
|
||||||
|
isHidden: boolean
|
||||||
|
}
|
Reference in New Issue
Block a user