SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
3 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
4.6 KB · 50 lines tsx
Raw Blame History
1"use client";2import { useCallback, useEffect, useRef, useState } from "react";3import { useRouter } from "next/navigation";4import { MapView, type MapPoint } from "@/components/map-view";5import { SearchBar } from "@/components/search-bar";6import { ListingCardView } from "@/components/listing-card";7import type { ListingCard } from "@/lib/types";8import { IconClose } from "@/components/icons";910export function CarteView() {11  const [market, setMarket] = useState<"achat" | "location">("achat");12  const [cats, setCats] = useState<string[]>([]);13  const [points, setPoints] = useState<MapPoint[]>([]);14  const [sel, setSel] = useState<ListingCard | null>(null);15  const [center, setCenter] = useState<[number, number] | undefined>([45.55, -73.65]);16  const [zoom, setZoom] = useState(11);17  const [n, setN] = useState(0);18  const router = useRouter(); const bboxRef = useRef<[number, number, number, number] | null>(null); const timer = useRef<ReturnType<typeof setTimeout> | null>(null);19  const load = useCallback((bbox: [number, number, number, number], z: number) => {20    bboxRef.current = bbox;21    if (timer.current) clearTimeout(timer.current);22    timer.current = setTimeout(async () => {23      const q = new URLSearchParams({ marche: market, bbox: bbox.join(","), format: "points", tri: "recent" }); if (cats.length) q.set("type", cats.join(","));24      const r = await fetch(`/api/search?${q}`).then((r) => r.json()).catch(() => null);25      if (r?.points) { setN(r.points.length); setPoints(r.points); }26    }, 250);27  }, [market, cats]);28  useEffect(() => { if (bboxRef.current) load(bboxRef.current, zoom); }, [market, cats, load, zoom]);29  const pick = async (p: MapPoint) => { const r = await fetch(`/api/search?marche=${market}&q=&limit=1&bbox=${p.lng - 1e-6},${p.lat - 1e-6},${p.lng + 1e-6},${p.lat + 1e-6}`).then((r) => r.json()).catch(() => null); const item = r?.items?.find((i: ListingCard) => i.uid === p.uid) ?? r?.items?.[0]; setSel(item ?? null); };30  const CATS = market === "location" ? [["logement", "Logements"], ["condo", "Condos"], ["maison", "Maisons"]] : [["maison", "Maisons"], ["condo", "Condos"], ["plex", "Plex"], ["terrain", "Terrains"]];31  return (32    <div className="relative h-[calc(100dvh-56px-64px)] md:h-[calc(100dvh-64px)]">33      <MapView points={points} center={center} zoom={zoom} rent={market === "location"} activeUid={sel?.uid ?? null} onSelect={pick} onMove={(b, z) => { setZoom(z); load(b, z); }} />34      <div className="absolute top-3 left-3 right-3 md:right-auto md:w-[520px] flex flex-col gap-2">35        <div className="tv-card p-2 flex gap-2 items-center">36          <div className="inline-flex rounded-full border border-line p-0.5 bg-white shrink-0"><button type="button" onClick={() => { setMarket("achat"); setCats([]); }} className={`px-3 py-1.5 rounded-full text-xs font-semibold ${market === "achat" ? "bg-brand text-white" : "text-ink-2"}`}>Acheter</button><button type="button" onClick={() => { setMarket("location"); setCats([]); }} className={`px-3 py-1.5 rounded-full text-xs font-semibold ${market === "location" ? "bg-brand text-white" : "text-ink-2"}`}>Louer</button></div>37          <div className="flex-1 min-w-0"><SearchBar market={market} size="md" onPick={async (city) => { const r = await fetch(`/api/suggest?q=${encodeURIComponent(city)}`).then((r) => r.json()); const pl = r.places?.[0]; const c = await fetch(`/api/search?marche=${market}&ville=${encodeURIComponent(pl?.name ?? city)}&limit=1&format=points`).then((r) => r.json()); const p = c.points?.[0]; if (p) { setCenter([p.lat, p.lng]); setZoom(12); } }} /></div>38        </div>39        <div className="flex gap-2 overflow-x-auto no-scrollbar">{CATS.map(([v, l]) => <button key={v} type="button" className="tv-chip bg-white shrink-0" data-active={cats.includes(v)} onClick={() => setCats((c) => c.includes(v) ? c.filter((x) => x !== v) : [...c, v])}>{l}</button>)}<span className="tv-chip bg-white shrink-0 !text-ink-3">{n >= 2000 ? "2 000+" : n.toLocaleString("fr-CA")} dans la zone{n >= 2000 ? " · zoomez pour tout voir" : ""}</span></div>40      </div>41      {sel && (42        <div className="absolute bottom-3 left-3 right-3 md:left-auto md:w-96">43          <div className="relative"><ListingCardView c={sel} /><button type="button" onClick={() => setSel(null)} className="absolute -top-2 -right-2 p-1.5 rounded-full bg-white shadow border border-line" aria-label="Fermer"><IconClose width={14} height={14} /></button></div>44          <button type="button" onClick={() => router.push(`/annonce/${encodeURIComponent(sel.uid)}`)} className="tv-btn tv-btn-primary w-full mt-2">Voir la fiche</button>45        </div>46      )}47    </div>48  );49}50