From 39fcda015b467be6c08d134fd45ec94204b08a09 Mon Sep 17 00:00:00 2001 From: lexafaxine <40200356+lexafaxine@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:00:36 +0900 Subject: feat: adding search history #1541 (#1627) * feat: adding search history * fix popover should close when no matched history * remove unnecessary react import * replace current Input component with CommandInput for better UX * add i18n for recent searches label * fix bug * refactor local storage logic to make code reusable * using zod schema to validate search history and revert debounce change * Consolidate some of the files --------- Co-authored-by: Mohamed Bassem --- packages/shared-react/hooks/search-history.ts | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 packages/shared-react/hooks/search-history.ts (limited to 'packages/shared-react') diff --git a/packages/shared-react/hooks/search-history.ts b/packages/shared-react/hooks/search-history.ts new file mode 100644 index 00000000..bc3c4e3d --- /dev/null +++ b/packages/shared-react/hooks/search-history.ts @@ -0,0 +1,102 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { z } from "zod"; + +const searchHistorySchema = z.array(z.string()); + +const BOOKMARK_SEARCH_HISTORY_KEY = "karakeep_search_history"; +const MAX_STORED_ITEMS = 50; + +class SearchHistoryUtil { + constructor( + private storage: { + getItem(key: string): Promise | string | null; + setItem(key: string, value: string): Promise | void; + removeItem(key: string): Promise | void; + }, + ) {} + + async getSearchHistory(): Promise { + try { + const rawHistory = await this.storage.getItem( + BOOKMARK_SEARCH_HISTORY_KEY, + ); + if (rawHistory) { + const parsed = JSON.parse(rawHistory) as unknown; + const result = searchHistorySchema.safeParse(parsed); + if (result.success) { + return result.data; + } + } + return []; + } catch (error) { + console.error("Failed to parse search history:", error); + return []; + } + } + + async addSearchTermToHistory(term: string): Promise { + if (!term || term.trim().length === 0) { + return; + } + try { + const currentHistory = await this.getSearchHistory(); + const filteredHistory = currentHistory.filter( + (item) => item.toLowerCase() !== term.toLowerCase(), + ); + const newHistory = [term, ...filteredHistory]; + const finalHistory = newHistory.slice(0, MAX_STORED_ITEMS); + await this.storage.setItem( + BOOKMARK_SEARCH_HISTORY_KEY, + JSON.stringify(finalHistory), + ); + } catch (error) { + console.error("Failed to save search history:", error); + } + } + + async clearSearchHistory(): Promise { + try { + await this.storage.removeItem(BOOKMARK_SEARCH_HISTORY_KEY); + } catch (error) { + console.error("Failed to clear search history:", error); + } + } +} + +export function useSearchHistory(adapter: { + getItem(key: string): Promise | string | null; + setItem(key: string, value: string): Promise | void; + removeItem(key: string): Promise | void; +}) { + const [history, setHistory] = useState([]); + const searchHistoryUtil = useMemo(() => new SearchHistoryUtil(adapter), []); + + const loadHistory = useCallback(async () => { + const storedHistory = await searchHistoryUtil.getSearchHistory(); + setHistory(storedHistory); + }, [searchHistoryUtil]); + + useEffect(() => { + loadHistory(); + }, [loadHistory]); + + const addTerm = useCallback( + async (term: string) => { + await searchHistoryUtil.addSearchTermToHistory(term); + await loadHistory(); + }, + [searchHistoryUtil, loadHistory], + ); + + const clearHistory = useCallback(async () => { + await searchHistoryUtil.clearSearchHistory(); + setHistory([]); + }, [searchHistoryUtil]); + + return { + history, + addTerm, + clearHistory, + refreshHistory: loadHistory, + }; +} -- cgit v1.2.3-70-g09d2