Compare commits
6 Commits
footer
...
87339d577e
Author | SHA1 | Date | |
---|---|---|---|
87339d577e | |||
ad5dcc03e3 | |||
132e39b87e | |||
aef3915dde | |||
e22828039f | |||
320b7500e0 |
@ -2,8 +2,9 @@ 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 } 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';
|
||||||
|
|
||||||
function useIsMobile(breakpoint = 767) {
|
function useIsMobile(breakpoint = 767) {
|
||||||
const [isMobile, setIsMobile] = React.useState(false);
|
const [isMobile, setIsMobile] = React.useState(false);
|
||||||
@ -111,6 +112,22 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
|
|||||||
return transformed;
|
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 BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -155,6 +172,20 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Получаем навигационные категории с иконками
|
||||||
|
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
|
// Обновляем данные табов когда получаем данные от API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
||||||
@ -356,27 +387,49 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
|
|||||||
<div className="w-layout-hflex flex-block-90">
|
<div className="w-layout-hflex flex-block-90">
|
||||||
<div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}>
|
<div className="w-layout-vflex flex-block-88" style={{ maxHeight: "60vh", overflowY: "auto" }}>
|
||||||
{/* Меню с иконками - показываем все категории из API */}
|
{/* Меню с иконками - показываем все категории из API */}
|
||||||
{tabData.map((tab, idx) => (
|
{tabData.map((tab, idx) => {
|
||||||
<a
|
// Получаем catalogId для поиска иконки
|
||||||
href="#"
|
const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[idx]?.id || `fallback_${idx}`;
|
||||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
console.log(`🏷️ Обрабатываем категорию ${idx}: "${tab.label}" с catalogId: "${catalogId}"`);
|
||||||
key={tab.label}
|
const icon = navigationData?.navigationCategories ? findCategoryIcon(catalogId, navigationData.navigationCategories) : null;
|
||||||
onClick={() => {
|
console.log(`🎨 Для категории "${tab.label}" будет показана ${icon ? 'иконка: ' + icon : 'звездочка (fallback)'}`);
|
||||||
setActiveTabIndex(idx);
|
|
||||||
}}
|
return (
|
||||||
style={{ cursor: "pointer" }}
|
<a
|
||||||
>
|
href="#"
|
||||||
<div className="div-block-29">
|
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||||
<div className="code-embed-12 w-embed">
|
key={tab.label}
|
||||||
{/* SVG-звезда */}
|
onClick={() => {
|
||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
setActiveTabIndex(idx);
|
||||||
<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>
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<div className="div-block-29">
|
||||||
|
<div className="code-embed-12 w-embed">
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
alt={tab.label}
|
||||||
|
width="21"
|
||||||
|
height="20"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
</div>
|
<div className="text-block-47">{tab.label}</div>
|
||||||
<div className="text-block-47">{tab.label}</div>
|
</a>
|
||||||
</a>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Правая часть меню с подкатегориями и картинками */}
|
{/* Правая часть меню с подкатегориями и картинками */}
|
||||||
<div className="w-layout-vflex flex-block-89">
|
<div className="w-layout-vflex flex-block-89">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useQuery, useLazyQuery } from '@apollo/client';
|
||||||
|
import { GET_PARTSINDEX_CATEGORIES } from '@/lib/graphql';
|
||||||
|
|
||||||
function useIsMobile(breakpoint = 767) {
|
function useIsMobile(breakpoint = 767) {
|
||||||
const [isMobile, setIsMobile] = React.useState(false);
|
const [isMobile, setIsMobile] = React.useState(false);
|
||||||
@ -13,35 +14,38 @@ function useIsMobile(breakpoint = 767) {
|
|||||||
return isMobile;
|
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 {
|
interface PartsIndexTabData {
|
||||||
label: string;
|
label: string;
|
||||||
heading: string;
|
heading: string;
|
||||||
links: string[];
|
links: string[];
|
||||||
catalogId: 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 статичные данные
|
// Fallback статичные данные
|
||||||
@ -51,57 +55,66 @@ const fallbackTabData: PartsIndexTabData[] = [
|
|||||||
heading: "Детали ТО",
|
heading: "Детали ТО",
|
||||||
catalogId: "parts_to",
|
catalogId: "parts_to",
|
||||||
links: ["Детали ТО"],
|
links: ["Детали ТО"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Масла",
|
label: "Масла",
|
||||||
heading: "Масла",
|
heading: "Масла",
|
||||||
catalogId: "oils",
|
catalogId: "oils",
|
||||||
links: ["Масла"],
|
links: ["Масла"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Шины",
|
label: "Шины",
|
||||||
heading: "Шины",
|
heading: "Шины",
|
||||||
catalogId: "tyres",
|
catalogId: "tyres",
|
||||||
links: ["Шины"],
|
links: ["Шины"],
|
||||||
|
groupsLoaded: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Сервис для работы с Parts Index API
|
// Создаем базовые табы только с названиями каталогов
|
||||||
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com';
|
const createBaseTabData = (catalogs: PartsIndexCatalog[]): PartsIndexTabData[] => {
|
||||||
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
|
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[]> {
|
// Преобразуем данные PartsIndex в формат нашего меню с группами
|
||||||
try {
|
const transformPartsIndexToTabData = (catalog: PartsIndexCatalog): string[] => {
|
||||||
const response = await fetch(`${PARTS_INDEX_API_BASE}/v1/catalogs?lang=ru`, {
|
console.log(`📝 Обрабатываем группы каталога: "${catalog.name}"`);
|
||||||
headers: { 'Accept': 'application/json' },
|
|
||||||
});
|
let links: string[] = [];
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
const data = await response.json();
|
if (catalog.groups && catalog.groups.length > 0) {
|
||||||
return data.list;
|
// Для каждой группы проверяем есть ли подгруппы
|
||||||
} catch (error) {
|
catalog.groups.forEach(group => {
|
||||||
console.error('Ошибка получения каталогов Parts Index:', error);
|
if (group.subgroups && group.subgroups.length > 0) {
|
||||||
return [];
|
// Если есть подгруппы, добавляем их названия
|
||||||
}
|
links.push(...group.subgroups.slice(0, 9 - links.length).map(subgroup => subgroup.name));
|
||||||
}
|
} else {
|
||||||
|
// Если подгрупп нет, добавляем название самой группы
|
||||||
async function fetchCatalogGroup(catalogId: string): Promise<PartsIndexGroup | null> {
|
if (links.length < 9) {
|
||||||
try {
|
links.push(group.name);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Если подкатегорий нет, показываем название категории
|
||||||
|
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 BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => void }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@ -109,7 +122,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
const [mobileCategory, setMobileCategory] = useState<null | any>(null);
|
||||||
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
const [tabData, setTabData] = useState<PartsIndexTabData[]>(fallbackTabData);
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loadingGroups, setLoadingGroups] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Пагинация категорий
|
// Пагинация категорий
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
@ -126,52 +139,116 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
}
|
}
|
||||||
}, [menuOpen]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
if (catalogsData?.partsIndexCategoriesWithGroups && catalogsData.partsIndexCategoriesWithGroups.length > 0) {
|
||||||
if (tabData === fallbackTabData) { // Загружаем только если еще не загружали
|
console.log('✅ Обновляем базовое меню PartsIndex:', catalogsData.partsIndexCategoriesWithGroups.length, 'каталогов');
|
||||||
setLoading(true);
|
|
||||||
try {
|
const baseTabData = createBaseTabData(catalogsData.partsIndexCategoriesWithGroups);
|
||||||
console.log('🔄 Загружаем каталоги Parts Index...');
|
setTabData(baseTabData);
|
||||||
const catalogs = await fetchCatalogs();
|
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} каталогов`);
|
setTabData(prevTabs => {
|
||||||
|
const newTabs = [...prevTabs];
|
||||||
// Загружаем группы для первых нескольких каталогов
|
newTabs[tabIndex] = {
|
||||||
const catalogsToLoad = catalogs.slice(0, 10);
|
...newTabs[tabIndex],
|
||||||
const tabDataPromises = catalogsToLoad.map(async (catalog) => {
|
links,
|
||||||
const group = await fetchCatalogGroup(catalog.id);
|
group: catalog.groups?.[0],
|
||||||
|
groupsLoaded: true
|
||||||
// Получаем подкатегории из entityNames или повторяем название категории
|
};
|
||||||
const links = group?.entityNames && group.entityNames.length > 0
|
return newTabs;
|
||||||
? 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
} 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) => {
|
const handleCategoryClick = (catalogId: string, categoryName: string, entityId?: string) => {
|
||||||
@ -184,7 +261,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
query: {
|
query: {
|
||||||
partsIndexCatalog: catalogId,
|
partsIndexCatalog: catalogId,
|
||||||
categoryName: encodeURIComponent(categoryName),
|
categoryName: encodeURIComponent(categoryName),
|
||||||
...(entityId && { entityId })
|
...(entityId && { partsIndexCategory: entityId })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -294,6 +371,12 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
className="mobile-subcategory"
|
className="mobile-subcategory"
|
||||||
key={cat.catalogId}
|
key={cat.catalogId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Загружаем группы для категории если нужно
|
||||||
|
const catIndex = tabData.findIndex(tab => tab.catalogId === cat.catalogId);
|
||||||
|
if (catIndex !== -1) {
|
||||||
|
loadGroupsForTab(catIndex);
|
||||||
|
}
|
||||||
|
|
||||||
const categoryWithData = {
|
const categoryWithData = {
|
||||||
...cat,
|
...cat,
|
||||||
catalogId: cat.catalogId,
|
catalogId: cat.catalogId,
|
||||||
@ -304,6 +387,9 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
{cat.label}
|
{cat.label}
|
||||||
|
{loadingGroups.has(tabData.findIndex(tab => tab.catalogId === cat.catalogId)) && (
|
||||||
|
<span className="text-xs text-gray-500 ml-2">(загрузка...)</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -367,9 +453,8 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
href="#"
|
href="#"
|
||||||
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
className={`link-block-7 w-inline-block${activeTabIndex === idx ? " w--current" : ""}`}
|
||||||
key={tab.catalogId}
|
key={tab.catalogId}
|
||||||
onClick={() => {
|
onClick={() => handleTabClick(idx)}
|
||||||
setActiveTabIndex(idx);
|
onMouseEnter={() => handleTabHover(idx)}
|
||||||
}}
|
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="div-block-29">
|
<div className="div-block-29">
|
||||||
@ -388,6 +473,7 @@ const BottomHeadPartsIndex = ({ menuOpen, onClose }: { menuOpen: boolean; onClos
|
|||||||
<h3 className="heading-16">
|
<h3 className="heading-16">
|
||||||
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
{currentPageCategories[activeTabIndex]?.heading || currentPageCategories[0]?.heading}
|
||||||
{loading && <span className="text-sm text-gray-500 ml-2">(обновление...)</span>}
|
{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>
|
</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">
|
||||||
|
@ -272,175 +272,179 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-layout-hflex core-product-search-s1">
|
<div className="w-layout-hflex core-product-search-s1">
|
||||||
<div className="w-layout-vflex core-product-s1">
|
<div className="w-layout-vflex flex-block-48-copy">
|
||||||
<div className="w-layout-vflex flex-block-47">
|
<div className="w-layout-vflex product-list-search-s1">
|
||||||
<div className="div-block-19">
|
<div className="w-layout-vflex core-product-s1">
|
||||||
<img src="/images/info.svg" loading="lazy" alt="info" className="image-9" />
|
<div className="w-layout-vflex flex-block-47">
|
||||||
</div>
|
<div className="div-block-19">
|
||||||
<div className="w-layout-vflex flex-block-50">
|
<img src="/images/info.svg" loading="lazy" alt="info" className="image-9" />
|
||||||
<div className="w-layout-hflex flex-block-79">
|
</div>
|
||||||
<h3 className="heading-10 name">{brand}</h3>
|
<div className="w-layout-vflex flex-block-50">
|
||||||
<h3 className="heading-10">{article}</h3>
|
<div className="w-layout-hflex flex-block-79">
|
||||||
<div
|
<h3 className="heading-10 name">{brand}</h3>
|
||||||
className="favorite-icon w-embed"
|
<h3 className="heading-10">{article}</h3>
|
||||||
onClick={handleFavoriteClick}
|
<div
|
||||||
style={{ cursor: 'pointer', marginLeft: '10px', color: isItemFavorite ? '#e53935' : undefined }}
|
className="favorite-icon w-embed"
|
||||||
>
|
onClick={handleFavoriteClick}
|
||||||
<svg width="24" height="24" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
style={{ cursor: 'pointer', marginLeft: '10px', color: isItemFavorite ? '#e53935' : undefined }}
|
||||||
<path
|
>
|
||||||
d="M15 25L13.405 23.5613C7.74 18.4714 4 15.1035 4 10.9946C4 7.6267 6.662 5 10.05 5C11.964 5 13.801 5.88283 15 7.26703C16.199 5.88283 18.036 5 19.95 5C23.338 5 26 7.6267 26 10.9946C26 15.1035 22.26 18.4714 16.595 23.5613L15 25Z"
|
<svg width="24" height="24" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
fill={isItemFavorite ? "#e53935" : "currentColor"}
|
<path
|
||||||
/>
|
d="M15 25L13.405 23.5613C7.74 18.4714 4 15.1035 4 10.9946C4 7.6267 6.662 5 10.05 5C11.964 5 13.801 5.88283 15 7.26703C16.199 5.88283 18.036 5 19.95 5C23.338 5 26 7.6267 26 10.9946C26 15.1035 22.26 18.4714 16.595 23.5613L15 25Z"
|
||||||
</svg>
|
fill={isItemFavorite ? "#e53935" : "currentColor"}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-block-21">{name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-block-21">{name}</div>
|
{image && (
|
||||||
</div>
|
<div className="div-block-20">
|
||||||
</div>
|
<img src={image} loading="lazy" alt={name} className="image-10" />
|
||||||
{image && (
|
{partsIndexPowered && (
|
||||||
<div className="div-block-20">
|
<div className="text-xs text-gray-500 mt-1 text-center">
|
||||||
<img src={image} loading="lazy" alt={name} className="image-10" />
|
powered by <span className="font-semibold text-blue-600">Parts Index</span>
|
||||||
{partsIndexPowered && (
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1 text-center">
|
)}
|
||||||
powered by <span className="font-semibold text-blue-600">Parts Index</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-layout-vflex flex-block-48-copy">
|
||||||
</div>
|
<div className="w-layout-vflex product-list-search-s1">
|
||||||
<div className="w-layout-vflex flex-block-48-copy">
|
<div className="w-layout-hflex sort-list-s1">
|
||||||
<div className="w-layout-hflex sort-list-s1">
|
<div className="w-layout-hflex flex-block-49">
|
||||||
<div className="w-layout-hflex flex-block-49">
|
<div className="sort-item first">Наличие</div>
|
||||||
<div className="sort-item first">Наличие</div>
|
<div className="sort-item">Доставка</div>
|
||||||
<div className="sort-item">Доставка</div>
|
|
||||||
</div>
|
|
||||||
<div className="sort-item price">Цена</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-vflex product-list-search-s1">
|
|
||||||
{displayedOffers.map((offer, idx) => {
|
|
||||||
const isLast = idx === displayedOffers.length - 1;
|
|
||||||
const maxCount = parseStock(offer.pcs);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-layout-hflex product-item-search-s1"
|
|
||||||
key={idx}
|
|
||||||
style={isLast ? { borderBottom: 'none' } : undefined}
|
|
||||||
>
|
|
||||||
<div className="w-layout-hflex flex-block-81">
|
|
||||||
<div className="w-layout-hflex info-block-search-s1">
|
|
||||||
<div className="pcs-search-s1">{offer.pcs}</div>
|
|
||||||
<div className="pcs-search">{offer.days}</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex info-block-product-card-search-s1">
|
|
||||||
{offer.recommended && (
|
|
||||||
<>
|
|
||||||
<div className="w-layout-hflex item-recommend">
|
|
||||||
<img src="/images/ri_refund-fill.svg" loading="lazy" alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="text-block-25-s1">Рекомендуем</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="price-s1">{offer.price}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex add-to-cart-block-s1">
|
<div className="sort-item price">Цена</div>
|
||||||
<div className="w-layout-hflex flex-block-82">
|
</div>
|
||||||
<div className="w-layout-hflex pcs-cart-s1">
|
{displayedOffers.map((offer, idx) => {
|
||||||
<div
|
const isLast = idx === displayedOffers.length - 1;
|
||||||
className="minus-plus"
|
const maxCount = parseStock(offer.pcs);
|
||||||
onClick={() => handleMinus(idx)}
|
return (
|
||||||
style={{ cursor: 'pointer' }}
|
<div
|
||||||
aria-label="Уменьшить количество"
|
className="w-layout-hflex product-item-search-s1"
|
||||||
tabIndex={0}
|
key={idx}
|
||||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleMinus(idx)}
|
style={isLast ? { borderBottom: 'none' } : undefined}
|
||||||
role="button"
|
>
|
||||||
>
|
<div className="w-layout-hflex flex-block-81">
|
||||||
<div className="pluspcs w-embed">
|
<div className="w-layout-hflex info-block-search-s1">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div className="pcs-search-s1">{offer.pcs}</div>
|
||||||
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor" />
|
<div className="pcs-search">{offer.days}</div>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input-pcs">
|
<div className="w-layout-hflex info-block-product-card-search-s1">
|
||||||
<input
|
{offer.recommended && (
|
||||||
type="number"
|
<>
|
||||||
min={1}
|
<div className="w-layout-hflex item-recommend">
|
||||||
max={maxCount}
|
<img src="/images/ri_refund-fill.svg" loading="lazy" alt="" />
|
||||||
value={inputValues[idx]}
|
</div>
|
||||||
onChange={e => handleInputChange(idx, e.target.value)}
|
<div className="text-block-25-s1">Рекомендуем</div>
|
||||||
onBlur={() => handleInputBlur(idx)}
|
</>
|
||||||
className="text-block-26 w-full text-center outline-none"
|
)}
|
||||||
aria-label="Количество"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="price-s1">{offer.price}</div>
|
||||||
className="minus-plus"
|
</div>
|
||||||
onClick={() => handlePlus(idx, maxCount)}
|
<div className="w-layout-hflex add-to-cart-block-s1">
|
||||||
style={{ cursor: 'pointer' }}
|
<div className="w-layout-hflex flex-block-82">
|
||||||
aria-label="Увеличить количество"
|
<div className="w-layout-hflex pcs-cart-s1">
|
||||||
tabIndex={0}
|
<div
|
||||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handlePlus(idx, maxCount)}
|
className="minus-plus"
|
||||||
role="button"
|
onClick={() => handleMinus(idx)}
|
||||||
>
|
style={{ cursor: 'pointer' }}
|
||||||
<div className="pluspcs w-embed">
|
aria-label="Уменьшить количество"
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
tabIndex={0}
|
||||||
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor" />
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleMinus(idx)}
|
||||||
</svg>
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="input-pcs">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={maxCount}
|
||||||
|
value={inputValues[idx]}
|
||||||
|
onChange={e => handleInputChange(idx, e.target.value)}
|
||||||
|
onBlur={() => handleInputBlur(idx)}
|
||||||
|
className="text-block-26 w-full text-center outline-none"
|
||||||
|
aria-label="Количество"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="minus-plus"
|
||||||
|
onClick={() => handlePlus(idx, maxCount)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Увеличить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handlePlus(idx, maxCount)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleAddToCart(offer, idx)}
|
||||||
|
className="button-icon w-inline-block"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Добавить в корзину"
|
||||||
|
>
|
||||||
|
<div className="div-block-26">
|
||||||
|
<img loading="lazy" src="/images/cart_icon.svg" alt="В корзину" className="image-11" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleAddToCart(offer, idx)}
|
|
||||||
className="button-icon w-inline-block"
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
aria-label="Добавить в корзину"
|
|
||||||
>
|
|
||||||
<div className="div-block-26">
|
|
||||||
<img loading="lazy" src="/images/cart_icon.svg" alt="В корзину" className="image-11" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{hasMoreOffers || visibleOffersCount > INITIAL_OFFERS_LIMIT ? (
|
||||||
|
<div
|
||||||
|
className="w-layout-hflex show-more-search"
|
||||||
|
onClick={() => {
|
||||||
|
if (hasMoreOffers) {
|
||||||
|
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
||||||
|
} else {
|
||||||
|
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
aria-label={hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
if (hasMoreOffers) {
|
||||||
|
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
||||||
|
} else {
|
||||||
|
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-block-27">
|
||||||
|
{hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src="/images/arrow_drop_down.svg"
|
||||||
|
loading="lazy"
|
||||||
|
alt=""
|
||||||
|
className={`transition-transform duration-200 ${!hasMoreOffers ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{hasMoreOffers || visibleOffersCount > INITIAL_OFFERS_LIMIT ? (
|
|
||||||
<div
|
|
||||||
className="w-layout-hflex show-more-search"
|
|
||||||
onClick={() => {
|
|
||||||
if (hasMoreOffers) {
|
|
||||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
|
||||||
} else {
|
|
||||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
aria-label={hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть предложения'}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
if (hasMoreOffers) {
|
|
||||||
setVisibleOffersCount(prev => Math.min(prev + 10, offers.length));
|
|
||||||
} else {
|
|
||||||
setVisibleOffersCount(INITIAL_OFFERS_LIMIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="text-block-27">
|
|
||||||
{hasMoreOffers ? `Еще ${offers.length - visibleOffersCount} предложений` : 'Скрыть'}
|
|
||||||
</div>
|
</div>
|
||||||
<img
|
|
||||||
src="/images/arrow_drop_down.svg"
|
|
||||||
loading="lazy"
|
|
||||||
alt=""
|
|
||||||
className={`transition-transform duration-200 ${!hasMoreOffers ? 'rotate-180' : ''}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -373,6 +373,8 @@ export const CREATE_PAYMENT = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const GET_ORDERS = gql`
|
export const GET_ORDERS = gql`
|
||||||
query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) {
|
query GetOrders($clientId: String, $status: OrderStatus, $search: String, $limit: Int, $offset: Int) {
|
||||||
orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) {
|
orders(clientId: $clientId, status: $status, search: $search, limit: $limit, offset: $offset) {
|
||||||
@ -1367,6 +1369,23 @@ export const GET_PARTSINDEX_CATEGORIES = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Навигационные категории с иконками
|
||||||
|
export const GET_NAVIGATION_CATEGORIES = gql`
|
||||||
|
query GetNavigationCategories {
|
||||||
|
navigationCategories {
|
||||||
|
id
|
||||||
|
partsIndexCatalogId
|
||||||
|
partsIndexGroupId
|
||||||
|
name
|
||||||
|
catalogName
|
||||||
|
groupName
|
||||||
|
icon
|
||||||
|
sortOrder
|
||||||
|
isHidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// Новый запрос для получения товаров каталога PartsIndex
|
// Новый запрос для получения товаров каталога PartsIndex
|
||||||
export const GET_PARTSINDEX_CATALOG_ENTITIES = gql`
|
export const GET_PARTSINDEX_CATALOG_ENTITIES = gql`
|
||||||
query GetPartsIndexCatalogEntities(
|
query GetPartsIndexCatalogEntities(
|
||||||
|
@ -138,7 +138,7 @@ export default function Catalog() {
|
|||||||
limit: ITEMS_PER_PAGE,
|
limit: ITEMS_PER_PAGE,
|
||||||
page: partsIndexPage,
|
page: partsIndexPage,
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
|
params: undefined // Будем обновлять через refetch
|
||||||
},
|
},
|
||||||
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
||||||
fetchPolicy: 'cache-and-network'
|
fetchPolicy: 'cache-and-network'
|
||||||
@ -146,7 +146,7 @@ export default function Catalog() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Загружаем параметры фильтрации для PartsIndex
|
// Загружаем параметры фильтрации для PartsIndex
|
||||||
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
|
const { data: paramsData, loading: paramsLoading, error: paramsError, refetch: refetchParams } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
|
||||||
GET_PARTSINDEX_CATALOG_PARAMS,
|
GET_PARTSINDEX_CATALOG_PARAMS,
|
||||||
{
|
{
|
||||||
variables: {
|
variables: {
|
||||||
@ -154,7 +154,7 @@ export default function Catalog() {
|
|||||||
groupId: groupId as string,
|
groupId: groupId as string,
|
||||||
lang: 'ru',
|
lang: 'ru',
|
||||||
q: searchQuery || undefined,
|
q: searchQuery || undefined,
|
||||||
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
|
params: undefined // Будем обновлять через refetch
|
||||||
},
|
},
|
||||||
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
|
||||||
fetchPolicy: 'cache-first'
|
fetchPolicy: 'cache-first'
|
||||||
@ -215,18 +215,44 @@ export default function Catalog() {
|
|||||||
}
|
}
|
||||||
}, [entitiesData]);
|
}, [entitiesData]);
|
||||||
|
|
||||||
|
// Преобразование выбранных фильтров в формат PartsIndex API
|
||||||
|
const convertFiltersToPartsIndexParams = useCallback((): Record<string, any> => {
|
||||||
|
if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiParams: Record<string, any> = {};
|
||||||
|
|
||||||
|
paramsData.partsIndexCatalogParams.list.forEach((param: any) => {
|
||||||
|
const selectedValues = selectedFilters[param.name];
|
||||||
|
if (selectedValues && selectedValues.length > 0) {
|
||||||
|
// Находим соответствующие значения из API данных
|
||||||
|
const matchingValues = param.values.filter((value: any) =>
|
||||||
|
selectedValues.includes(value.title || value.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingValues.length > 0) {
|
||||||
|
// Используем ID параметра из API и значения
|
||||||
|
apiParams[param.id] = matchingValues.map((v: any) => v.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return apiParams;
|
||||||
|
}, [paramsData, selectedFilters]);
|
||||||
|
|
||||||
// Генерация фильтров для PartsIndex на основе параметров API
|
// Генерация фильтров для PartsIndex на основе параметров API
|
||||||
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
|
||||||
if (!paramsData?.partsIndexCatalogParams?.list) {
|
if (!paramsData?.partsIndexCatalogParams?.list) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return paramsData.partsIndexCatalogParams.list.map(param => {
|
return paramsData.partsIndexCatalogParams.list.map((param: any) => {
|
||||||
if (param.type === 'range') {
|
if (param.type === 'range') {
|
||||||
// Для range фильтров ищем min и max значения
|
// Для range фильтров ищем min и max значения
|
||||||
const numericValues = param.values
|
const numericValues = param.values
|
||||||
.map(v => parseFloat(v.value))
|
.map((v: any) => parseFloat(v.value))
|
||||||
.filter(v => !isNaN(v));
|
.filter((v: number) => !isNaN(v));
|
||||||
|
|
||||||
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
|
const min = numericValues.length > 0 ? Math.min(...numericValues) : 0;
|
||||||
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
|
const max = numericValues.length > 0 ? Math.max(...numericValues) : 100;
|
||||||
@ -243,8 +269,8 @@ export default function Catalog() {
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
title: param.name,
|
title: param.name,
|
||||||
options: param.values
|
options: param.values
|
||||||
.filter(value => value.available) // Показываем только доступные
|
.filter((value: any) => value.available) // Показываем только доступные
|
||||||
.map(value => value.title || value.value),
|
.map((value: any) => value.title || value.value),
|
||||||
multi: true,
|
multi: true,
|
||||||
showAll: true
|
showAll: true
|
||||||
};
|
};
|
||||||
@ -252,6 +278,8 @@ export default function Catalog() {
|
|||||||
});
|
});
|
||||||
}, [paramsData]);
|
}, [paramsData]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPartsIndexMode) {
|
if (isPartsIndexMode) {
|
||||||
// Для PartsIndex генерируем фильтры на основе параметров API
|
// Для PartsIndex генерируем фильтры на основе параметров API
|
||||||
@ -426,16 +454,38 @@ export default function Catalog() {
|
|||||||
// При изменении поиска или фильтров сбрасываем пагинацию
|
// При изменении поиска или фильтров сбрасываем пагинацию
|
||||||
setShowEmptyState(false);
|
setShowEmptyState(false);
|
||||||
|
|
||||||
// Если изменился поисковый запрос, нужно перезагрузить данные с сервера
|
// Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера
|
||||||
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
|
||||||
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
|
||||||
setPartsIndexPage(1);
|
setPartsIndexPage(1);
|
||||||
setHasMoreEntities(true);
|
setHasMoreEntities(true);
|
||||||
// refetch будет автоматически вызван при изменении partsIndexPage
|
|
||||||
|
// Перезагружаем данные с новыми параметрами фильтрации
|
||||||
|
const apiParams = convertFiltersToPartsIndexParams();
|
||||||
|
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
|
||||||
|
|
||||||
|
// Также обновляем параметры фильтрации
|
||||||
|
refetchParams({
|
||||||
|
catalogId: catalogId as string,
|
||||||
|
groupId: groupId as string,
|
||||||
|
lang: 'ru',
|
||||||
|
q: searchQuery || undefined,
|
||||||
|
params: paramsString
|
||||||
|
});
|
||||||
|
|
||||||
|
refetchEntities({
|
||||||
|
catalogId: catalogId as string,
|
||||||
|
groupId: groupId as string,
|
||||||
|
lang: 'ru',
|
||||||
|
limit: ITEMS_PER_PAGE,
|
||||||
|
page: 1,
|
||||||
|
q: searchQuery || undefined,
|
||||||
|
params: paramsString
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]);
|
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]);
|
||||||
|
|
||||||
// Управляем показом пустого состояния с задержкой
|
// Управляем показом пустого состояния с задержкой
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -377,4 +377,5 @@ button,
|
|||||||
.tooltip-title {
|
.tooltip-title {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,9 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-block-40 {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
input.text-block-31 {
|
input.text-block-31 {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
@ -406,8 +408,15 @@ input.input-receiver:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.core-product-s1 {
|
.core-product-s1{
|
||||||
max-width: 320px ;
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-10 {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
|
||||||
|
white-space: nowrap; /* если хотите, чтобы текст не переносился */
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-112 {
|
.flex-block-112 {
|
||||||
@ -743,7 +752,7 @@ a.link-block-2.w-inline-block {
|
|||||||
height: 140px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.div-block-128 {
|
.div-block-128 {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
@ -836,10 +845,57 @@ a.link-block-2.w-inline-block {
|
|||||||
max-width: 33%;
|
max-width: 33%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-block-44 {
|
||||||
|
grid-column-gap: 0px;
|
||||||
|
grid-row-gap: 0px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-list-s1 {
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.show-more-search {
|
||||||
|
padding: 6px 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.flex-block-37 {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
.w-layout-vflex.flex-block-40 {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block-47 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important; /* по центру по горизонтали */
|
||||||
|
gap: 16px !important;
|
||||||
|
}
|
||||||
|
.flex-block-50 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 16px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.flex-block-79 {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.text-block-21 {
|
.text-block-21 {
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-45 {
|
.flex-block-45 {
|
||||||
@ -1046,4 +1102,27 @@ a.link-block-2.w-inline-block {
|
|||||||
.hide-on-991 {
|
.hide-on-991 {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.flex-block-50 {
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.div-block-19 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.core-product-s1 {
|
||||||
|
flex-direction: row !important; /* или column, если нужно вертикально */
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
}
|
}
|
@ -2324,8 +2324,7 @@ body {
|
|||||||
.text-block-21 {
|
.text-block-21 {
|
||||||
color: var(--_fonts---color--light-blue-grey);
|
color: var(--_fonts---color--light-blue-grey);
|
||||||
font-size: var(--_fonts---font-size--small-font-size);
|
font-size: var(--_fonts---font-size--small-font-size);
|
||||||
align-self: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-block-22 {
|
.text-block-22 {
|
||||||
@ -2396,13 +2395,7 @@ body {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-47 {
|
|
||||||
grid-column-gap: 15px;
|
|
||||||
grid-row-gap: 15px;
|
|
||||||
flex-flow: row;
|
|
||||||
flex: 1;
|
|
||||||
align-self: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-10 {
|
.image-10 {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@ -3716,9 +3709,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-block-79 {
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-80 {
|
.flex-block-80 {
|
||||||
grid-column-gap: 20px;
|
grid-column-gap: 20px;
|
||||||
@ -4491,16 +4482,7 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
grid-row-gap: 10px;
|
|
||||||
flex-flow: row-reverse;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-self: stretch;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-48-copy {
|
.flex-block-48-copy {
|
||||||
grid-column-gap: 16px;
|
grid-column-gap: 16px;
|
||||||
@ -6767,14 +6749,7 @@ body {
|
|||||||
flex: 0 auto;
|
flex: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: column;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 270px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-product-search-s2 {
|
.core-product-search-s2 {
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
@ -7037,10 +7012,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: column;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-product-search-s2 {
|
.core-product-search-s2 {
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
@ -7869,9 +7841,7 @@ body {
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
flex-flow: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-list-s1 {
|
.sort-list-s1 {
|
||||||
padding-right: 210px;
|
padding-right: 210px;
|
||||||
@ -9498,9 +9468,9 @@ body {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-block-21 {
|
/* .text-block-21 {
|
||||||
line-height: 140%;
|
line-height: 140%;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.flex-block-45 {
|
.flex-block-45 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -10057,13 +10027,7 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-product-s1 {
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
grid-row-gap: 10px;
|
|
||||||
flex-flow: column-reverse wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-block-48-copy {
|
.flex-block-48-copy {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
12
src/types/index.ts
Normal file
12
src/types/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Навигационные категории
|
||||||
|
export interface NavigationCategory {
|
||||||
|
id: string
|
||||||
|
partsIndexCatalogId: string
|
||||||
|
partsIndexGroupId: string | null
|
||||||
|
name: string
|
||||||
|
catalogName: string
|
||||||
|
groupName: string | null
|
||||||
|
icon: string | null
|
||||||
|
sortOrder: number
|
||||||
|
isHidden: boolean
|
||||||
|
}
|
Reference in New Issue
Block a user