102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
|
|
import * as React from "react"
|
||
|
|
|
||
|
|
interface UseSearchHistoryOptions {
|
||
|
|
limit: number
|
||
|
|
storageKey?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useSearchHistory({
|
||
|
|
limit,
|
||
|
|
storageKey,
|
||
|
|
}: UseSearchHistoryOptions) {
|
||
|
|
const [items, setItems] = React.useState<readonly string[]>(() =>
|
||
|
|
readSearchHistory(storageKey, limit)
|
||
|
|
)
|
||
|
|
|
||
|
|
React.useEffect(() => {
|
||
|
|
const syncFromStorage = () => setItems(readSearchHistory(storageKey, limit))
|
||
|
|
const handleStorage = (event: StorageEvent) => {
|
||
|
|
if (event.key === storageKey) syncFromStorage()
|
||
|
|
}
|
||
|
|
|
||
|
|
syncFromStorage()
|
||
|
|
if (!storageKey || typeof window === "undefined") return
|
||
|
|
|
||
|
|
window.addEventListener("storage", handleStorage)
|
||
|
|
return () => window.removeEventListener("storage", handleStorage)
|
||
|
|
}, [limit, storageKey])
|
||
|
|
|
||
|
|
const update = React.useCallback(
|
||
|
|
(updater: (current: readonly string[]) => readonly string[]) => {
|
||
|
|
setItems((current) => {
|
||
|
|
const next = updater(current).slice(0, limit)
|
||
|
|
writeSearchHistory(storageKey, next)
|
||
|
|
return next
|
||
|
|
})
|
||
|
|
},
|
||
|
|
[limit, storageKey]
|
||
|
|
)
|
||
|
|
|
||
|
|
const add = React.useCallback(
|
||
|
|
(query: string) => {
|
||
|
|
const normalizedQuery = query.trim()
|
||
|
|
if (!normalizedQuery) return
|
||
|
|
|
||
|
|
update((current) => [
|
||
|
|
normalizedQuery,
|
||
|
|
...current.filter((item) => item !== normalizedQuery),
|
||
|
|
])
|
||
|
|
},
|
||
|
|
[update]
|
||
|
|
)
|
||
|
|
|
||
|
|
const remove = React.useCallback(
|
||
|
|
(query: string) =>
|
||
|
|
update((current) => current.filter((item) => item !== query)),
|
||
|
|
[update]
|
||
|
|
)
|
||
|
|
const clear = React.useCallback(() => update(() => []), [update])
|
||
|
|
|
||
|
|
return { add, clear, items, remove }
|
||
|
|
}
|
||
|
|
|
||
|
|
function readSearchHistory(storageKey: string | undefined, limit: number) {
|
||
|
|
if (!storageKey || typeof window === "undefined") return []
|
||
|
|
|
||
|
|
try {
|
||
|
|
const stored = window.localStorage.getItem(storageKey)
|
||
|
|
if (!stored) return []
|
||
|
|
|
||
|
|
const parsed: unknown = JSON.parse(stored)
|
||
|
|
if (!Array.isArray(parsed)) return []
|
||
|
|
|
||
|
|
return Array.from(
|
||
|
|
new Set(
|
||
|
|
parsed
|
||
|
|
.filter((item): item is string => typeof item === "string")
|
||
|
|
.map((item) => item.trim())
|
||
|
|
.filter(Boolean)
|
||
|
|
)
|
||
|
|
).slice(0, limit)
|
||
|
|
} catch {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeSearchHistory(
|
||
|
|
storageKey: string | undefined,
|
||
|
|
items: readonly string[]
|
||
|
|
) {
|
||
|
|
if (!storageKey || typeof window === "undefined") return
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (items.length === 0) {
|
||
|
|
window.localStorage.removeItem(storageKey)
|
||
|
|
} else {
|
||
|
|
window.localStorage.setItem(storageKey, JSON.stringify(items))
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// Storage can be disabled or unavailable in private browsing contexts.
|
||
|
|
}
|
||
|
|
}
|