SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
4.4 KB · 52 lines tsx
Raw Blame History
1"use client";2import { useEffect, useRef, useState } from "react";3import { useRouter } from "next/navigation";4import { IconSearch, IconPin } from "./icons";56type Suggest = { places: { kind: string; name: string; region: string | null; nSale: number; nRent: number }[]; addresses: { uid: string; address: string; city: string; market: string }[] };78export function SearchBar({ market = "achat", placeholder = "Ville, quartier ou adresse", autoFocus = false, size = "lg", onPick }: { market?: "achat" | "location"; placeholder?: string; autoFocus?: boolean; size?: "lg" | "md"; onPick?: (city: string) => void }) {9  const [q, setQ] = useState(""); const [sug, setSug] = useState<Suggest | null>(null); const [open, setOpen] = useState(false); const [active, setActive] = useState(-1);10  const router = useRouter(); const box = useRef<HTMLDivElement>(null);11  useEffect(() => {12    if (q.trim().length < 2) { setSug(null); return; }13    const ctl = new AbortController(); const t = setTimeout(() => {14      fetch(`/api/suggest?q=${encodeURIComponent(q)}`, { signal: ctl.signal }).then((r) => r.json()).then((d) => { setSug(d); setOpen(true); setActive(-1); }).catch(() => {});15    }, 150);16    return () => { clearTimeout(t); ctl.abort(); };17  }, [q]);18  useEffect(() => { const h = (e: MouseEvent) => { if (!box.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);19  const items = [...(sug?.places ?? []).map((p) => ({ type: "place" as const, ...p })), ...(sug?.addresses ?? []).map((a) => ({ type: "address" as const, ...a }))];20  const go = (i: number) => {21    const it = items[i];22    if (!it) { if (q.trim()) router.push(`/recherche?marche=${market}&q=${encodeURIComponent(q.trim())}`); return; }23    setOpen(false);24    if (it.type === "place") { if (onPick) { onPick(it.name); return; } router.push(it.kind === "region" ? `/recherche?marche=${market}&region=${encodeURIComponent(it.name)}` : `/recherche?marche=${market}&ville=${encodeURIComponent(it.name)}`); }25    else router.push(`/annonce/${encodeURIComponent(it.uid)}`);26  };27  return (28    <div ref={box} className="relative w-full">29      <form role="search" onSubmit={(e) => { e.preventDefault(); go(active); }} className="relative">30        <IconSearch className="absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-3" />31        <input value={q} onChange={(e) => setQ(e.target.value)} onFocus={() => sug && setOpen(true)} autoFocus={autoFocus} placeholder={placeholder} aria-label="Rechercher une ville, un quartier ou une adresse" aria-expanded={open} aria-controls="tv-suggest" role="combobox" aria-autocomplete="list"32          onKeyDown={(e) => { if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(a + 1, items.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(a - 1, -1)); } else if (e.key === "Escape") setOpen(false); }}33          className={`tv-input ${size === "lg" ? "py-3.5 text-base pr-28" : "text-sm pr-24"}`} />34        <button type="submit" className={`tv-btn tv-btn-primary absolute right-1.5 top-1/2 -translate-y-1/2 ${size === "lg" ? "" : "!py-1.5 text-xs"}`}>Explorer</button>35      </form>36      {open && items.length > 0 && (37        <ul id="tv-suggest" role="listbox" className="absolute z-30 mt-2 w-full tv-card p-1.5 max-h-80 overflow-auto">38          {items.map((it, i) => (39            <li key={it.type === "place" ? `${it.kind}:${it.name}` : it.uid} role="option" aria-selected={i === active}>40              <button type="button" onMouseDown={() => go(i)} className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg text-sm ${i === active ? "bg-surface-2" : "hover:bg-surface-2"}`}>41                <IconPin className="text-ink-3 shrink-0" width={16} height={16} />42                {it.type === "place" ? (<span className="min-w-0"><span className="font-medium">{it.name}</span><span className="text-ink-3"> · {it.kind === "region" ? "Région" : it.region ?? "Municipalité"} · {market === "location" ? `${it.nRent} à louer` : `${it.nSale} à vendre`}</span></span>)43                  : (<span className="min-w-0 truncate"><span className="font-medium">{it.address}</span><span className="text-ink-3">, {it.city} · {it.market === "rent" ? "à louer" : "à vendre"}</span></span>)}44              </button>45            </li>46          ))}47        </ul>48      )}49    </div>50  );51}52