diff options
| author | lexafaxine <40200356+lexafaxine@users.noreply.github.com> | 2025-07-14 09:00:36 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-14 01:00:36 +0100 |
| commit | 39fcda015b467be6c08d134fd45ec94204b08a09 (patch) | |
| tree | ce78ec11e2cbbb349ec7d02ba69fad8fe16e19a1 /packages | |
| parent | ecb13cec5d5c646308b34c714401a716f3cdf199 (diff) | |
| download | karakeep-39fcda015b467be6c08d134fd45ec94204b08a09.tar.zst | |
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 <me@mbassem.com>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/shared-react/hooks/search-history.ts | 102 |
1 files changed, 102 insertions, 0 deletions
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> | string | null; + setItem(key: string, value: string): Promise<void> | void; + removeItem(key: string): Promise<void> | void; + }, + ) {} + + async getSearchHistory(): Promise<string[]> { + 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<void> { + 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<void> { + 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> | string | null; + setItem(key: string, value: string): Promise<void> | void; + removeItem(key: string): Promise<void> | void; +}) { + const [history, setHistory] = useState<string[]>([]); + 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, + }; +} |
