// ----------------------------------------------------------------------------- // Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // pages/Home.tsx : accueil — héro, statistiques, filtres avancés, grille + carte // ----------------------------------------------------------------------------- import { Suspense, lazy, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { Facets, Listing, ListingFilters, Stats, fetchFacets, fetchListings, fetchSources, fetchStats, registerSourceNames, sourceName, } from "../api"; import ListingCard from "../components/ListingCard"; const MapView = lazy(() => import("../components/MapView")); const PRICE_STEPS = [100000, 200000, 300000, 400000, 500000, 600000, 750000, 1000000, 1500000, 2000000, 3000000]; const AREA_STEPS = [800, 1000, 1500, 2000, 3000]; const PAGE = 12; const fmtK = (n: number) => n >= 1_000_000 ? `${n / 1_000_000} M$` : `${Math.round(n / 1000)} k$`; // fenêtre de pagination : 1 … (p-1) p (p+1) … N function pageNumbers(p: number, n: number): (number | "…")[] { if (n <= 7) return Array.from({ length: n }, (_, i) => i + 1); const out: (number | "…")[] = [1]; const lo = Math.max(2, p - 1), hi = Math.min(n - 1, p + 1); if (lo > 2) out.push("…"); for (let i = lo; i <= hi; i++) out.push(i); if (hi < n - 1) out.push("…"); out.push(n); return out; } export default function Home() { const [listings, setListings] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); // pagination 12/page const [facets, setFacets] = useState(null); const [sectors, setSectors] = useState([]); const [stats, setStats] = useState(null); const [error, setError] = useState(null); const [params] = useSearchParams(); const [q, setQ] = useState(params.get("q") ?? ""); const [city, setCity] = useState(params.get("city") ?? ""); const [sector, setSector] = useState(params.get("sector") ?? ""); const [ptype, setPtype] = useState(params.get("property_type") ?? ""); const [source, setSource] = useState(params.get("source") ?? ""); const [priceMin, setPriceMin] = useState(params.get("price_min") ?? ""); const [priceMax, setPriceMax] = useState(params.get("price_max") ?? ""); const [bedsMin, setBedsMin] = useState(params.get("bedrooms_min") ?? ""); const [bathsMin, setBathsMin] = useState(params.get("bathrooms_min") ?? ""); const [areaMin, setAreaMin] = useState(params.get("area_min") ?? ""); const [sort, setSort] = useState(params.get("sort") ?? "recent"); const [sheetOpen, setSheetOpen] = useState(false); const [advOpen, setAdvOpen] = useState(false); const activeFilters = [q, city, sector, ptype, source, priceMin, priceMax, bedsMin, bathsMin, areaMin].filter(Boolean).length; const advCount = [bedsMin, bathsMin, areaMin, source, sector].filter(Boolean).length; const [view, setView] = useState<"liste" | "carte">(params.get("view") === "carte" ? "carte" : "liste"); useEffect(() => { setView(params.get("view") === "carte" ? "carte" : "liste"); }, [params]); const filters: ListingFilters = useMemo(() => ({ q, city, sector, region: "", property_type: ptype, source, price_min: priceMin, price_max: priceMax, bedrooms_min: bedsMin, bathrooms_min: bathsMin, area_min: areaMin, sort, }), [q, city, sector, ptype, source, priceMin, priceMax, bedsMin, bathsMin, areaMin, sort]); useEffect(() => { fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {}); fetchFacets().then(setFacets).catch(() => {}); fetchStats().then(setStats).catch(() => {}); }, []); useEffect(() => { fetchFacets(city || undefined).then((f) => setSectors(f.sectors)).catch(() => setSectors([])); }, [city]); // revenir à la page 1 quand les filtres changent useEffect(() => { setPage(1); }, [filters]); // charger la page courante (12 annonces) — remplace la grille useEffect(() => { let cancelled = false; setListings(null); setError(null); fetchListings(filters, PAGE, (page - 1) * PAGE) .then((r) => { if (!cancelled) { setListings(r.listings); setTotal(r.total); } }) .catch((e) => !cancelled && setError(String(e))); return () => { cancelled = true; }; }, [filters, page]); const totalPages = Math.max(1, Math.ceil(total / PAGE)); const gotoPage = (p: number) => { setPage(Math.min(Math.max(1, p), totalPages)); document.getElementById("results-top")?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const resetAll = () => { setQ(""); setCity(""); setSector(""); setPtype(""); setSource(""); setPriceMin(""); setPriceMax(""); setBedsMin(""); setBathsMin(""); setAreaMin(""); }; const pills: { label: string; clear: () => void }[] = []; if (q) pills.push({ label: `« ${q} »`, clear: () => setQ("") }); if (city) pills.push({ label: city, clear: () => { setCity(""); setSector(""); } }); if (sector) pills.push({ label: sector, clear: () => setSector("") }); if (ptype) pills.push({ label: ptype, clear: () => setPtype("") }); if (priceMin) pills.push({ label: `≥ ${fmtK(Number(priceMin))}`, clear: () => setPriceMin("") }); if (priceMax) pills.push({ label: `≤ ${fmtK(Number(priceMax))}`, clear: () => setPriceMax("") }); if (bedsMin) pills.push({ label: `${bedsMin}+ ch.`, clear: () => setBedsMin("") }); if (bathsMin) pills.push({ label: `${bathsMin}+ sdb`, clear: () => setBathsMin("") }); if (areaMin) pills.push({ label: `≥ ${areaMin} pi²`, clear: () => setAreaMin("") }); if (source) pills.push({ label: sourceName(source), clear: () => setSource("") }); const topTypes = (facets?.property_types ?? []).slice(0, 7); return (
Agrégateur — Toutes les agences du Québec

Toutes les propriétés à vendre,
un seul endroit.

Immo-Ka rassemble les propriétés à vendre affichées par les agences de courtage immobilier du Québec — RE/MAX, Sutton, Via Capitale, Proprio Direct, Barnes et plus — mises à jour automatiquement, avec toutes les photos, les détails complets et un lien direct vers l'annonce originale.

Données synchronisées en continu {stats && ( <> {stats.total.toLocaleString("fr-CA")} propriétés {stats.sources} agences {stats.cities?.toLocaleString("fr-CA")} villes {stats.avg_price != null && ( prix moyen {Math.round(stats.avg_price).toLocaleString("fr-CA")} $ )} )}
{sheetOpen &&
setSheetOpen(false)} aria-hidden="true" />}
Filtres
setQ(e.target.value)} aria-label="Recherche" /> {q && }
Prix
{(advOpen || sheetOpen) && (
{["1", "2", "3", "4", "5"].map((n) => ( ))}
{["1", "2", "3"].map((n) => ( ))}
{AREA_STEPS.map((a) => ( ))}
)}
{topTypes.map((t) => ( ))}
{pills.length > 0 && (
{pills.map((p) => ( ))}
)}

Propriétés à vendre

{listings && {total.toLocaleString("fr-CA")} résultat{total > 1 ? "s" : ""}}
{error && (
⚠️

Impossible de charger les annonces

{error}

)} {!error && view === "liste" && listings === null && (
{Array.from({ length: 8 }).map((_, i) => (
))}
)} {!error && view === "liste" && listings !== null && listings.length === 0 && (
🔍

Aucune propriété ne correspond

Essayez d'élargir vos critères {activeFilters > 0 && <> — ou }.

)} {!error && view === "carte" && (
{(listings ?? []).map((l) => )}
Chargement de la carte…
}>
)} {!error && view === "liste" && listings !== null && listings.length > 0 && ( <>
{listings.map((l) => )}
{totalPages > 1 && ( )} )}
); }