diff --git a/package-lock.json b/package-lock.json
index a9c2acf..aafa03d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@types/uuid": "^10.0.0",
"graphql": "^16.11.0",
"next": "15.3.3",
+ "node-fetch": "^3.3.2",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1540,6 +1541,15 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@@ -1602,6 +1612,29 @@
"node": ">=6"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -1615,6 +1648,18 @@
"node": ">=8"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2140,6 +2185,44 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -2690,6 +2773,15 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
diff --git a/package.json b/package.json
index 220be71..01cf716 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@types/uuid": "^10.0.0",
"graphql": "^16.11.0",
"next": "15.3.3",
+ "node-fetch": "^3.3.2",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/src/components/BottomHead.tsx b/src/components/BottomHead.tsx
index 16a63ef..cced1ad 100644
--- a/src/components/BottomHead.tsx
+++ b/src/components/BottomHead.tsx
@@ -79,21 +79,29 @@ const transformPartsIndexToTabData = (catalogs: PartsIndexCatalog[]) => {
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);
- }
+ group.subgroups.forEach(subgroup => {
+ 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);
+ }
+ });
+ }
}
- // Если подкатегорий нет, показываем название категории как указано в требованиях
+ // Если подкатегорий всё ещё нет, добавляем название самой категории
if (links.length === 0) {
links = [catalog.name];
}
@@ -258,20 +266,21 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
{mobileCategory.label}
- {mobileCategory.links.length === 1 ? (
+ {mobileCategory.links.map((link: string, linkIndex: number) => (
{
- let subcategoryId = `${mobileCategory.catalogId}_0`;
+ 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 === mobileCategory.links[0]);
+ const foundSubgroup = group.subgroups.find((subgroup: any) => subgroup.name === link);
if (foundSubgroup) {
subcategoryId = foundSubgroup.id;
break;
}
- } else if (group.name === mobileCategory.links[0]) {
+ } else if (group.name === link) {
subcategoryId = group.id;
break;
}
@@ -279,42 +288,12 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
}
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)];
const catalogId = activeCatalog?.id || 'fallback';
- handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
+ handleCategoryClick(catalogId, link, subcategoryId);
}}
- style={{ cursor: "pointer" }}
>
- Показать все
+ {link}
- ) : (
- 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}
-
- ))
- )}
+ ))}
) : (
@@ -518,66 +497,37 @@ const BottomHead = ({ menuOpen, onClose }: { menuOpen: boolean; onClose: () => v
{tab.heading}
- {tab.links.length === 1 ? (
-
{
- 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" }}
- >
- Показать все
-
- ) : (
- tab.links.map((link: string, linkIndex: number) => {
- const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
- let subcategoryId = `fallback_${idx}_${linkIndex}`;
- 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 === link);
- if (foundSubgroup) {
- subcategoryId = foundSubgroup.id;
- break;
- }
- } else if (group.name === link) {
- subcategoryId = group.id;
+ {tab.links.map((link: string, linkIndex: number) => {
+ const catalog = catalogsData?.partsIndexCategoriesWithGroups?.[idx];
+ let subcategoryId = `fallback_${idx}_${linkIndex}`;
+ 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 === link);
+ if (foundSubgroup) {
+ subcategoryId = foundSubgroup.id;
break;
}
+ } else if (group.name === link) {
+ subcategoryId = group.id;
+ break;
}
}
- return (
-
{
- const catalogId = catalog?.id || 'fallback';
- handleCategoryClick(catalogId, link, subcategoryId);
- }}
- style={{ cursor: "pointer" }}
- >
- {link}
-
- );
- })
- )}
+ }
+ return (
+
{
+ const catalogId = catalog?.id || 'fallback';
+ handleCategoryClick(catalogId, link, subcategoryId);
+ }}
+ style={{ cursor: "pointer" }}
+ >
+ {link}
+
+ );
+ })}

diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index e495215..a4533b3 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -9,6 +9,8 @@ import { FIND_LAXIMO_VEHICLE, DOC_FIND_OEM, FIND_LAXIMO_VEHICLE_BY_PLATE_GLOBAL,
import { LaximoVehicleSearchResult, LaximoDocFindOEMResult, LaximoVehiclesByPartResult } from '@/types/laximo';
import Link from "next/link";
import CartButton from './CartButton';
+import SearchHistoryDropdown from './SearchHistoryDropdown';
+import { GET_RECENT_SEARCH_QUERIES, PartsSearchHistoryItem } from '@/lib/graphql/search-history';
interface HeaderProps {
onOpenAuthModal?: () => void;
@@ -25,9 +27,14 @@ const Header: React.FC
= ({ onOpenAuthModal = () => console.log('Au
const [vehiclesByPartResults, setVehiclesByPartResults] = useState(null);
const [searchType, setSearchType] = useState<'vin' | 'oem' | 'plate' | 'text'>('text');
const [oemSearchMode, setOemSearchMode] = useState<'parts' | 'vehicles'>('parts');
+ const [showSearchHistory, setShowSearchHistory] = useState(false);
+ const [searchHistoryItems, setSearchHistoryItems] = useState([]);
+ const [inputFocused, setInputFocused] = useState(false);
+ const [showPlaceholder, setShowPlaceholder] = useState(true);
const router = useRouter();
const searchFormRef = useRef(null);
const searchDropdownRef = useRef(null);
+ const searchInputRef = useRef(null);
const isClient = useIsClient();
// Эффект для восстановления поискового запроса из URL
@@ -111,11 +118,28 @@ const Header: React.FC = ({ onOpenAuthModal = () => console.log('Au
}
});
+ // Запрос для получения истории поиска
+ const [getSearchHistory, { loading: historyLoading }] = useLazyQuery(GET_RECENT_SEARCH_QUERIES, {
+ onCompleted: (data) => {
+ setSearchHistoryItems(data.partsSearchHistory?.items || []);
+ },
+ onError: (error) => {
+ console.error('❌ Ошибка загрузки истории поиска:', error);
+ setSearchHistoryItems([]);
+ }
+ });
+
// Закрытие результатов при клике вне области
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchDropdownRef.current && !searchDropdownRef.current.contains(event.target as Node)) {
setShowResults(false);
+ setShowSearchHistory(false);
+ setInputFocused(false);
+ // Показываем placeholder обратно только если поле пустое
+ if (searchQuery.trim() === '') {
+ setShowPlaceholder(true);
+ }
}
};
@@ -356,6 +380,54 @@ const Header: React.FC = ({ onOpenAuthModal = () => console.log('Au
router.push(url);
};
+ // Обработчик фокуса на поле ввода
+ const handleInputFocus = () => {
+ setInputFocused(true);
+ setShowResults(false);
+ setShowPlaceholder(false);
+ if (searchQuery.trim() === '') {
+ setShowSearchHistory(true);
+ getSearchHistory({ variables: { limit: 5 } });
+ }
+ };
+
+ // Обработчик изменения значения поля ввода
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setSearchQuery(value);
+
+ // Управляем placeholder в зависимости от наличия текста
+ if (value.trim() === '') {
+ setShowPlaceholder(false); // Скрываем placeholder пока в фокусе
+ setShowSearchHistory(true);
+ setShowResults(false);
+ getSearchHistory({ variables: { limit: 5 } });
+ } else {
+ setShowPlaceholder(false); // Скрываем placeholder когда есть текст
+ setShowSearchHistory(false);
+ }
+ };
+
+ // Обработчик потери фокуса
+ const handleInputBlur = () => {
+ // Показываем placeholder обратно только если поле пустое
+ if (searchQuery.trim() === '') {
+ setShowPlaceholder(true);
+ }
+ };
+
+ // Обработчик клика по элементу истории
+ const handleHistoryItemClick = (searchQuery: string) => {
+ setSearchQuery(searchQuery);
+ setShowSearchHistory(false);
+ setInputFocused(false);
+ setShowPlaceholder(false); // Скрываем placeholder так как теперь есть текст
+ // Фокусируем поле ввода для возможности редактирования
+ if (searchInputRef.current) {
+ searchInputRef.current.focus();
+ }
+ };
+
return (
<>
{/*
@@ -421,7 +493,7 @@ const Header: React.FC = ({ onOpenAuthModal = () => console.log('Au
-
setSearchQuery(e.target.value)}
+ onChange={handleInputChange}
+ onFocus={handleInputFocus}
+ onBlur={handleInputBlur}
disabled={isSearching}
/>
+ {/* История поиска */}
+
+
{/* Результаты поиска VIN */}
{showResults && searchResults.length > 0 && (searchType === 'vin' || searchType === 'plate') && (
diff --git a/src/components/SearchHistoryDropdown.tsx b/src/components/SearchHistoryDropdown.tsx
new file mode 100644
index 0000000..df2bc98
--- /dev/null
+++ b/src/components/SearchHistoryDropdown.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { PartsSearchHistoryItem } from '@/lib/graphql/search-history';
+
+interface SearchHistoryDropdownProps {
+ isVisible: boolean;
+ historyItems: PartsSearchHistoryItem[];
+ onItemClick: (searchQuery: string) => void;
+ loading?: boolean;
+}
+
+const SearchHistoryDropdown: React.FC
= ({
+ isVisible,
+ historyItems,
+ onItemClick,
+ loading = false
+}) => {
+ if (!isVisible) return null;
+
+ // Фильтруем уникальные запросы
+ const uniqueQueries = Array.from(
+ new Map(
+ historyItems.map(item => [item.searchQuery.toLowerCase(), item])
+ ).values()
+ );
+
+ const getSearchTypeLabel = (type: string) => {
+ switch (type) {
+ case 'VIN':
+ return 'VIN';
+ case 'PLATE':
+ return 'Госномер';
+ case 'OEM':
+ case 'ARTICLE':
+ return 'Артикул';
+ default:
+ return 'Поиск';
+ }
+ };
+
+ return (
+
+ {loading ? (
+
+
+
+ Загрузка истории...
+
+
+ ) : uniqueQueries.length > 0 ? (
+ <>
+
+
+ Последние запросы
+
+
+ {uniqueQueries.map((item) => (
+
+ ))}
+ >
+ ) : (
+
+ )}
+
+ );
+};
+
+export default SearchHistoryDropdown;
\ No newline at end of file
diff --git a/src/lib/graphql/search-history.ts b/src/lib/graphql/search-history.ts
index 76cfb9c..fe461ae 100644
--- a/src/lib/graphql/search-history.ts
+++ b/src/lib/graphql/search-history.ts
@@ -24,6 +24,20 @@ export const GET_PARTS_SEARCH_HISTORY = gql`
}
`;
+// Запрос для получения последних поисковых запросов для автодополнения
+export const GET_RECENT_SEARCH_QUERIES = gql`
+ query GetRecentSearchQueries($limit: Int = 5) {
+ partsSearchHistory(limit: $limit, offset: 0) {
+ items {
+ id
+ searchQuery
+ searchType
+ createdAt
+ }
+ }
+ }
+`;
+
export const DELETE_SEARCH_HISTORY_ITEM = gql`
mutation DeletePartsSearchHistoryItem($id: ID!) {
deletePartsSearchHistoryItem(id: $id)