first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:59 +03:00
commit d44874775c
450 changed files with 76635 additions and 0 deletions

850
src/pages/catalog.tsx Normal file
View File

@ -0,0 +1,850 @@
import Head from "next/head";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import ProductListCard from "@/components/ProductListCard";
import Filters, { FilterConfig } from "@/components/Filters";
import FiltersWithSearch from "@/components/FiltersWithSearch";
import CatalogProductCard from "@/components/CatalogProductCard";
import CatalogPagination from "@/components/CatalogPagination";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import CatalogInfoHeader from "@/components/CatalogInfoHeader";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { useQuery } from '@apollo/client';
import FiltersPanelMobile from '@/components/FiltersPanelMobile';
import MobileMenuBottomSection from '../components/MobileMenuBottomSection';
import { GET_PARTSAPI_ARTICLES, GET_PARTSAPI_MAIN_IMAGE, SEARCH_PRODUCT_OFFERS, GET_PARTSINDEX_CATALOG_ENTITIES, GET_PARTSINDEX_CATALOG_PARAMS } from '@/lib/graphql';
import { PartsAPIArticlesData, PartsAPIArticlesVariables, PartsAPIArticle, PartsAPIMainImageData, PartsAPIMainImageVariables } from '@/types/partsapi';
import { PartsIndexEntitiesData, PartsIndexEntitiesVariables, PartsIndexEntity, PartsIndexParamsData, PartsIndexParamsVariables } from '@/types/partsindex';
import LoadingSpinner from '@/components/LoadingSpinner';
import ArticleCard from '@/components/ArticleCard';
import CatalogEmptyState from '@/components/CatalogEmptyState';
import { useProductPrices } from '@/hooks/useProductPrices';
import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton';
import { useCart } from '@/contexts/CartContext';
import toast from 'react-hot-toast';
const mockData = Array(12).fill({
image: "",
discount: "-35%",
price: "от 17 087 ₽",
oldPrice: "22 347 ₽",
title: 'Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60',
brand: "Borsehung",
});
const ITEMS_PER_PAGE = 20;
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
export default function Catalog() {
const router = useRouter();
const { addItem } = useCart();
const {
partsApiCategory: strId,
categoryName,
partsIndexCatalog: catalogId,
partsIndexCategory: groupId
} = router.query;
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
const [showSortMobile, setShowSortMobile] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({});
const [visibleArticles, setVisibleArticles] = useState<PartsAPIArticle[]>([]);
const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [entitiesPage, setEntitiesPage] = useState(1); // Страница для PartsIndex
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [hasMoreEntities, setHasMoreEntities] = useState(true); // Есть ли еще товары на сервере
const [showAllBrands, setShowAllBrands] = useState(false);
const [catalogFilters, setCatalogFilters] = useState<FilterConfig[]>([]);
const [filtersLoading, setFiltersLoading] = useState(true);
const [sortActive, setSortActive] = useState(0);
const [visibleProductsCount, setVisibleProductsCount] = useState(0); // Счетчик товаров с предложениями
const [filtersGenerating, setFiltersGenerating] = useState(false); // Состояние генерации фильтров
const [targetVisibleCount, setTargetVisibleCount] = useState(ITEMS_PER_PAGE); // Целевое количество видимых товаров
const [loadedArticlesCount, setLoadedArticlesCount] = useState(ITEMS_PER_PAGE); // Количество загруженных артикулов
const [showEmptyState, setShowEmptyState] = useState(false);
const [partsIndexPage, setPartsIndexPage] = useState(1); // Текущая страница для PartsIndex
const [totalPages, setTotalPages] = useState(1); // Общее количество страниц
// Карта видимости товаров по индексу
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
// Обработчик изменения видимости товара
const handleVisibilityChange = useCallback((index: number, isVisible: boolean) => {
setVisibilityMap(prev => {
const currentVisibility = prev.get(index);
// Обновляем только если значение действительно изменилось
if (currentVisibility === isVisible) {
return prev;
}
const newMap = new Map(prev);
newMap.set(index, isVisible);
return newMap;
});
}, []);
// Пересчитываем количество видимых товаров при изменении карты видимости
useEffect(() => {
const visibleCount = Array.from(visibilityMap.values()).filter(Boolean).length;
setVisibleProductsCount(visibleCount);
}, [visibilityMap]);
// Определяем режим работы
const isPartsAPIMode = Boolean(strId && categoryName);
const isPartsIndexMode = Boolean(catalogId && categoryName && groupId); // Требуем groupId для PartsIndex
const isPartsIndexCatalogOnly = Boolean(catalogId && categoryName && !groupId); // Каталог без группы
// Отладочная информация
console.log('🔍 Режимы работы каталога:', {
catalogId,
groupId,
categoryName,
isPartsAPIMode,
isPartsIndexMode,
isPartsIndexCatalogOnly
});
// Загружаем артикулы PartsAPI
const { data: articlesData, loading: articlesLoading, error: articlesError } = useQuery<PartsAPIArticlesData, PartsAPIArticlesVariables>(
GET_PARTSAPI_ARTICLES,
{
variables: {
strId: parseInt(strId as string),
carId: 9877,
carType: 'PC'
},
skip: !isPartsAPIMode,
fetchPolicy: 'cache-first'
}
);
const allArticles = articlesData?.partsAPIArticles || [];
// Загружаем товары PartsIndex
const { data: entitiesData, loading: entitiesLoading, error: entitiesError, refetch: refetchEntities } = useQuery<PartsIndexEntitiesData, PartsIndexEntitiesVariables>(
GET_PARTSINDEX_CATALOG_ENTITIES,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
limit: ITEMS_PER_PAGE,
page: partsIndexPage,
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-and-network'
}
);
// Загружаем параметры фильтрации для PartsIndex
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
GET_PARTSINDEX_CATALOG_PARAMS,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-first'
}
);
// allEntities больше не используется - используем allLoadedEntities
// Хук для загрузки цен товаров PartsIndex
const productsForPrices = visibleEntities.map(entity => ({
id: entity.id,
code: entity.code,
brand: entity.brand.name
}));
const { getPrice, isLoadingPrice, loadPriceOnDemand } = useProductPrices(productsForPrices);
useEffect(() => {
if (articlesData?.partsAPIArticles) {
// Загружаем изначально только ITEMS_PER_PAGE товаров
const initialLoadCount = Math.min(ITEMS_PER_PAGE, articlesData.partsAPIArticles.length);
setVisibleArticles(articlesData.partsAPIArticles.slice(0, initialLoadCount));
setLoadedArticlesCount(initialLoadCount);
setTargetVisibleCount(ITEMS_PER_PAGE);
setCurrentPage(1);
}
}, [articlesData]);
useEffect(() => {
if (entitiesData?.partsIndexCatalogEntities?.list) {
console.log('📊 Обновляем entitiesData:', {
listLength: entitiesData.partsIndexCatalogEntities.list.length,
pagination: entitiesData.partsIndexCatalogEntities.pagination,
currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1
});
const newEntities = entitiesData.partsIndexCatalogEntities.list;
const pagination = entitiesData.partsIndexCatalogEntities.pagination;
// Обновляем список товаров
setVisibleEntities(newEntities);
// Обновляем информацию о пагинации
const currentPage = pagination?.page?.current || 1;
const hasNext = pagination?.page?.next !== null;
const hasPrev = pagination?.page?.prev !== null;
setPartsIndexPage(currentPage);
setHasMoreEntities(hasNext);
// Вычисляем общее количество страниц (приблизительно)
if (hasNext) {
setTotalPages(currentPage + 1); // Минимум еще одна страница
} else {
setTotalPages(currentPage); // Это последняя страница
}
console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev });
}
}, [entitiesData]);
// Генерация фильтров для PartsIndex на основе параметров API
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
if (!paramsData?.partsIndexCatalogParams?.list) {
return [];
}
return paramsData.partsIndexCatalogParams.list.map(param => {
if (param.type === 'range') {
// Для range фильтров ищем min и max значения
const numericValues = param.values
.map(v => parseFloat(v.value))
.filter(v => !isNaN(v));
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
return {
type: 'range' as const,
title: param.name,
min,
max
};
} else {
// Для dropdown фильтров
return {
type: 'dropdown' as const,
title: param.name,
options: param.values
.filter(value => value.available) // Показываем только доступные
.map(value => value.title || value.value),
multi: true,
showAll: true
};
}
});
}, [paramsData]);
useEffect(() => {
if (isPartsIndexMode) {
// Для PartsIndex генерируем фильтры на основе параметров API
const filters = generatePartsIndexFilters();
setCatalogFilters(filters);
setFiltersLoading(paramsLoading);
} else {
// Для других режимов убираем запрос на catalog-filters
setFiltersLoading(false);
}
}, [isPartsIndexMode, generatePartsIndexFilters, paramsLoading]);
// Генерируем динамические фильтры для PartsAPI
const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
if (!allArticles.length) return [];
// Получаем список видимых товаров из карты видимости
const visibleIndices = Array.from(visibilityMap.entries())
.filter(([_, isVisible]) => isVisible)
.map(([index]) => index);
// Если еще нет данных о видимости, используем все товары (для начальной загрузки)
const articlesToProcess = visibilityMap.size === 0 ? allArticles :
visibleIndices.map(index => allArticles[index]).filter(Boolean);
const brandCounts = new Map<string, number>();
const productGroups = new Set<string>();
// Подсчитываем количество товаров для каждого бренда (только видимые)
articlesToProcess.forEach(article => {
if (article?.artSupBrand) {
brandCounts.set(article.artSupBrand, (brandCounts.get(article.artSupBrand) || 0) + 1);
}
if (article?.productGroup) productGroups.add(article.productGroup);
});
const filters: FilterConfig[] = [];
if (brandCounts.size > 1) {
// Сортируем бренды по количеству товаров (по убыванию)
const sortedBrands = Array.from(brandCounts.entries())
.sort((a, b) => b[1] - a[1]) // Сортируем по количеству товаров
.map(([brand]) => brand);
// Показываем либо первые N брендов, либо все (если нажата кнопка "Показать еще")
const brandsToShow = showAllBrands ? sortedBrands : sortedBrands.slice(0, MAX_BRANDS_DISPLAY);
filters.push({
type: "dropdown",
title: "Бренд",
options: brandsToShow.sort(), // Сортируем по алфавиту для удобства
multi: true,
showAll: true,
defaultOpen: true,
hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY,
onShowMore: () => setShowAllBrands(true)
});
}
if (productGroups.size > 1) {
filters.push({
type: "dropdown",
title: "Группа товаров",
options: Array.from(productGroups).sort(),
multi: true,
showAll: true,
defaultOpen: true,
});
}
return filters;
}, [allArticles, showAllBrands, visibilityMap]);
const dynamicFilters = useMemo(() => {
if (isPartsIndexMode) {
return generatePartsIndexFilters();
} else if (isPartsAPIMode) {
return generatePartsAPIFilters();
}
return [];
}, [isPartsIndexMode, isPartsAPIMode, generatePartsIndexFilters, generatePartsAPIFilters]);
// Отдельный useEffect для управления состоянием загрузки фильтров
useEffect(() => {
if ((isPartsAPIMode && allArticles.length > 0) || (isPartsIndexMode && visibleEntities.length > 0)) {
setFiltersGenerating(true);
const timer = setTimeout(() => {
setFiltersGenerating(false);
}, 300);
return () => clearTimeout(timer);
} else {
setFiltersGenerating(false);
}
}, [isPartsAPIMode, allArticles.length, isPartsIndexMode, visibleEntities.length]);
const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => {
setSelectedFilters(prev => ({
...prev,
[filterTitle]: Array.isArray(value) ? value : [value]
}));
};
const handleMobileFilterChange = (type: string, value: any) => {
setSelectedFilters(prev => ({
...prev,
[type]: Array.isArray(value) ? value : [value]
}));
};
// Функция для сброса всех фильтров
const handleResetFilters = useCallback(() => {
setSearchQuery('');
setSelectedFilters({});
setShowAllBrands(false);
setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую
}, []);
// Фильтрация по поиску и фильтрам для PartsAPI
const filteredArticles = useMemo(() => {
return allArticles.filter(article => {
// Фильтрация по поиску
if (searchQuery.trim()) {
const searchLower = searchQuery.toLowerCase();
const articleTitle = [
article.artSupBrand || '',
article.artArticleNr || '',
article.productGroup || ''
].join(' ').toLowerCase();
if (!articleTitle.includes(searchLower)) {
return false;
}
}
// Фильтрация по выбранным фильтрам
const brandFilter = selectedFilters['Бренд'] || [];
if (brandFilter.length > 0 && !brandFilter.includes(article.artSupBrand || '')) {
return false;
}
const groupFilter = selectedFilters['Группа товаров'] || [];
if (groupFilter.length > 0 && !groupFilter.includes(article.productGroup || '')) {
return false;
}
return true;
});
}, [allArticles, searchQuery, selectedFilters]);
// Упрощенная логика - показываем все загруженные товары без клиентской фильтрации
const filteredEntities = visibleEntities;
// Обновляем видимые артикулы при изменении поиска или фильтров для PartsAPI
useEffect(() => {
if (isPartsAPIMode) {
setVisibleArticles(filteredArticles.slice(0, ITEMS_PER_PAGE));
setCurrentPage(1);
setIsLoadingMore(false);
setVisibilityMap(new Map()); // Сбрасываем карту видимости при изменении фильтров
setVisibleProductsCount(0); // Сбрасываем счетчик
setLoadedArticlesCount(ITEMS_PER_PAGE); // Сбрасываем счетчик загруженных
setShowEmptyState(false); // Сбрасываем пустое состояние
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsAPIMode, searchQuery, JSON.stringify(selectedFilters), filteredArticles.length]);
// Обновляем видимые товары при изменении поиска или фильтров для PartsIndex
useEffect(() => {
if (isPartsIndexMode) {
// При изменении поиска или фильтров сбрасываем пагинацию
setShowEmptyState(false);
// Если изменился поисковый запрос, нужно перезагрузить данные с сервера
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
setPartsIndexPage(1);
setHasMoreEntities(true);
// refetch будет автоматически вызван при изменении partsIndexPage
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]);
// Управляем показом пустого состояния с задержкой
useEffect(() => {
if (isPartsAPIMode && !articlesLoading && !articlesError) {
// Если товаров вообще нет - показываем сразу
if (allArticles.length === 0) {
setShowEmptyState(true);
return;
}
// Если товары есть, но нет видимых - ждем 2 секунды
const timer = setTimeout(() => {
setShowEmptyState(visibleProductsCount === 0 && allArticles.length > 0);
}, 2000); // Даем 2 секунды на загрузку данных о предложениях
return () => clearTimeout(timer);
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
// Для PartsIndex показываем пустое состояние если нет товаров
setShowEmptyState(visibleEntities.length === 0);
} else {
setShowEmptyState(false);
}
}, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length,
isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, filteredEntities.length]);
// Функции для навигации по страницам PartsIndex
const handleNextPage = useCallback(() => {
if (hasMoreEntities && !entitiesLoading) {
setPartsIndexPage(prev => prev + 1);
}
}, [hasMoreEntities, entitiesLoading]);
const handlePrevPage = useCallback(() => {
if (partsIndexPage > 1 && !entitiesLoading) {
setPartsIndexPage(prev => prev - 1);
}
}, [partsIndexPage, entitiesLoading]);
// Функция для загрузки следующей порции товаров по кнопке (только для PartsAPI)
const handleLoadMorePartsAPI = useCallback(async () => {
if (isLoadingMore || !isPartsAPIMode) {
return;
}
setIsLoadingMore(true);
try {
const additionalCount = Math.min(ITEMS_PER_PAGE, filteredArticles.length - loadedArticlesCount);
if (additionalCount > 0) {
const newArticles = filteredArticles.slice(loadedArticlesCount, loadedArticlesCount + additionalCount);
setVisibleArticles(prev => [...prev, ...newArticles]);
setLoadedArticlesCount(prev => prev + additionalCount);
setTargetVisibleCount(prev => prev + ITEMS_PER_PAGE);
}
} catch (error) {
console.error('❌ Ошибка загрузки дополнительных товаров:', error);
} finally {
setIsLoadingMore(false);
}
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles, isLoadingMore]);
// Определяем есть ли еще товары для загрузки (только для PartsAPI)
const hasMoreItems = useMemo(() => {
if (isPartsAPIMode) {
return loadedArticlesCount < filteredArticles.length;
}
return false;
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]);
if (filtersLoading) {
return <div className="py-8 text-center">Загрузка фильтров...</div>;
}
return (
<>
<Head>
<title>Catalog</title>
<meta name="description" content="Catalog" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CatalogInfoHeader
title={
isPartsAPIMode ? decodeURIComponent(categoryName as string || 'Запчасти') :
isPartsIndexMode ? decodeURIComponent(categoryName as string || 'Товары') :
"Аккумуляторы"
}
count={
isPartsAPIMode ?
(visibilityMap.size === 0 && allArticles.length > 0 ? undefined : visibleProductsCount) :
isPartsIndexMode ?
(searchQuery.trim() || Object.keys(selectedFilters).length > 0 ?
filteredEntities.length :
entitiesData?.partsIndexCatalogEntities?.pagination?.limit || visibleEntities.length) :
3587
}
productName={
isPartsAPIMode ? "запчасть" :
isPartsIndexMode ? "товар" :
"аккумулятор"
}
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Каталог" },
...((isPartsAPIMode || isPartsIndexMode) ? [{ label: decodeURIComponent(categoryName as string || 'Товары') }] : [])
]}
showCount={true}
showProductHelp={true}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-13">
<div className="w-layout-hflex flex-block-84">
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
<div 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">
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>Фильтры</div>
</div>
</div>
{isPartsAPIMode ? (
<div className="filters-desktop">
<Filters
filters={dynamicFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersGenerating}
/>
</div>
) : isPartsIndexMode ? (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
) : (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
)}
<FiltersPanelMobile
open={showFiltersMobile}
onClose={() => setShowFiltersMobile(false)}
filters={isPartsAPIMode ? dynamicFilters : catalogFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filterValues={selectedFilters}
onFilterChange={handleMobileFilterChange}
/>
<div className="w-layout-vflex flex-block-14-copy-copy">
{/* Индикатор загрузки для PartsAPI */}
{isPartsAPIMode && articlesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем артикулы..." />
</div>
)}
{/* Индикатор загрузки для PartsIndex */}
{isPartsIndexMode && entitiesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем товары..." />
</div>
)}
{/* Сообщение об ошибке */}
{isPartsAPIMode && articlesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки артикулов: {articlesError.message}</div>
</div>
)}
{/* Сообщение об ошибке для PartsIndex */}
{isPartsIndexMode && entitiesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки товаров: {entitiesError.message}</div>
</div>
)}
{/* Отображение артикулов PartsAPI */}
{isPartsAPIMode && visibleArticles.length > 0 && (
<>
{visibleArticles.map((article, idx) => (
<ArticleCard
key={`${article.artId}_${idx}`}
article={article}
index={idx}
onVisibilityChange={handleVisibilityChange}
/>
))}
{/* Кнопка "Показать еще" */}
{hasMoreItems && (
<div className="w-layout-hflex pagination">
<button
onClick={handleLoadMorePartsAPI}
disabled={isLoadingMore}
className="button_strock w-button"
>
{isLoadingMore ? (
<>
Загружаем...
</>
) : (
<>
Показать еще
</>
)}
</button>
</div>
)}
</>
)}
{/* Отображение товаров PartsIndex */}
{isPartsIndexMode && filteredEntities.length > 0 && (
<>
{filteredEntities
.map((entity, idx) => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(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 displayCurrency = "RUB";
let priceElement;
if (isLoadingPriceData) {
priceElement = <PriceSkeleton />;
} else if (priceData && priceData.price) {
displayPrice = `${priceData.price.toLocaleString('ru-RU')}`;
displayCurrency = priceData.currency || "RUB";
}
return (
<CatalogProductCard
key={`${entity.id}_${idx}`}
title={entity.originalName || entity.name?.name || 'Товар без названия'}
brand={entity.brand.name}
articleNumber={entity.code}
brandName={entity.brand.name}
image={entity.images?.[0] || ''}
price={isLoadingPriceData ? "" : displayPrice}
priceElement={priceElement}
oldPrice=""
discount=""
currency={displayCurrency}
productId={entity.id}
artId={entity.id}
offerKey={priceData?.offerKey}
onAddToCart={() => {
// Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) {
loadPriceOnDemand(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,
deliveryTime: '1-3 дня',
warehouse: 'Parts Index',
supplier: 'Parts Index',
isExternal: true,
image: entity.images?.[0] || '',
};
addItem(itemToAdd);
// Показываем уведомление
toast.success(`Товар "${entity.brand.name} ${entity.code}" добавлен в корзину за ${priceData.price.toLocaleString('ru-RU')}`);
} else {
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
}
}}
/>
);
})}
{/* Пагинация для PartsIndex */}
<div className="w-layout-hflex pagination">
<button
onClick={handlePrevPage}
disabled={partsIndexPage <= 1 || entitiesLoading}
className="button_strock w-button mr-2"
>
Назад
</button>
<span className="flex items-center px-4 text-gray-600">
Страница {partsIndexPage} {totalPages > partsIndexPage && `из ${totalPages}+`}
</span>
<button
onClick={handleNextPage}
disabled={!hasMoreEntities || entitiesLoading}
className="button_strock w-button ml-2"
>
{entitiesLoading ? 'Загрузка...' : 'Вперед →'}
</button>
</div>
{/* Отладочная информация */}
{isPartsIndexMode && (
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<div>🔍 Отладка PartsIndex:</div>
<div> hasMoreItems: {hasMoreItems ? 'да' : 'нет'}</div>
<div> hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}</div>
<div> entitiesPage: {entitiesPage}</div>
<div> visibleEntities: {visibleEntities.length}</div>
<div> filteredEntities: {filteredEntities.length}</div>
<div> groupId: {groupId || 'отсутствует'}</div>
<div> isLoadingMore: {isLoadingMore ? 'да' : 'нет'}</div>
<div> entitiesLoading: {entitiesLoading ? 'да' : 'нет'}</div>
<div> catalogId: {catalogId || 'отсутствует'}</div>
<div> Пагинация: {JSON.stringify(entitiesData?.partsIndexCatalogEntities?.pagination)}</div>
</div>
)}
</>
)}
{/* Пустое состояние для PartsAPI */}
{isPartsAPIMode && !articlesLoading && !articlesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Пустое состояние для PartsIndex */}
{isPartsIndexMode && !entitiesLoading && !entitiesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Каталог PartsIndex без группы */}
{isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Выберите подкатегорию</div>
<div className="text-gray-400 text-sm">Для просмотра товаров необходимо выбрать конкретную подкатегорию из меню.</div>
</div>
)}
{/* Обычные товары (не PartsAPI/PartsIndex) */}
{!isPartsAPIMode && !isPartsIndexMode && !isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Раздел в разработке</div>
<div className="text-gray-400 text-sm">Данные для этой категории скоро появятся.</div>
</div>
)}
</div>
</div>
</div>
</section>
{!isPartsAPIMode && !isPartsIndexMode && <CatalogPagination />}
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}