Compare commits
6 Commits
fix1407
...
5fd2cf1b8c
Author | SHA1 | Date | |
---|---|---|---|
5fd2cf1b8c | |||
2703137ca1 | |||
3e98f8fed6 | |||
9c152501db | |||
074eb120b4 | |||
4dfc081214 |
92
package-lock.json
generated
92
package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@ -1540,6 +1541,15 @@
|
|||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decamelize": {
|
"node_modules/decamelize": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
@ -1602,6 +1612,29 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
@ -1615,6 +1648,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@ -2140,6 +2185,44 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
@ -2690,6 +2773,15 @@
|
|||||||
"uuid": "dist/esm/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which-module": {
|
"node_modules/which-module": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
@ -3,16 +3,17 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
interface CatalogSortDropdownProps {
|
interface CatalogSortDropdownProps {
|
||||||
active: number;
|
active: number;
|
||||||
onChange: (index: number) => void;
|
onChange: (index: number) => void;
|
||||||
|
options?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortOptions = [
|
const defaultSortOptions = [
|
||||||
'По популярности',
|
'По популярности',
|
||||||
'Сначала дешевле',
|
'Сначала дешевле',
|
||||||
'Сначала дороже',
|
'Сначала дороже',
|
||||||
'Высокий рейтинг',
|
'Высокий рейтинг',
|
||||||
];
|
];
|
||||||
|
|
||||||
const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onChange }) => {
|
const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onChange, options = defaultSortOptions }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ const CatalogSortDropdown: React.FC<CatalogSortDropdownProps> = ({ active, onCha
|
|||||||
<div>Сортировка</div>
|
<div>Сортировка</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className={`dropdown-list-2 w-dropdown-list${isOpen ? ' w--open' : ''}`} style={{ minWidth: 180, whiteSpace: 'normal' }}>
|
<nav className={`dropdown-list-2 w-dropdown-list${isOpen ? ' w--open' : ''}`} style={{ minWidth: 180, whiteSpace: 'normal' }}>
|
||||||
{sortOptions.map((option, index) => (
|
{options.map((option: string, index: number) => (
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href="#"
|
href="#"
|
||||||
|
28
src/components/CloseIcon.tsx
Normal file
28
src/components/CloseIcon.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CloseIconProps {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloseIcon: React.FC<CloseIconProps> = ({ size = 20, color = '#fff' }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M18 6L6 18M6 6L18 18"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloseIcon;
|
@ -3,6 +3,7 @@ import { useCart } from "@/contexts/CartContext";
|
|||||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import CartIcon from "./CartIcon";
|
import CartIcon from "./CartIcon";
|
||||||
|
import { isDeliveryDate } from "@/lib/utils";
|
||||||
|
|
||||||
const INITIAL_OFFERS_LIMIT = 5;
|
const INITIAL_OFFERS_LIMIT = 5;
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
const { addItem } = useCart();
|
const { addItem } = useCart();
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
||||||
|
const [sortBy, setSortBy] = useState<'stock' | 'delivery' | 'price'>('price'); // Локальная сортировка для каждого товара
|
||||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||||
);
|
);
|
||||||
@ -63,8 +65,52 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
setQuantities(offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {}));
|
setQuantities(offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {}));
|
||||||
}, [offers.length]);
|
}, [offers.length]);
|
||||||
|
|
||||||
const displayedOffers = offers.slice(0, visibleOffersCount);
|
// Функция для парсинга цены из строки
|
||||||
const hasMoreOffers = visibleOffersCount < offers.length;
|
const parsePrice = (priceStr: string): number => {
|
||||||
|
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||||
|
return parseFloat(cleanPrice) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для парсинга количества в наличии
|
||||||
|
const parseStock = (stockStr: string): number => {
|
||||||
|
const match = stockStr.match(/\d+/);
|
||||||
|
return match ? parseInt(match[0]) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для парсинга времени доставки
|
||||||
|
const parseDeliveryTime = (daysStr: string): string => {
|
||||||
|
// Если это дата (содержит название месяца), возвращаем как есть
|
||||||
|
if (isDeliveryDate(daysStr)) {
|
||||||
|
return daysStr;
|
||||||
|
}
|
||||||
|
// Иначе парсим как количество дней (для обратной совместимости)
|
||||||
|
const match = daysStr.match(/\d+/);
|
||||||
|
return match ? `${match[0]} дней` : daysStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция сортировки предложений
|
||||||
|
const sortOffers = (offers: CoreProductCardOffer[]) => {
|
||||||
|
const sorted = [...offers];
|
||||||
|
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'stock':
|
||||||
|
return sorted.sort((a, b) => parseStock(b.pcs) - parseStock(a.pcs));
|
||||||
|
case 'delivery':
|
||||||
|
return sorted.sort((a, b) => {
|
||||||
|
const aDelivery = a.deliveryTime || 999;
|
||||||
|
const bDelivery = b.deliveryTime || 999;
|
||||||
|
return aDelivery - bDelivery;
|
||||||
|
});
|
||||||
|
case 'price':
|
||||||
|
return sorted.sort((a, b) => parsePrice(a.price) - parsePrice(b.price));
|
||||||
|
default:
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedOffers = sortOffers(offers);
|
||||||
|
const displayedOffers = sortedOffers.slice(0, visibleOffersCount);
|
||||||
|
const hasMoreOffers = visibleOffersCount < sortedOffers.length;
|
||||||
|
|
||||||
// Проверяем, есть ли товар в избранном
|
// Проверяем, есть ли товар в избранном
|
||||||
const isItemFavorite = isFavorite(
|
const isItemFavorite = isFavorite(
|
||||||
@ -74,24 +120,6 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
brand
|
brand
|
||||||
);
|
);
|
||||||
|
|
||||||
// Функция для парсинга цены из строки
|
|
||||||
const parsePrice = (priceStr: string): number => {
|
|
||||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
|
||||||
return parseFloat(cleanPrice) || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для парсинга времени доставки
|
|
||||||
const parseDeliveryTime = (daysStr: string): string => {
|
|
||||||
const match = daysStr.match(/\d+/);
|
|
||||||
return match ? `${match[0]} дней` : daysStr;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для парсинга количества в наличии
|
|
||||||
const parseStock = (stockStr: string): number => {
|
|
||||||
const match = stockStr.match(/\d+/);
|
|
||||||
return match ? parseInt(match[0]) : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (idx: number, val: string) => {
|
const handleInputChange = (idx: number, val: string) => {
|
||||||
setInputValues(prev => ({ ...prev, [idx]: val }));
|
setInputValues(prev => ({ ...prev, [idx]: val }));
|
||||||
if (val === "") return;
|
if (val === "") return;
|
||||||
@ -316,10 +344,28 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<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
|
||||||
<div className="sort-item">Доставка</div>
|
className={`sort-item first ${sortBy === 'stock' ? 'active' : ''}`}
|
||||||
|
onClick={() => setSortBy('stock')}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Наличие
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`sort-item ${sortBy === 'delivery' ? 'active' : ''}`}
|
||||||
|
onClick={() => setSortBy('delivery')}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Доставим
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`sort-item price ${sortBy === 'price' ? 'active' : ''}`}
|
||||||
|
onClick={() => setSortBy('price')}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Цена
|
||||||
</div>
|
</div>
|
||||||
<div className="sort-item price">Цена</div>
|
|
||||||
</div>
|
</div>
|
||||||
{displayedOffers.map((offer, idx) => {
|
{displayedOffers.map((offer, idx) => {
|
||||||
const isLast = idx === displayedOffers.length - 1;
|
const isLast = idx === displayedOffers.length - 1;
|
||||||
@ -414,7 +460,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
className="w-layout-hflex show-more-search"
|
className="w-layout-hflex show-more-search"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hasMoreOffers) {
|
if (hasMoreOffers) {
|
||||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
setVisibleOffersCount(prev => Math.min(prev + 10, sortedOffers.length));
|
||||||
} else {
|
} else {
|
||||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||||
}
|
}
|
||||||
@ -422,11 +468,11 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
aria-label={hasMoreOffers ? `Еще ${sortedOffers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
if (hasMoreOffers) {
|
if (hasMoreOffers) {
|
||||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
setVisibleOffersCount(prev => Math.min(prev + 10, sortedOffers.length));
|
||||||
} else {
|
} else {
|
||||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||||
}
|
}
|
||||||
@ -434,7 +480,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-block-27">
|
<div className="text-block-27">
|
||||||
{hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
{hasMoreOffers ? `Еще ${sortedOffers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
src="/images/arrow_drop_down.svg"
|
src="/images/arrow_drop_down.svg"
|
||||||
|
@ -9,6 +9,8 @@ import { FIND_LAXIMO_VEHICLE, DOC_FIND_OEM, FIND_LAXIMO_VEHICLE_BY_PLATE_GLOBAL,
|
|||||||
import { LaximoVehicleSearchResult, LaximoDocFindOEMResult, LaximoVehiclesByPartResult } from '@/types/laximo';
|
import { LaximoVehicleSearchResult, LaximoDocFindOEMResult, LaximoVehiclesByPartResult } from '@/types/laximo';
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import CartButton from './CartButton';
|
import CartButton from './CartButton';
|
||||||
|
import SearchHistoryDropdown from './SearchHistoryDropdown';
|
||||||
|
import { GET_RECENT_SEARCH_QUERIES, PartsSearchHistoryItem } from '@/lib/graphql/search-history';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onOpenAuthModal?: () => void;
|
onOpenAuthModal?: () => void;
|
||||||
@ -25,9 +27,14 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
const [vehiclesByPartResults, setVehiclesByPartResults] = useState<LaximoVehiclesByPartResult | null>(null);
|
const [vehiclesByPartResults, setVehiclesByPartResults] = useState<LaximoVehiclesByPartResult | null>(null);
|
||||||
const [searchType, setSearchType] = useState<'vin' | 'oem' | 'plate' | 'text'>('text');
|
const [searchType, setSearchType] = useState<'vin' | 'oem' | 'plate' | 'text'>('text');
|
||||||
const [oemSearchMode, setOemSearchMode] = useState<'parts' | 'vehicles'>('parts');
|
const [oemSearchMode, setOemSearchMode] = useState<'parts' | 'vehicles'>('parts');
|
||||||
|
const [showSearchHistory, setShowSearchHistory] = useState(false);
|
||||||
|
const [searchHistoryItems, setSearchHistoryItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||||
|
const [inputFocused, setInputFocused] = useState(false);
|
||||||
|
const [showPlaceholder, setShowPlaceholder] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchFormRef = useRef<HTMLFormElement>(null);
|
const searchFormRef = useRef<HTMLFormElement>(null);
|
||||||
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
const searchDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const isClient = useIsClient();
|
const isClient = useIsClient();
|
||||||
|
|
||||||
// Эффект для восстановления поискового запроса из URL
|
// Эффект для восстановления поискового запроса из URL
|
||||||
@ -111,11 +118,28 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Запрос для получения истории поиска
|
||||||
|
const [getSearchHistory, { loading: historyLoading }] = useLazyQuery(GET_RECENT_SEARCH_QUERIES, {
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSearchHistoryItems(data.partsSearchHistory?.items || []);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('❌ Ошибка загрузки истории поиска:', error);
|
||||||
|
setSearchHistoryItems([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Закрытие результатов при клике вне области
|
// Закрытие результатов при клике вне области
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (searchDropdownRef.current && !searchDropdownRef.current.contains(event.target as Node)) {
|
if (searchDropdownRef.current && !searchDropdownRef.current.contains(event.target as Node)) {
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
|
setShowSearchHistory(false);
|
||||||
|
setInputFocused(false);
|
||||||
|
// Показываем placeholder обратно только если поле пустое
|
||||||
|
if (searchQuery.trim() === '') {
|
||||||
|
setShowPlaceholder(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -356,6 +380,54 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
router.push(url);
|
router.push(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик фокуса на поле ввода
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
setInputFocused(true);
|
||||||
|
setShowResults(false);
|
||||||
|
setShowPlaceholder(false);
|
||||||
|
if (searchQuery.trim() === '') {
|
||||||
|
setShowSearchHistory(true);
|
||||||
|
getSearchHistory({ variables: { limit: 5 } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик изменения значения поля ввода
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchQuery(value);
|
||||||
|
|
||||||
|
// Управляем placeholder в зависимости от наличия текста
|
||||||
|
if (value.trim() === '') {
|
||||||
|
setShowPlaceholder(false); // Скрываем placeholder пока в фокусе
|
||||||
|
setShowSearchHistory(true);
|
||||||
|
setShowResults(false);
|
||||||
|
getSearchHistory({ variables: { limit: 5 } });
|
||||||
|
} else {
|
||||||
|
setShowPlaceholder(false); // Скрываем placeholder когда есть текст
|
||||||
|
setShowSearchHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик потери фокуса
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
// Показываем placeholder обратно только если поле пустое
|
||||||
|
if (searchQuery.trim() === '') {
|
||||||
|
setShowPlaceholder(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик клика по элементу истории
|
||||||
|
const handleHistoryItemClick = (searchQuery: string) => {
|
||||||
|
setSearchQuery(searchQuery);
|
||||||
|
setShowSearchHistory(false);
|
||||||
|
setInputFocused(false);
|
||||||
|
setShowPlaceholder(false); // Скрываем placeholder так как теперь есть текст
|
||||||
|
// Фокусируем поле ввода для возможности редактирования
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <section className="top_head">
|
{/* <section className="top_head">
|
||||||
@ -421,7 +493,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
</svg></div>
|
</svg></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="searcj w-form" style={{ position: 'relative' }}>
|
<div className="searcj w-form" style={{ position: 'relative' }} ref={searchDropdownRef}>
|
||||||
<form
|
<form
|
||||||
id="custom-search-form"
|
id="custom-search-form"
|
||||||
name="custom-search-form"
|
name="custom-search-form"
|
||||||
@ -444,23 +516,33 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
className="text-field w-input"
|
className="text-field w-input"
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
name="customSearch"
|
name="customSearch"
|
||||||
data-custom-input="true"
|
data-custom-input="true"
|
||||||
placeholder="Введите код запчасти, VIN номер или госномер автомобиля"
|
placeholder={showPlaceholder ? "Введите код запчасти, VIN номер или госномер автомобиля" : ""}
|
||||||
type="text"
|
type="text"
|
||||||
id="customSearchInput"
|
id="customSearchInput"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={handleInputChange}
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
disabled={isSearching}
|
disabled={isSearching}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/* История поиска */}
|
||||||
|
<SearchHistoryDropdown
|
||||||
|
isVisible={showSearchHistory && !showResults}
|
||||||
|
historyItems={searchHistoryItems}
|
||||||
|
onItemClick={handleHistoryItemClick}
|
||||||
|
loading={historyLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Результаты поиска VIN */}
|
{/* Результаты поиска VIN */}
|
||||||
{showResults && searchResults.length > 0 && (searchType === 'vin' || searchType === 'plate') && (
|
{showResults && searchResults.length > 0 && (searchType === 'vin' || searchType === 'plate') && (
|
||||||
<div
|
<div
|
||||||
ref={searchDropdownRef}
|
|
||||||
className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg mt-2 z-50 max-h-80 overflow-y-auto"
|
className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg mt-2 z-50 max-h-80 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div className="p-3 border-b border-gray-100">
|
<div className="p-3 border-b border-gray-100">
|
||||||
|
170
src/components/SearchHistoryDropdown.tsx
Normal file
170
src/components/SearchHistoryDropdown.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PartsSearchHistoryItem } from '@/lib/graphql/search-history';
|
||||||
|
|
||||||
|
interface SearchHistoryDropdownProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
historyItems: PartsSearchHistoryItem[];
|
||||||
|
onItemClick: (searchQuery: string) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchHistoryDropdown: React.FC<SearchHistoryDropdownProps> = ({
|
||||||
|
isVisible,
|
||||||
|
historyItems,
|
||||||
|
onItemClick,
|
||||||
|
loading = false
|
||||||
|
}) => {
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
// Фильтруем уникальные запросы
|
||||||
|
const uniqueQueries = Array.from(
|
||||||
|
new Map(
|
||||||
|
historyItems.map(item => [item.searchQuery.toLowerCase(), item])
|
||||||
|
).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSearchTypeLabel = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'VIN':
|
||||||
|
return 'VIN';
|
||||||
|
case 'PLATE':
|
||||||
|
return 'Госномер';
|
||||||
|
case 'OEM':
|
||||||
|
case 'ARTICLE':
|
||||||
|
return 'Артикул';
|
||||||
|
default:
|
||||||
|
return 'Поиск';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-history-dropdown-custom">
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-4 text-center text-gray-500">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<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>
|
||||||
|
Загрузка истории...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : uniqueQueries.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{uniqueQueries.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => onItemClick(item.searchQuery)}
|
||||||
|
className="search-history-item-custom"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="search-history-icon-custom">
|
||||||
|
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 8v4l3 3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span className="search-history-inline">
|
||||||
|
<span className="search-history-query-custom">{item.searchQuery}</span>
|
||||||
|
<span className="search-history-type-custom">{getSearchTypeLabel(item.searchType)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-gray-500">
|
||||||
|
<p className="text-sm">История поиска пуста</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<style>{`
|
||||||
|
.search-history-dropdown-custom {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(44,62,80,0.10), 0 1.5px 4px rgba(44,62,80,0.08);
|
||||||
|
margin-top: 12px;
|
||||||
|
z-index: 50;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
padding: 6px 0;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE и Edge */
|
||||||
|
}
|
||||||
|
.search-history-dropdown-custom::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
.search-history-item-custom {
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: background 0.18s;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.search-history-item-custom:hover, .search-history-item-custom:focus {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
.search-history-item-custom .flex {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
.search-history-icon-custom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #222;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.search-history-item-custom:hover .search-history-icon-custom,
|
||||||
|
.search-history-item-custom:focus .search-history-icon-custom {
|
||||||
|
background: #ec1c24;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.search-history-inline {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.search-history-query-custom {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.search-history-type-custom {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8e9aac;
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchHistoryDropdown;
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { useCart } from "@/contexts/CartContext";
|
import { useCart } from "@/contexts/CartContext";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import CartIcon from "../CartIcon";
|
import CartIcon from "../CartIcon";
|
||||||
|
import { isDeliveryDate } from "@/lib/utils";
|
||||||
|
|
||||||
interface ProductBuyBlockProps {
|
interface ProductBuyBlockProps {
|
||||||
offer?: any;
|
offer?: any;
|
||||||
@ -51,7 +52,9 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
|
|||||||
brand: offer.brand,
|
brand: offer.brand,
|
||||||
article: offer.articleNumber,
|
article: offer.articleNumber,
|
||||||
supplier: offer.supplier || (offer.type === 'external' ? 'AutoEuro' : 'Внутренний'),
|
supplier: offer.supplier || (offer.type === 'external' ? 'AutoEuro' : 'Внутренний'),
|
||||||
deliveryTime: offer.deliveryTime ? String(offer.deliveryTime) + ' дней' : '1 день',
|
deliveryTime: offer.deliveryTime ? (typeof offer.deliveryTime === 'string' && isDeliveryDate(offer.deliveryTime)
|
||||||
|
? offer.deliveryTime
|
||||||
|
: String(offer.deliveryTime) + ' дней') : '1 день',
|
||||||
isExternal: offer.type === 'external'
|
isExternal: offer.type === 'external'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { isDeliveryDate } from "@/lib/utils";
|
||||||
|
|
||||||
interface ProductInfoProps {
|
interface ProductInfoProps {
|
||||||
offer?: any;
|
offer?: any;
|
||||||
@ -17,6 +18,11 @@ const ProductInfo: React.FC<ProductInfoProps> = ({ offer }) => {
|
|||||||
|
|
||||||
// Форматируем срок доставки
|
// Форматируем срок доставки
|
||||||
const formatDeliveryTime = (deliveryTime: number | string) => {
|
const formatDeliveryTime = (deliveryTime: number | string) => {
|
||||||
|
// Если это уже дата (содержит название месяца), возвращаем как есть
|
||||||
|
if (typeof deliveryTime === 'string' && isDeliveryDate(deliveryTime)) {
|
||||||
|
return deliveryTime;
|
||||||
|
}
|
||||||
|
|
||||||
const days = typeof deliveryTime === 'string' ? parseInt(deliveryTime) : deliveryTime;
|
const days = typeof deliveryTime === 'string' ? parseInt(deliveryTime) : deliveryTime;
|
||||||
|
|
||||||
if (!days || days === 0) {
|
if (!days || days === 0) {
|
||||||
|
@ -1,39 +1,72 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
const CategoryNavSection: React.FC = () => (
|
interface CategoryNavItem {
|
||||||
<section className="catnav">
|
id: string;
|
||||||
<div className="w-layout-blockcontainer batd w-container">
|
name: string;
|
||||||
<div className="w-layout-hflex flex-block-108-copy">
|
image?: string;
|
||||||
<div className="ci1">
|
}
|
||||||
<div className="text-block-54-copy">Детали для ТО</div>
|
|
||||||
</div>
|
const FALLBACK_CATEGORIES: CategoryNavItem[] = [
|
||||||
<div className="ci2">
|
{ id: '1', name: 'Детали для ТО', image: '/images/catalog_item.png' },
|
||||||
<div className="text-block-54">Шины</div>
|
{ id: '2', name: 'Шины', image: '/images/catalog_item2.png' },
|
||||||
</div>
|
{ id: '3', name: 'Диски', image: '/images/catalog_item3.png' },
|
||||||
<div className="ci3">
|
{ id: '4', name: 'Масла и жидкости', image: '/images/catalog_item4.png' },
|
||||||
<div className="text-block-54">Диски</div>
|
{ id: '5', name: 'Инструменты', image: '/images/catalog_item5.png' },
|
||||||
</div>
|
{ id: '6', name: 'Автохимия', image: '/images/catalog_item6.png' },
|
||||||
<div className="ci4">
|
{ id: '7', name: 'Аксессуары', image: '/images/catalog_item7.png' },
|
||||||
<div className="text-block-54">Масла и жидкости</div>
|
{ id: '8', name: 'Электрика', image: '/images/catalog_item8.png' },
|
||||||
</div>
|
{ id: '9', name: 'АКБ', image: '/images/catalog_item9.png' },
|
||||||
<div className="ci5">
|
];
|
||||||
<div className="text-block-54">Инструменты</div>
|
|
||||||
</div>
|
const CategoryNavSection: React.FC = () => {
|
||||||
<div className="ci6">
|
const router = useRouter();
|
||||||
<div className="text-block-54">Автохимия</div>
|
|
||||||
</div>
|
const { data } = useQuery<{ partsIndexCategoriesWithGroups: CategoryNavItem[] }>(
|
||||||
<div className="ci7">
|
GET_PARTSINDEX_CATEGORIES,
|
||||||
<div className="text-block-54">Аксессуары</div>
|
{
|
||||||
</div>
|
variables: { lang: 'ru' },
|
||||||
<div className="ci8">
|
errorPolicy: 'all',
|
||||||
<div className="text-block-54">Электрика</div>
|
fetchPolicy: 'cache-first',
|
||||||
</div>
|
}
|
||||||
<div className="ci9">
|
);
|
||||||
<div className="text-block-54">АКБ</div>
|
|
||||||
|
const categories = (data?.partsIndexCategoriesWithGroups && data.partsIndexCategoriesWithGroups.length > 0)
|
||||||
|
? data.partsIndexCategoriesWithGroups.slice(0, 9)
|
||||||
|
: FALLBACK_CATEGORIES;
|
||||||
|
|
||||||
|
const handleCategoryClick = (category: CategoryNavItem) => {
|
||||||
|
router.push({
|
||||||
|
pathname: '/catalog',
|
||||||
|
query: {
|
||||||
|
categoryId: category.id,
|
||||||
|
categoryName: encodeURIComponent(category.name)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="catnav">
|
||||||
|
<div className="w-layout-blockcontainer batd w-container">
|
||||||
|
<div className="w-layout-hflex flex-block-108-copy">
|
||||||
|
{categories.map((category, idx) => (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className={`ci${idx + 1}`}
|
||||||
|
style={category.image ? { cursor: 'pointer', backgroundImage: `url('${category.image}')`, backgroundSize: 'cover', backgroundPosition: 'center' } : { cursor: 'pointer' }}
|
||||||
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
>
|
||||||
|
<div className={idx === 0 ? 'text-block-54-copy' : 'text-block-54'} style={{ textAlign: 'center' }}>
|
||||||
|
{category.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default CategoryNavSection;
|
export default CategoryNavSection;
|
@ -1,6 +1,23 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { GET_HERO_BANNERS } from '@/lib/graphql';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface HeroBanner {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
imageUrl: string;
|
||||||
|
linkUrl?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
const HeroSlider = () => {
|
const HeroSlider = () => {
|
||||||
|
const { data, loading, error } = useQuery(GET_HERO_BANNERS, {
|
||||||
|
errorPolicy: 'all'
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined" && window.Webflow && window.Webflow.require) {
|
if (typeof window !== "undefined" && window.Webflow && window.Webflow.require) {
|
||||||
if (window.Webflow.destroy) {
|
if (window.Webflow.destroy) {
|
||||||
@ -12,118 +29,152 @@ const HeroSlider = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Фильтруем только активные баннеры и сортируем их
|
||||||
|
const banners: HeroBanner[] = data?.heroBanners
|
||||||
|
?.filter((banner: HeroBanner) => banner.isActive)
|
||||||
|
?.slice()
|
||||||
|
?.sort((a: HeroBanner, b: HeroBanner) => a.sortOrder - b.sortOrder) || [];
|
||||||
|
|
||||||
|
// Если нет данных или происходит загрузка, показываем дефолтный баннер
|
||||||
|
if (loading || error || banners.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className="section-5" style={{ overflow: 'hidden' }}>
|
||||||
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
|
<div data-delay="4000" data-animation="slide" className="slider w-slider" data-autoplay="false" data-easing="ease"
|
||||||
|
data-hide-arrows="false" data-disable-swipe="false" data-autoplay-limit="0" data-nav-spacing="3"
|
||||||
|
data-duration="500" data-infinite="true">
|
||||||
|
<div className="mask w-slider-mask">
|
||||||
|
<div className="slide w-slide">
|
||||||
|
<div className="w-layout-vflex flex-block-100">
|
||||||
|
<div className="div-block-35">
|
||||||
|
<img src="/images/imgfb.png" loading="lazy"
|
||||||
|
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||||
|
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w"
|
||||||
|
alt="Автозапчасти ProteK"
|
||||||
|
className="image-21" />
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-vflex flex-block-99">
|
||||||
|
<h2 className="heading-17">ШИРОКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
||||||
|
<div className="text-block-51">
|
||||||
|
Сотрудничаем только с проверенными поставщиками. Постоянно обновляем
|
||||||
|
ассортимент, чтобы предложить самые лучшие и актуальные детали.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-101">
|
||||||
|
<div className="w-layout-hflex flex-block-102">
|
||||||
|
<img src="/images/1.png" loading="lazy" alt="" className="image-20" />
|
||||||
|
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-102">
|
||||||
|
<img src="/images/2.png" loading="lazy" alt="" className="image-20" />
|
||||||
|
<div className="text-block-52">Высокое качество продукции</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-102">
|
||||||
|
<img src="/images/3.png" loading="lazy" alt="" className="image-20" />
|
||||||
|
<div className="text-block-52">Выгодные цены</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-102">
|
||||||
|
<img src="/images/4.png" loading="lazy" alt="" className="image-20" />
|
||||||
|
<div className="text-block-52">Профессиональная консультация</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="left-arrow w-slider-arrow-left">
|
||||||
|
<div className="div-block-34">
|
||||||
|
<div className="icon-2 w-icon-slider-left"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="right-arrow w-slider-arrow-right">
|
||||||
|
<div className="div-block-34">
|
||||||
|
<div className="icon-2 w-icon-slider-right"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSlide = (banner: HeroBanner) => {
|
||||||
|
const slideContent = (
|
||||||
|
<div className="w-layout-vflex flex-block-100">
|
||||||
|
<div className="div-block-35">
|
||||||
|
<img
|
||||||
|
src={banner.imageUrl}
|
||||||
|
loading="lazy"
|
||||||
|
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
||||||
|
alt={banner.title}
|
||||||
|
className="image-21"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-vflex flex-block-99">
|
||||||
|
<h2 className="heading-17">{banner.title}</h2>
|
||||||
|
{banner.subtitle && (
|
||||||
|
<div className="text-block-51">{banner.subtitle}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если есть ссылка, оборачиваем в Link
|
||||||
|
if (banner.linkUrl) {
|
||||||
|
return (
|
||||||
|
<Link href={banner.linkUrl} className="slide w-slide" style={{ cursor: 'pointer' }}>
|
||||||
|
{slideContent}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="slide w-slide">
|
||||||
|
{slideContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="section-5">
|
<section className="section-5" style={{ overflow: 'hidden' }}>
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div data-delay="4000" data-animation="slide" className="slider w-slider" data-autoplay="false" data-easing="ease"
|
<div
|
||||||
data-hide-arrows="false" data-disable-swipe="false" data-autoplay-limit="0" data-nav-spacing="3"
|
data-delay="4000"
|
||||||
data-duration="500" data-infinite="true">
|
data-animation="slide"
|
||||||
|
className="slider w-slider"
|
||||||
|
data-autoplay="true"
|
||||||
|
data-easing="ease"
|
||||||
|
data-hide-arrows="false"
|
||||||
|
data-disable-swipe="false"
|
||||||
|
data-autoplay-limit="0"
|
||||||
|
data-nav-spacing="3"
|
||||||
|
data-duration="500"
|
||||||
|
data-infinite="true"
|
||||||
|
>
|
||||||
<div className="mask w-slider-mask">
|
<div className="mask w-slider-mask">
|
||||||
<div className="slide w-slide">
|
{banners.map((banner) => (
|
||||||
<div className="w-layout-vflex flex-block-100">
|
<React.Fragment key={banner.id}>
|
||||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
{renderSlide(banner)}
|
||||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
</React.Fragment>
|
||||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
))}
|
||||||
className="image-21" /></div>
|
</div>
|
||||||
<div className="w-layout-vflex flex-block-99">
|
|
||||||
<h2 className="heading-17">ШИРОКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
{/* Показываем стрелки и навигацию только если баннеров больше одного */}
|
||||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
{banners.length > 1 && (
|
||||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
<>
|
||||||
</div>
|
<div className="left-arrow w-slider-arrow-left">
|
||||||
<div className="w-layout-hflex flex-block-101">
|
<div className="div-block-34">
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
<div className="icon-2 w-icon-slider-left"></div>
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Высокое качество продукции</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Выгодные цены</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Профессиональная консультация</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="right-arrow w-slider-arrow-right">
|
||||||
<div className="w-slide">
|
<div className="div-block-34">
|
||||||
<div className="w-layout-vflex flex-block-100">
|
<div className="icon-2 w-icon-slider-right"></div>
|
||||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
|
||||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
|
||||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
|
||||||
className="image-21" /></div>
|
|
||||||
<div className="w-layout-vflex flex-block-99">
|
|
||||||
<h2 className="heading-17">УЗКИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
|
||||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
|
||||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-101">
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Высокое качество продукции</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Выгодные цены</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Профессиональная консультация</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
||||||
<div className="w-slide">
|
</>
|
||||||
<div className="w-layout-vflex flex-block-100">
|
)}
|
||||||
<div className="div-block-35"><img src="/images/imgfb.png" loading="lazy"
|
|
||||||
sizes="(max-width: 767px) 100vw, (max-width: 991px) 728px, 940px"
|
|
||||||
srcSet="/images/imgfb-p-500.png 500w, /images/imgfb-p-800.png 800w, /images/imgfb.png 1027w" alt=""
|
|
||||||
className="image-21" /></div>
|
|
||||||
<div className="w-layout-vflex flex-block-99">
|
|
||||||
<h2 className="heading-17">ЛУЧШИЙ ВЫБОР АВТОЗАПЧАСТЕЙ</h2>
|
|
||||||
<div className="text-block-51">Сотрудничаем только с проверенными поставщиками.Постоянно обновляем
|
|
||||||
ассортимент, чтобы предложить самые лучшие и актуальные детали.</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-101">
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/1.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Быстрая доставка по всей стране</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/2.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Высокое качество продукции</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/3.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Выгодные цены</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-102"><img src="/images/4.png" loading="lazy" alt=""
|
|
||||||
className="image-20" />
|
|
||||||
<div className="text-block-52">Профессиональная консультация</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="left-arrow w-slider-arrow-left">
|
|
||||||
<div className="div-block-34">
|
|
||||||
<div className="icon-2 w-icon-slider-left"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="right-arrow w-slider-arrow-right">
|
|
||||||
<div className="div-block-34">
|
|
||||||
<div className="icon-2 w-icon-slider-right"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
248
src/components/index/ProductOfDayBanner.tsx
Normal file
248
src/components/index/ProductOfDayBanner.tsx
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { GET_HERO_BANNERS } from '@/lib/graphql';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface HeroBanner {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
imageUrl: string;
|
||||||
|
linkUrl?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим CSS для стрелок
|
||||||
|
const arrowStyles = `
|
||||||
|
.pod-slider-arrow {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.pod-slider-arrow-left { left: 12px; }
|
||||||
|
.pod-slider-arrow-right { right: 12px; }
|
||||||
|
.pod-slider-arrow .arrow-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.pod-slider-arrow:hover .arrow-circle,
|
||||||
|
.pod-slider-arrow:focus .arrow-circle {
|
||||||
|
background: #ec1c24;
|
||||||
|
}
|
||||||
|
.pod-slider-arrow .arrow-svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
transition: stroke 0.2s;
|
||||||
|
stroke: #222;
|
||||||
|
}
|
||||||
|
.pod-slider-arrow:hover .arrow-svg,
|
||||||
|
.pod-slider-arrow:focus .arrow-svg {
|
||||||
|
stroke: #fff;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const slideStyles = `
|
||||||
|
.pod-slider-slide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(40px) scale(0.98);
|
||||||
|
transition: opacity 0.5s cubic-bezier(.4,0,.2,1), transform 0.5s cubic-bezier(.4,0,.2,1);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.pod-slider-slide.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.pod-slider-slide.prev {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-40px) scale(0.98);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.pod-slider-slide.next {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(40px) scale(0.98);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.mask.w-slider-mask { position: relative; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProductOfDayBanner: React.FC = () => {
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
const [showArrows, setShowArrows] = useState(false);
|
||||||
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { data } = useQuery(GET_HERO_BANNERS, { errorPolicy: 'all' });
|
||||||
|
|
||||||
|
const banners: HeroBanner[] = data?.heroBanners
|
||||||
|
?.filter((banner: HeroBanner) => banner.isActive)
|
||||||
|
?.slice()
|
||||||
|
?.sort((a: HeroBanner, b: HeroBanner) => a.sortOrder - b.sortOrder) || [];
|
||||||
|
|
||||||
|
const allBanners = banners.length > 0 ? banners : [{
|
||||||
|
id: 'default',
|
||||||
|
title: 'ДОСТАВИМ БЫСТРО!',
|
||||||
|
subtitle: 'Дополнительная скидка на товары с местного склада',
|
||||||
|
imageUrl: '/images/imgfb.png',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
sortOrder: 0
|
||||||
|
}];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allBanners.length > 1) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentSlide(prev => (prev + 1) % allBanners.length);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [allBanners.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSlide >= allBanners.length) {
|
||||||
|
setCurrentSlide(0);
|
||||||
|
}
|
||||||
|
}, [allBanners.length, currentSlide]);
|
||||||
|
|
||||||
|
const handlePrevSlide = () => {
|
||||||
|
setCurrentSlide(prev => prev === 0 ? allBanners.length - 1 : prev - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextSlide = () => {
|
||||||
|
setCurrentSlide(prev => (prev + 1) % allBanners.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSlideIndicator = (index: number) => {
|
||||||
|
setCurrentSlide(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Показывать стрелки при наведении на слайдер или стрелки
|
||||||
|
const handleMouseEnter = () => setShowArrows(true);
|
||||||
|
const handleMouseLeave = () => setShowArrows(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="slider w-slider"
|
||||||
|
ref={sliderRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
>
|
||||||
|
{/* Вставляем стили для стрелок */}
|
||||||
|
<style>{arrowStyles}{slideStyles}</style>
|
||||||
|
<div className="mask w-slider-mask">
|
||||||
|
{allBanners.map((banner, idx) => {
|
||||||
|
let slideClass = 'pod-slider-slide';
|
||||||
|
if (idx === currentSlide) slideClass += ' active';
|
||||||
|
else if (idx === (currentSlide === 0 ? allBanners.length - 1 : currentSlide - 1)) slideClass += ' prev';
|
||||||
|
else if (idx === (currentSlide + 1) % allBanners.length) slideClass += ' next';
|
||||||
|
const slideContent = (
|
||||||
|
<div
|
||||||
|
className="div-block-128"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${banner.imageUrl})`,
|
||||||
|
// backgroundSize: 'cover',
|
||||||
|
// backgroundPosition: 'center',
|
||||||
|
// backgroundRepeat: 'no-repeat',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Можно добавить текст поверх баннера, если нужно */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={slideClass + ' slide w-slide'}
|
||||||
|
key={banner.id}
|
||||||
|
// style={{ display: idx === currentSlide ? 'block' : 'none', position: 'relative' }}
|
||||||
|
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
{banner.linkUrl ? (
|
||||||
|
<Link href={banner.linkUrl} style={{ display: 'block', width: '100%', height: '100%' }}>{slideContent}</Link>
|
||||||
|
) : slideContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* SVG-стрелки как в Webflow, поверх баннера, с hover-эффектом */}
|
||||||
|
<button
|
||||||
|
className="pod-slider-arrow pod-slider-arrow-left"
|
||||||
|
onClick={handlePrevSlide}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
style={{
|
||||||
|
opacity: showArrows ? 1 : 0,
|
||||||
|
pointerEvents: showArrows ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label="Предыдущий баннер"
|
||||||
|
>
|
||||||
|
<span className="arrow-circle">
|
||||||
|
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="pod-slider-arrow pod-slider-arrow-right"
|
||||||
|
onClick={handleNextSlide}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
style={{
|
||||||
|
opacity: showArrows ? 1 : 0,
|
||||||
|
pointerEvents: showArrows ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label="Следующий баннер"
|
||||||
|
>
|
||||||
|
<span className="arrow-circle">
|
||||||
|
<svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round">
|
||||||
|
{allBanners.map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="w-slider-dot"
|
||||||
|
style={{
|
||||||
|
background: idx === currentSlide ? 'white' : 'rgba(255,255,255,0.5)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
margin: 4,
|
||||||
|
display: 'inline-block',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => handleSlideIndicator(idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductOfDayBanner;
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { GET_DAILY_PRODUCTS, PARTS_INDEX_SEARCH_BY_ARTICLE } from '@/lib/graphql';
|
import { GET_DAILY_PRODUCTS, PARTS_INDEX_SEARCH_BY_ARTICLE } from '@/lib/graphql';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import ProductOfDayBanner from './ProductOfDayBanner';
|
||||||
|
|
||||||
interface DailyProduct {
|
interface DailyProduct {
|
||||||
id: string;
|
id: string;
|
||||||
@ -31,7 +32,6 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
|
|
||||||
// Состояние для текущего слайда
|
// Состояние для текущего слайда
|
||||||
const [currentSlide, setCurrentSlide] = useState(0);
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { data, loading, error } = useQuery<{ dailyProducts: DailyProduct[] }>(
|
const { data, loading, error } = useQuery<{ dailyProducts: DailyProduct[] }>(
|
||||||
GET_DAILY_PRODUCTS,
|
GET_DAILY_PRODUCTS,
|
||||||
@ -111,9 +111,9 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчики для слайдера
|
// Обработчики для навигации по товарам дня
|
||||||
const handlePrevSlide = (e: React.MouseEvent) => {
|
const handlePrevSlide = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -158,63 +158,7 @@ const ProductOfDaySection: React.FC = () => {
|
|||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer batd w-container">
|
<div className="w-layout-blockcontainer batd w-container">
|
||||||
<div className="w-layout-hflex flex-block-108">
|
<div className="w-layout-hflex flex-block-108">
|
||||||
<div
|
<ProductOfDayBanner />
|
||||||
ref={sliderRef}
|
|
||||||
className="slider w-slider"
|
|
||||||
>
|
|
||||||
<div className="mask w-slider-mask">
|
|
||||||
{activeProducts.map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`slide w-slide ${index === currentSlide ? 'w--current' : ''}`}
|
|
||||||
style={{
|
|
||||||
display: index === currentSlide ? 'block' : 'none'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="div-block-128"></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Стрелки слайдера (показываем только если товаров больше 1) */}
|
|
||||||
{activeProducts.length > 1 && (
|
|
||||||
<>
|
|
||||||
<div className="left-arrow w-slider-arrow-left">
|
|
||||||
<div className="div-block-34">
|
|
||||||
<div className="code-embed-14 w-embed">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="right-arrow w-slider-arrow-right">
|
|
||||||
<div className="div-block-34 right">
|
|
||||||
<div className="code-embed-14 w-embed">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Индикаторы слайдов */}
|
|
||||||
{activeProducts.length > 1 && (
|
|
||||||
<div className="slide-nav w-slider-nav w-slider-nav-invert w-round">
|
|
||||||
{activeProducts.map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`w-slider-dot ${index === currentSlide ? 'w--current' : ''}`}
|
|
||||||
onClick={() => handleSlideIndicator(index)}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
style={{ cursor: 'pointer', zIndex: 10 }}
|
|
||||||
></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="div-block-129">
|
<div className="div-block-129">
|
||||||
<div className="w-layout-hflex flex-block-109">
|
<div className="w-layout-hflex flex-block-109">
|
||||||
|
@ -284,7 +284,7 @@ const ProfileHistoryMain = () => {
|
|||||||
|
|
||||||
if (loading && historyItems.length === 0) {
|
if (loading && historyItems.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center text-base min-h-[526px] h-full">
|
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px] h-full">
|
||||||
<div className="flex justify-center items-center h-40">
|
<div className="flex justify-center items-center h-40">
|
||||||
<div className="text-gray-500">Загрузка истории поиска...</div>
|
<div className="text-gray-500">Загрузка истории поиска...</div>
|
||||||
</div>
|
</div>
|
||||||
@ -294,7 +294,7 @@ const ProfileHistoryMain = () => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center text-base min-h-[526px]">
|
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px]">
|
||||||
<div className="flex justify-center items-center h-40">
|
<div className="flex justify-center items-center h-40">
|
||||||
<div className="text-red-500">Ошибка загрузки истории поиска</div>
|
<div className="text-red-500">Ошибка загрузки истории поиска</div>
|
||||||
</div>
|
</div>
|
||||||
@ -303,7 +303,7 @@ const ProfileHistoryMain = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-[526px]">
|
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px]">
|
||||||
<div className="flex gap-5 items-center px-8 py-3 w-full leading-snug text-gray-400 whitespace-nowrap bg-white rounded-lg max-md:px-5 max-md:max-w-full">
|
<div className="flex gap-5 items-center px-8 py-3 w-full leading-snug text-gray-400 whitespace-nowrap bg-white rounded-lg max-md:px-5 max-md:max-w-full">
|
||||||
<div className="flex-1 shrink self-stretch my-auto text-gray-400 basis-0 text-ellipsis max-md:max-w-full max-md:w-full">
|
<div className="flex-1 shrink self-stretch my-auto text-gray-400 basis-0 text-ellipsis max-md:max-w-full max-md:w-full">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
@ -404,16 +404,19 @@ const KnotIn: React.FC<KnotInProps> = ({
|
|||||||
onClick={() => setIsImageModalOpen(false)}
|
onClick={() => setIsImageModalOpen(false)}
|
||||||
style={{ cursor: 'zoom-out' }}
|
style={{ cursor: 'zoom-out' }}
|
||||||
>
|
>
|
||||||
<img
|
<div className="relative">
|
||||||
src={imageUrl}
|
<img
|
||||||
alt={unitName || unitInfo?.name || "Изображение узла"}
|
src={imageUrl}
|
||||||
className="max-h-[90vh] max-w-[90vw] rounded shadow-lg"
|
alt={unitName || unitInfo?.name || "Изображение узла"}
|
||||||
onClick={e => e.stopPropagation()}
|
className="max-h-[90vh] max-w-[90vw] rounded shadow-lg"
|
||||||
style={{ background: '#fff' }}
|
onClick={e => e.stopPropagation()}
|
||||||
/>
|
style={{ background: '#fff' }}
|
||||||
|
/>
|
||||||
|
{/* Убираем интерактивные точки в модальном окне */}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsImageModalOpen(false)}
|
onClick={() => setIsImageModalOpen(false)}
|
||||||
className="absolute top-4 right-4 text-white text-3xl font-bold bg-black bg-opacity-40 rounded-full w-10 h-10 flex items-center justify-center"
|
className="absolute top-4 right-4 text-white text-3xl font-bold bg-black bg-opacity-40 rounded-full w-10 h-10 flex items-center justify-center hover:bg-black hover:bg-opacity-60 transition-colors"
|
||||||
aria-label="Закрыть"
|
aria-label="Закрыть"
|
||||||
style={{ zIndex: 10000 }}
|
style={{ zIndex: 10000 }}
|
||||||
>
|
>
|
||||||
|
@ -36,7 +36,9 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
||||||
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
const [tooltipPart, setTooltipPart] = useState<any>(null);
|
||||||
|
const [clickedPart, setClickedPart] = useState<string | number | null>(null);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Отладочные логи для проверки данных
|
// Отладочные логи для проверки данных
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -63,8 +65,31 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
|
|
||||||
// Обработчик клика по детали в списке
|
// Обработчик клика по детали в списке
|
||||||
const handlePartClick = (part: any) => {
|
const handlePartClick = (part: any) => {
|
||||||
if (part.codeonimage && onPartSelect) {
|
const codeOnImage = part.codeonimage || part.detailid;
|
||||||
onPartSelect(part.codeonimage);
|
if (codeOnImage && onPartSelect) {
|
||||||
|
onPartSelect(codeOnImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Также подсвечиваем деталь на схеме при клике
|
||||||
|
if (codeOnImage && onPartHover) {
|
||||||
|
// Очищаем предыдущий таймер, если он есть
|
||||||
|
if (clickTimeoutRef.current) {
|
||||||
|
clearTimeout(clickTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем состояние кликнутой детали
|
||||||
|
setClickedPart(codeOnImage);
|
||||||
|
|
||||||
|
// Подсвечиваем на схеме
|
||||||
|
onPartHover(codeOnImage);
|
||||||
|
|
||||||
|
// Убираем подсветку через интервал
|
||||||
|
clickTimeoutRef.current = setTimeout(() => {
|
||||||
|
setClickedPart(null);
|
||||||
|
if (onPartHover) {
|
||||||
|
onPartHover(null);
|
||||||
|
}
|
||||||
|
}, 1500); // Подсветка будет видна 1.5 секунды
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,6 +175,9 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (clickTimeoutRef.current) {
|
||||||
|
clearTimeout(clickTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -213,12 +241,17 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
|
|
||||||
<div className="knot-parts">
|
<div className="knot-parts">
|
||||||
{parts.map((part, idx) => {
|
{parts.map((part, idx) => {
|
||||||
|
const codeOnImage = part.codeonimage || part.detailid;
|
||||||
const isHighlighted = highlightedCodeOnImage !== null && highlightedCodeOnImage !== undefined && (
|
const isHighlighted = highlightedCodeOnImage !== null && highlightedCodeOnImage !== undefined && (
|
||||||
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage.toString()) ||
|
(part.codeonimage && part.codeonimage.toString() === highlightedCodeOnImage.toString()) ||
|
||||||
(part.detailid && part.detailid.toString() === highlightedCodeOnImage.toString())
|
(part.detailid && part.detailid.toString() === highlightedCodeOnImage.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelected = selectedParts.has(part.detailid || part.codeonimage || idx.toString());
|
const isSelected = selectedParts.has(part.detailid || part.codeonimage || idx.toString());
|
||||||
|
const isClicked = clickedPart !== null && (
|
||||||
|
(part.codeonimage && part.codeonimage.toString() === clickedPart.toString()) ||
|
||||||
|
(part.detailid && part.detailid.toString() === clickedPart.toString())
|
||||||
|
);
|
||||||
|
|
||||||
// Создаем уникальный ключ
|
// Создаем уникальный ключ
|
||||||
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
const uniqueKey = `part-${idx}-${part.detailid || part.oem || part.name || 'unknown'}`;
|
||||||
@ -226,12 +259,14 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={uniqueKey}
|
key={uniqueKey}
|
||||||
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-colors ${
|
className={`w-layout-hflex knotlistitem rounded-lg cursor-pointer transition-all duration-300 ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-green-100 border-green-500'
|
? 'bg-green-100 border-green-500'
|
||||||
: isHighlighted
|
: isClicked
|
||||||
? 'bg-slate-200'
|
? 'bg-red-100 border-red-400 shadow-md'
|
||||||
: 'bg-white border-gray-200 hover:border-gray-300'
|
: isHighlighted
|
||||||
|
? 'bg-slate-200'
|
||||||
|
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handlePartClick(part)}
|
onClick={() => handlePartClick(part)}
|
||||||
onMouseEnter={() => handlePartMouseEnter(part)}
|
onMouseEnter={() => handlePartMouseEnter(part)}
|
||||||
@ -240,13 +275,37 @@ const KnotParts: React.FC<KnotPartsProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="w-layout-hflex flex-block-116">
|
<div className="w-layout-hflex flex-block-116">
|
||||||
<div
|
<div
|
||||||
className={`nuberlist ${isSelected ? 'text-green-700 font-bold' : isHighlighted ? ' font-bold' : ''}`}
|
className={`nuberlist ${
|
||||||
|
isSelected
|
||||||
|
? 'text-green-700 font-bold'
|
||||||
|
: isClicked
|
||||||
|
? 'text-red-700 font-bold'
|
||||||
|
: isHighlighted
|
||||||
|
? 'font-bold'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{part.codeonimage || idx + 1}
|
{part.codeonimage || idx + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className={`oemnuber ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>{part.oem}</div>
|
<div className={`oemnuber ${
|
||||||
|
isSelected
|
||||||
|
? 'text-green-800 font-semibold'
|
||||||
|
: isClicked
|
||||||
|
? 'text-red-800 font-semibold'
|
||||||
|
: isHighlighted
|
||||||
|
? 'font-semibold'
|
||||||
|
: ''
|
||||||
|
}`}>{part.oem}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`partsname ${isSelected ? 'text-green-800 font-semibold' : isHighlighted ? ' font-semibold' : ''}`}>
|
<div className={`partsname ${
|
||||||
|
isSelected
|
||||||
|
? 'text-green-800 font-semibold'
|
||||||
|
: isClicked
|
||||||
|
? 'text-red-800 font-semibold'
|
||||||
|
: isHighlighted
|
||||||
|
? 'font-semibold'
|
||||||
|
: ''
|
||||||
|
}`}>
|
||||||
{part.name}
|
{part.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-117">
|
<div className="w-layout-hflex flex-block-117">
|
||||||
|
@ -4,7 +4,7 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } fr
|
|||||||
import { useMutation, useQuery } from '@apollo/client'
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries'
|
import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries'
|
||||||
import DeleteCartIcon from '@/components/DeleteCartIcon'
|
import CloseIcon from '@/components/CloseIcon'
|
||||||
|
|
||||||
// Типы
|
// Типы
|
||||||
export interface FavoriteItem {
|
export interface FavoriteItem {
|
||||||
@ -135,7 +135,7 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
|
|||||||
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast('Товар удален из избранного', {
|
toast('Товар удален из избранного', {
|
||||||
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
icon: <CloseIcon size={20} color="#fff" />,
|
||||||
style: {
|
style: {
|
||||||
background: '#6b7280', // Серый фон
|
background: '#6b7280', // Серый фон
|
||||||
color: '#fff', // Белый текст
|
color: '#fff', // Белый текст
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useLazyQuery } from '@apollo/client';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
import { SEARCH_PRODUCT_OFFERS } from '@/lib/graphql';
|
import { SEARCH_PRODUCT_OFFERS } from '@/lib/graphql';
|
||||||
|
|
||||||
@ -33,17 +33,19 @@ interface ProductPriceVariables {
|
|||||||
brand: string;
|
brand: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProductPrices = (products: Array<{ code: string; brand: string; id: string }>) => {
|
export const useProductPrices = () => {
|
||||||
const [pricesMap, setPricesMap] = useState<Map<string, ProductOffer | null>>(new Map());
|
const [pricesMap, setPricesMap] = useState<Map<string, ProductOffer | null>>(new Map());
|
||||||
const [loadingPrices, setLoadingPrices] = useState<Set<string>>(new Set());
|
const [loadingPrices, setLoadingPrices] = useState<Set<string>>(new Set());
|
||||||
|
const [loadedPrices, setLoadedPrices] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const [searchOffers] = useLazyQuery<ProductPriceData, ProductPriceVariables>(SEARCH_PRODUCT_OFFERS);
|
const [searchOffers] = useLazyQuery<ProductPriceData, ProductPriceVariables>(SEARCH_PRODUCT_OFFERS);
|
||||||
|
|
||||||
const loadPrice = async (product: { code: string; brand: string; id: string }) => {
|
const loadPrice = useCallback(async (product: { code: string; brand: string; id: string }) => {
|
||||||
const key = `${product.id}_${product.code}_${product.brand}`;
|
const key = `${product.id}_${product.code}_${product.brand}`;
|
||||||
|
|
||||||
if (pricesMap.has(key) || loadingPrices.has(key)) {
|
// Если уже загружено или загружается - не делаем повторный запрос
|
||||||
return; // Уже загружено или загружается
|
if (loadedPrices.has(key) || loadingPrices.has(key)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('💰 Загружаем цену для:', product.code, product.brand);
|
console.log('💰 Загружаем цену для:', product.code, product.brand);
|
||||||
@ -87,35 +89,31 @@ export const useProductPrices = (products: Array<{ code: string; brand: string;
|
|||||||
newSet.delete(key);
|
newSet.delete(key);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
|
setLoadedPrices(prev => new Set([...prev, key]));
|
||||||
}
|
}
|
||||||
};
|
}, [searchOffers, loadedPrices, loadingPrices]);
|
||||||
|
|
||||||
useEffect(() => {
|
const getPrice = useCallback((product: { code: string; brand: string; id: string }) => {
|
||||||
// Загружаем цены для всех товаров с небольшой задержкой между запросами
|
|
||||||
products.forEach((product, index) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
loadPrice(product);
|
|
||||||
}, index * 100); // Задержка 100мс между запросами
|
|
||||||
});
|
|
||||||
}, [products]);
|
|
||||||
|
|
||||||
const getPrice = (product: { code: string; brand: string; id: string }) => {
|
|
||||||
const key = `${product.id}_${product.code}_${product.brand}`;
|
const key = `${product.id}_${product.code}_${product.brand}`;
|
||||||
return pricesMap.get(key);
|
return pricesMap.get(key);
|
||||||
};
|
}, [pricesMap]);
|
||||||
|
|
||||||
const isLoadingPrice = (product: { code: string; brand: string; id: string }) => {
|
const isLoadingPrice = useCallback((product: { code: string; brand: string; id: string }) => {
|
||||||
const key = `${product.id}_${product.code}_${product.brand}`;
|
const key = `${product.id}_${product.code}_${product.brand}`;
|
||||||
return loadingPrices.has(key);
|
return loadingPrices.has(key);
|
||||||
};
|
}, [loadingPrices]);
|
||||||
|
|
||||||
const loadPriceOnDemand = (product: { code: string; brand: string; id: string }) => {
|
const ensurePriceLoaded = useCallback((product: { code: string; brand: string; id: string }) => {
|
||||||
loadPrice(product);
|
const key = `${product.id}_${product.code}_${product.brand}`;
|
||||||
};
|
if (!loadedPrices.has(key) && !loadingPrices.has(key)) {
|
||||||
|
loadPrice(product);
|
||||||
|
}
|
||||||
|
}, [loadPrice, loadedPrices, loadingPrices]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPrice,
|
getPrice,
|
||||||
isLoadingPrice,
|
isLoadingPrice,
|
||||||
loadPriceOnDemand
|
loadPrice,
|
||||||
|
ensurePriceLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -45,6 +45,20 @@ export const GET_TOP_SALES_PRODUCTS = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const GET_HERO_BANNERS = gql`
|
||||||
|
query GetHeroBanners {
|
||||||
|
heroBanners {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
subtitle
|
||||||
|
imageUrl
|
||||||
|
linkUrl
|
||||||
|
isActive
|
||||||
|
sortOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const CHECK_CLIENT_BY_PHONE = gql`
|
export const CHECK_CLIENT_BY_PHONE = gql`
|
||||||
mutation CheckClientByPhone($phone: String!) {
|
mutation CheckClientByPhone($phone: String!) {
|
||||||
checkClientByPhone(phone: $phone) {
|
checkClientByPhone(phone: $phone) {
|
||||||
|
@ -24,6 +24,20 @@ export const GET_PARTS_SEARCH_HISTORY = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Запрос для получения последних поисковых запросов для автодополнения
|
||||||
|
export const GET_RECENT_SEARCH_QUERIES = gql`
|
||||||
|
query GetRecentSearchQueries($limit: Int = 5) {
|
||||||
|
partsSearchHistory(limit: $limit, offset: 0) {
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
searchQuery
|
||||||
|
searchType
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_SEARCH_HISTORY_ITEM = gql`
|
export const DELETE_SEARCH_HISTORY_ITEM = gql`
|
||||||
mutation DeletePartsSearchHistoryItem($id: ID!) {
|
mutation DeletePartsSearchHistoryItem($id: ID!) {
|
||||||
deletePartsSearchHistoryItem(id: $id)
|
deletePartsSearchHistoryItem(id: $id)
|
||||||
|
@ -92,4 +92,14 @@ export const memoize = <T extends (...args: any[]) => any>(
|
|||||||
// Очистка кэша мемоизации
|
// Очистка кэша мемоизации
|
||||||
export const clearMemoCache = () => {
|
export const clearMemoCache = () => {
|
||||||
memoCache.clear();
|
memoCache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверка, является ли строка датой доставки
|
||||||
|
export const isDeliveryDate = (dateString: string): boolean => {
|
||||||
|
const months = [
|
||||||
|
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||||
|
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||||
|
];
|
||||||
|
|
||||||
|
return months.some(month => dateString.includes(month));
|
||||||
};
|
};
|
@ -52,18 +52,20 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-right"
|
position="top-center"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
style: {
|
style: {
|
||||||
background: '#363636',
|
background: '#363636',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
|
marginTop: '80px', // Отступ сверху, чтобы не закрывать кнопки меню
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
style: {
|
style: {
|
||||||
background: '#22c55e', // Зеленый фон для успешных уведомлений
|
background: '#22c55e', // Зеленый фон для успешных уведомлений
|
||||||
color: '#fff', // Белый текст
|
color: '#fff', // Белый текст
|
||||||
|
marginTop: '80px', // Отступ сверху для успешных уведомлений
|
||||||
},
|
},
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: '#22c55e',
|
primary: '#22c55e',
|
||||||
@ -72,6 +74,9 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
|
style: {
|
||||||
|
marginTop: '80px', // Отступ сверху для ошибок
|
||||||
|
},
|
||||||
iconTheme: {
|
iconTheme: {
|
||||||
primary: '#ef4444',
|
primary: '#ef4444',
|
||||||
secondary: '#fff',
|
secondary: '#fff',
|
||||||
|
@ -38,7 +38,8 @@ const mockData = Array(12).fill({
|
|||||||
brand: "Borsehung",
|
brand: "Borsehung",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 50; // Целевое количество товаров на странице
|
||||||
|
const PARTSINDEX_PAGE_SIZE = 25; // Размер страницы PartsIndex API (фиксированный)
|
||||||
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
|
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
|
||||||
|
|
||||||
export default function Catalog() {
|
export default function Catalog() {
|
||||||
@ -72,6 +73,13 @@ export default function Catalog() {
|
|||||||
const [showEmptyState, setShowEmptyState] = useState(false);
|
const [showEmptyState, setShowEmptyState] = useState(false);
|
||||||
const [partsIndexPage, setPartsIndexPage] = useState(1); // Текущая страница для PartsIndex
|
const [partsIndexPage, setPartsIndexPage] = useState(1); // Текущая страница для PartsIndex
|
||||||
const [totalPages, setTotalPages] = useState(1); // Общее количество страниц
|
const [totalPages, setTotalPages] = useState(1); // Общее количество страниц
|
||||||
|
|
||||||
|
// Новые состояния для логики автоподгрузки PartsIndex
|
||||||
|
const [accumulatedEntities, setAccumulatedEntities] = useState<PartsIndexEntity[]>([]); // Все накопленные товары
|
||||||
|
const [entitiesWithOffers, setEntitiesWithOffers] = useState<PartsIndexEntity[]>([]); // Товары с предложениями
|
||||||
|
const [isAutoLoading, setIsAutoLoading] = useState(false); // Автоматическая подгрузка в процессе
|
||||||
|
const [currentUserPage, setCurrentUserPage] = useState(1); // Текущая пользовательская страница
|
||||||
|
const [entitiesCache, setEntitiesCache] = useState<Map<number, PartsIndexEntity[]>>(new Map()); // Кэш страниц
|
||||||
|
|
||||||
// Карта видимости товаров по индексу
|
// Карта видимости товаров по индексу
|
||||||
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
|
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
|
||||||
@ -108,7 +116,8 @@ export default function Catalog() {
|
|||||||
categoryName,
|
categoryName,
|
||||||
isPartsAPIMode,
|
isPartsAPIMode,
|
||||||
isPartsIndexMode,
|
isPartsIndexMode,
|
||||||
isPartsIndexCatalogOnly
|
isPartsIndexCatalogOnly,
|
||||||
|
'router.query': router.query
|
||||||
});
|
});
|
||||||
|
|
||||||
// Загружаем артикулы PartsAPI
|
// Загружаем артикулы PartsAPI
|
||||||
@ -135,7 +144,7 @@ export default function Catalog() {
|
|||||||
catalogId: catalogId as string,
|
catalogId: catalogId as string,
|
||||||
groupId: groupId as string,
|
groupId: groupId as string,
|
||||||
lang: 'ru',
|
lang: 'ru',
|
||||||
limit: ITEMS_PER_PAGE,
|
limit: PARTSINDEX_PAGE_SIZE,
|
||||||
page: partsIndexPage,
|
page: partsIndexPage,
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: undefined // Будем обновлять через refetch
|
params: undefined // Будем обновлять через refetch
|
||||||
@ -164,12 +173,24 @@ export default function Catalog() {
|
|||||||
// allEntities больше не используется - используем allLoadedEntities
|
// allEntities больше не используется - используем allLoadedEntities
|
||||||
|
|
||||||
// Хук для загрузки цен товаров PartsIndex
|
// Хук для загрузки цен товаров PartsIndex
|
||||||
const productsForPrices = visibleEntities.map(entity => ({
|
const { getPrice, isLoadingPrice, ensurePriceLoaded } = useProductPrices();
|
||||||
id: entity.id,
|
|
||||||
code: entity.code,
|
// Загружаем цены для видимых товаров PartsIndex
|
||||||
brand: entity.brand.name
|
useEffect(() => {
|
||||||
}));
|
if (isPartsIndexMode && visibleEntities.length > 0) {
|
||||||
const { getPrice, isLoadingPrice, loadPriceOnDemand } = useProductPrices(productsForPrices);
|
visibleEntities.forEach((entity, index) => {
|
||||||
|
const productForPrice = {
|
||||||
|
id: entity.id,
|
||||||
|
code: entity.code,
|
||||||
|
brand: entity.brand.name
|
||||||
|
};
|
||||||
|
// Загружаем с небольшой задержкой чтобы не перегружать сервер
|
||||||
|
setTimeout(() => {
|
||||||
|
ensurePriceLoaded(productForPrice);
|
||||||
|
}, index * 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isPartsIndexMode, visibleEntities, ensurePriceLoaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (articlesData?.partsAPIArticles) {
|
if (articlesData?.partsAPIArticles) {
|
||||||
@ -193,9 +214,6 @@ export default function Catalog() {
|
|||||||
const newEntities = entitiesData.partsIndexCatalogEntities.list;
|
const newEntities = entitiesData.partsIndexCatalogEntities.list;
|
||||||
const pagination = entitiesData.partsIndexCatalogEntities.pagination;
|
const pagination = entitiesData.partsIndexCatalogEntities.pagination;
|
||||||
|
|
||||||
// Обновляем список товаров
|
|
||||||
setVisibleEntities(newEntities);
|
|
||||||
|
|
||||||
// Обновляем информацию о пагинации
|
// Обновляем информацию о пагинации
|
||||||
const currentPage = pagination?.page?.current || 1;
|
const currentPage = pagination?.page?.current || 1;
|
||||||
const hasNext = pagination?.page?.next !== null;
|
const hasNext = pagination?.page?.next !== null;
|
||||||
@ -204,6 +222,20 @@ export default function Catalog() {
|
|||||||
setPartsIndexPage(currentPage);
|
setPartsIndexPage(currentPage);
|
||||||
setHasMoreEntities(hasNext);
|
setHasMoreEntities(hasNext);
|
||||||
|
|
||||||
|
// Сохраняем в кэш
|
||||||
|
setEntitiesCache(prev => new Map(prev).set(currentPage, newEntities));
|
||||||
|
|
||||||
|
// Если это первая страница или сброс, заменяем накопленные товары
|
||||||
|
if (currentPage === 1) {
|
||||||
|
setAccumulatedEntities(newEntities);
|
||||||
|
// Устанавливаем visibleEntities сразу, не дожидаясь проверки цен
|
||||||
|
setVisibleEntities(newEntities);
|
||||||
|
console.log('✅ Установлены visibleEntities для первой страницы:', newEntities.length);
|
||||||
|
} else {
|
||||||
|
// Добавляем к накопленным товарам
|
||||||
|
setAccumulatedEntities(prev => [...prev, ...newEntities]);
|
||||||
|
}
|
||||||
|
|
||||||
// Вычисляем общее количество страниц (приблизительно)
|
// Вычисляем общее количество страниц (приблизительно)
|
||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
setTotalPages(currentPage + 1); // Минимум еще одна страница
|
setTotalPages(currentPage + 1); // Минимум еще одна страница
|
||||||
@ -216,7 +248,7 @@ export default function Catalog() {
|
|||||||
}, [entitiesData]);
|
}, [entitiesData]);
|
||||||
|
|
||||||
// Преобразование выбранных фильтров в формат PartsIndex API
|
// Преобразование выбранных фильтров в формат PartsIndex API
|
||||||
const convertFiltersToPartsIndexParams = useCallback((): Record<string, any> => {
|
const convertFiltersToPartsIndexParams = useMemo((): Record<string, any> => {
|
||||||
if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) {
|
if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -241,6 +273,84 @@ export default function Catalog() {
|
|||||||
return apiParams;
|
return apiParams;
|
||||||
}, [paramsData, selectedFilters]);
|
}, [paramsData, selectedFilters]);
|
||||||
|
|
||||||
|
// Функция автоматической подгрузки дополнительных страниц PartsIndex
|
||||||
|
const autoLoadMoreEntities = useCallback(async () => {
|
||||||
|
if (isAutoLoading || !hasMoreEntities || !isPartsIndexMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Автоподгрузка: проверяем товары с предложениями...');
|
||||||
|
|
||||||
|
// Восстанавливаем автоподгрузку
|
||||||
|
console.log('🔄 Автоподгрузка активна');
|
||||||
|
|
||||||
|
// Подсчитываем текущее количество товаров с предложениями
|
||||||
|
const currentEntitiesWithOffers = accumulatedEntities.filter(entity => {
|
||||||
|
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||||
|
const priceData = getPrice(productForPrice);
|
||||||
|
const isLoadingPriceData = isLoadingPrice(productForPrice);
|
||||||
|
// Товар считается "с предложениями" если у него есть реальная цена (не null и не undefined)
|
||||||
|
return (priceData && priceData.price && priceData.price > 0) || isLoadingPriceData;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📊 Автоподгрузка: текущее состояние:', {
|
||||||
|
накопленоТоваров: accumulatedEntities.length,
|
||||||
|
сПредложениями: currentEntitiesWithOffers.length,
|
||||||
|
целевоеКоличество: ITEMS_PER_PAGE,
|
||||||
|
естьЕщеТовары: hasMoreEntities
|
||||||
|
});
|
||||||
|
|
||||||
|
// Если у нас уже достаточно товаров с предложениями, не загружаем
|
||||||
|
if (currentEntitiesWithOffers.length >= ITEMS_PER_PAGE) {
|
||||||
|
console.log('✅ Автоподгрузка: достаточно товаров с предложениями');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Даем время на загрузку цен товаров, если их слишком много загружается
|
||||||
|
const loadingCount = accumulatedEntities.filter(entity => {
|
||||||
|
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||||
|
return isLoadingPrice(productForPrice);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
// Ждем только если загружается больше 5 товаров одновременно
|
||||||
|
if (loadingCount > 5) {
|
||||||
|
console.log('⏳ Автоподгрузка: ждем загрузки цен для', loadingCount, 'товаров (больше 5)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если накопили уже много товаров, но мало с предложениями - прекращаем попытки
|
||||||
|
if (accumulatedEntities.length >= ITEMS_PER_PAGE * 8) { // Увеличили лимит с 4 до 8 страниц
|
||||||
|
console.log('⚠️ Автоподгрузка: достигли лимита попыток, прекращаем');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAutoLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔄 Автоподгрузка: загружаем следующую страницу PartsIndex...');
|
||||||
|
|
||||||
|
const apiParams = convertFiltersToPartsIndexParams;
|
||||||
|
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
||||||
|
|
||||||
|
const result = await refetchEntities({
|
||||||
|
catalogId: catalogId as string,
|
||||||
|
groupId: groupId as string,
|
||||||
|
lang: 'ru',
|
||||||
|
limit: PARTSINDEX_PAGE_SIZE,
|
||||||
|
page: partsIndexPage + 1,
|
||||||
|
q: searchQuery || undefined,
|
||||||
|
params: paramsString
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Автоподгрузка: страница загружена, результат:', result.data?.partsIndexCatalogEntities?.list?.length || 0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Автоподгрузка: ошибка загрузки следующей страницы:', error);
|
||||||
|
} finally {
|
||||||
|
setIsAutoLoading(false);
|
||||||
|
}
|
||||||
|
}, [isAutoLoading, hasMoreEntities, isPartsIndexMode, accumulatedEntities.length, partsIndexPage, refetchEntities, catalogId, groupId, searchQuery]);
|
||||||
|
|
||||||
// Генерация фильтров для PartsIndex на основе параметров API
|
// Генерация фильтров для PartsIndex на основе параметров API
|
||||||
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
||||||
if (!paramsData?.partsIndexCatalogParams?.list) {
|
if (!paramsData?.partsIndexCatalogParams?.list) {
|
||||||
@ -292,6 +402,114 @@ export default function Catalog() {
|
|||||||
}
|
}
|
||||||
}, [isPartsIndexMode, generatePartsIndexFilters, paramsLoading]);
|
}, [isPartsIndexMode, generatePartsIndexFilters, paramsLoading]);
|
||||||
|
|
||||||
|
// Автоматическая подгрузка товаров с задержкой для загрузки цен
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPartsIndexMode || accumulatedEntities.length === 0 || isAutoLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Даем время на загрузку цен (3 секунды после последнего изменения)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
autoLoadMoreEntities();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isPartsIndexMode, accumulatedEntities.length, isAutoLoading]);
|
||||||
|
|
||||||
|
// Дополнительный триггер автоподгрузки при изменении количества товаров с предложениями
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🔍 Проверка триггера автоподгрузки:', {
|
||||||
|
isPartsIndexMode,
|
||||||
|
entitiesWithOffersLength: entitiesWithOffers.length,
|
||||||
|
isAutoLoading,
|
||||||
|
hasMoreEntities,
|
||||||
|
targetItemsPerPage: ITEMS_PER_PAGE
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isPartsIndexMode || entitiesWithOffers.length === 0 || isAutoLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если товаров с предложениями мало, запускаем автоподгрузку через 1 секунду
|
||||||
|
if (entitiesWithOffers.length < ITEMS_PER_PAGE && hasMoreEntities) {
|
||||||
|
console.log('🚀 Запускаем автоподгрузку: товаров', entitiesWithOffers.length, 'из', ITEMS_PER_PAGE);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.log('🚀 Дополнительная автоподгрузка: недостаточно товаров с предложениями');
|
||||||
|
autoLoadMoreEntities();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Автоподгрузка не нужна: товаров достаточно или нет больше данных');
|
||||||
|
}
|
||||||
|
}, [isPartsIndexMode, entitiesWithOffers.length, hasMoreEntities, isAutoLoading]);
|
||||||
|
|
||||||
|
// Обновляем список товаров с предложениями при изменении накопленных товаров или цен
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPartsIndexMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем все товары, но отдельно считаем те, у которых есть цены
|
||||||
|
const entitiesWithOffers = accumulatedEntities;
|
||||||
|
|
||||||
|
// Подсчитываем количество товаров с реальными ценами для автоподгрузки
|
||||||
|
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
|
||||||
|
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||||
|
const priceData = getPrice(productForPrice);
|
||||||
|
return priceData && priceData.price && priceData.price > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📊 Обновляем entitiesWithOffers:', {
|
||||||
|
накопленоТоваров: accumulatedEntities.length,
|
||||||
|
отображаемыхТоваров: entitiesWithOffers.length,
|
||||||
|
сРеальнымиЦенами: entitiesWithRealPrices.length,
|
||||||
|
целевоеКоличество: ITEMS_PER_PAGE
|
||||||
|
});
|
||||||
|
|
||||||
|
setEntitiesWithOffers(entitiesWithOffers);
|
||||||
|
|
||||||
|
// Показываем товары для текущей пользовательской страницы
|
||||||
|
const startIndex = (currentUserPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||||
|
const visibleForCurrentPage = entitiesWithOffers.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
console.log('📊 Обновляем visibleEntities:', {
|
||||||
|
currentUserPage,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
visibleForCurrentPage: visibleForCurrentPage.length,
|
||||||
|
entitiesWithOffers: entitiesWithOffers.length
|
||||||
|
});
|
||||||
|
|
||||||
|
setVisibleEntities(visibleForCurrentPage);
|
||||||
|
|
||||||
|
}, [isPartsIndexMode, accumulatedEntities, currentUserPage]);
|
||||||
|
|
||||||
|
// Отдельный useEffect для обновления статистики цен (без влияния на visibleEntities)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPartsIndexMode || accumulatedEntities.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статистику каждые 2 секунды
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
|
||||||
|
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||||
|
const priceData = getPrice(productForPrice);
|
||||||
|
return priceData && priceData.price && priceData.price > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('💰 Обновление статистики цен:', {
|
||||||
|
накопленоТоваров: accumulatedEntities.length,
|
||||||
|
сРеальнымиЦенами: entitiesWithRealPrices.length,
|
||||||
|
процентЗагрузки: Math.round((entitiesWithRealPrices.length / accumulatedEntities.length) * 100)
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isPartsIndexMode, accumulatedEntities.length, getPrice]);
|
||||||
|
|
||||||
// Генерируем динамические фильтры для PartsAPI
|
// Генерируем динамические фильтры для PartsAPI
|
||||||
const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
|
const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
|
||||||
if (!allArticles.length) return [];
|
if (!allArticles.length) return [];
|
||||||
@ -431,9 +649,6 @@ export default function Catalog() {
|
|||||||
});
|
});
|
||||||
}, [allArticles, searchQuery, selectedFilters]);
|
}, [allArticles, searchQuery, selectedFilters]);
|
||||||
|
|
||||||
// Упрощенная логика - показываем все загруженные товары без клиентской фильтрации
|
|
||||||
const filteredEntities = visibleEntities;
|
|
||||||
|
|
||||||
// Обновляем видимые артикулы при изменении поиска или фильтров для PartsAPI
|
// Обновляем видимые артикулы при изменении поиска или фильтров для PartsAPI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPartsAPIMode) {
|
if (isPartsAPIMode) {
|
||||||
@ -458,10 +673,14 @@ export default function Catalog() {
|
|||||||
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
||||||
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
||||||
setPartsIndexPage(1);
|
setPartsIndexPage(1);
|
||||||
|
setCurrentUserPage(1);
|
||||||
setHasMoreEntities(true);
|
setHasMoreEntities(true);
|
||||||
|
setAccumulatedEntities([]);
|
||||||
|
setEntitiesWithOffers([]);
|
||||||
|
setEntitiesCache(new Map());
|
||||||
|
|
||||||
// Перезагружаем данные с новыми параметрами фильтрации
|
// Перезагружаем данные с новыми параметрами фильтрации
|
||||||
const apiParams = convertFiltersToPartsIndexParams();
|
const apiParams = convertFiltersToPartsIndexParams;
|
||||||
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
||||||
|
|
||||||
// Также обновляем параметры фильтрации
|
// Также обновляем параметры фильтрации
|
||||||
@ -477,7 +696,7 @@ export default function Catalog() {
|
|||||||
catalogId: catalogId as string,
|
catalogId: catalogId as string,
|
||||||
groupId: groupId as string,
|
groupId: groupId as string,
|
||||||
lang: 'ru',
|
lang: 'ru',
|
||||||
limit: ITEMS_PER_PAGE,
|
limit: PARTSINDEX_PAGE_SIZE,
|
||||||
page: 1,
|
page: 1,
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: paramsString
|
params: paramsString
|
||||||
@ -503,26 +722,55 @@ export default function Catalog() {
|
|||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
|
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
|
||||||
// Для PartsIndex показываем пустое состояние если нет товаров
|
// Для PartsIndex показываем пустое состояние если нет товаров И данные уже загружены
|
||||||
setShowEmptyState(visibleEntities.length === 0);
|
const hasLoadedData = accumulatedEntities.length > 0 || Boolean(entitiesData?.partsIndexCatalogEntities?.list);
|
||||||
|
setShowEmptyState(hasLoadedData && visibleEntities.length === 0);
|
||||||
|
console.log('📊 Определяем showEmptyState для PartsIndex:', {
|
||||||
|
hasLoadedData,
|
||||||
|
visibleEntitiesLength: visibleEntities.length,
|
||||||
|
accumulatedEntitiesLength: accumulatedEntities.length,
|
||||||
|
showEmptyState: hasLoadedData && visibleEntities.length === 0
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setShowEmptyState(false);
|
setShowEmptyState(false);
|
||||||
}
|
}
|
||||||
}, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length,
|
}, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length,
|
||||||
isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, filteredEntities.length]);
|
isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, accumulatedEntities.length, entitiesData]);
|
||||||
|
|
||||||
// Функции для навигации по страницам PartsIndex
|
// Функции для навигации по пользовательским страницам
|
||||||
const handleNextPage = useCallback(() => {
|
const handleNextPage = useCallback(() => {
|
||||||
if (hasMoreEntities && !entitiesLoading) {
|
const maxUserPage = Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE);
|
||||||
setPartsIndexPage(prev => prev + 1);
|
console.log('🔄 Нажата кнопка "Вперед":', {
|
||||||
|
currentUserPage,
|
||||||
|
maxUserPage,
|
||||||
|
accumulatedEntitiesLength: accumulatedEntities.length,
|
||||||
|
ITEMS_PER_PAGE
|
||||||
|
});
|
||||||
|
if (currentUserPage < maxUserPage) {
|
||||||
|
setCurrentUserPage(prev => {
|
||||||
|
console.log('✅ Переходим на страницу:', prev + 1);
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Нельзя перейти вперед: уже на последней странице');
|
||||||
}
|
}
|
||||||
}, [hasMoreEntities, entitiesLoading]);
|
}, [currentUserPage, accumulatedEntities.length]);
|
||||||
|
|
||||||
const handlePrevPage = useCallback(() => {
|
const handlePrevPage = useCallback(() => {
|
||||||
if (partsIndexPage > 1 && !entitiesLoading) {
|
console.log('🔄 Нажата кнопка "Назад":', {
|
||||||
setPartsIndexPage(prev => prev - 1);
|
currentUserPage,
|
||||||
|
accumulatedEntitiesLength: accumulatedEntities.length
|
||||||
|
});
|
||||||
|
if (currentUserPage > 1) {
|
||||||
|
setCurrentUserPage(prev => {
|
||||||
|
const newPage = prev - 1;
|
||||||
|
console.log('✅ Переходим на страницу:', newPage);
|
||||||
|
return newPage;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Нельзя перейти назад: уже на первой странице');
|
||||||
}
|
}
|
||||||
}, [partsIndexPage, entitiesLoading]);
|
}, [currentUserPage, accumulatedEntities.length]);
|
||||||
|
|
||||||
// Функция для загрузки следующей порции товаров по кнопке (только для PartsAPI)
|
// Функция для загрузки следующей порции товаров по кнопке (только для PartsAPI)
|
||||||
const handleLoadMorePartsAPI = useCallback(async () => {
|
const handleLoadMorePartsAPI = useCallback(async () => {
|
||||||
@ -592,9 +840,7 @@ export default function Catalog() {
|
|||||||
isPartsAPIMode ?
|
isPartsAPIMode ?
|
||||||
(visibilityMap.size === 0 && allArticles.length > 0 ? undefined : visibleProductsCount) :
|
(visibilityMap.size === 0 && allArticles.length > 0 ? undefined : visibleProductsCount) :
|
||||||
isPartsIndexMode ?
|
isPartsIndexMode ?
|
||||||
(searchQuery.trim() || Object.keys(selectedFilters).length > 0 ?
|
entitiesWithOffers.length :
|
||||||
filteredEntities.length :
|
|
||||||
entitiesData?.partsIndexCatalogEntities?.pagination?.limit || visibleEntities.length) :
|
|
||||||
3587
|
3587
|
||||||
}
|
}
|
||||||
productName={
|
productName={
|
||||||
@ -740,25 +986,21 @@ export default function Catalog() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Отображение товаров PartsIndex */}
|
{/* Отображение товаров PartsIndex */}
|
||||||
{isPartsIndexMode && filteredEntities.length > 0 && (
|
{isPartsIndexMode && (() => {
|
||||||
|
console.log('🎯 Проверяем отображение PartsIndex товаров:', {
|
||||||
|
isPartsIndexMode,
|
||||||
|
visibleEntitiesLength: visibleEntities.length,
|
||||||
|
visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name }))
|
||||||
|
});
|
||||||
|
return visibleEntities.length > 0;
|
||||||
|
})() && (
|
||||||
<>
|
<>
|
||||||
{filteredEntities
|
{visibleEntities
|
||||||
.map((entity, idx) => {
|
.map((entity, idx) => {
|
||||||
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
|
||||||
const priceData = getPrice(productForPrice);
|
const priceData = getPrice(productForPrice);
|
||||||
const isLoadingPriceData = isLoadingPrice(productForPrice);
|
const isLoadingPriceData = isLoadingPrice(productForPrice);
|
||||||
|
|
||||||
return {
|
|
||||||
entity,
|
|
||||||
idx,
|
|
||||||
productForPrice,
|
|
||||||
priceData,
|
|
||||||
isLoadingPriceData,
|
|
||||||
hasOffer: priceData !== null || isLoadingPriceData
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(item => item.hasOffer) // Показываем только товары с предложениями или загружающиеся
|
|
||||||
.map(({ entity, idx, productForPrice, priceData, isLoadingPriceData }) => {
|
|
||||||
// Определяем цену для отображения
|
// Определяем цену для отображения
|
||||||
let displayPrice = "Цена по запросу";
|
let displayPrice = "Цена по запросу";
|
||||||
let displayCurrency = "RUB";
|
let displayCurrency = "RUB";
|
||||||
@ -790,7 +1032,7 @@ export default function Catalog() {
|
|||||||
onAddToCart={async () => {
|
onAddToCart={async () => {
|
||||||
// Если цена не загружена, загружаем её и добавляем в корзину
|
// Если цена не загружена, загружаем её и добавляем в корзину
|
||||||
if (!priceData && !isLoadingPriceData) {
|
if (!priceData && !isLoadingPriceData) {
|
||||||
loadPriceOnDemand(productForPrice);
|
ensurePriceLoaded(productForPrice);
|
||||||
console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name);
|
console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -843,40 +1085,61 @@ export default function Catalog() {
|
|||||||
{/* Пагинация для PartsIndex */}
|
{/* Пагинация для PartsIndex */}
|
||||||
<div className="w-layout-hflex pagination">
|
<div className="w-layout-hflex pagination">
|
||||||
<button
|
<button
|
||||||
onClick={handlePrevPage}
|
onClick={() => {
|
||||||
disabled={partsIndexPage <= 1 || entitiesLoading}
|
console.log('🖱️ Клик по кнопке "Назад"');
|
||||||
|
handlePrevPage();
|
||||||
|
}}
|
||||||
|
disabled={currentUserPage <= 1}
|
||||||
className="button_strock w-button mr-2"
|
className="button_strock w-button mr-2"
|
||||||
>
|
>
|
||||||
← Назад
|
← Назад
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="flex items-center px-4 text-gray-600">
|
<span className="flex items-center px-4 text-gray-600">
|
||||||
Страница {partsIndexPage} {totalPages > partsIndexPage && `из ${totalPages}+`}
|
Страница {currentUserPage} из {Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE) || 1}
|
||||||
|
{isAutoLoading && ' (загружаем...)'}
|
||||||
|
<span className="ml-2 text-xs text-gray-400">
|
||||||
|
(товаров: {accumulatedEntities.length})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleNextPage}
|
onClick={() => {
|
||||||
disabled={!hasMoreEntities || entitiesLoading}
|
console.log('🖱️ Клик по кнопке "Вперед"');
|
||||||
|
handleNextPage();
|
||||||
|
}}
|
||||||
|
disabled={currentUserPage >= Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE)}
|
||||||
className="button_strock w-button ml-2"
|
className="button_strock w-button ml-2"
|
||||||
>
|
>
|
||||||
{entitiesLoading ? 'Загрузка...' : 'Вперед →'}
|
Вперед →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Отладочная информация */}
|
{/* Отладочная информация */}
|
||||||
{isPartsIndexMode && (
|
{isPartsIndexMode && (
|
||||||
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
|
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
|
||||||
<div>🔍 Отладка PartsIndex:</div>
|
<div>🔍 Отладка PartsIndex (исправленная логика):</div>
|
||||||
<div>• hasMoreItems: {hasMoreItems ? 'да' : 'нет'}</div>
|
<div>• accumulatedEntities: {accumulatedEntities.length}</div>
|
||||||
<div>• hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}</div>
|
<div>• entitiesWithOffers: {entitiesWithOffers.length}</div>
|
||||||
<div>• entitiesPage: {entitiesPage}</div>
|
|
||||||
<div>• visibleEntities: {visibleEntities.length}</div>
|
<div>• visibleEntities: {visibleEntities.length}</div>
|
||||||
<div>• filteredEntities: {filteredEntities.length}</div>
|
<div>• currentUserPage: {currentUserPage}</div>
|
||||||
<div>• groupId: {groupId || 'отсутствует'}</div>
|
<div>• partsIndexPage (API): {partsIndexPage}</div>
|
||||||
<div>• isLoadingMore: {isLoadingMore ? 'да' : 'нет'}</div>
|
<div>• isAutoLoading: {isAutoLoading ? 'да' : 'нет'}</div>
|
||||||
|
<div>• hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}</div>
|
||||||
<div>• entitiesLoading: {entitiesLoading ? 'да' : 'нет'}</div>
|
<div>• entitiesLoading: {entitiesLoading ? 'да' : 'нет'}</div>
|
||||||
<div>• catalogId: {catalogId || 'отсутствует'}</div>
|
<div>• groupId: {groupId || 'отсутствует'}</div>
|
||||||
<div>• Пагинация: {JSON.stringify(entitiesData?.partsIndexCatalogEntities?.pagination)}</div>
|
<div>• Target: {ITEMS_PER_PAGE} товаров на страницу</div>
|
||||||
|
<div>• showEmptyState: {showEmptyState ? 'да' : 'нет'}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
console.log('🔧 Ручной запуск автоподгрузки');
|
||||||
|
autoLoadMoreEntities();
|
||||||
|
}}
|
||||||
|
className="mt-2 px-3 py-1 bg-blue-500 text-white text-xs rounded"
|
||||||
|
disabled={isAutoLoading}
|
||||||
|
>
|
||||||
|
{isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -892,7 +1155,16 @@ export default function Catalog() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Пустое состояние для PartsIndex */}
|
{/* Пустое состояние для PartsIndex */}
|
||||||
{isPartsIndexMode && !entitiesLoading && !entitiesError && showEmptyState && (
|
{isPartsIndexMode && !entitiesLoading && !entitiesError && (() => {
|
||||||
|
console.log('🎯 Проверяем пустое состояние PartsIndex:', {
|
||||||
|
isPartsIndexMode,
|
||||||
|
entitiesLoading,
|
||||||
|
entitiesError,
|
||||||
|
showEmptyState,
|
||||||
|
visibleEntitiesLength: visibleEntities.length
|
||||||
|
});
|
||||||
|
return showEmptyState;
|
||||||
|
})() && (
|
||||||
<CatalogEmptyState
|
<CatalogEmptyState
|
||||||
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
|
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
|
||||||
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
|
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
|
||||||
|
@ -17,6 +17,7 @@ import MetaTags from "@/components/MetaTags";
|
|||||||
import { getMetaByPath } from "@/lib/meta-config";
|
import { getMetaByPath } from "@/lib/meta-config";
|
||||||
import JsonLdScript from "@/components/JsonLdScript";
|
import JsonLdScript from "@/components/JsonLdScript";
|
||||||
import { generateOrganizationSchema, generateWebSiteSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
|
import { generateOrganizationSchema, generateWebSiteSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
|
||||||
|
import HeroSlider from "@/components/index/HeroSlider";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const metaData = getMetaByPath('/');
|
const metaData = getMetaByPath('/');
|
||||||
|
@ -36,10 +36,10 @@ const ProfileHistoryPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetaTags {...metaData} />
|
<MetaTags {...metaData} />
|
||||||
<div className="page-wrapper h-full flex flex-col flex-1">
|
<div className="page-wrapper">
|
||||||
<ProfileInfo />
|
<ProfileInfo />
|
||||||
<div className="flex flex-col pt-10 pb-16 max-md:px-5 h-full flex-1">
|
<div className="flex flex-col pt-10 pb-16 max-md:px-5">
|
||||||
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto min-h-[526px] max-w-[1580px] w-full h-full">
|
<div className="flex relative gap-8 items-start self-stretch max-md:gap-5 max-sm:flex-col max-sm:gap-4 justify-center mx-auto max-w-[1580px] w-full h-full">
|
||||||
<LKMenu ref={menuRef} />
|
<LKMenu ref={menuRef} />
|
||||||
<ProfileHistoryMain />
|
<ProfileHistoryMain />
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,11 +21,23 @@ import { createProductMeta } from "@/lib/meta-config";
|
|||||||
|
|
||||||
const ANALOGS_CHUNK_SIZE = 5;
|
const ANALOGS_CHUNK_SIZE = 5;
|
||||||
|
|
||||||
const sortOptions = [
|
// Функция для расчета даты доставки
|
||||||
"По цене",
|
const calculateDeliveryDate = (deliveryDays: number): string => {
|
||||||
"По рейтингу",
|
const today = new Date();
|
||||||
"По количеству"
|
const deliveryDate = new Date(today);
|
||||||
];
|
deliveryDate.setDate(today.getDate() + deliveryDays);
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||||
|
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||||
|
];
|
||||||
|
|
||||||
|
const day = deliveryDate.getDate();
|
||||||
|
const month = months[deliveryDate.getMonth()];
|
||||||
|
const year = deliveryDate.getFullYear();
|
||||||
|
|
||||||
|
return `${day} ${month} ${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Функция для создания динамических фильтров
|
// Функция для создания динамических фильтров
|
||||||
const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
const createFilters = (result: any, loadedAnalogs: any): FilterConfig[] => {
|
||||||
@ -175,15 +187,18 @@ const getBestOffers = (offers: any[]) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Убрано: функция сортировки теперь в CoreProductCard
|
||||||
|
|
||||||
const transformOffersForCard = (offers: any[]) => {
|
const transformOffersForCard = (offers: any[]) => {
|
||||||
return offers.map(offer => {
|
return offers.map(offer => {
|
||||||
const isExternal = offer.type === 'external';
|
const isExternal = offer.type === 'external';
|
||||||
|
const deliveryDays = isExternal ? offer.deliveryTime : offer.deliveryDays;
|
||||||
return {
|
return {
|
||||||
id: offer.id,
|
id: offer.id,
|
||||||
productId: offer.productId,
|
productId: offer.productId,
|
||||||
offerKey: offer.offerKey,
|
offerKey: offer.offerKey,
|
||||||
pcs: `${offer.quantity} шт.`,
|
pcs: `${offer.quantity} шт.`,
|
||||||
days: `${isExternal ? offer.deliveryTime : offer.deliveryDays} дн.`,
|
days: deliveryDays ? calculateDeliveryDate(deliveryDays) : 'Уточняйте',
|
||||||
recommended: !isExternal && offer.available,
|
recommended: !isExternal && offer.available,
|
||||||
price: `${offer.price.toLocaleString('ru-RU')} ₽`,
|
price: `${offer.price.toLocaleString('ru-RU')} ₽`,
|
||||||
count: "1",
|
count: "1",
|
||||||
@ -191,7 +206,7 @@ const transformOffersForCard = (offers: any[]) => {
|
|||||||
currency: offer.currency || "RUB",
|
currency: offer.currency || "RUB",
|
||||||
warehouse: offer.warehouse,
|
warehouse: offer.warehouse,
|
||||||
supplier: offer.supplier,
|
supplier: offer.supplier,
|
||||||
deliveryTime: isExternal ? offer.deliveryTime : offer.deliveryDays,
|
deliveryTime: deliveryDays,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -200,7 +215,7 @@ export default function SearchResult() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { article, brand, q, artId } = router.query;
|
const { article, brand, q, artId } = router.query;
|
||||||
|
|
||||||
const [sortActive, setSortActive] = useState(0);
|
// Убрано: глобальная сортировка теперь не используется
|
||||||
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
||||||
const [showSortMobile, setShowSortMobile] = useState(false);
|
const [showSortMobile, setShowSortMobile] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
@ -542,7 +557,7 @@ export default function SearchResult() {
|
|||||||
<section className="main mobile-only">
|
<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} /> */}
|
{/* Глобальная сортировка убрана - теперь каждый товар сортируется индивидуально */}
|
||||||
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
|
||||||
<span className="code-embed-9 w-embed">
|
<span className="code-embed-9 w-embed">
|
||||||
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@ -574,7 +589,7 @@ export default function SearchResult() {
|
|||||||
)}
|
)}
|
||||||
{/* Лучшие предложения */}
|
{/* Лучшие предложения */}
|
||||||
{bestOffersData.length > 0 && (
|
{bestOffersData.length > 0 && (
|
||||||
<section className="section-6">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-vflex flex-block-36">
|
<div className="w-layout-vflex flex-block-36">
|
||||||
{bestOffersData.map(({ offer, type }, index) => (
|
{bestOffersData.map(({ offer, type }, index) => (
|
||||||
@ -584,7 +599,7 @@ export default function SearchResult() {
|
|||||||
title={`${offer.brand} ${offer.articleNumber}${offer.isAnalog ? ' (аналог)' : ''}`}
|
title={`${offer.brand} ${offer.articleNumber}${offer.isAnalog ? ' (аналог)' : ''}`}
|
||||||
description={offer.name}
|
description={offer.name}
|
||||||
price={`${offer.price.toLocaleString()} ₽`}
|
price={`${offer.price.toLocaleString()} ₽`}
|
||||||
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
|
delivery={offer.deliveryDuration ? calculateDeliveryDate(offer.deliveryDuration) : 'Уточняйте'}
|
||||||
stock={`${offer.quantity} шт.`}
|
stock={`${offer.quantity} шт.`}
|
||||||
offer={offer}
|
offer={offer}
|
||||||
/>
|
/>
|
||||||
|
@ -499,7 +499,27 @@ input#VinSearchInput {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading-9-copy {
|
||||||
|
|
||||||
|
text-align: right;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.pcs-search {
|
||||||
|
color: var(--_fonts---color--black);
|
||||||
|
font-size: var(--_fonts---font-size--core);
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.heading-9-copy {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.w-layout-hflex.flex-block-6 {
|
.w-layout-hflex.flex-block-6 {
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
}
|
}
|
||||||
@ -1147,4 +1167,30 @@ a.link-block-2.w-inline-block {
|
|||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
align-items: flex-start !important;
|
align-items: flex-start !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mask.w-slider-mask {
|
||||||
|
height: 100px !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.search-history-dropdown,
|
||||||
|
.search-results-dropdown,
|
||||||
|
.dropdown-search,
|
||||||
|
.dropdown-list-3.w--open {
|
||||||
|
position: fixed !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
top: 72px !important; /* подберите под ваш header */
|
||||||
|
width: 100vw !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
border-radius: 0 0 16px 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(44,62,80,0.10), 0 1.5px 4px rgba(44,62,80,0.08) !important;
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user