first commit
This commit is contained in:
439
src/components/BottomHeadPartsIndex.tsx
Normal file
439
src/components/BottomHeadPartsIndex.tsx
Normal file
@ -0,0 +1,439 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
function useIsMobile(breakpoint = 767) {
|
||||
const [isMobile, setIsMobile] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const check = () => setIsMobile(window.innerWidth <= breakpoint);
|
||||
check();
|
||||
window.addEventListener("resize", check);
|
||||
return () => window.removeEventListener("resize", check);
|
||||
}, [breakpoint]);
|
||||
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;
|
||||
}
|
||||
|
||||
// Fallback статичные данные
|
||||
const fallbackTabData: PartsIndexTabData[] = [
|
||||
{
|
||||
label: "Детали ТО",
|
||||
heading: "Детали ТО",
|
||||
catalogId: "parts_to",
|
||||
links: ["Детали ТО"],
|
||||
},
|
||||
{
|
||||
label: "Масла",
|
||||
heading: "Масла",
|
||||
catalogId: "oils",
|
||||
links: ["Масла"],
|
||||
},
|
||||
{
|
||||
label: "Шины",
|
||||
heading: "Шины",
|
||||
catalogId: "tyres",
|
||||
links: ["Шины"],
|
||||
},
|
||||
];
|
||||
|
||||
// Сервис для работы с Parts Index API
|
||||
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
|
||||
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Ошибка получения группы каталога ${catalogId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const router = useRouter();
|
||||
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
||||
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Пагинация категорий
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const categoriesPerPage = 6; // Количество категорий на странице
|
||||
|
||||
// --- Overlay animation state ---
|
||||
const [showOverlay, setShowOverlay] = useState(false);
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
setShowOverlay(true);
|
||||
} else {
|
||||
const timeout = setTimeout(() => setShowOverlay(false), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
// Загрузка каталогов и их групп
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (tabData === fallbackTabData) { // Загружаем только если еще не загружали
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('🔄 Загружаем каталоги Parts Index...');
|
||||
const catalogs = await fetchCatalogs();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Обработка клика по категории для перехода в каталог
|
||||
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
|
||||
console.log('🔍 Клик по категории Parts Index:', { catalogId, categoryName, entityId });
|
||||
|
||||
onClose();
|
||||
|
||||
router.push({
|
||||
pathname: '/catalog',
|
||||
query: {
|
||||
partsIndexCatalog: catalogId,
|
||||
categoryName: encodeURIComponent(categoryName),
|
||||
...(entityId && { entityId })
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Получаем текущие категории для отображения с пагинацией
|
||||
const getCurrentPageCategories = () => {
|
||||
const startIndex = currentPage * categoriesPerPage;
|
||||
const endIndex = startIndex + categoriesPerPage;
|
||||
return tabData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Проверяем, есть ли следующая/предыдущая страница
|
||||
const hasNextPage = (currentPage + 1) * categoriesPerPage < tabData.length;
|
||||
const hasPrevPage = currentPage > 0;
|
||||
|
||||
// Обработчики пагинации
|
||||
const handleNextPage = () => {
|
||||
if (hasNextPage) {
|
||||
setCurrentPage(prev => prev + 1);
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (hasPrevPage) {
|
||||
setCurrentPage(prev => prev - 1);
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const currentPageCategories = getCurrentPageCategories();
|
||||
|
||||
// Только мобильный UX
|
||||
if (isMobile && menuOpen) {
|
||||
return (
|
||||
<>
|
||||
{showOverlay && (
|
||||
<div
|
||||
className={`fixed inset-0 bg-black/7 z-40 transition-opacity duration-300 ${menuOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
aria-label="Закрыть меню"
|
||||
/>
|
||||
)}
|
||||
{/* Экран подкатегорий */}
|
||||
{mobileCategory ? (
|
||||
<div className="mobile-category-overlay z-50">
|
||||
<div className="mobile-header">
|
||||
<button className="mobile-back-btn" onClick={() => setMobileCategory(null)}>
|
||||
←
|
||||
</button>
|
||||
<span>{mobileCategory.label}</span>
|
||||
</div>
|
||||
<div className="mobile-subcategories">
|
||||
{mobileCategory.links.map((link: string, index: number) => (
|
||||
<div
|
||||
className="mobile-subcategory"
|
||||
key={link}
|
||||
onClick={() => {
|
||||
const entityId = mobileCategory.group?.entityNames?.[index]?.id;
|
||||
handleCategoryClick(mobileCategory.catalogId, link, entityId);
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Экран выбора категории
|
||||
<div className="mobile-category-overlay z-50">
|
||||
<div className="mobile-header">
|
||||
<button className="mobile-back-btn" onClick={onClose} aria-label="Закрыть меню">
|
||||
<svg width="24" height="24" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M4.11 2.697L2.698 4.11 6.586 8l-3.89 3.89 1.415 1.413L8 9.414l3.89 3.89 1.413-1.415L9.414 8l3.89-3.89-1.415-1.413L8 6.586l-3.89-3.89z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span>Категории Parts Index</span>
|
||||
{loading && <span className="text-sm text-gray-500 ml-2">(загрузка...)</span>}
|
||||
</div>
|
||||
|
||||
{/* Пагинация для мобильной версии */}
|
||||
{tabData.length > categoriesPerPage && (
|
||||
<div className="flex justify-between items-center px-4 py-2 bg-gray-50 border-b">
|
||||
<button
|
||||
onClick={handlePrevPage}
|
||||
disabled={!hasPrevPage}
|
||||
className="text-sm text-blue-600 disabled:text-gray-400"
|
||||
>
|
||||
← Предыдущие
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentPage + 1} из {Math.ceil(tabData.length / categoriesPerPage)}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={!hasNextPage}
|
||||
className="text-sm text-blue-600 disabled:text-gray-400"
|
||||
>
|
||||
Следующие →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mobile-subcategories">
|
||||
{currentPageCategories.map((cat) => (
|
||||
<div
|
||||
className="mobile-subcategory"
|
||||
key={cat.catalogId}
|
||||
onClick={() => {
|
||||
const categoryWithData = {
|
||||
...cat,
|
||||
catalogId: cat.catalogId,
|
||||
group: cat.group
|
||||
};
|
||||
setMobileCategory(categoryWithData);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{cat.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Если не мобильный или меню закрыто, возвращаем пустой элемент
|
||||
if (!menuOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Desktop версия
|
||||
return (
|
||||
<>
|
||||
{showOverlay && (
|
||||
<div
|
||||
className={`fixed inset-0 bg-black/7 z-40 transition-opacity duration-300 ${menuOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
aria-label="Закрыть меню"
|
||||
/>
|
||||
)}
|
||||
<div className="menu-all">
|
||||
<div className="div-block-28">
|
||||
<div className="w-layout-hflex flex-block-90">
|
||||
<div className="w-layout-vflex flex-block-88">
|
||||
{/* Кнопки пагинации */}
|
||||
{tabData.length > categoriesPerPage && (
|
||||
<div className="flex justify-between items-center mb-4 px-3">
|
||||
<button
|
||||
onClick={handlePrevPage}
|
||||
disabled={!hasPrevPage}
|
||||
className="flex items-center space-x-1 text-sm text-blue-600 disabled:text-gray-400 hover:underline"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Назад</span>
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentPage + 1} / {Math.ceil(tabData.length / categoriesPerPage)}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={!hasNextPage}
|
||||
className="flex items-center space-x-1 text-sm text-blue-600 disabled:text-gray-400 hover:underline"
|
||||
>
|
||||
<span>Далее</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Меню с иконками - показываем текущую страницу категорий */}
|
||||
{currentPageCategories.map((tab, idx) => (
|
||||
<a
|
||||
href="#"
|
||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||
key={tab.catalogId}
|
||||
onClick={() => {
|
||||
setActiveTabIndex(idx);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className="div-block-29">
|
||||
<div className="code-embed-12 w-embed">
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.3158 0.643914C10.4674 0.365938 10.8666 0.365938 11.0182 0.643914L14.0029 6.11673C14.0604 6.22222 14.1623 6.29626 14.2804 6.31838L20.4077 7.46581C20.7189 7.52409 20.8423 7.9037 20.6247 8.13378L16.3421 12.6636C16.2595 12.7509 16.2206 12.8707 16.2361 12.9899L17.0382 19.1718C17.079 19.4858 16.7561 19.7204 16.47 19.5847L10.8385 16.9114C10.73 16.8599 10.604 16.8599 10.4955 16.9114L4.86394 19.5847C4.5779 19.7204 4.25499 19.4858 4.29573 19.1718L5.0979 12.9899C5.11336 12.8707 5.07444 12.7509 4.99189 12.6636L0.709252 8.13378C0.491728 7.9037 0.615069 7.52409 0.926288 7.46581L7.05357 6.31838C7.17168 6.29626 7.27358 6.22222 7.33112 6.11673L10.3158 0.643914Z" fill="CurrentColor"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-block-47">{tab.label}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Правая часть меню с подкатегориями и картинками */}
|
||||
<div className="w-layout-vflex flex-block-89">
|
||||
<h3 className="heading-16">
|
||||
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
||||
{loading && <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">
|
||||
{(currentPageCategories[activeTabIndex]?.links || currentPageCategories[0]?.links || []).map((link, index) => {
|
||||
const activeCategory = currentPageCategories[activeTabIndex] || currentPageCategories[0];
|
||||
const entityId = activeCategory?.group?.entityNames?.[index]?.id;
|
||||
return (
|
||||
<div
|
||||
className="link-2"
|
||||
key={link}
|
||||
onClick={() => handleCategoryClick(activeCategory.catalogId, link, entityId)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-91-copy">
|
||||
<img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" className="image-17" />
|
||||
<img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" className="image-17" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Табы */}
|
||||
<div className="w-layout-hflex flex-block-93">
|
||||
<div className="w-layout-vflex flex-block-95">
|
||||
<div className="w-layout-hflex flex-block-94">
|
||||
<div className="text-block-48">Parts Index API</div>
|
||||
<div className="text-block-48">Каталоги ТО</div>
|
||||
<div className="text-block-48">Каталоги запчастей</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-96">
|
||||
<div className="text-block-49">Все каталоги</div>
|
||||
<img src="/images/Arrow_right.svg" loading="lazy" alt="" className="image-19" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-vflex flex-block-97">
|
||||
<img src="/images/img3.png" loading="lazy" alt="" className="image-18" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomHeadPartsIndex;
|
Reference in New Issue
Block a user