6 Commits

Author SHA1 Message Date
f4facf146c build fix 2025-07-30 15:27:32 +03:00
80ed5826e2 novyie parvki 2025-07-30 15:25:18 +03:00
9adc737028 Merge pull request 'catalog and bags' (#32) from bag3007 into main
Reviewed-on: #32
2025-07-30 13:57:50 +03:00
95e6b33b56 catalog and bags 2025-07-30 13:57:16 +03:00
a8f783767f Merge pull request 'catalog' (#31) from catalog into main
Reviewed-on: #31
2025-07-30 00:25:53 +03:00
b363b88e33 catalog 2025-07-30 00:25:14 +03:00
13 changed files with 367 additions and 262 deletions

View File

@ -5,6 +5,7 @@ import { useArticleImage } from '@/hooks/useArticleImage';
import { useCatalogPrices } from '@/hooks/useCatalogPrices';
import { PartsAPIArticle } from '@/types/partsapi';
import toast from 'react-hot-toast';
import { useCart } from '@/contexts/CartContext';
interface ArticleCardProps {
article: PartsAPIArticle;
@ -17,6 +18,9 @@ const ArticleCard: React.FC<ArticleCardProps> = memo(({ article, index, onVisibi
const [shouldShow, setShouldShow] = useState(false);
const [isChecking, setIsChecking] = useState(true);
// Cart context
const { isInCart: isItemInCart } = useCart();
// Используем хук для получения изображения
const { imageUrl, isLoading: imageLoading, error } = useArticleImage(article.artId, {
enabled: !!article.artId
@ -85,6 +89,8 @@ const ArticleCard: React.FC<ArticleCardProps> = memo(({ article, index, onVisibi
const title = [brandName || 'N/A', articleNumber || 'N/A'].filter(part => part !== 'N/A').join(', ');
const brand = brandName || 'Unknown';
let priceText = 'от 17 087 ₽';
const isInCartFlag = isItemInCart(undefined, undefined, articleNumber, brandName);
return (
<CatalogProductCard
image={fallbackImage}
@ -97,6 +103,7 @@ const ArticleCard: React.FC<ArticleCardProps> = memo(({ article, index, onVisibi
brandName={brandName}
artId={article.artId}
onAddToCart={() => {}}
isInCart={isInCartFlag}
/>
);
}
@ -109,6 +116,8 @@ const ArticleCard: React.FC<ArticleCardProps> = memo(({ article, index, onVisibi
const brand = brandName || 'Unknown';
const isInCartFlag = isItemInCart(undefined, undefined, articleNumber, brandName);
// Формируем цену для отображения
let priceText = '';
if (priceData.isLoading) {
@ -144,6 +153,7 @@ const ArticleCard: React.FC<ArticleCardProps> = memo(({ article, index, onVisibi
brandName={brandName}
artId={article.artId}
onAddToCart={handleAddToCart}
isInCart={isInCartFlag}
/>
);
});

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext";
import toast from "react-hot-toast";
@ -13,6 +13,7 @@ interface BestPriceItemProps {
article?: string;
productId?: string;
onAddToCart?: (e: React.MouseEvent) => void;
isInCart?: boolean;
}
const BestPriceItem: React.FC<BestPriceItemProps> = ({
@ -25,9 +26,16 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
article,
productId,
onAddToCart,
isInCart = false,
}) => {
const { addItem } = useCart();
const { addItem, isInCart: isItemInCart, state: cartState } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
// Determine inCart via context if not provided
const inCartContext = isItemInCart(productId, undefined, article, brand);
const inCart = isInCart || inCartContext;
// Проверяем, есть ли товар в избранном
const isItemFavorite = isFavorite(productId, undefined, article, brand);
@ -42,6 +50,9 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
// Если передан кастомный обработчик, используем его
if (onAddToCart) {
@ -166,10 +177,16 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
<div className="nameitembp">{title}</div>
<a
href="#"
className="button-icon w-inline-block"
onClick={handleAddToCart}
style={{ cursor: 'pointer' }}
aria-label="Добавить в корзину"
className={`button-icon w-inline-block ${inCart || localInCart ? 'in-cart' : ''}`}
onClick={inCart ? undefined : handleAddToCart}
style={{
cursor: inCart || localInCart ? 'default' : 'pointer',
textDecoration: 'none',
opacity: inCart || localInCart ? 0.5 : 1,
backgroundColor: inCart || localInCart ? '#9ca3af' : undefined
}}
aria-label={inCart || localInCart ? "Товар уже в корзине" : "Добавить в корзину"}
title={inCart || localInCart ? "Товар уже в корзине" : "Добавить в корзину"}
>
<div className="div-block-26">
<div className="icon-setting w-embed">

View File

@ -277,8 +277,7 @@
}
}
}
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
const catalogId = activeCatalog?.id || 'fallback';
const catalogId = mobileCategory.catalogId || 'fallback';
handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
}}
style={{ cursor: "pointer" }}
@ -306,8 +305,7 @@
}
}
}
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
const catalogId = activeCatalog?.id || 'fallback';
const catalogId = mobileCategory.catalogId || 'fallback';
handleCategoryClick(catalogId, link, subcategoryId);
}}
>
@ -333,7 +331,7 @@
{tabData.map((cat, index) => {
// Получаем ID каталога из данных PartsIndex или создаем fallback ID
const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.id || `fallback_${index}`;
const groups = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.groups || [];
return (
<div
className="mobile-subcategory"
@ -343,7 +341,7 @@
const categoryWithData = {
...cat,
catalogId,
groups: catalogsData?.partsIndexCategoriesWithGroups?.[index]?.groups
groups
};
setMobileCategory(categoryWithData);
}}

View File

@ -1,5 +1,4 @@
import Link from "next/link";
import React from "react";
import React, { useState } from "react";
import { useFavorites } from "@/contexts/FavoritesContext";
interface CatalogProductCardProps {
@ -15,7 +14,7 @@ interface CatalogProductCardProps {
productId?: string;
offerKey?: string;
currency?: string;
priceElement?: React.ReactNode; // Элемент для отображения цены (например, скелетон)
priceElement?: React.ReactNode;
onAddToCart?: (e: React.MouseEvent) => void | Promise<void>;
isInCart?: boolean;
}
@ -38,41 +37,31 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
isInCart = false,
}) => {
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
// Создаем ссылку на card с параметрами товара
const cardUrl = articleNumber && brandName
? `/card?article=${encodeURIComponent(articleNumber)}&brand=${encodeURIComponent(brandName)}${artId ? `&artId=${artId}` : ''}`
: '/card'; // Fallback на card если нет данных
: '/card';
// Проверяем, есть ли товар в избранном
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brandName || brand);
// Обработчик клика по сердечку
const handleFavoriteClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Извлекаем цену как число
const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0;
if (isItemFavorite) {
// Находим товар в избранном по правильному ID
const favoriteItem = favorites.find((fav: any) => {
// Проверяем по разным комбинациям идентификаторов
if (productId && fav.productId === productId) return true;
if (offerKey && fav.offerKey === offerKey) return true;
if (fav.article === articleNumber && fav.brand === (brandName || brand)) return true;
return false;
});
if (favoriteItem) {
removeFromFavorites(favoriteItem.id);
}
} else {
// Добавляем в избранное
addToFavorites({
productId,
offerKey,
@ -86,30 +75,25 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
}
};
// Обработчик клика по кнопке "Купить"
const handleBuyClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isInCart && !localInCart) {
setLocalInCart(true);
}
if (onAddToCart) {
onAddToCart(e);
} else {
// Fallback - переходим на страницу товара
window.location.href = cardUrl;
}
};
return (
<div className="w-layout-vflex flex-block-15-copy" data-article-card="visible" itemScope itemType="https://schema.org/Product">
<div
className="w-layout-vflex flex-block-15-copy"
data-article-card="visible"
itemScope
itemType="https://schema.org/Product"
>
<div
className={`favcardcat ${isItemFavorite ? 'favorite-active' : ''}`}
className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
onClick={handleFavoriteClick}
style={{
cursor: 'pointer',
color: isItemFavorite ? '#ff4444' : '#ccc'
}}
style={{ cursor: 'pointer', color: isItemFavorite ? '#ff4444' : '#ccc' }}
>
<div className="icon-setting w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -117,9 +101,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
</svg>
</div>
</div>
{/* Делаем картинку и контент кликабельными для перехода на card */}
<Link href={cardUrl} className="div-block-4" style={{ textDecoration: 'none', color: 'inherit' }}>
<div className="div-block-4">
<img
src={displayImage}
loading="lazy"
@ -129,10 +111,18 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
className="image-5"
itemProp="image"
/>
<div className="text-block-7">{discount}</div>
</Link>
<Link href={cardUrl} className="div-block-3" style={{ textDecoration: 'none', color: 'inherit' }}>
<div
className="text-block-7"
style={{
background: discount ? undefined : 'transparent',
color: discount ? undefined : 'transparent',
border: discount ? undefined : 'none',
}}
>
{discount || ''}
</div>
</div>
<div className="div-block-3">
<div className="w-layout-hflex flex-block-16">
{priceElement ? (
<div className="text-block-8">{priceElement}</div>
@ -144,31 +134,36 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
)}
<div className="text-block-9">{oldPrice}</div>
</div>
<div className="w-layout-hflex flex-block-122">
<div className="w-layout-vflex">
<div className="text-block-10" itemProp="name">{title}</div>
<div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand">
<span itemProp="name">{brand}</span>
</div>
<meta itemProp="sku" content={articleNumber || ''} />
</Link>
{/* Обновляем кнопку купить */}
<div
className="catc w-inline-block"
</div>
<a
href="#"
className="button-icon w-inline-block"
onClick={handleBuyClick}
style={{
cursor: isInCart ? 'default' : 'pointer',
opacity: isInCart ? 0.5 : 1,
filter: isInCart ? 'grayscale(1)' : 'none'
cursor: isInCart || localInCart ? 'default' : 'pointer',
opacity: isInCart || localInCart ? 0.5 : 1,
filter: isInCart || localInCart ? 'grayscale(1)' : 'none',
background: isInCart || localInCart ? '#2563eb' : undefined
}}
aria-label={isInCart || localInCart ? 'В корзине' : 'Купить'}
tabIndex={0}
>
<div className="div-block-25">
<div className="div-block-26">
<div className="icon-setting w-embed">
<svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"></path>
</svg>
</div>
</div>
<div className="text-block-6">{isInCart ? 'В корзине' : 'Купить'}</div>
</a>
</div>
<meta itemProp="sku" content={articleNumber || ''} />
</div>
</div>
);

View File

@ -63,6 +63,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {})
);
const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({});
const [localInCart, setLocalInCart] = useState<{ [key: number]: boolean }>({});
useEffect(() => {
setInputValues(offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {}));
@ -158,6 +159,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
};
const handleAddToCart = async (offer: CoreProductCardOffer, index: number) => {
setLocalInCart(prev => ({ ...prev, [index]: true }));
const quantity = quantities[index] || 1;
const availableStock = parseStock(offer.pcs);
const inCart = offer.isInCart || false; // Use backend flag
@ -407,7 +409,8 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
{displayedOffers.map((offer, idx) => {
const isLast = idx === displayedOffers.length - 1;
const maxCount = parseStock(offer.pcs);
const inCart = offer.isInCart || false; // Use backend flag
const inCart = offer.isInCart || false;
const isLocallyInCart = !!localInCart[idx];
// Backend now provides isInCart flag directly
@ -484,23 +487,23 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
<button
type="button"
onClick={() => handleAddToCart(offer, idx)}
className={`button-icon w-inline-block ${inCart ? 'in-cart' : ''}`}
className={`button-icon w-inline-block ${inCart || isLocallyInCart ? 'in-cart' : ''}`}
style={{
cursor: 'pointer',
opacity: inCart ? 0.5 : 1,
backgroundColor: inCart ? '#9ca3af' : undefined
opacity: inCart || isLocallyInCart ? 0.5 : 1,
backgroundColor: inCart || isLocallyInCart ? '#2563eb' : undefined
}}
aria-label={inCart ? "Товар уже в корзине" : "Добавить в корзину"}
title={inCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
aria-label={inCart || isLocallyInCart ? "Товар уже в корзине" : "Добавить в корзину"}
title={inCart || isLocallyInCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
>
<div className="div-block-26">
<img
loading="lazy"
src="/images/cart_icon.svg"
alt={inCart ? "В корзине" : "В корзину"}
alt={inCart || isLocallyInCart ? "В корзине" : "В корзину"}
className="image-11"
style={{
filter: inCart ? 'brightness(0.7)' : undefined
filter: inCart || isLocallyInCart ? 'brightness(0.7)' : undefined
}}
/>
</div>

View File

@ -54,7 +54,13 @@ const SearchHistoryDropdown: React.FC<SearchHistoryDropdownProps> = ({
{uniqueQueries.map((item) => (
<button
key={item.id}
onClick={() => onItemClick(item.searchQuery)}
onClick={() => {
if ((item.searchType === 'ARTICLE' || item.searchType === 'OEM') && item.articleNumber) {
onItemClick(item.articleNumber);
} else {
onItemClick(item.searchQuery);
}
}}
className="search-history-item-custom"
style={{ cursor: 'pointer' }}
>

View File

@ -1,5 +1,5 @@
import Link from "next/link";
import React from "react";
import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext";
import toast from "react-hot-toast";
@ -13,6 +13,7 @@ interface TopSalesItemProps {
productId?: string;
onAddToCart?: (e: React.MouseEvent) => void;
discount?: string; // Новый пропс для лейбла/скидки
isInCart?: boolean;
}
const TopSalesItem: React.FC<TopSalesItemProps> = ({
@ -24,12 +25,16 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
productId,
onAddToCart,
discount = 'Топ продаж', // По умолчанию как раньше
// isInCart = false, // Удаляем из пропсов
}) => {
const { addItem } = useCart();
const { addItem, isInCart: isItemInCart } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
const isItemFavorite = isFavorite(productId, undefined, article, brand);
const isInCart = isItemInCart(productId, undefined, article, brand);
const parsePrice = (priceStr: string): number => {
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
return parseFloat(cleanPrice) || 0;
@ -38,6 +43,9 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
if (onAddToCart) {
onAddToCart(e);
return;
@ -134,9 +142,15 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
<a
href="#"
className="button-icon w-inline-block"
onClick={handleAddToCart}
style={{ cursor: 'pointer' }}
aria-label="Добавить в корзину"
onClick={isInCart ? undefined : handleAddToCart}
style={{
cursor: isInCart ? 'default' : (localInCart ? 'default' : 'pointer'),
background: isInCart ? '#9ca3af' : (localInCart ? '#2563eb' : undefined),
opacity: isInCart || localInCart ? 0.5 : 1,
filter: isInCart || localInCart ? 'grayscale(1)' : 'none'
}}
aria-label={isInCart ? 'В корзине' : (localInCart ? 'Добавлено' : 'Добавить в корзину')}
title={isInCart ? 'Товар уже в корзине - нажмите для добавления еще' : 'Добавить в корзину'}
>
<div className="div-block-26">
<div className="icon-setting w-embed">

View File

@ -4,9 +4,10 @@ interface FilterRangeProps {
title: string;
min?: number;
max?: number;
isMobile?: boolean; // Добавляем флаг для мобильной версии
value?: [number, number] | null; // Текущее значение диапазона
isMobile?: boolean;
value?: [number, number] | null;
onChange?: (value: [number, number]) => void;
defaultOpen?: boolean; // Добавляем параметр defaultOpen
}
const DEFAULT_MIN = 1;
@ -14,14 +15,22 @@ const DEFAULT_MAX = 32000;
const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max));
const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => {
const FilterRange: React.FC<FilterRangeProps> = ({
title,
min = DEFAULT_MIN,
max = DEFAULT_MAX,
isMobile = false,
value = null,
onChange,
defaultOpen = false // Устанавливаем по умолчанию false
}) => {
const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min));
const [to, setTo] = useState<string>(value ? String(value[1]) : String(max));
const [confirmedFrom, setConfirmedFrom] = useState<number>(value ? value[0] : min);
const [confirmedTo, setConfirmedTo] = useState<number>(value ? value[1] : max);
const [dragging, setDragging] = useState<null | "from" | "to">(null);
const [trackWidth, setTrackWidth] = useState(0);
const [open, setOpen] = useState(true);
const [open, setOpen] = useState(isMobile || defaultOpen); // Учитываем isMobile и defaultOpen
const trackRef = useRef<HTMLDivElement>(null);
// Обновляем локальное состояние при изменении внешнего значения или границ
@ -199,7 +208,7 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
style={{
position: 'absolute',
top: 6,
left: pxFrom ,
left: pxFrom,
zIndex: 3,
cursor: 'pointer'
}}

View File

@ -3,10 +3,17 @@ import { useQuery } from '@apollo/client';
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
import { useRouter } from 'next/router';
interface CategoryNavGroup {
id: string;
name: string;
image?: string;
}
interface CategoryNavItem {
id: string;
name: string;
image?: string;
groups?: CategoryNavGroup[];
}
const FALLBACK_CATEGORIES: CategoryNavItem[] = [
@ -38,11 +45,15 @@ const CategoryNavSection: React.FC = () => {
: FALLBACK_CATEGORIES;
const handleCategoryClick = (category: CategoryNavItem) => {
// Получаем первую доступную группу для навигации в PartsIndex режим
const firstGroupId = category.groups && category.groups.length > 0 ? category.groups[0].id : undefined;
router.push({
pathname: '/catalog',
query: {
categoryId: category.id,
categoryName: encodeURIComponent(category.name)
partsIndexCatalog: category.id,
categoryName: encodeURIComponent(category.name),
...(firstGroupId && { partsIndexCategory: firstGroupId })
}
});
};

View File

@ -2,6 +2,7 @@ import React, { useRef } from "react";
import { useQuery } from "@apollo/client";
import TopSalesItem from "../TopSalesItem";
import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql";
import { useCart } from "@/contexts/CartContext";
interface TopSalesProductData {
id: string;
@ -22,6 +23,7 @@ const SCROLL_AMOUNT = 340; // px, ширина одной карточки + о
const TopSalesSection: React.FC = () => {
const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS);
const { isInCart: isItemInCart } = useCart();
const scrollRef = useRef<HTMLDivElement>(null);
const scrollLeft = () => {
@ -212,6 +214,7 @@ const TopSalesSection: React.FC = () => {
const title = product.name;
const brand = product.brand || 'Неизвестный бренд';
const isInCart = isItemInCart(product.id, undefined, product.article, brand);
return (
<TopSalesItem
@ -222,6 +225,7 @@ const TopSalesSection: React.FC = () => {
brand={brand}
article={product.article}
productId={product.id}
isInCart={isInCart}
/>
);
})}

View File

@ -38,13 +38,13 @@ const mockData = Array(12).fill({
brand: "Borsehung",
});
const ITEMS_PER_PAGE = 50; // Уменьшено для быстрой загрузки и лучшего UX
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
export default function Catalog() {
const ITEMS_PER_PAGE = 24; // Показывать 12 карточек за раз
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
const MAX_BRANDS_DISPLAY = 24; // Сколько брендов показывать изначально
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const router = useRouter();
const { addItem } = useCart();
const { addItem, isInCart: isItemInCart } = useCart();
const {
partsApiCategory: strId,
categoryName,
@ -336,12 +336,6 @@ export default function Catalog() {
естьЕщеТовары: hasMoreEntities
});
// Если у нас уже достаточно товаров, не загружаем
if (currentEntitiesCount >= ITEMS_PER_PAGE) {
console.log('✅ Автоподгрузка: достаточно товаров');
return;
}
// Даем время на загрузку цен товаров, если их слишком много загружается
const loadingCount = accumulatedEntities.filter(entity => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
@ -407,7 +401,8 @@ export default function Catalog() {
type: 'range' as const,
title: param.name,
min,
max
max,
defaultOpen: false,
};
} else {
// Для dropdown фильтров
@ -418,7 +413,8 @@ export default function Catalog() {
.filter((value: any) => value.available) // Показываем только доступные
.map((value: any) => value.title || value.value),
multi: true,
showAll: true
showAll: true,
defaultOpen: false,
};
}
});
@ -564,7 +560,7 @@ export default function Catalog() {
options: brandsToShow.sort(), // Сортируем по алфавиту для удобства
multi: true,
showAll: true,
defaultOpen: true,
defaultOpen: false,
hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY,
onShowMore: () => setShowAllBrands(true)
});
@ -577,7 +573,7 @@ export default function Catalog() {
options: Array.from(productGroups).sort(),
multi: true,
showAll: true,
defaultOpen: true,
defaultOpen: false,
});
}
@ -933,6 +929,20 @@ export default function Catalog() {
return false;
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]);
useEffect(() => {
// Сбросить все состояния при смене каталога или подкатегории
setAccumulatedEntities([]);
setVisibleEntities([]);
setEntitiesWithOffers([]);
setEntitiesCache(new Map());
setCurrentUserPage(1);
setPartsIndexPage(1);
setHasMoreEntities(true);
setShowEmptyState(false);
setIsFilterChanging(false);
setVisibleCount(ITEMS_PER_PAGE);
}, [catalogId, groupId]);
if (filtersLoading) {
return <div className="py-8 text-center">Загрузка фильтров...</div>;
}
@ -1007,7 +1017,7 @@ export default function Catalog() {
</div>
</div>
{isPartsAPIMode ? (
<div className="filters-desktop">
<div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters
filters={dynamicFilters}
onFilterChange={handleDesktopFilterChange}
@ -1018,7 +1028,7 @@ export default function Catalog() {
/>
</div>
) : isPartsIndexMode ? (
<div className="filters-desktop">
<div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
@ -1029,7 +1039,7 @@ export default function Catalog() {
/>
</div>
) : (
<div className="filters-desktop">
<div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
@ -1125,21 +1135,14 @@ export default function Catalog() {
)}
{/* Отображение товаров PartsIndex */}
{isPartsIndexMode && !isFilterChanging && (() => {
console.log('🎯 Проверяем отображение PartsIndex товаров:', {
isPartsIndexMode,
visibleEntitiesLength: visibleEntities.length,
visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name })),
isFilterChanging
});
return visibleEntities.length > 0;
})() && (
{isPartsIndexMode && !isFilterChanging && accumulatedEntities.length > 0 && (
<>
{visibleEntities
.map((entity, idx) => {
{accumulatedEntities.slice(0, visibleCount).map((entity, idx) => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
const isLoadingPriceData = isLoadingPrice(productForPrice);
// Fallback cart check via frontend context
const inCartFallback = isItemInCart(entity.id, priceData?.offerKey, entity.code, entity.brand.name);
// Определяем цену для отображения (все товары уже отфильтрованы на сервере)
let displayPrice = "";
@ -1173,7 +1176,7 @@ export default function Catalog() {
productId={entity.id}
artId={entity.id}
offerKey={priceData?.offerKey}
isInCart={priceData?.isInCart}
isInCart={priceData?.isInCart || inCartFallback}
onAddToCart={async () => {
// Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) {
@ -1227,40 +1230,19 @@ export default function Catalog() {
);
})}
{/* Пагинация для PartsIndex */}
{/* Кнопка "Показать еще" */}
{visibleCount < accumulatedEntities.length && (
<div className="w-layout-hflex pagination">
<button
onClick={() => {
console.log('🖱️ Клик по кнопке "Назад"');
handlePrevPage();
}}
disabled={currentUserPage <= 1}
className="button_strock w-button mr-2"
onClick={() => setVisibleCount(c => Math.min(c + ITEMS_PER_PAGE, accumulatedEntities.length))}
className="button_strock w-button"
>
Назад
</button>
<span className="flex items-center px-4 text-gray-600">
Страница {currentUserPage} из {Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE) || 1}
{isAutoLoading && ' (загружаем...)'}
<span className="ml-2 text-xs text-gray-400">
(товаров: {accumulatedEntities.length})
</span>
</span>
<button
onClick={() => {
console.log('🖱️ Клик по кнопке "Вперед"');
handleNextPage();
}}
disabled={currentUserPage >= Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE)}
className="button_strock w-button ml-2"
>
Вперед
Показать еще
</button>
</div>
)}
{/* Отладочная информация */}
{/* Отладочная информация
{isPartsIndexMode && (
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<div>🔍 Отладка PartsIndex (исправленная логика):</div>
@ -1286,7 +1268,7 @@ export default function Catalog() {
{isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
</button>
</div>
)}
)} */}
</>
)}

View File

@ -51,8 +51,14 @@
}
.flex-block-39-copy {
max-width: 300px;
min-width: 0;
}
.flex-block-40 {
background-color: #fff;
padding-top: 10px;
}
@ -266,7 +272,18 @@ header.section-4 {
display: none; /* Chrome, Safari, Opera */
}
.heading-2 {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
min-width: 0;
}
.dropdown-toggle.w-dropdown-toggle {
min-width: 0;
}
.mobile-category-overlay {
position: fixed;
@ -348,6 +365,12 @@ input.input-receiver:focus {
box-shadow: none;
}
.flex-block-122 {
width: 100% !important;
}
.button-icon.w-inline-block {
margin-left: auto;
}
.text-block-10 {
display: -webkit-box;
-webkit-line-clamp: 2;
@ -403,6 +426,25 @@ input.input-receiver:focus {
width: 100% !important;
}
.text-block-7 {
border-radius: var(--_round---small-8);
background-color: var(--green);
color: var(--_fonts---color--white);
padding: 5px;
font-weight: 600;
position: relative;
top: -35px;
height: 30px;
}
.div-block-3 {
grid-column-gap: 5px;
grid-row-gap: 5px;
flex-flow: column;
align-self: auto;
margin-top: -30px;
display: flex;
}
.sort-item.active {
color: #111;
font-weight: 700;
@ -491,19 +533,20 @@ input#VinSearchInput {
line-height: 1.4em;
}
.text-block-21-copy {
.text-block-21-copy,
.heading-9-copy {
width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.heading-9-copy {
/* .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);
@ -513,11 +556,11 @@ input#VinSearchInput {
@media (max-width: 767px) {
.heading-9-copy {
/* .heading-9-copy {
text-align: left;
display: block;
}
} */
.w-layout-hflex.flex-block-6 {
flex-direction: column !important;
@ -756,7 +799,7 @@ a.link-block-2.w-inline-block {
}
@media screen and (max-width: 991px) {
.flex-block-108, .flex-block-14-copy-copy {
.flex-block-108 {
flex-flow: column;
justify-content: space-between;
@ -944,8 +987,9 @@ a.link-block-2.w-inline-block {
}
.flex-block-15-copy {
width: 232px!important;
min-width: 232px!important;
width: 240px!important;
height: 315px;
min-width: 240px!important;
}
.nameitembp {
@ -959,8 +1003,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy {
grid-column-gap: 5px;
grid-row-gap: 5px;
width: 160px !important;
min-width: 160px !important;
width: 210px !important;
min-width: 210px !important;
padding: 15px;
}
.div-block-3 {
@ -978,8 +1022,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy {
grid-column-gap: 5px;
grid-row-gap: 5px;
width: 160px !important;
min-width: 160px !important;
width: 180px !important;
min-width: 180px !important;
padding: 15px;
}
.nameitembp {
@ -1268,8 +1312,20 @@ a.link-block-2.w-inline-block {
max-width: 160px;
flex: 0 0 160px;
}
.heading-9-copy {
/* .heading-9-copy {
text-align: left !important;
margin-left: 0 !important;
} */
}
@media (max-width: 1200px) {
.pcs-cart-s1 {
display: none !important;
}
}
@media (max-width: 767px) {
.filters-desktop {
display: none !important;
}
}

View File

@ -1539,7 +1539,7 @@ body {
grid-row-gap: 5px;
flex-flow: column;
align-self: auto;
margin-top: -30px;
/* margin-top: -30px; */
display: flex;
}