"use client" import { useEffect, useRef, useState } from "react" import { useEntrySearch } from "@/lib/queries/use-entry-search" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" function getMatchSnippet(text: string, query: string): string | null { const stripped = text.replace(/<[^>]*>/g, "") const lowerStripped = stripped.toLowerCase() const matchIndex = lowerStripped.indexOf(query) if (matchIndex === -1) return null const start = Math.max(0, matchIndex - 40) const end = Math.min(stripped.length, matchIndex + query.length + 80) const prefix = start > 0 ? "\u2026" : "" const suffix = end < stripped.length ? "\u2026" : "" return prefix + stripped.slice(start, end) + suffix } function highlightText(text: string, query: string): React.ReactNode { if (!query) return text const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") const parts = text.split(new RegExp(`(${escapedQuery})`, "gi")) return parts.map((part, index) => part.toLowerCase() === query.toLowerCase() ? ( {part} ) : ( part ) ) } interface SearchOverlayProperties { onClose: () => void } export function SearchOverlay({ onClose }: SearchOverlayProperties) { const [searchQuery, setSearchQuery] = useState("") const [selectedResultIndex, setSelectedResultIndex] = useState(-1) const inputReference = useRef(null) const resultListReference = useRef(null) const { data: results, isLoading } = useEntrySearch(searchQuery) const setSelectedEntryIdentifier = useUserInterfaceStore( (state) => state.setSelectedEntryIdentifier ) useEffect(() => { inputReference.current?.focus() }, []) useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { onClose() } } document.addEventListener("keydown", handleKeyDown) return () => document.removeEventListener("keydown", handleKeyDown) }, [onClose]) function handleSelectEntry(entryIdentifier: string) { setSelectedEntryIdentifier(entryIdentifier) onClose() } function handleInputKeyDown(event: React.KeyboardEvent) { if (event.key === "Backspace" && searchQuery === "") { onClose() return } if (!results || results.length === 0) return if (event.key === "ArrowDown") { event.preventDefault() setSelectedResultIndex((previous) => { const nextIndex = previous < results.length - 1 ? previous + 1 : 0 scrollResultIntoView(nextIndex) return nextIndex }) } else if (event.key === "ArrowUp") { event.preventDefault() setSelectedResultIndex((previous) => { const nextIndex = previous > 0 ? previous - 1 : results.length - 1 scrollResultIntoView(nextIndex) return nextIndex }) } else if (event.key === "Enter" && selectedResultIndex >= 0) { event.preventDefault() handleSelectEntry(results[selectedResultIndex].entryIdentifier) } } function scrollResultIntoView(index: number) { const container = resultListReference.current if (!container) return const items = container.querySelectorAll("[data-result-item]") items[index]?.scrollIntoView({ block: "nearest" }) } function handleBackdropClick(event: React.MouseEvent) { if (event.target === event.currentTarget) { onClose() } } return (
{ setSearchQuery(event.target.value) setSelectedResultIndex(-1) }} onKeyDown={handleInputKeyDown} placeholder="search entries..." className="w-full bg-transparent text-text-primary outline-none placeholder:text-text-dim" />
{isLoading && searchQuery.trim().length >= 2 && (

searching...

)} {!isLoading && searchQuery.trim().length >= 2 && results?.length === 0 && (

no results

)} {results?.map((entry, index) => { const query = searchQuery.trim().toLowerCase() const titleMatches = (entry.entryTitle ?? "").toLowerCase().includes(query) const summarySnippet = !titleMatches && entry.summary ? getMatchSnippet(entry.summary, query) : null return ( ) })}
) }