SPB Git forge

spb/uqo-eval

Public
55commits 2branches 0releases
134.4 MBsize
maindefault branch
2 h agolast push
TypeScript 90.1% JavaScript 3.2% Python 3.2% CSS 1.8% HTML 1.8%
6.0 KB · 169 lines tsx
Raw Blame History
1// UQO Éval — laboratoire pédagogique d'évaluation immobilière2"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  variant = "default",19}: {20  /** Si fourni : appelé au choix d'une adresse (le champ se vide) au lieu de naviguer. */21  onPick?: (s: Suggestion) => void;22  placeholder?: string;23  /** « hero » : grand champ sur aplat encre (page d'accueil). */24  variant?: "default" | "hero";25}) {26  const { t, lang } = useLang();27  const fr = lang === "fr";28  const router = useRouter();29  const [q, setQ] = useState("");30  const [sugg, setSugg] = useState<Suggestion[]>([]);31  const [open, setOpen] = useState(false);32  const [searched, setSearched] = useState(false);33  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);34  const input = useRef<HTMLInputElement>(null);3536  const onChange = (value: string) => {37    setQ(value);38    if (timer.current) clearTimeout(timer.current);39    if (value.trim().length < 3) {40      setSugg([]);41      setSearched(false);42      return;43    }44    timer.current = setTimeout(async () => {45      const res = await fetch(`/api/search?q=${encodeURIComponent(value)}`);46      const data = (await res.json()) as { results: Suggestion[] };47      setSugg(data.results);48      setSearched(true);49      setOpen(true);50    }, 200);51  };5253  const pick = (s: Suggestion) => {54    if (onPick) {55      onPick(s);56      setQ("");57      setSugg([]);58      setSearched(false);59    } else {60      router.push(`/estimation/${s.id}`);61    }62  };6364  const hero = variant === "hero";6566  return (67    <div className="relative w-full">68      <div className={hero ? "flex items-stretch gap-0" : undefined}>69        <input70          id={variant === "hero" ? "vp-adresse" : undefined}71          ref={input}72          value={q}73          onChange={(e) => onChange(e.target.value)}74          onFocus={() => setOpen(true)}75          onBlur={() => setTimeout(() => setOpen(false), 200)}76          placeholder={77            placeholder ?? (hero ? (fr ? "ex. 861 route Elgin" : "e.g. 861 route Elgin") : t("searchPlaceholder"))78          }79          className={80            hero81              ? "min-h-[56px] w-full min-w-0 flex-1 border-0 border-b border-[rgba(245,243,238,0.45)] bg-transparent px-1 py-3 text-[16px] text-[var(--paper-on-ink)] outline-none transition-colors placeholder:text-[rgba(245,243,238,0.45)] focus:border-[var(--accent-bright)] sm:text-[19px]"82              : "vp-input text-base"83          }84          aria-label={placeholder ?? t("searchPlaceholder")}85        />86        {hero && (87          <button88            type="button"89            aria-label={fr ? "Analyser cette adresse" : "Analyze this address"}90            onMouseDown={(e) => {91              // mousedown : devance le blur de l'input (sinon le menu se ferme avant)92              e.preventDefault();93              if (sugg.length > 0) pick(sugg[0]);94              else input.current?.focus();95            }}96            className="vp-display -mb-px flex min-h-[56px] shrink-0 items-center gap-2 self-end bg-[var(--accent-bright)] px-4 text-[14px] font-bold text-ink transition-colors hover:bg-[#ff6b63] sm:px-7 sm:text-[15px]"97          >98            {fr ? "Analyser" : "Analyze"}99            <span aria-hidden="true">→</span>100          </button>101        )}102      </div>103      {open && sugg.length > 0 && (104        <ul105          className={`absolute z-[var(--z-dropdown,700)] mt-2 w-full overflow-hidden rounded-[2px] ${106            hero107              ? "border border-[rgba(245,243,238,0.25)] bg-[var(--ink-deep)] shadow-[0_14px_40px_rgba(0,0,0,0.5)]"108              : "border border-[var(--line-strong)] bg-surface shadow-[0_10px_30px_rgba(20,24,20,0.14)]"109          }`}110        >111          {sugg.map((s, i) => (112            <li113              key={s.id}114              className={115                hero116                  ? "border-b border-[rgba(245,243,238,0.1)] last:border-0"117                  : "border-b border-[rgba(20,24,20,0.08)] last:border-0"118              }119            >120              <button121                className={`flex w-full items-baseline justify-between gap-3 px-4 py-3 text-left transition-colors ${122                  hero ? "hover:bg-[rgba(245,243,238,0.08)]" : "hover:bg-surface-2"123                }`}124                onMouseDown={() => pick(s)}125              >126                <span className="flex min-w-0 items-baseline gap-2.5">127                  <span128                    className={`vp-mono shrink-0 text-[10px] ${129                      hero ? "text-[var(--accent-bright)]" : "text-accent"130                    }`}131                  >132                    {String(i + 1).padStart(2, "0")}133                  </span>134                  <span135                    className={`vp-display truncate font-bold ${136                      hero ? "text-[var(--paper-on-ink)]" : ""137                    }`}138                  >139                    {s.adresse}140                    {s.apt ? ` app. ${s.apt}` : ""}141                  </span>142                </span>143                <span144                  className={`vp-mono shrink-0 text-[10.5px] uppercase tracking-[0.05em] ${145                    hero ? "text-[rgba(245,243,238,0.55)]" : "text-ink-3"146                  }`}147                >148                  {s.municipalite}149                </span>150              </button>151            </li>152          ))}153        </ul>154      )}155      {open && searched && sugg.length === 0 && q.trim().length >= 3 && (156        <div157          className={`vp-mono absolute z-[var(--z-dropdown,700)] mt-2 w-full rounded-[2px] px-4 py-3 text-[12px] uppercase tracking-[0.04em] ${158            hero159              ? "border border-[rgba(245,243,238,0.25)] bg-[var(--ink-deep)] text-[rgba(245,243,238,0.7)]"160              : "border border-[var(--line-strong)] bg-surface text-ink-2"161          }`}162        >163          {t("searchNoResult")}164        </div>165      )}166    </div>167  );168}169