2 Commits

Author SHA1 Message Date
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
9 changed files with 123 additions and 40 deletions

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext"; 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";
@ -13,6 +13,7 @@ interface BestPriceItemProps {
article?: string; article?: string;
productId?: string; productId?: string;
onAddToCart?: (e: React.MouseEvent) => void; onAddToCart?: (e: React.MouseEvent) => void;
isInCart?: boolean;
} }
const BestPriceItem: React.FC<BestPriceItemProps> = ({ const BestPriceItem: React.FC<BestPriceItemProps> = ({
@ -25,9 +26,11 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
article, article,
productId, productId,
onAddToCart, onAddToCart,
isInCart = false,
}) => { }) => {
const { addItem } = useCart(); const { addItem } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
// Проверяем, есть ли товар в избранном // Проверяем, есть ли товар в избранном
const isItemFavorite = isFavorite(productId, undefined, article, brand); const isItemFavorite = isFavorite(productId, undefined, article, brand);
@ -42,6 +45,9 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
const handleAddToCart = async (e: React.MouseEvent) => { const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
// Если передан кастомный обработчик, используем его // Если передан кастомный обработчик, используем его
if (onAddToCart) { if (onAddToCart) {
@ -167,8 +173,13 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
<a <a
href="#" href="#"
className="button-icon w-inline-block" className="button-icon w-inline-block"
onClick={handleAddToCart} onClick={isInCart ? undefined : handleAddToCart}
style={{ cursor: 'pointer' }} 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="Добавить в корзину" aria-label="Добавить в корзину"
> >
<div className="div-block-26"> <div className="div-block-26">

View File

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

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
interface CatalogProductCardProps { interface CatalogProductCardProps {
@ -37,6 +37,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
isInCart = false, isInCart = false,
}) => { }) => {
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo='; const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
@ -77,6 +78,9 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
const handleBuyClick = (e: React.MouseEvent) => { const handleBuyClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!isInCart && !localInCart) {
setLocalInCart(true);
}
if (onAddToCart) { if (onAddToCart) {
onAddToCart(e); onAddToCart(e);
} else { } else {
@ -141,8 +145,13 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
href="#" href="#"
className="button-icon w-inline-block" className="button-icon w-inline-block"
onClick={handleBuyClick} onClick={handleBuyClick}
style={{ cursor: isInCart ? 'default' : 'pointer', opacity: isInCart ? 0.5 : 1, filter: isInCart ? 'grayscale(1)' : 'none' }} style={{
aria-label={isInCart ? 'В корзине' : 'Купить'} 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} tabIndex={0}
> >
<div className="div-block-26"> <div className="div-block-26">

View File

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

View File

@ -54,7 +54,13 @@ const SearchHistoryDropdown: React.FC<SearchHistoryDropdownProps> = ({
{uniqueQueries.map((item) => ( {uniqueQueries.map((item) => (
<button <button
key={item.id} 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" className="search-history-item-custom"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >

View File

@ -1,5 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext"; 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";
@ -13,6 +13,7 @@ interface TopSalesItemProps {
productId?: string; productId?: string;
onAddToCart?: (e: React.MouseEvent) => void; onAddToCart?: (e: React.MouseEvent) => void;
discount?: string; // Новый пропс для лейбла/скидки discount?: string; // Новый пропс для лейбла/скидки
// isInCart?: boolean; // Удаляем из пропсов
} }
const TopSalesItem: React.FC<TopSalesItemProps> = ({ const TopSalesItem: React.FC<TopSalesItemProps> = ({
@ -24,11 +25,17 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
productId, productId,
onAddToCart, onAddToCart,
discount = 'Топ продаж', // По умолчанию как раньше discount = 'Топ продаж', // По умолчанию как раньше
// isInCart = false, // Удаляем из пропсов
}) => { }) => {
const { addItem } = useCart(); const { addItem, cartItems = [] } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
const isItemFavorite = isFavorite(productId, undefined, article, brand); const isItemFavorite = isFavorite(productId, undefined, article, brand);
const isInCart = cartItems.some(item =>
(productId && item.productId === productId) ||
(article && brand && item.article === article && item.brand === brand)
);
const parsePrice = (priceStr: string): number => { const parsePrice = (priceStr: string): number => {
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.'); const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
@ -38,6 +45,9 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
const handleAddToCart = (e: React.MouseEvent) => { const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
if (onAddToCart) { if (onAddToCart) {
onAddToCart(e); onAddToCart(e);
return; return;
@ -134,9 +144,14 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
<a <a
href="#" href="#"
className="button-icon w-inline-block" className="button-icon w-inline-block"
onClick={handleAddToCart} onClick={isInCart ? undefined : handleAddToCart}
style={{ cursor: 'pointer' }} style={{
aria-label="Добавить в корзину" 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 ? 'Добавлено' : 'Добавить в корзину')}
> >
<div className="div-block-26"> <div className="div-block-26">
<div className="icon-setting w-embed"> <div className="icon-setting w-embed">

View File

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

View File

@ -39,9 +39,9 @@ const mockData = Array(12).fill({
}); });
export default function Catalog() { export default function Catalog() {
const ITEMS_PER_PAGE = 12; // Показывать 12 карточек за раз const ITEMS_PER_PAGE = 24; // Показывать 12 карточек за раз
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально const MAX_BRANDS_DISPLAY = 24; // Сколько брендов показывать изначально
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE); const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const router = useRouter(); const router = useRouter();
const { addItem } = useCart(); const { addItem } = useCart();
@ -336,12 +336,6 @@ export default function Catalog() {
естьЕщеТовары: hasMoreEntities естьЕщеТовары: hasMoreEntities
}); });
// Если у нас уже достаточно товаров, не загружаем
if (currentEntitiesCount >= ITEMS_PER_PAGE) {
console.log('✅ Автоподгрузка: достаточно товаров');
return;
}
// Даем время на загрузку цен товаров, если их слишком много загружается // Даем время на загрузку цен товаров, если их слишком много загружается
const loadingCount = accumulatedEntities.filter(entity => { const loadingCount = accumulatedEntities.filter(entity => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
@ -935,6 +929,20 @@ export default function Catalog() {
return false; return false;
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]); }, [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) { if (filtersLoading) {
return <div className="py-8 text-center">Загрузка фильтров...</div>; return <div className="py-8 text-center">Загрузка фильтров...</div>;
} }

View File

@ -51,8 +51,14 @@
} }
.flex-block-39-copy {
max-width: 300px;
min-width: 0;
}
.flex-block-40 { .flex-block-40 {
background-color: #fff;
padding-top: 10px; padding-top: 10px;
} }
@ -266,7 +272,18 @@ header.section-4 {
display: none; /* Chrome, Safari, Opera */ 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 { .mobile-category-overlay {
position: fixed; position: fixed;
@ -782,7 +799,7 @@ a.link-block-2.w-inline-block {
} }
@media screen and (max-width: 991px) { @media screen and (max-width: 991px) {
.flex-block-108, .flex-block-14-copy-copy { .flex-block-108 {
flex-flow: column; flex-flow: column;
justify-content: space-between; justify-content: space-between;
@ -986,8 +1003,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy { .flex-block-15-copy {
grid-column-gap: 5px; grid-column-gap: 5px;
grid-row-gap: 5px; grid-row-gap: 5px;
width: 160px !important; width: 210px !important;
min-width: 160px !important; min-width: 210px !important;
padding: 15px; padding: 15px;
} }
.div-block-3 { .div-block-3 {
@ -1005,8 +1022,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy { .flex-block-15-copy {
grid-column-gap: 5px; grid-column-gap: 5px;
grid-row-gap: 5px; grid-row-gap: 5px;
width: 160px !important; width: 180px !important;
min-width: 160px !important; min-width: 180px !important;
padding: 15px; padding: 15px;
} }
.nameitembp { .nameitembp {
@ -1300,3 +1317,15 @@ a.link-block-2.w-inline-block {
margin-left: 0 !important; margin-left: 0 !important;
} */ } */
} }
@media (max-width: 1200px) {
.pcs-cart-s1 {
display: none !important;
}
}
@media (max-width: 767px) {
.filters-desktop {
display: none !important;
}
}