Добавлено получение навигационных категорий с иконками и обновление логики отображения иконок в компоненте BottomHead. Обновлены типы данных и стили для навигационных иконок. Оптимизирована загрузка групп для категорий в компоненте BottomHeadPartsIndex.
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQuery, useLazyQuery } from '@apollo/client';
|
||||
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
||||
|
||||
function useIsMobile(breakpoint = 767) {
|
||||
const [isMobile, setIsMobile] = React.useState(false);
|
||||
@ -13,35 +14,38 @@ function useIsMobile(breakpoint = 767) {
|
||||
return isMobile;
|
||||
}
|
||||
|
||||
// Типы для Parts Index API
|
||||
interface PartsIndexCatalog {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface PartsIndexEntityName {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PartsIndexGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
lang: string;
|
||||
image: string;
|
||||
lft: number;
|
||||
rgt: number;
|
||||
entityNames: PartsIndexEntityName[];
|
||||
subgroups: PartsIndexGroup[];
|
||||
}
|
||||
|
||||
// Типы данных
|
||||
interface PartsIndexTabData {
|
||||
label: string;
|
||||
heading: string;
|
||||
links: string[];
|
||||
catalogId: string;
|
||||
group?: PartsIndexGroup;
|
||||
group?: any;
|
||||
groupsLoaded?: boolean; // флаг что группы загружены
|
||||
}
|
||||
|
||||
interface PartsIndexCatalog {
|
||||
id: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
groups?: PartsIndexGroup[];
|
||||
}
|
||||
|
||||
interface PartsIndexGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
entityNames?: { id: string; name: string }[];
|
||||
subgroups?: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
// GraphQL типы
|
||||
interface PartsIndexCatalogsData {
|
||||
partsIndexCategoriesWithGroups: PartsIndexCatalog[];
|
||||
}
|
||||
|
||||
interface PartsIndexCatalogsVariables {
|
||||
lang?: 'ru' | 'en';
|
||||
}
|
||||
|
||||
// Fallback статичные данные
|
||||
@ -51,57 +55,66 @@ const fallbackTabData: PartsIndexTabData[] = [
|
||||
heading: "Детали ТО",
|
||||
catalogId: "parts_to",
|
||||
links: ["Детали ТО"],
|
||||
groupsLoaded: false,
|
||||
},
|
||||
{
|
||||
label: "Масла",
|
||||
heading: "Масла",
|
||||
catalogId: "oils",
|
||||
links: ["Масла"],
|
||||
groupsLoaded: false,
|
||||
},
|
||||
{
|
||||
label: "Шины",
|
||||
heading: "Шины",
|
||||
catalogId: "tyres",
|
||||
links: ["Шины"],
|
||||
groupsLoaded: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Сервис для работы с Parts Index API
|
||||
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
|
||||
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
||||
// Создаем базовые табы только с названиями каталогов
|
||||
const createBaseTabData = (catalogs: PartsIndexCatalog[]): PartsIndexTabData[] => {
|
||||
console.log('🔄 Создаем базовые табы из каталогов:', catalogs.length, 'элементов');
|
||||
|
||||
return catalogs.map(catalog => ({
|
||||
label: catalog.name,
|
||||
heading: catalog.name,
|
||||
links: [catalog.name], // Изначально показываем только название каталога
|
||||
catalogId: catalog.id,
|
||||
groupsLoaded: false, // Группы еще не загружены
|
||||
}));
|
||||
};
|
||||
|
||||
async function fetchCatalogs(): Promise<PartsIndexCatalog[]> {
|
||||
try {
|
||||
const response = await fetch(`${PARTS_INDEX_API_BASE}/v1/catalogs?lang=ru`, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return data.list;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения каталогов Parts Index:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCatalogGroup(catalogId: string): Promise<PartsIndexGroup | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${PARTS_INDEX_API_BASE}/v1/catalogs/${catalogId}/groups?lang=ru`,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': API_KEY,
|
||||
},
|
||||
// Преобразуем данные PartsIndex в формат нашего меню с группами
|
||||
const transformPartsIndexToTabData = (catalog: PartsIndexCatalog): string[] => {
|
||||
console.log(`📝 Обрабатываем группы каталога: "${catalog.name}"`);
|
||||
|
||||
let links: string[] = [];
|
||||
|
||||
if (catalog.groups && catalog.groups.length > 0) {
|
||||
// Для каждой группы проверяем есть ли подгруппы
|
||||
catalog.groups.forEach(group => {
|
||||
if (group.subgroups && group.subgroups.length > 0) {
|
||||
// Если есть подгруппы, добавляем их названия
|
||||
links.push(...group.subgroups.slice(0, 9 - links.length).map(subgroup => subgroup.name));
|
||||
} else {
|
||||
// Если подгрупп нет, добавляем название самой группы
|
||||
if (links.length < 9) {
|
||||
links.push(group.name);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Ошибка получения группы каталога ${catalogId}:`, error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Если подкатегорий нет, показываем название категории
|
||||
if (links.length === 0) {
|
||||
links = [catalog.name];
|
||||
}
|
||||
|
||||
console.log(`🔗 Подкатегории для "${catalog.name}":`, links);
|
||||
return links.slice(0, 9); // Ограничиваем максимум 9 элементов
|
||||
};
|
||||
|
||||
const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||
const isMobile = useIsMobile();
|
||||
@ -109,7 +122,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
||||
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingGroups, setLoadingGroups] = useState<Set<number>>(new Set());
|
||||
|
||||
// Пагинация категорий
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
@ -126,52 +139,116 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
// Загрузка каталогов и их групп
|
||||
// Получаем только каталоги PartsIndex (без групп для начальной загрузки)
|
||||
const { data: catalogsData, loading, error } = useQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
|
||||
GET_PARTSINDEX_CATEGORIES,
|
||||
{
|
||||
variables: {
|
||||
lang: 'ru'
|
||||
},
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-first', // Используем кэширование агрессивно
|
||||
nextFetchPolicy: 'cache-first', // Продолжаем использовать кэш
|
||||
notifyOnNetworkStatusChange: false,
|
||||
onCompleted: (data) => {
|
||||
console.log('🎉 PartsIndex каталоги получены через GraphQL (базовые):', data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ Ошибка загрузки PartsIndex каталогов:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Ленивый запрос для загрузки групп конкретного каталога
|
||||
const [loadCatalogGroups, { loading: groupsLoading }] = useLazyQuery<PartsIndexCatalogsData, PartsIndexCatalogsVariables>(
|
||||
GET_PARTSINDEX_CATEGORIES,
|
||||
{
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-first',
|
||||
nextFetchPolicy: 'cache-first',
|
||||
notifyOnNetworkStatusChange: false,
|
||||
onCompleted: (data) => {
|
||||
console.log('🎉 Группы каталога загружены:', data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ Ошибка загрузки групп каталога:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Обновляем базовые данные табов когда получаем каталоги
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (tabData === fallbackTabData) { // Загружаем только если еще не загружали
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('🔄 Загружаем каталоги Parts Index...');
|
||||
const catalogs = await fetchCatalogs();
|
||||
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
||||
console.log('✅ Обновляем базовое меню PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов');
|
||||
|
||||
const baseTabData = createBaseTabData(catalogsData.partsIndexCategoriesWithGroups);
|
||||
setTabData(baseTabData);
|
||||
setActiveTabIndex(0);
|
||||
} else if (error) {
|
||||
console.warn('⚠️ Используем fallback данные из-за ошибки PartsIndex:', error);
|
||||
setTabData(fallbackTabData);
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
}, [catalogsData, error]);
|
||||
|
||||
// Функция для ленивой загрузки групп при наведении на таб
|
||||
const loadGroupsForTab = async (tabIndex: number) => {
|
||||
const tab = tabData[tabIndex];
|
||||
if (!tab || tab.groupsLoaded || loadingGroups.has(tabIndex)) {
|
||||
return; // Группы уже загружены или загружаются
|
||||
}
|
||||
|
||||
console.log('🔄 Загружаем группы для каталога:', tab.catalogId);
|
||||
setLoadingGroups(prev => new Set([...prev, tabIndex]));
|
||||
|
||||
try {
|
||||
const result = await loadCatalogGroups({
|
||||
variables: {
|
||||
lang: 'ru'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.data?.partsIndexCategoriesWithGroups) {
|
||||
const catalog = result.data.partsIndexCategoriesWithGroups.find(c => c.id === tab.catalogId);
|
||||
if (catalog) {
|
||||
const links = transformPartsIndexToTabData(catalog);
|
||||
|
||||
if (catalogs.length > 0) {
|
||||
console.log(`✅ Получено ${catalogs.length} каталогов`);
|
||||
|
||||
// Загружаем группы для первых нескольких каталогов
|
||||
const catalogsToLoad = catalogs.slice(0, 10);
|
||||
const tabDataPromises = catalogsToLoad.map(async (catalog) => {
|
||||
const group = await fetchCatalogGroup(catalog.id);
|
||||
|
||||
// Получаем подкатегории из entityNames или повторяем название категории
|
||||
const links = group?.entityNames && group.entityNames.length > 0
|
||||
? group.entityNames.slice(0, 9).map(entity => entity.name)
|
||||
: [catalog.name]; // Если нет подкатегорий, повторяем название категории
|
||||
|
||||
return {
|
||||
label: catalog.name,
|
||||
heading: catalog.name,
|
||||
links,
|
||||
catalogId: catalog.id,
|
||||
group
|
||||
};
|
||||
});
|
||||
|
||||
const apiTabData = await Promise.all(tabDataPromises);
|
||||
console.log('✅ Данные обновлены:', apiTabData.length, 'категорий');
|
||||
setTabData(apiTabData as PartsIndexTabData[]);
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных Parts Index:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Обновляем конкретный таб с загруженными группами
|
||||
setTabData(prevTabs => {
|
||||
const newTabs = [...prevTabs];
|
||||
newTabs[tabIndex] = {
|
||||
...newTabs[tabIndex],
|
||||
links,
|
||||
group: catalog.groups?.[0],
|
||||
groupsLoaded: true
|
||||
};
|
||||
return newTabs;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки групп для каталога:', tab.catalogId, error);
|
||||
} finally {
|
||||
setLoadingGroups(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(tabIndex);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
// Обработчик наведения на таб - загружаем группы
|
||||
const handleTabHover = (tabIndex: number) => {
|
||||
loadGroupsForTab(tabIndex);
|
||||
};
|
||||
|
||||
// Обработчик клика на таб
|
||||
const handleTabClick = (tabIndex: number) => {
|
||||
setActiveTabIndex(tabIndex);
|
||||
|
||||
// Загружаем группы если еще не загружены
|
||||
loadGroupsForTab(tabIndex);
|
||||
};
|
||||
|
||||
// Обработка клика по категории для перехода в каталог
|
||||
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
|
||||
@ -184,7 +261,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
query: {
|
||||
partsIndexCatalog: catalogId,
|
||||
categoryName: encodeURIComponent(categoryName),
|
||||
...(entityId && { entityId })
|
||||
...(entityId && { partsIndexCategory: entityId })
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -294,6 +371,12 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
className="mobile-subcategory"
|
||||
key={cat.catalogId}
|
||||
onClick={() => {
|
||||
// Загружаем группы для категории если нужно
|
||||
const catIndex = tabData.findIndex(tab => tab.catalogId === cat.catalogId);
|
||||
if (catIndex !== -1) {
|
||||
loadGroupsForTab(catIndex);
|
||||
}
|
||||
|
||||
const categoryWithData = {
|
||||
...cat,
|
||||
catalogId: cat.catalogId,
|
||||
@ -304,6 +387,9 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{cat.label}
|
||||
{loadingGroups.has(tabData.findIndex(tab => tab.catalogId === cat.catalogId)) && (
|
||||
<span className="text-xs text-gray-500 ml-2">(загрузка...)</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -367,9 +453,8 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
href="#"
|
||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||
key={tab.catalogId}
|
||||
onClick={() => {
|
||||
setActiveTabIndex(idx);
|
||||
}}
|
||||
onClick={() => handleTabClick(idx)}
|
||||
onMouseEnter={() => handleTabHover(idx)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className="div-block-29">
|
||||
@ -388,6 +473,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
||||
<h3 className="heading-16">
|
||||
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
||||
{loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>}
|
||||
{loadingGroups.has(activeTabIndex) && <span className="text-sm text-gray-500 ml-2">(загрузка групп...)</span>}
|
||||
</h3>
|
||||
<div className="w-layout-hflex flex-block-92">
|
||||
<div className="w-layout-vflex flex-block-91">
|
||||
|
Reference in New Issue
Block a user