"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { Search, MapPin, LandPlot, Building2, X, Loader2 } from "lucide-react"; import { Input } from "@/shared/components/ui/input"; import { Button } from "@/shared/components/ui/button"; import { cn } from "@/shared/lib/utils"; import type { SearchResult } from "../types"; type SearchBarProps = { onResultSelect: (result: SearchResult) => void; className?: string; }; export function SearchBar({ onResultSelect, className }: SearchBarProps) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [selectedIdx, setSelectedIdx] = useState(-1); const containerRef = useRef(null); const debounceRef = useRef>(undefined); // Debounced search const doSearch = useCallback((q: string) => { if (debounceRef.current) clearTimeout(debounceRef.current); if (q.length < 2) { setResults([]); setOpen(false); return; } debounceRef.current = setTimeout(() => { setLoading(true); fetch(`/api/geoportal/search?q=${encodeURIComponent(q)}&limit=15`) .then((r) => (r.ok ? r.json() : Promise.reject(r.status))) .then((data: { results: SearchResult[] }) => { setResults(data.results); setOpen(data.results.length > 0); setSelectedIdx(-1); }) .catch(() => { setResults([]); setOpen(false); }) .finally(() => setLoading(false)); }, 300); }, []); // Click outside to close useEffect(() => { const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); const handleSelect = (result: SearchResult) => { setOpen(false); setQuery(result.label); onResultSelect(result); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!open || results.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIdx((i) => Math.min(i + 1, results.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIdx((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && selectedIdx >= 0) { e.preventDefault(); const sel = results[selectedIdx]; if (sel) handleSelect(sel); } else if (e.key === "Escape") { setOpen(false); } }; const typeIcon = (type: SearchResult["type"]) => { switch (type) { case "uat": return ; case "parcel": return ; case "building": return ; } }; return (
{ setQuery(e.target.value); doSearch(e.target.value); }} onFocus={() => { if (results.length > 0) setOpen(true); }} onKeyDown={handleKeyDown} className="pl-8 pr-8 h-8 text-sm bg-background/95 backdrop-blur-sm" /> {loading && ( )} {query && ( )}
{/* Results dropdown */} {open && results.length > 0 && (
{results.map((r, i) => ( ))}
)}
); }