From b363b88e33572a49af81b82ac70759d30431f535 Mon Sep 17 00:00:00 2001 From: egortriston Date: Wed, 30 Jul 2025 00:25:14 +0300 Subject: [PATCH] catalog --- src/components/CatalogProductCard.tsx | 118 +++++------- src/components/filters/FilterRange.tsx | 19 +- src/pages/catalog.tsx | 252 +++++++++++-------------- src/styles/my.css | 45 ++++- src/styles/protekproject.webflow.css | 2 +- 5 files changed, 215 insertions(+), 221 deletions(-) diff --git a/src/components/CatalogProductCard.tsx b/src/components/CatalogProductCard.tsx index b778f0e..3515bec 100644 --- a/src/components/CatalogProductCard.tsx +++ b/src/components/CatalogProductCard.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import React from "react"; import { useFavorites } from "@/contexts/FavoritesContext"; @@ -15,7 +14,7 @@ interface CatalogProductCardProps { productId?: string; offerKey?: string; currency?: string; - priceElement?: React.ReactNode; // Элемент для отображения цены (например, скелетон) + priceElement?: React.ReactNode; onAddToCart?: (e: React.MouseEvent) => void | Promise; isInCart?: boolean; } @@ -39,40 +38,29 @@ const CatalogProductCard: React.FC = ({ }) => { const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); - // Обрабатываем пустое изображение - используем 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,53 +74,51 @@ const CatalogProductCard: React.FC = ({ } }; - // Обработчик клика по кнопке "Купить" const handleBuyClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); if (onAddToCart) { onAddToCart(e); } else { - // Fallback - переходим на страницу товара window.location.href = cardUrl; } }; return ( -
-
+
- +
- - {/* Делаем картинку и контент кликабельными для перехода на card */} - - {title} + {title} -
{discount}
- - - +
+ {discount || ''} +
+
+
{priceElement ? (
{priceElement}
@@ -144,31 +130,31 @@ const CatalogProductCard: React.FC = ({ )}
{oldPrice}
-
{title}
-
- {brand} +
+
+
{title}
+
+ {brand} +
+
+ +
+
+ + + +
+
+
- - - {/* Обновляем кнопку купить */} -
-
-
- - - -
-
-
{isInCart ? 'В корзине' : 'Купить'}
); diff --git a/src/components/filters/FilterRange.tsx b/src/components/filters/FilterRange.tsx index f67a15c..a79f5c2 100644 --- a/src/components/filters/FilterRange.tsx +++ b/src/components/filters/FilterRange.tsx @@ -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 = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => { +const FilterRange: React.FC = ({ + title, + min = DEFAULT_MIN, + max = DEFAULT_MAX, + isMobile = false, + value = null, + onChange, + defaultOpen = false // Устанавливаем по умолчанию false +}) => { const [from, setFrom] = useState(value ? String(value[0]) : String(min)); const [to, setTo] = useState(value ? String(value[1]) : String(max)); const [confirmedFrom, setConfirmedFrom] = useState(value ? value[0] : min); const [confirmedTo, setConfirmedTo] = useState(value ? value[1] : max); const [dragging, setDragging] = useState(null); const [trackWidth, setTrackWidth] = useState(0); - const [open, setOpen] = useState(true); + const [open, setOpen] = useState(isMobile || defaultOpen); // Учитываем isMobile и defaultOpen const trackRef = useRef(null); // Обновляем локальное состояние при изменении внешнего значения или границ @@ -199,7 +208,7 @@ const FilterRange: React.FC = ({ title, min = DEFAULT_MIN, max style={{ position: 'absolute', top: 6, - left: pxFrom , + left: pxFrom, zIndex: 3, cursor: 'pointer' }} diff --git a/src/pages/catalog.tsx b/src/pages/catalog.tsx index ba8eb18..92b525d 100644 --- a/src/pages/catalog.tsx +++ b/src/pages/catalog.tsx @@ -38,11 +38,11 @@ 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 = 12; // Показывать 12 карточек за раз + const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости + const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально + const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE); const router = useRouter(); const { addItem } = useCart(); const { @@ -407,7 +407,8 @@ export default function Catalog() { type: 'range' as const, title: param.name, min, - max + max, + defaultOpen: false, }; } else { // Для dropdown фильтров @@ -418,7 +419,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 +566,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 +579,7 @@ export default function Catalog() { options: Array.from(productGroups).sort(), multi: true, showAll: true, - defaultOpen: true, + defaultOpen: false, }); } @@ -1007,7 +1009,7 @@ export default function Catalog() {
{isPartsAPIMode ? ( -
+
) : isPartsIndexMode ? ( -
+
) : ( -
+
{ - 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) => { - const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; - const priceData = getPrice(productForPrice); - const isLoadingPriceData = isLoadingPrice(productForPrice); - - // Определяем цену для отображения (все товары уже отфильтрованы на сервере) - let displayPrice = ""; - let displayCurrency = "RUB"; - let priceElement; - - if (isLoadingPriceData) { - // Показываем скелетон загрузки вместо текста - priceElement = ; - } else if (priceData && priceData.price) { - displayPrice = `${priceData.price.toLocaleString('ru-RU')} ₽`; - displayCurrency = priceData.currency || "RUB"; - } else { - // Если нет данных о цене, показываем скелетон (товар должен загрузиться) - priceElement = ; - } + {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); + + // Определяем цену для отображения (все товары уже отфильтрованы на сервере) + let displayPrice = ""; + let displayCurrency = "RUB"; + let priceElement; + + if (isLoadingPriceData) { + // Показываем скелетон загрузки вместо текста + priceElement = ; + } else if (priceData && priceData.price) { + displayPrice = `${priceData.price.toLocaleString('ru-RU')} ₽`; + displayCurrency = priceData.currency || "RUB"; + } else { + // Если нет данных о цене, показываем скелетон (товар должен загрузиться) + priceElement = ; + } - return ( - { - // Если цена не загружена, загружаем её и добавляем в корзину - if (!priceData && !isLoadingPriceData) { - ensurePriceLoaded(productForPrice); - console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name); - return; - } + return ( + { + // Если цена не загружена, загружаем её и добавляем в корзину + if (!priceData && !isLoadingPriceData) { + ensurePriceLoaded(productForPrice); + console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name); + return; + } - // Если цена есть, добавляем в корзину - if (priceData && priceData.price) { - const itemToAdd = { - productId: entity.id, - offerKey: priceData.offerKey, - name: entity.originalName || entity.name?.name || 'Товар без названия', - description: `${entity.brand.name} ${entity.code}`, - brand: entity.brand.name, - article: entity.code, - price: priceData.price, - currency: priceData.currency || 'RUB', - quantity: 1, - stock: undefined, // информация о наличии не доступна для PartsIndex - deliveryTime: '1-3 дня', - warehouse: 'Parts Index', - supplier: 'Parts Index', - isExternal: true, - image: entity.images?.[0] || '', - }; + // Если цена есть, добавляем в корзину + if (priceData && priceData.price) { + const itemToAdd = { + productId: entity.id, + offerKey: priceData.offerKey, + name: entity.originalName || entity.name?.name || 'Товар без названия', + description: `${entity.brand.name} ${entity.code}`, + brand: entity.brand.name, + article: entity.code, + price: priceData.price, + currency: priceData.currency || 'RUB', + quantity: 1, + stock: undefined, // информация о наличии не доступна для PartsIndex + deliveryTime: '1-3 дня', + warehouse: 'Parts Index', + supplier: 'Parts Index', + isExternal: true, + image: entity.images?.[0] || '', + }; - const result = await addItem(itemToAdd); - - if (result.success) { - // Показываем уведомление - toast.success( -
-
Товар добавлен в корзину!
-
{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}
-
, - { - duration: 3000, - icon: , - } - ); - } else { - toast.error(result.error || 'Ошибка при добавлении товара в корзину'); - } + const result = await addItem(itemToAdd); + + if (result.success) { + // Показываем уведомление + toast.success( +
+
Товар добавлен в корзину!
+
{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}
+
, + { + duration: 3000, + icon: , + } + ); } else { - toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.'); + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); } - }} - /> - ); - })} + } else { + toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.'); + } + }} + /> + ); + })} - {/* Пагинация для PartsIndex */} -
- - - - Страница {currentUserPage} из {Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE) || 1} - {isAutoLoading && ' (загружаем...)'} - - (товаров: {accumulatedEntities.length}) - - - - -
+ {/* Кнопка "Показать еще" */} + {visibleCount < accumulatedEntities.length && ( +
+ +
+ )} - {/* Отладочная информация */} + {/* Отладочная информация {isPartsIndexMode && (
🔍 Отладка PartsIndex (исправленная логика):
@@ -1286,7 +1258,7 @@ export default function Catalog() { {isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
- )} + )} */} )} diff --git a/src/styles/my.css b/src/styles/my.css index 3769d08..155f933 100644 --- a/src/styles/my.css +++ b/src/styles/my.css @@ -348,6 +348,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 +409,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 +516,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 +539,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; @@ -944,8 +970,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 { @@ -1268,8 +1295,8 @@ 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; - } + } */ } \ No newline at end of file diff --git a/src/styles/protekproject.webflow.css b/src/styles/protekproject.webflow.css index 6e812fb..b11ad36 100644 --- a/src/styles/protekproject.webflow.css +++ b/src/styles/protekproject.webflow.css @@ -1539,7 +1539,7 @@ body { grid-row-gap: 5px; flex-flow: column; align-self: auto; - margin-top: -30px; + /* margin-top: -30px; */ display: flex; } -- 2.49.0