● Zion Boggan
repos/Sightline/frontend/src/components/OmniSearch.tsx
zionboggan.com ↗
94 lines · tsx
History for this file →
1
"use client";
2
import { useEffect, useRef, useState } from "react";
3
import { useRouter } from "next/navigation";
4
import { api, OmniResults } from "@/lib/api";
5
 
6
export default function OmniSearch() {
7
  const [q, setQ] = useState("");
8
  const [res, setRes] = useState<OmniResults | null>(null);
9
  const [open, setOpen] = useState(false);
10
  const router = useRouter();
11
  const box = useRef<HTMLDivElement>(null);
12
  const timer = useRef<any>(null);
13
 
14
  useEffect(() => {
15
    function onClick(e: MouseEvent) {
16
      if (box.current && !box.current.contains(e.target as Node)) setOpen(false);
17
    }
18
    document.addEventListener("mousedown", onClick);
19
    return () => document.removeEventListener("mousedown", onClick);
20
  }, []);
21
 
22
  function onChange(v: string) {
23
    setQ(v);
24
    clearTimeout(timer.current);
25
    if (v.trim().length < 2) { setRes(null); return; }
26
    timer.current = setTimeout(() => {
27
      api.omniSearch(v.trim()).then((r) => { setRes(r); setOpen(true); }).catch(() => setRes(null));
28
    }, 220);
29
  }
30
 
31
  const hasResults = res && (res.plates.length || res.persons.length || res.vehicles.length || res.detections.length || res.cameras.length);
32
 
33
  return (
34
    <div className="omni" ref={box}>
35
      <span className="mag">⌕</span>
36
      <input value={q} placeholder="Search anything - plate, name, make/model, clothing, camera operator…"
37
             onChange={(e) => onChange(e.target.value)} onFocus={() => res && setOpen(true)} />
38
      {open && hasResults && (
39
        <div className="omni-results">
40
          {res!.plates.length > 0 && (
41
            <div className="omni-sec"><div className="lbl">Plates</div>
42
              {res!.plates.map((p) => (
43
                <div key={p.plate_norm} className="omni-row" onClick={() => { router.push(`/vehicles/${p.plate_norm}`); setOpen(false); }}>
44
                  <span className="badge accent mono">{p.plate_norm}</span>
45
                  <span className="muted">{p.state} · {p.reads} reads</span>
46
                </div>
47
              ))}
48
            </div>
49
          )}
50
          {res!.persons.length > 0 && (
51
            <div className="omni-sec"><div className="lbl">Persons</div>
52
              {res!.persons.map((p) => (
53
                <div key={p.id} className="omni-row" onClick={() => { router.push(`/persons/${p.id}`); setOpen(false); }}>
54
                  <span>{p.full_name}</span>
55
                  {p.wanted && <span className="badge danger">WANTED</span>}
56
                  <span className="muted">{p.associated_plate || ""}</span>
57
                </div>
58
              ))}
59
            </div>
60
          )}
61
          {res!.vehicles.length > 0 && (
62
            <div className="omni-sec"><div className="lbl">Vehicles</div>
63
              {res!.vehicles.map((v, i) => (
64
                <div key={i} className="omni-row" onClick={() => { if (v.plate_norm) router.push(`/vehicles/${v.plate_norm}`); setOpen(false); }}>
65
                  <span className="badge">{[v.color, v.make, v.model].filter(Boolean).join(" ")}</span>
66
                  <span className="muted mono">{v.plate_norm}</span>
67
                </div>
68
              ))}
69
            </div>
70
          )}
71
          {res!.detections.length > 0 && (
72
            <div className="omni-sec"><div className="lbl">Detections (clothing / articles)</div>
73
              {res!.detections.map((d) => (
74
                <div key={d.id} className="omni-row" onClick={() => { router.push(`/search?det=${d.id}`); setOpen(false); }}>
75
                  <span className="badge purple">{d.clothing_top}</span>
76
                  <span className="muted">{(d.articles || []).join(", ")} {d.plate_norm ? `· ${d.plate_norm}` : ""}</span>
77
                </div>
78
              ))}
79
            </div>
80
          )}
81
          {res!.cameras.length > 0 && (
82
            <div className="omni-sec"><div className="lbl">Cameras</div>
83
              {res!.cameras.map((c) => (
84
                <div key={c.id} className="omni-row" onClick={() => { router.push(`/cameras/${c.id}`); setOpen(false); }}>
85
                  <span>{c.name}</span><span className="muted">{c.operator}</span>
86
                </div>
87
              ))}
88
            </div>
89
          )}
90
        </div>
91
      )}
92
    </div>
93
  );
94
}