spb/valoplex Public
ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.
TypeScript 90.3%
Python 7.1%
CSS 2.5%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2"use client";3import { useRef, useState } from "react";4import { useRouter } from "next/navigation";5import { useLang } from "./LangContext";67export interface Suggestion {8 id: string;9 adresse: string | null;10 apt: string | null;11 municipalite: string | null;12 typeProp: string;13}1415export default function SearchBox({16 onPick,17 placeholder,18}: {19 /** Si fourni : appelé au choix d'une adresse (le champ se vide) au lieu de naviguer. */20 onPick?: (s: Suggestion) => void;21 placeholder?: string;22}) {23 const { t } = useLang();24 const router = useRouter();25 const [q, setQ] = useState("");26 const [sugg, setSugg] = useState<Suggestion[]>([]);27 const [open, setOpen] = useState(false);28 const [searched, setSearched] = useState(false);29 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);3031 const onChange = (value: string) => {32 setQ(value);33 if (timer.current) clearTimeout(timer.current);34 if (value.trim().length < 3) {35 setSugg([]);36 setSearched(false);37 return;38 }39 timer.current = setTimeout(async () => {40 const res = await fetch(`/api/search?q=${encodeURIComponent(value)}`);41 const data = (await res.json()) as { results: Suggestion[] };42 setSugg(data.results);43 setSearched(true);44 setOpen(true);45 }, 200);46 };4748 return (49 <div className="relative w-full">50 <input51 value={q}52 onChange={(e) => onChange(e.target.value)}53 onFocus={() => setOpen(true)}54 onBlur={() => setTimeout(() => setOpen(false), 200)}55 placeholder={placeholder ?? t("searchPlaceholder")}56 className="vp-input text-base"57 aria-label={placeholder ?? t("searchPlaceholder")}58 />59 {open && sugg.length > 0 && (60 <ul className="absolute z-[1200] mt-2 w-full overflow-hidden rounded-[10px] border-[1.5px] border-ink bg-surface shadow-[6px_6px_0_rgba(20,24,20,0.85)]">61 {sugg.map((s) => (62 <li key={s.id} className="border-b border-[rgba(20,24,20,0.1)] last:border-0">63 <button64 className="flex w-full items-baseline justify-between gap-2 px-4 py-3 text-left transition-colors hover:bg-lime-soft"65 onMouseDown={() => {66 if (onPick) {67 onPick(s);68 setQ("");69 setSugg([]);70 setSearched(false);71 } else {72 router.push(`/estimation/${s.id}`);73 }74 }}75 >76 <span className="vp-display font-bold">77 {s.adresse}78 {s.apt ? ` app. ${s.apt}` : ""}79 </span>80 <span className="vp-mono shrink-0 text-[11px] uppercase tracking-[0.05em] text-ink-3">81 {s.municipalite}82 </span>83 </button>84 </li>85 ))}86 </ul>87 )}88 {open && searched && sugg.length === 0 && q.trim().length >= 3 && (89 <div className="vp-mono absolute z-[1200] mt-2 w-full rounded-[10px] border-[1.5px] border-ink bg-surface px-4 py-3 text-[12px] uppercase tracking-[0.04em] text-ink-2 shadow-[6px_6px_0_rgba(20,24,20,0.85)]">90 {t("searchNoResult")}91 </div>92 )}93 </div>94 );95}96