import { useEffect, useRef, useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Search, Briefcase, Users, UserPlus, FileText, Loader2 } from "lucide-react"; import { supabase } from "@/integrations/supabase/client"; import { cn } from "@/lib/utils"; type SearchResult = { id: string; label: string; sublabel?: string; kind: "case" | "client" | "contact" | "document"; to: string; }; const KIND_ICON = { case: Briefcase, client: Users, contact: UserPlus, document: FileText, } as const; const KIND_LABEL = { case: "Case", client: "Client", contact: "Contact", document: "Document", } as const; export function GlobalSearch() { const navigate = useNavigate(); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [activeIdx, setActiveIdx] = useState(0); const containerRef = useRef(null); // Close on outside click useEffect(() => { const onClick = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, []); // Debounced search useEffect(() => { const q = query.trim(); if (q.length < 2) { setResults([]); setLoading(false); return; } setLoading(true); const handle = setTimeout(async () => { const like = `%${q}%`; const [casesRes, clientsRes, contactsRes, docsRes] = await Promise.all([ supabase .from("cases") .select("id,title,case_number") .or(`title.ilike.${like},case_number.ilike.${like}`) .is("archived_at", null) .limit(6), supabase .from("clients") .select("id,name") .ilike("name", like) .is("archived_at", null) .limit(6), supabase .from("contacts") .select("id,name,company,email") .or(`name.ilike.${like},company.ilike.${like},email.ilike.${like}`) .is("archived_at", null) .limit(6), supabase .from("documents") .select("id,name,case_id") .ilike("name", like) .limit(6), ]); const out: SearchResult[] = []; casesRes.data?.forEach((c) => out.push({ id: c.id, label: c.title, sublabel: c.case_number, kind: "case", to: `/cases/${c.id}`, }), ); clientsRes.data?.forEach((c) => out.push({ id: c.id, label: c.name, kind: "client", to: `/clients/${c.id}` }), ); contactsRes.data?.forEach((c) => out.push({ id: c.id, label: c.name, sublabel: c.company || c.email || undefined, kind: "contact", to: `/contacts/${c.id}`, }), ); docsRes.data?.forEach((d) => out.push({ id: d.id, label: d.name, sublabel: "Document", kind: "document", to: d.case_id ? `/cases/${d.case_id}` : `/files`, }), ); setResults(out); setActiveIdx(0); setLoading(false); }, 250); return () => clearTimeout(handle); }, [query]); const select = (r: SearchResult) => { setOpen(false); setQuery(""); setResults([]); navigate({ to: r.to }); }; const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setActiveIdx((i) => Math.min(i + 1, results.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIdx((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && results[activeIdx]) { e.preventDefault(); select(results[activeIdx]); } else if (e.key === "Escape") { setOpen(false); } }; return (
{ setQuery(e.target.value); setOpen(true); }} onFocus={() => setOpen(true)} onKeyDown={onKeyDown} placeholder="Search…" className="w-full h-8 pl-8 pr-2 text-sm rounded-md bg-sidebar-accent/40 text-sidebar-foreground placeholder:text-sidebar-foreground/50 border border-sidebar-border focus:outline-none focus:ring-1 focus:ring-sidebar-primary" /> {loading && ( )}
{open && query.trim().length >= 2 && (
{results.length === 0 && !loading ? (
No results
) : (
    {results.map((r, idx) => { const Icon = KIND_ICON[r.kind]; return (
  • ); })}
)}
)}
); }