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

View 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;