Merge remote changes, resolve conflicts in BottomHead.tsx

This commit is contained in:
Bivekich
2025-07-14 10:03:35 +03:00
2 changed files with 585 additions and 741 deletions

View File

@ -1,12 +1,12 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql'; import { GET_PARTSINDEX_CATEGORIES, GET_NAVIGATION_CATEGORIES } from '@/lib/graphql';
import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex'; import { PartsIndexCatalogsData, PartsIndexCatalogsVariables, PartsIndexCatalog } from '@/types/partsindex';
import { NavigationCategory } from '@/types'; import { NavigationCategory } from '@/types';
function useIsMobile(breakpoint = 767) { function useIsMobile(breakpoint = 767) {
const [isMobile, setIsMobile] = React.useState(false); const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
const check = () => setIsMobile(window.innerWidth <= breakpoint); const check = () => setIsMobile(window.innerWidth <= breakpoint);
@ -15,12 +15,12 @@ function useIsMobile(breakpoint = 767) {
return () => window.removeEventListener("resize", check); return () => window.removeEventListener("resize", check);
}, [breakpoint]); }, [breakpoint]);
return isMobile; return isMobile;
} }
// Fallback статичные данные // Fallback статичные данные
const fallbackTabData = [ const fallbackTabData = [
{ {
label: "Оригинальные каталоги", label: "Оригинальные каталоги",
heading: "Оригинальные каталоги", heading: "Оригинальные каталоги",
@ -66,10 +66,10 @@ const fallbackTabData = [
"Промывочные жидкости", "Промывочные жидкости",
], ],
}, },
]; ];
// Преобразуем данные PartsIndex в формат нашего меню // Преобразуем данные PartsIndex в формат нашего меню
const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => { const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
console.log('🔄 Преобразуем каталоги PartsIndex:', catalogs.length, 'элементов'); console.log('🔄 Преобразуем каталоги PartsIndex:', catalogs.length, 'элементов');
const transformed = catalogs.map(catalog => { const transformed = catalogs.map(catalog => {
@ -79,29 +79,21 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
let links: string[] = []; let links: string[] = [];
if (catalog.groups && catalog.groups.length > 0) { if (catalog.groups && catalog.groups.length > 0) {
// Сначала собираем все подгруппы из всех групп // Для каждой группы проверяем есть ли подгруппы
catalog.groups.forEach(group => { catalog.groups.forEach(group => {
if (group.subgroups && group.subgroups.length > 0) { if (group.subgroups && group.subgroups.length > 0) {
// Если есть подгруппы, добавляем их названия // Если есть подгруппы, добавляем их названия
group.subgroups.forEach(subgroup => { links.push(...group.subgroups.slice(0, 9 - links.length).map(subgroup => subgroup.name));
} else {
// Если подгрупп нет, добавляем название самой группы
if (links.length < 9) { if (links.length < 9) {
links.push(subgroup.name);
}
});
}
});
// Если подгрупп не набралось достаточно, добавляем названия групп
if (links.length < 9) {
catalog.groups.forEach(group => {
if (links.length < 9 && !links.includes(group.name)) {
links.push(group.name); links.push(group.name);
} }
}
}); });
} }
}
// Если подкатегорий всё ещё нет, добавляем название самой категории // Если подкатегорий нет, показываем название категории как указано в требованиях
if (links.length === 0) { if (links.length === 0) {
links = [catalog.name]; links = [catalog.name];
} }
@ -118,10 +110,10 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
console.log('✅ Преобразование завершено:', transformed.length, 'табов'); console.log('✅ Преобразование завершено:', transformed.length, 'табов');
return transformed; return transformed;
}; };
// Функция для поиска иконки для категории // Функция для поиска иконки для категории
const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => { const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCategory[]): string | null => {
console.log('🔍 Ищем иконку для catalogId:', catalogId); console.log('🔍 Ищем иконку для catalogId:', catalogId);
console.log('📋 Доступные навигационные категории:', navigationCategories); console.log('📋 Доступные навигационные категории:', navigationCategories);
@ -134,9 +126,9 @@ const findCategoryIcon = (catalogId: string, navigationCategories: NavigationCat
console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null); console.log('🖼️ Возвращаемая иконка:', categoryIcon?.icon || null);
return categoryIcon?.icon || null; return categoryIcon?.icon || null;
}; };
const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => { const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const [mobileCategory, setMobileCategory] = useState<null | any>(null); const [mobileCategory, setMobileCategory] = useState<null | any>(null);
@ -266,7 +258,35 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
<span>{mobileCategory.label}</span> <span>{mobileCategory.label}</span>
</div> </div>
<div className="mobile-subcategories"> <div className="mobile-subcategories">
{mobileCategory.links.map((link: string, linkIndex: number) => ( {mobileCategory.links.length === 1 ? (
<div
className="mobile-subcategory"
onClick={() => {
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" }}
>
Показать все
</div>
) : (
mobileCategory.links.map((link: string, linkIndex: number) => (
<div <div
className="mobile-subcategory" className="mobile-subcategory"
key={link} key={link}
@ -293,7 +313,8 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
> >
{link} {link}
</div> </div>
))} ))
)}
</div> </div>
</div> </div>
) : ( ) : (
@ -497,7 +518,35 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
<h3 className="heading-16">{tab.heading}</h3> <h3 className="heading-16">{tab.heading}</h3>
<div className="w-layout-hflex flex-block-92"> <div className="w-layout-hflex flex-block-92">
<div className="w-layout-vflex flex-block-91"> <div className="w-layout-vflex flex-block-91">
{tab.links.map((link: string, linkIndex: number) => { {tab.links.length === 1 ? (
<div
className="link-2"
onClick={() => {
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
let subcategoryId = `fallback_${idx}_0`;
if (catalog?.groups) {
for (const group of catalog.groups) {
if (group.subgroups && group.subgroups.length > 0) {
const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === tab.links[0]);
if (foundSubgroup) {
subcategoryId = foundSubgroup.id;
break;
}
} else if (group.name === tab.links[0]) {
subcategoryId = group.id;
break;
}
}
}
const catalogId = catalog?.id || 'fallback';
handleCategoryClick(catalogId, tab.links[0], subcategoryId);
}}
style={{ cursor: "pointer" }}
>
Показать все
</div>
) : (
tab.links.map((link: string, linkIndex: number) => {
const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx]; const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
let subcategoryId = `fallback_${idx}_${linkIndex}`; let subcategoryId = `fallback_${idx}_${linkIndex}`;
if (catalog?.groups) { if (catalog?.groups) {
@ -527,7 +576,8 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
{link} {link}
</div> </div>
); );
})} })
)}
</div> </div>
<div className="w-layout-vflex flex-block-91-copy"> <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" />
@ -543,6 +593,6 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
</nav> </nav>
</> </>
); );
}; };
export default BottomHead; export default BottomHead;

View File

@ -1,270 +1,64 @@
import React, { useState, useEffect } from 'react'; import * as React from "react";
import { CookiePreferences, initializeAnalytics, initializeMarketing } from '@/lib/cookie-utils';
interface CookieConsentProps { const CookieConsent: React.FC = () => {
onAccept?: () => void; const [isVisible, setIsVisible] = React.useState(false);
onDecline?: () => void;
onConfigure?: (preferences: CookiePreferences) => void;
}
const CookieConsent: React.FC<CookieConsentProps> = ({ onAccept, onDecline, onConfigure }) => { React.useEffect(() => {
const [isVisible, setIsVisible] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>({
necessary: true, // Всегда включены
analytics: false,
marketing: false,
functional: false,
});
useEffect(() => {
// Проверяем, есть ли уже согласие в localStorage
const cookieConsent = localStorage.getItem('cookieConsent'); const cookieConsent = localStorage.getItem('cookieConsent');
if (!cookieConsent) { if (!cookieConsent) {
setIsVisible(true); setIsVisible(true);
} }
}, []); }, []);
const handleAcceptAll = () => { const handleAccept = () => {
const allAccepted = {
necessary: true,
analytics: true,
marketing: true,
functional: true,
};
localStorage.setItem('cookieConsent', 'accepted'); localStorage.setItem('cookieConsent', 'accepted');
localStorage.setItem('cookiePreferences', JSON.stringify(allAccepted));
// Инициализируем сервисы после согласия
initializeAnalytics();
initializeMarketing();
setIsVisible(false); 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; if (!isVisible) return null;
return ( return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-lg cookie-consent-enter"> <>
<div className="max-w-7xl mx-auto p-6 max-md:p-4"> <link
{!showDetails ? ( href="https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700&display=swap"
// Основной вид rel="stylesheet"
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> />
{/* Текст согласия */} <div
<div className="flex-1"> layer-name="cookie"
<div className="flex items-start gap-3"> className="box-border flex gap-16 justify-between items-center px-16 py-10 mx-auto my-0 w-full bg-white rounded-3xl shadow-sm max-w-[1240px] max-md:flex-col max-md:gap-10 max-md:px-10 max-md:py-8 max-md:text-center max-sm:gap-5 max-sm:p-5 max-sm:rounded-2xl fixed bottom-6 left-1/2 -translate-x-1/2 z-5000"
{/* Иконка cookie */} >
<div className="flex-shrink-0 mt-1"> <div
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className="text-gray-600"> layer-name="Мы используем cookie-файлы, чтобы получить статистику, которая помогает нам улучшать сайт для Вас. Нажимая Принять, вы даёте согласие на использование ваших cookie-файлов. Подробнее о том, как мы используем ваши персональные данные, в нашей Политике обработки персональных данных."
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1.5 3.5c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm3 2c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-6 1c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm2.5 3c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm4.5-1c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-2 4c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1zm-3.5-2c.55 0 1-.45 1-1s-.45-1-1-1-1 .45-1 1 .45 1 1 1z" fill="currentColor"/> className="flex-1 text-base font-medium leading-5 text-red-600 max-w-[933px] max-md:max-w-full max-sm:text-sm"
</svg> >
</div> <span className="text-base text-gray-600">
Мы используем cookie-файлы, чтобы получить статистику, которая
<div> помогает нам улучшать сайт для Вас. Нажимая Принять, вы даёте
<h3 className="text-lg font-semibold text-gray-950 mb-2"> согласие на использование ваших cookie-файлов. Подробнее о том, как
Мы используем файлы cookie мы используем ваши персональные данные, в нашей{' '}
</h3> </span>
<p className="text-sm text-gray-600 leading-relaxed">
Наш сайт использует файлы cookie для улучшения работы сайта, персонализации контента и анализа трафика.
Продолжая использовать сайт, вы соглашаетесь с нашей{' '}
<a <a
href="/privacy-policy" href="/privacy-policy"
className="text-red-600 hover:text-red-700 underline" className="text-base text-red-600 underline hover:text-red-700"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
политикой конфиденциальности Политике обработки персональных данных.
</a> </a>
{' '}и использованием файлов cookie.
</p>
</div> </div>
</div>
</div>
{/* Кнопки */}
<div className="flex flex-col sm:flex-row gap-3 md:flex-shrink-0">
<button <button
onClick={() => setShowDetails(true)} onClick={handleAccept}
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 min-w-[120px]" className="box-border flex gap-5 justify-center items-center px-8 py-4 bg-red-600 hover:bg-red-700 rounded-xl h-[51px] min-w-[126px] max-md:w-full max-md:max-w-[200px] max-sm:px-5 max-sm:py-3.5 max-sm:w-full max-sm:h-auto focus:outline-none focus:ring-2 focus:ring-red-400 transition-colors duration-200"
> >
Настроить <span
</button> layer-name="Принять"
<button className="text-base font-semibold leading-5 text-center text-white max-sm:text-sm"
onClick={handleDeclineAll}
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 min-w-[120px]"
> >
Отклонить Принять
</button> </span>
<button
onClick={handleAcceptAll}
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 min-w-[120px]"
>
Принять все
</button> </button>
</div> </div>
</div> </>
) : (
// Детальный вид с настройками
<div className="space-y-6">
{/* Заголовок */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-950">
Настройки файлов cookie
</h3>
<button
onClick={() => setShowDetails(false)}
className="text-gray-500 hover:text-gray-700 p-1"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M15 5L5 15M5 5l10 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</div>
{/* Настройки cookies */}
<div className="space-y-4">
{/* Необходимые cookies */}
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h4 className="font-medium text-gray-950">Необходимые cookies</h4>
<span className="text-xs px-2 py-1 bg-gray-200 text-gray-600 rounded">Обязательные</span>
</div>
<p className="text-sm text-gray-600">
Эти файлы cookie необходимы для работы сайта и не могут быть отключены.
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div className="w-12 h-6 bg-red-600 rounded-full flex items-center justify-end px-1">
<div className="w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
</div>
{/* Аналитические cookies */}
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-gray-950 mb-2">Аналитические cookies</h4>
<p className="text-sm text-gray-600">
Помогают нам понять, как посетители взаимодействуют с сайтом.
</p>
</div>
<div className="flex-shrink-0 ml-4">
<button
onClick={() => togglePreference('analytics')}
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
preferences.analytics ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
} px-1`}
>
<div className="w-4 h-4 bg-white rounded-full"></div>
</button>
</div>
</div>
{/* Маркетинговые cookies */}
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-gray-950 mb-2">Маркетинговые cookies</h4>
<p className="text-sm text-gray-600">
Используются для отслеживания посетителей и показа релевантной рекламы.
</p>
</div>
<div className="flex-shrink-0 ml-4">
<button
onClick={() => togglePreference('marketing')}
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
preferences.marketing ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
} px-1`}
>
<div className="w-4 h-4 bg-white rounded-full"></div>
</button>
</div>
</div>
{/* Функциональные cookies */}
<div className="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-gray-950 mb-2">Функциональные cookies</h4>
<p className="text-sm text-gray-600">
Обеспечивают расширенную функциональность и персонализацию.
</p>
</div>
<div className="flex-shrink-0 ml-4">
<button
onClick={() => togglePreference('functional')}
className={`w-12 h-6 rounded-full flex items-center transition-colors duration-200 ${
preferences.functional ? 'bg-red-600 justify-end' : 'bg-gray-300 justify-start'
} px-1`}
>
<div className="w-4 h-4 bg-white rounded-full"></div>
</button>
</div>
</div>
</div>
{/* Кнопки действий */}
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-gray-200">
<button
onClick={handleDeclineAll}
className="px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
>
Только необходимые
</button>
<button
onClick={handleSavePreferences}
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
>
Сохранить настройки
</button>
<button
onClick={handleAcceptAll}
className="px-6 py-3 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 flex-1 sm:flex-initial min-w-[120px]"
>
Принять все
</button>
</div>
</div>
)}
</div>
</div>
); );
}; };