diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 5f2c1d1d..1cac834c 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -15,6 +15,8 @@ import { AutoUpdateSubHeader } from "./auto-update-sub-header"; import { setFilters, setLibrarySearchQuery } from "@renderer/features"; import cn from "classnames"; import { SearchDropdown } from "@renderer/components"; +import { buildGameDetailsPath } from "@renderer/helpers"; +import type { GameShop } from "@types"; const pathTitle: Record = { "/": "home", @@ -161,11 +163,11 @@ export function Header() { const handleSelectSuggestion = (suggestion: { title: string; objectId: string; - shop: string; + shop: GameShop; }) => { setIsDropdownVisible(false); inputRef.current?.blur(); - navigate(`/game/${suggestion.shop}/${suggestion.objectId}`); + navigate(buildGameDetailsPath(suggestion)); }; const handleClearSearch = () => { diff --git a/src/renderer/src/components/search-dropdown/highlight-text.tsx b/src/renderer/src/components/search-dropdown/highlight-text.tsx index 0d5f0fe4..cbcd917d 100644 --- a/src/renderer/src/components/search-dropdown/highlight-text.tsx +++ b/src/renderer/src/components/search-dropdown/highlight-text.tsx @@ -19,24 +19,25 @@ export function HighlightText({ text, query }: Readonly) { return <>{text}; } - const textWords = text.split(/\b/); - const matches: { start: number; end: number; text: string }[] = []; + const matches: { start: number; end: number }[] = []; + const textLower = text.toLowerCase(); - let currentIndex = 0; - textWords.forEach((word) => { - const wordLower = word.toLowerCase(); + queryWords.forEach((queryWord) => { + const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp( + `(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`, + "gi" + ); - queryWords.forEach((queryWord) => { - if (wordLower === queryWord) { - matches.push({ - start: currentIndex, - end: currentIndex + word.length, - text: word, - }); - } - }); + let match; + while ((match = regex.exec(textLower)) !== null) { + const matchedText = match[0]; + const leadingSpace = matchedText.startsWith(" ") ? 1 : 0; + const start = match.index + leadingSpace; + const end = start + queryWord.length; - currentIndex += word.length; + matches.push({ start, end }); + } }); if (matches.length === 0) { @@ -46,16 +47,14 @@ export function HighlightText({ text, query }: Readonly) { matches.sort((a, b) => a.start - b.start); const mergedMatches: { start: number; end: number }[] = []; - - if (matches.length === 0) { - return <>{text}; - } - let current = matches[0]; for (let i = 1; i < matches.length; i++) { if (matches[i].start <= current.end) { - current.end = Math.max(current.end, matches[i].end); + current = { + start: current.start, + end: Math.max(current.end, matches[i].end), + }; } else { mergedMatches.push(current); current = matches[i]; diff --git a/src/renderer/src/hooks/use-search-history.ts b/src/renderer/src/hooks/use-search-history.ts index dac6d391..6b327c81 100644 --- a/src/renderer/src/hooks/use-search-history.ts +++ b/src/renderer/src/hooks/use-search-history.ts @@ -1,4 +1,5 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { levelDBService } from "@renderer/services/leveldb.service"; export interface SearchHistoryEntry { query: string; @@ -6,22 +7,47 @@ export interface SearchHistoryEntry { context: "library" | "catalogue"; } -const STORAGE_KEY = "search-history"; +const LEVELDB_KEY = "searchHistory"; +const LEGACY_STORAGE_KEY = "search-history"; const MAX_HISTORY_ENTRIES = 15; export function useSearchHistory() { const [history, setHistory] = useState([]); + const isInitialized = useRef(false); useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { + const loadHistory = async () => { + if (isInitialized.current) return; + isInitialized.current = true; + try { - const parsed = JSON.parse(stored) as SearchHistoryEntry[]; - setHistory(parsed); + let data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as + | SearchHistoryEntry[] + | null; + + if (!data) { + const legacyData = localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacyData) { + try { + const parsed = JSON.parse(legacyData) as SearchHistoryEntry[]; + await levelDBService.put(LEVELDB_KEY, parsed, null, "json"); + localStorage.removeItem(LEGACY_STORAGE_KEY); + data = parsed; + } catch { + localStorage.removeItem(LEGACY_STORAGE_KEY); + } + } + } + + if (data) { + setHistory(data); + } } catch { - localStorage.removeItem(STORAGE_KEY); + setHistory([]); } - } + }; + + loadHistory(); }, []); const addToHistory = useCallback( @@ -39,7 +65,7 @@ export function useSearchHistory() { (entry) => entry.query.toLowerCase() !== query.toLowerCase().trim() ); const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES); - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + levelDBService.put(LEVELDB_KEY, updated, null, "json"); return updated; }); }, @@ -49,14 +75,14 @@ export function useSearchHistory() { const removeFromHistory = useCallback((query: string) => { setHistory((prev) => { const updated = prev.filter((entry) => entry.query !== query); - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + levelDBService.put(LEVELDB_KEY, updated, null, "json"); return updated; }); }, []); const clearHistory = useCallback(() => { setHistory([]); - localStorage.removeItem(STORAGE_KEY); + levelDBService.del(LEVELDB_KEY, null); }, []); const getRecentHistory = useCallback( diff --git a/src/renderer/src/hooks/use-search-suggestions.ts b/src/renderer/src/hooks/use-search-suggestions.ts index d647cc5a..b8986775 100644 --- a/src/renderer/src/hooks/use-search-suggestions.ts +++ b/src/renderer/src/hooks/use-search-suggestions.ts @@ -2,11 +2,12 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useAppSelector } from "./redux"; import { debounce } from "lodash-es"; import { logger } from "@renderer/logger"; +import type { GameShop } from "@types"; export interface SearchSuggestion { title: string; objectId: string; - shop: string; + shop: GameShop; iconUrl: string | null; source: "library" | "catalogue"; } @@ -89,7 +90,7 @@ export function useSearchSuggestions( { title: string; objectId: string; - shop: string; + shop: GameShop; iconUrl: string | null; }[] >("/catalogue/search/suggestions", {