"use client"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { Search, FileText, BookOpen, FolderOpen, Loader2 } from "lucide-react"; import { useGlobalSearch, type CaseHit, type DocumentHit, } from "@/lib/api/global-search"; import type { SearchHit as PrecedentHit } from "@/lib/api/precedent-library"; const DEBOUNCE_MS = 250; /** * Header global search — debounced input + per-source result panel. * * Three independent fan-out queries (cases / precedent / documents) each * render their own section with an own loading/empty state, so the fast * SQL source paints immediately while the slower vector sources stream in. */ export function GlobalSearch() { const [raw, setRaw] = useState(""); const [debounced, setDebounced] = useState(""); const [open, setOpen] = useState(false); const inputRef = useRef(null); const wrapperRef = useRef(null); const listboxId = useId(); // Debounce. useEffect(() => { const t = setTimeout(() => setDebounced(raw), DEBOUNCE_MS); return () => clearTimeout(t); }, [raw]); // Cmd/Ctrl+K — focus + select. useEffect(() => { const onKey = (e: KeyboardEvent) => { const isModK = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k"; if (isModK) { e.preventDefault(); inputRef.current?.focus(); inputRef.current?.select(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); // Click-outside. useEffect(() => { if (!open) return; const onClick = (e: MouseEvent) => { if (!wrapperRef.current?.contains(e.target as Node)) setOpen(false); }; window.addEventListener("mousedown", onClick); return () => window.removeEventListener("mousedown", onClick); }, [open]); // Route changes don't need a separate listener: clicking any link // (nav, search result, in-page) triggers mousedown outside wrapperRef, // and the click-outside effect closes the panel. const { cases, precedent, documents, isQueryReady, anyLoading } = useGlobalSearch(debounced); const showPanel = open && isQueryReady; const hasAnyResults = useMemo(() => { return ( (cases.data?.items.length ?? 0) > 0 || (precedent.data?.items.length ?? 0) > 0 || (documents.data?.length ?? 0) > 0 ); }, [cases.data, precedent.data, documents.data]); return (
{showPanel && (
{anyLoading && !hasAnyResults && (
)} {!anyLoading && !hasAnyResults && (
לא נמצא כלום עבור {debounced}.
נסה מילים נרדפות או חפש לפי מספר תיק.
)}
)}
); } // ─── Result groups + rows ──────────────────────────────────────────── function ResultGroup({ title, icon, isLoading, isError, count, seeMoreHref, children, }: { title: string; icon: React.ReactNode; isLoading: boolean; isError: boolean; count: number | undefined; seeMoreHref?: string; children: React.ReactNode; }) { const hasItems = (count ?? 0) > 0; return (
{icon} {title} {count !== undefined && ( ({count}) )} {isLoading &&
{isError && (
שגיאה בטעינת תוצאות
)} {!isLoading && !isError && !hasItems && (
אין תוצאות בקטגוריה זו
)}
{children}
{seeMoreHref && hasItems && ( הצג עוד → )}
); } function CaseRow({ hit, onSelect }: { hit: CaseHit; onSelect: () => void }) { return (
ערר {hit.case_number} {hit.status && ( {hit.status} )}
{hit.title} {hit.property_address && · {hit.property_address}}
); } function PrecedentRow({ hit, onSelect }: { hit: PrecedentHit; onSelect: () => void }) { const href = `/precedents/${hit.case_law_id}`; const pct = Math.round(hit.score * 100); if (hit.type === "halacha") { return (
הלכה — {hit.rule_statement} {pct}%
{hit.case_name} · {hit.case_number}
); } return (
פסקה {pct}%
“{hit.content.slice(0, 160)}…”
{hit.case_name} · {hit.case_number}
); } function DocumentRow({ hit, onSelect }: { hit: DocumentHit; onSelect: () => void }) { const pct = Math.round(hit.score * 100); return (
{hit.document} {pct}%
“{hit.content.slice(0, 160)}…”
ערר {hit.case_number} {hit.page != null && · עמ׳ {hit.page}}
); }