diff --git a/src/components/BottomHead.tsx b/src/components/BottomHead.tsx index 16a63ef..5167b80 100644 --- a/src/components/BottomHead.tsx +++ b/src/components/BottomHead.tsx @@ -1,244 +1,366 @@ -import React, { useState, useEffect } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useQuery } from '@apollo/client'; -import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql'; -import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex'; -import { NavigationCategory } from '@/types'; + import React, { useState, useEffect } from "react"; + import Link from "next/link"; + import { useRouter } from "next/router"; + import { useQuery } from '@apollo/client'; + import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql'; + import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex'; + import { NavigationCategory } from '@/types'; -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; -} + 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; + } -// Fallback статичные данные -const fallbackTabData = [ - { - label: "Оригинальные каталоги", - heading: "Оригинальные каталоги", - links: [ - "Моторные масла", - "Трансмиссионные масла", - "Тормозные жидкости", - "Смазки", - "Дистиллированная вода", - "Жидкости для стеклоомывателей", - "Индустриальные жидкости", - "Антифриз и охлаждающие жидкости", - "Промывочные жидкости", - ], - }, - { - label: "Масла и технические жидкости", - heading: "Масла и технические жидкости", - links: [ - "Моторные масла", - "Трансмиссионные масла", - "Тормозные жидкости", - "Смазки", - "Дистиллированная вода", - "Жидкости для стеклоомывателей", - "Индустриальные жидкости", - "Антифриз и охлаждающие жидкости", - "Промывочные жидкости", - ], - }, - { - label: "Оборудование", - heading: "Оборудование", - links: [ - "Моторные масла", - "Трансмиссионные масла", - "Тормозные жидкости", - "Смазки", - "Дистиллированная вода", - "Жидкости для стеклоомывателей", - "Индустриальные жидкости", - "Антифриз и охлаждающие жидкости", - "Промывочные жидкости", - ], - }, -]; - -// Преобразуем данные PartsIndex в формат нашего меню -const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => { - console.log('🔄 Преобразуем каталоги PartsIndex:', catalogs.length, 'элементов'); - - const transformed = catalogs.map(catalog => { - const groupsCount = catalog.groups?.length || 0; - console.log(`📝 Каталог: "${catalog.name}" (${groupsCount} групп)`); - - 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 (links.length === 0) { - links = [catalog.name]; - } - - console.log(`🔗 Подкатегории для "${catalog.name}":`, links); - - return { - label: catalog.name, - heading: catalog.name, - links: links.slice(0, 9), // Ограничиваем максимум 9 элементов - catalogId: catalog.id // Сохраняем ID каталога для навигации - }; - }); - - console.log('✅ Преобразование завершено:', transformed.length, 'табов'); - return transformed; -}; - -// Функция для поиска иконки для категории -const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => { - console.log('🔍 Ищем иконку для catalogId:', catalogId); - console.log('📋 Доступные навигационные категории:', navigationCategories); - - // Ищем навигационную категорию для данного каталога (без группы) - const categoryIcon = navigationCategories.find( - nav => nav.partsIndexCatalogId === catalogId && (!nav.partsIndexGroupId || nav.partsIndexGroupId === '') - ); - - console.log('🎯 Найденная категория:', categoryIcon); - console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null); - - return categoryIcon?.icon || null; -}; - -const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => { - const isMobile = useIsMobile(); - const router = useRouter(); - const [mobileCategory, setMobileCategory] = useState(null); - const [tabData, setTabData] = useState(fallbackTabData); - const [activeTabIndex, setActiveTabIndex] = useState(0); - - console.log('🔄 BottomHead render:', { - menuOpen, - tabDataLength: tabData.length, - activeTabIndex, - isMobile - }); - - // --- Overlay animation state --- - const [showOverlay, setShowOverlay] = useState(false); - useEffect(() => { - if (menuOpen) { - setShowOverlay(true); - } else { - // Ждём окончания transition перед удалением из DOM - const timeout = setTimeout(() => setShowOverlay(false), 300); - return () => clearTimeout(timeout); - } - }, [menuOpen]); - // --- End overlay animation state --- - - // Получаем каталоги PartsIndex - const { data: catalogsData, loading, error } = useQuery( - GET_PARTSINDEX_CATEGORIES, + // Fallback статичные данные + const fallbackTabData = [ { - variables: { - lang: 'ru' - }, - errorPolicy: 'all', - onCompleted: (data) => { - console.log('🎉 Apollo Query onCompleted - данные получены:', data); - }, - onError: (error) => { - console.error('❌ Apollo Query onError:', error); - } - } - ); - - // Получаем навигационные категории с иконками - const { data: navigationData, loading: navigationLoading, error: navigationError } = useQuery<{ navigationCategories: NavigationCategory[] }>( - GET_NAVIGATION_CATEGORIES, + label: "Оригинальные каталоги", + heading: "Оригинальные каталоги", + links: [ + "Моторные масла", + "Трансмиссионные масла", + "Тормозные жидкости", + "Смазки", + "Дистиллированная вода", + "Жидкости для стеклоомывателей", + "Индустриальные жидкости", + "Антифриз и охлаждающие жидкости", + "Промывочные жидкости", + ], + }, { - errorPolicy: 'all', - onCompleted: (data) => { - console.log('🎉 Навигационные категории получены:', data); - }, - onError: (error) => { - console.error('❌ Ошибка загрузки навигационных категорий:', error); - } - } - ); + label: "Масла и технические жидкости", + heading: "Масла и технические жидкости", + links: [ + "Моторные масла", + "Трансмиссионные масла", + "Тормозные жидкости", + "Смазки", + "Дистиллированная вода", + "Жидкости для стеклоомывателей", + "Индустриальные жидкости", + "Антифриз и охлаждающие жидкости", + "Промывочные жидкости", + ], + }, + { + label: "Оборудование", + heading: "Оборудование", + links: [ + "Моторные масла", + "Трансмиссионные масла", + "Тормозные жидкости", + "Смазки", + "Дистиллированная вода", + "Жидкости для стеклоомывателей", + "Индустриальные жидкости", + "Антифриз и охлаждающие жидкости", + "Промывочные жидкости", + ], + }, + ]; - // Обновляем данные табов когда получаем данные от API - useEffect(() => { - if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) { - console.log('✅ Обновляем меню с данными PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов'); - console.log('🔍 Первые 3 каталога:', catalogsData.partsIndexCategoriesWithGroups.slice(0, 3).map(catalog => ({ - name: catalog.name, - id: catalog.id, - groupsCount: catalog.groups?.length || 0, - groups: catalog.groups?.slice(0, 3).map(group => group.name) - }))); + // Преобразуем данные PartsIndex в формат нашего меню + const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => { + console.log('🔄 Преобразуем каталоги PartsIndex:', catalogs.length, 'элементов'); + + const transformed = catalogs.map(catalog => { + const groupsCount = catalog.groups?.length || 0; + console.log(`📝 Каталог: "${catalog.name}" (${groupsCount} групп)`); - const apiTabData = transformPartsIndexToTabData(catalogsData.partsIndexCategoriesWithGroups); - setTabData(apiTabData); - // Сбрасываем активный таб на первый при обновлении данных - setActiveTabIndex(0); - } else if (error) { - console.warn('⚠️ Используем fallback данные из-за ошибки PartsIndex:', error); - setTabData(fallbackTabData); - setActiveTabIndex(0); - } - }, [catalogsData, error]); - - // Логирование для отладки - useEffect(() => { - if (loading) { - console.log('🔄 Загружаем каталоги PartsIndex...'); - } - if (error) { - console.error('❌ Ошибка загрузки каталогов PartsIndex:', error); - } - }, [loading, error]); - - // Обработка клика по категории для перехода в каталог с товарами - const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => { - console.log('🔍 Клик по категории:', { catalogId, categoryName, entityId }); - - // Закрываем меню - onClose(); - - // Переходим на страницу каталога с параметрами PartsIndex - router.push({ - pathname: '/catalog', - query: { - partsIndexCatalog: catalogId, - categoryName: encodeURIComponent(categoryName), - ...(entityId && { partsIndexCategory: entityId }) + 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 (links.length === 0) { + links = [catalog.name]; + } + + console.log(`🔗 Подкатегории для "${catalog.name}":`, links); + + return { + label: catalog.name, + heading: catalog.name, + links: links.slice(0, 9), // Ограничиваем максимум 9 элементов + catalogId: catalog.id // Сохраняем ID каталога для навигации + }; }); + + console.log('✅ Преобразование завершено:', transformed.length, 'табов'); + return transformed; }; - // Только мобильный UX - if (isMobile && menuOpen) { - // Оверлей для мобильного меню + // Функция для поиска иконки для категории + const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => { + console.log('🔍 Ищем иконку для catalogId:', catalogId); + console.log('📋 Доступные навигационные категории:', navigationCategories); + + // Ищем навигационную категорию для данного каталога (без группы) + const categoryIcon = navigationCategories.find( + nav => nav.partsIndexCatalogId === catalogId && (!nav.partsIndexGroupId || nav.partsIndexGroupId === '') + ); + + console.log('🎯 Найденная категория:', categoryIcon); + console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null); + + return categoryIcon?.icon || null; + }; + + const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => { + const isMobile = useIsMobile(); + const router = useRouter(); + const [mobileCategory, setMobileCategory] = useState(null); + const [tabData, setTabData] = useState(fallbackTabData); + const [activeTabIndex, setActiveTabIndex] = useState(0); + + console.log('🔄 BottomHead render:', { + menuOpen, + tabDataLength: tabData.length, + activeTabIndex, + isMobile + }); + + // --- Overlay animation state --- + const [showOverlay, setShowOverlay] = useState(false); + useEffect(() => { + if (menuOpen) { + setShowOverlay(true); + } else { + // Ждём окончания transition перед удалением из DOM + const timeout = setTimeout(() => setShowOverlay(false), 300); + return () => clearTimeout(timeout); + } + }, [menuOpen]); + // --- End overlay animation state --- + + // Получаем каталоги PartsIndex + const { data: catalogsData, loading, error } = useQuery( + GET_PARTSINDEX_CATEGORIES, + { + variables: { + lang: 'ru' + }, + errorPolicy: 'all', + onCompleted: (data) => { + console.log('🎉 Apollo Query onCompleted - данные получены:', data); + }, + onError: (error) => { + console.error('❌ Apollo Query onError:', error); + } + } + ); + + // Получаем навигационные категории с иконками + const { data: navigationData, loading: navigationLoading, error: navigationError } = useQuery<{ navigationCategories: NavigationCategory[] }>( + GET_NAVIGATION_CATEGORIES, + { + errorPolicy: 'all', + onCompleted: (data) => { + console.log('🎉 Навигационные категории получены:', data); + }, + onError: (error) => { + console.error('❌ Ошибка загрузки навигационных категорий:', error); + } + } + ); + + // Обновляем данные табов когда получаем данные от API + useEffect(() => { + if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) { + console.log('✅ Обновляем меню с данными PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов'); + console.log('🔍 Первые 3 каталога:', catalogsData.partsIndexCategoriesWithGroups.slice(0, 3).map(catalog => ({ + name: catalog.name, + id: catalog.id, + groupsCount: catalog.groups?.length || 0, + groups: catalog.groups?.slice(0, 3).map(group => group.name) + }))); + + const apiTabData = transformPartsIndexToTabData(catalogsData.partsIndexCategoriesWithGroups); + setTabData(apiTabData); + // Сбрасываем активный таб на первый при обновлении данных + setActiveTabIndex(0); + } else if (error) { + console.warn('⚠️ Используем fallback данные из-за ошибки PartsIndex:', error); + setTabData(fallbackTabData); + setActiveTabIndex(0); + } + }, [catalogsData, error]); + + // Логирование для отладки + useEffect(() => { + if (loading) { + console.log('🔄 Загружаем каталоги PartsIndex...'); + } + if (error) { + console.error('❌ Ошибка загрузки каталогов PartsIndex:', error); + } + }, [loading, error]); + + // Обработка клика по категории для перехода в каталог с товарами + const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => { + console.log('🔍 Клик по категории:', { catalogId, categoryName, entityId }); + + // Закрываем меню + onClose(); + + // Переходим на страницу каталога с параметрами PartsIndex + router.push({ + pathname: '/catalog', + query: { + partsIndexCatalog: catalogId, + categoryName: encodeURIComponent(categoryName), + ...(entityId && { partsIndexCategory: entityId }) + } + }); + }; + + // Только мобильный UX + if (isMobile && menuOpen) { + // Оверлей для мобильного меню + return ( + <> + {showOverlay && ( +
+ )} + {/* Экран подкатегорий */} + {mobileCategory ? ( +
+
+ + {mobileCategory.label} +
+
+ {mobileCategory.links.length === 1 ? ( +
{ + let subcategoryId = `${mobileCategory.catalogId}_0`; + if (mobileCategory.groups) { + for (const group of mobileCategory.groups) { + if (group.subgroups && group.subgroups.length > 0) { + const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === mobileCategory.links[0]); + if (foundSubgroup) { + subcategoryId = foundSubgroup.id; + break; + } + } else if (group.name === mobileCategory.links[0]) { + subcategoryId = group.id; + break; + } + } + } + const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)]; + const catalogId = activeCatalog?.id || 'fallback'; + handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId); + }} + style={{ cursor: "pointer" }} + > + Показать все +
+ ) : ( + mobileCategory.links.map((link: string, linkIndex: number) => ( +
{ + let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`; + if (mobileCategory.groups) { + for (const group of mobileCategory.groups) { + if (group.subgroups && group.subgroups.length > 0) { + const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link); + if (foundSubgroup) { + subcategoryId = foundSubgroup.id; + break; + } + } else if (group.name === link) { + subcategoryId = group.id; + break; + } + } + } + const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)]; + const catalogId = activeCatalog?.id || 'fallback'; + handleCategoryClick(catalogId, link, subcategoryId); + }} + > + {link} +
+ )) + )} +
+
+ ) : ( + // Экран выбора категории +
+
+ + Категории + {loading && (загрузка...)} +
+
+ {tabData.map((cat, index) => { + // Получаем ID каталога из данных PartsIndex или создаем fallback ID + const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.id || `fallback_${index}`; + + return ( +
{ + // Добавляем catalogId и groups для правильной обработки + const categoryWithData = { + ...cat, + catalogId, + groups: catalogsData?.partsIndexCategoriesWithGroups?.[index]?.groups + }; + setMobileCategory(categoryWithData); + }} + style={{ cursor: "pointer" }} + > + {cat.label} +
+ ); + })} +
+
+ )} + + ); + } + + // Десктоп: оставить всё как есть, но добавить оверлей return ( <> {showOverlay && ( @@ -248,157 +370,126 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v aria-label="Закрыть меню" /> )} - {/* Экран подкатегорий */} - {mobileCategory ? ( -
-
- - {mobileCategory.label} -
-
- {mobileCategory.links.length === 1 ? ( -
{ - let subcategoryId = `${mobileCategory.catalogId}_0`; - if (mobileCategory.groups) { - for (const group of mobileCategory.groups) { - if (group.subgroups && group.subgroups.length > 0) { - const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === mobileCategory.links[0]); - if (foundSubgroup) { - subcategoryId = foundSubgroup.id; - break; - } - } else if (group.name === mobileCategory.links[0]) { - subcategoryId = group.id; - break; - } - } - } - const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)]; - const catalogId = activeCatalog?.id || 'fallback'; - handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId); - }} - style={{ cursor: "pointer" }} - > - Показать все -
- ) : ( - mobileCategory.links.map((link: string, linkIndex: number) => ( -
{ - let subcategoryId = `${mobileCategory.catalogId}_${linkIndex}`; - if (mobileCategory.groups) { - for (const group of mobileCategory.groups) { + {showOverlay && ( +
+ )} + + + ); + }; -export default BottomHead; \ No newline at end of file + export default BottomHead; \ No newline at end of file diff --git a/src/components/CookieConsent.tsx b/src/components/CookieConsent.tsx index 34f2f76..a32c535 100644 --- a/src/components/CookieConsent.tsx +++ b/src/components/CookieConsent.tsx @@ -1,270 +1,64 @@ -import React, { useState, useEffect } from 'react'; -import { CookiePreferences, initializeAnalytics, initializeMarketing } from '@/lib/cookie-utils'; +import * as React from "react"; -interface CookieConsentProps { - onAccept?: () => void; - onDecline?: () => void; - onConfigure?: (preferences: CookiePreferences) => void; -} +const CookieConsent: React.FC = () => { + const [isVisible, setIsVisible] = React.useState(false); -const CookieConsent: React.FC = ({ onAccept, onDecline, onConfigure }) => { - const [isVisible, setIsVisible] = useState(false); - const [showDetails, setShowDetails] = useState(false); - const [preferences, setPreferences] = useState({ - necessary: true, // Всегда включены - analytics: false, - marketing: false, - functional: false, - }); - - useEffect(() => { - // Проверяем, есть ли уже согласие в localStorage + React.useEffect(() => { const cookieConsent = localStorage.getItem('cookieConsent'); if (!cookieConsent) { setIsVisible(true); } }, []); - const handleAcceptAll = () => { - const allAccepted = { - necessary: true, - analytics: true, - marketing: true, - functional: true, - }; + const handleAccept = () => { localStorage.setItem('cookieConsent', 'accepted'); - localStorage.setItem('cookiePreferences', JSON.stringify(allAccepted)); - - // Инициализируем сервисы после согласия - initializeAnalytics(); - initializeMarketing(); - setIsVisible(false); - onAccept?.(); - }; - - const handleDeclineAll = () => { - const onlyNecessary = { - necessary: true, - analytics: false, - marketing: false, - functional: false, - }; - localStorage.setItem('cookieConsent', 'declined'); - localStorage.setItem('cookiePreferences', JSON.stringify(onlyNecessary)); - setIsVisible(false); - onDecline?.(); - }; - - const handleSavePreferences = () => { - localStorage.setItem('cookieConsent', 'configured'); - localStorage.setItem('cookiePreferences', JSON.stringify(preferences)); - - // Инициализируем сервисы согласно настройкам - if (preferences.analytics) { - initializeAnalytics(); - } - if (preferences.marketing) { - initializeMarketing(); - } - - setIsVisible(false); - onConfigure?.(preferences); - }; - - const togglePreference = (key: keyof CookiePreferences) => { - if (key === 'necessary') return; // Необходимые cookies нельзя отключить - setPreferences(prev => ({ - ...prev, - [key]: !prev[key] - })); }; if (!isVisible) return null; return ( -
-
- {!showDetails ? ( - // Основной вид -
- {/* Текст согласия */} -
-
- {/* Иконка cookie */} -
- - - -
- -
-

- Мы используем файлы cookie -

-

- Наш сайт использует файлы cookie для улучшения работы сайта, персонализации контента и анализа трафика. - Продолжая использовать сайт, вы соглашаетесь с нашей{' '} - - политикой конфиденциальности - - {' '}и использованием файлов cookie. -

-
-
-
- - {/* Кнопки */} -
- - - -
-
- ) : ( - // Детальный вид с настройками -
- {/* Заголовок */} -
-

- Настройки файлов cookie -

- -
- - {/* Настройки cookies */} -
- {/* Необходимые cookies */} -
-
-
-

Необходимые cookies

- Обязательные -
-

- Эти файлы cookie необходимы для работы сайта и не могут быть отключены. -

-
-
-
-
-
-
-
- - {/* Аналитические cookies */} -
-
-

Аналитические cookies

-

- Помогают нам понять, как посетители взаимодействуют с сайтом. -

-
-
- -
-
- - {/* Маркетинговые cookies */} -
-
-

Маркетинговые cookies

-

- Используются для отслеживания посетителей и показа релевантной рекламы. -

-
-
- -
-
- - {/* Функциональные cookies */} -
-
-

Функциональные cookies

-

- Обеспечивают расширенную функциональность и персонализацию. -

-
-
- -
-
-
- - {/* Кнопки действий */} -
- - - -
-
- )} + <> + +
+
+ + Мы используем cookie-файлы, чтобы получить статистику, которая + помогает нам улучшать сайт для Вас. Нажимая Принять, вы даёте + согласие на использование ваших cookie-файлов. Подробнее о том, как + мы используем ваши персональные данные, в нашей{' '} + + + Политике обработки персональных данных. + +
+
-
+ ); };