Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
95e6b33b56 | |||
a8f783767f |
@ -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,11 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
||||
article,
|
||||
productId,
|
||||
onAddToCart,
|
||||
isInCart = false,
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [localInCart, setLocalInCart] = useState(false);
|
||||
|
||||
// Проверяем, есть ли товар в избранном
|
||||
const isItemFavorite = isFavorite(productId, undefined, article, brand);
|
||||
@ -42,6 +45,9 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
||||
const handleAddToCart = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!localInCart) {
|
||||
setLocalInCart(true);
|
||||
}
|
||||
|
||||
// Если передан кастомный обработчик, используем его
|
||||
if (onAddToCart) {
|
||||
@ -167,8 +173,13 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
||||
<a
|
||||
href="#"
|
||||
className="button-icon w-inline-block"
|
||||
onClick={handleAddToCart}
|
||||
style={{ cursor: 'pointer' }}
|
||||
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="Добавить в корзину"
|
||||
>
|
||||
<div className="div-block-26">
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
|
||||
interface CatalogProductCardProps {
|
||||
@ -37,6 +37,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
isInCart = false,
|
||||
}) => {
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [localInCart, setLocalInCart] = useState(false);
|
||||
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isInCart && !localInCart) {
|
||||
setLocalInCart(true);
|
||||
}
|
||||
if (onAddToCart) {
|
||||
onAddToCart(e);
|
||||
} else {
|
||||
@ -141,8 +145,13 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
href="#"
|
||||
className="button-icon w-inline-block"
|
||||
onClick={handleBuyClick}
|
||||
style={{ cursor: isInCart ? 'default' : 'pointer', opacity: isInCart ? 0.5 : 1, filter: isInCart ? 'grayscale(1)' : 'none' }}
|
||||
aria-label={isInCart ? 'В корзине' : 'Купить'}
|
||||
style={{
|
||||
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-26">
|
||||
|
@ -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>
|
||||
|
@ -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' }}
|
||||
>
|
||||
|
@ -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,11 +25,17 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
||||
productId,
|
||||
onAddToCart,
|
||||
discount = 'Топ продаж', // По умолчанию как раньше
|
||||
// isInCart = false, // Удаляем из пропсов
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addItem, cartItems = [] } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [localInCart, setLocalInCart] = useState(false);
|
||||
|
||||
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 cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
@ -38,6 +45,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 +144,14 @@ 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 ? 'Добавлено' : 'Добавить в корзину')}
|
||||
>
|
||||
<div className="div-block-26">
|
||||
<div className="icon-setting w-embed">
|
||||
|
@ -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 { cartItems = [] } = 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 = cartItems.some(cartItem => cartItem.productId === product.id);
|
||||
|
||||
return (
|
||||
<TopSalesItem
|
||||
@ -222,6 +225,7 @@ const TopSalesSection: React.FC = () => {
|
||||
brand={brand}
|
||||
article={product.article}
|
||||
productId={product.id}
|
||||
isInCart={isInCart}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -39,9 +39,9 @@ const mockData = Array(12).fill({
|
||||
});
|
||||
|
||||
export default function Catalog() {
|
||||
const ITEMS_PER_PAGE = 12; // Показывать 12 карточек за раз
|
||||
const ITEMS_PER_PAGE = 24; // Показывать 12 карточек за раз
|
||||
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
|
||||
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
|
||||
const MAX_BRANDS_DISPLAY = 24; // Сколько брендов показывать изначально
|
||||
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
|
||||
const router = useRouter();
|
||||
const { addItem } = useCart();
|
||||
@ -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 };
|
||||
@ -935,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>;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -782,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;
|
||||
|
||||
@ -986,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 {
|
||||
@ -1005,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 {
|
||||
@ -1299,4 +1316,16 @@ a.link-block-2.w-inline-block {
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user