TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useEffect, useMemo, useState } from "react";3import { useRouter, useSearchParams } from "next/navigation";4import type { SearchParams, SearchResult } from "@/lib/types";5import { ListingCardView } from "@/components/listing-card";6import { FilterBar } from "@/components/filters";7import { SearchBar } from "@/components/search-bar";8import { MapView, type MapPoint } from "@/components/map-view";9import { Empty } from "@/components/ui";10import { IconList, IconMap, IconHeart } from "@/components/icons";11import { useSavedSearches } from "@/lib/favorites";1213export function ResultsView({ initial, points, params, title }: { initial: SearchResult; points: MapPoint[]; params: SearchParams & { page: number }; title: string }) {14 const [mode, setMode] = useState<"list" | "map">("list");15 const [active, setActive] = useState<string | null>(null);16 const router = useRouter(); const sp = useSearchParams(); const saved = useSavedSearches();17 const rent = params.market === "rent";18 const pages = Math.max(1, Math.ceil(initial.total / initial.limit));19 const center = useMemo<[number, number] | undefined>(() => { if (!points.length) return undefined; const la = points.reduce((s, p) => s + p.lat, 0) / points.length, ln = points.reduce((s, p) => s + p.lng, 0) / points.length; return [la, ln]; }, [points]);20 const goPage = (n: number) => { const u = new URLSearchParams(sp.toString()); u.set("page", String(n)); router.push(`/recherche?${u}`); };21 useEffect(() => { setActive(null); }, [initial]);22 const href = `/recherche?${sp.toString()}`;23 return (24 <div className="mx-auto max-w-[1600px] px-4 py-4">25 <div className="flex flex-col md:flex-row md:items-center gap-3">26 <div className="md:w-96"><SearchBar market={rent ? "location" : "achat"} size="md" placeholder={params.city ?? params.region ?? "Ville, quartier ou adresse"} /></div>27 <div className="flex-1 min-w-0"><FilterBar /></div>28 </div>29 <div className="mt-3 flex flex-wrap items-center justify-between gap-2">30 <h1 className="text-base md:text-lg font-bold basis-full md:basis-auto">{title}</h1>31 <div className="flex items-center gap-2 shrink-0">32 <button type="button" onClick={() => saved.save(title, href)} className="tv-chip hidden sm:inline-flex" title="Sauvegarder cette recherche (local)"><IconHeart width={14} height={14} /> Sauvegarder</button>33 <div className="inline-flex md:hidden rounded-full border border-line p-0.5 bg-white">34 <button type="button" onClick={() => setMode("list")} className={`px-3 py-1 rounded-full text-xs font-semibold inline-flex items-center gap-1 ${mode === "list" ? "bg-ink text-white" : "text-ink-2"}`}><IconList width={14} height={14} /> Liste</button>35 <button type="button" onClick={() => setMode("map")} className={`px-3 py-1 rounded-full text-xs font-semibold inline-flex items-center gap-1 ${mode === "map" ? "bg-ink text-white" : "text-ink-2"}`}><IconMap width={14} height={14} /> Carte</button>36 </div>37 </div>38 </div>39 <div className="mt-3 grid md:grid-cols-[minmax(0,1fr)_minmax(360px,42%)] gap-4">40 <div className={mode === "map" ? "hidden md:block" : ""}>41 {initial.items.length === 0 ? (42 <Empty title="Aucune annonce ne correspond" hint="Élargissez la zone, retirez un filtre ou changez de marché." action={{ href: `/recherche?marche=${rent ? "location" : "achat"}`, label: "Réinitialiser" }} />43 ) : (44 <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">{initial.items.map((c, i) => <div key={c.uid} onMouseEnter={() => setActive(c.uid)} className={active === c.uid ? "ring-2 ring-brand rounded-card" : ""}><ListingCardView c={c} priority={i < 4} /></div>)}</div>45 )}46 {pages > 1 && (47 <nav className="mt-6 flex items-center justify-center gap-2 text-sm" aria-label="Pagination">48 <button type="button" disabled={params.page <= 1} onClick={() => goPage(params.page - 1)} className="tv-btn tv-btn-ghost !py-1.5 disabled:opacity-40">Précédent</button>49 <span className="text-ink-2">Page {params.page} sur {pages}</span>50 <button type="button" disabled={params.page >= pages} onClick={() => goPage(params.page + 1)} className="tv-btn tv-btn-ghost !py-1.5 disabled:opacity-40">Suivant</button>51 </nav>52 )}53 </div>54 <div className={`${mode === "list" ? "hidden md:block" : ""} md:sticky md:top-[76px] h-[calc(100dvh-180px)] md:h-[calc(100dvh-110px)] rounded-card overflow-hidden border border-line`}>55 <MapView points={points} center={center} zoom={center ? (params.city ? 12 : params.region ? 9 : 6.5) : 5.4} rent={rent} activeUid={active} fitToPoints={!!(params.city || params.region || params.q)}56 onSelect={(p) => router.push(`/annonce/${encodeURIComponent(p.uid)}`)} />57 </div>58 </div>59 <p className="mt-6 text-xs text-ink-3">Les annonces proviennent de sites publics ; chaque fiche indique sa source et renvoie à l'annonce d'origine. Les écarts « estimation vs prix » sont statistiques et indicatifs.</p>60 </div>61 );62}63