// ----------------------------------------------------------------------------- // Home-Ka — US real-estate aggregator (Groupe KA) // Author: Simon-Pierre Boucher — contact@spboucher.ai // pages/Home.tsx : home — hero, live stats, advanced filters, grid + map view // ----------------------------------------------------------------------------- import { Suspense, lazy, useEffect, useMemo, useRef, 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"; import { Ico } from "../components/Icons"; const MapView = lazy(() => import("../components/MapView")); const PRICE_STEPS = [100000, 200000, 300000, 400000, 500000, 600000, 750000, 1000000, 1500000, 2000000, 3000000]; const SQFT_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`; /** Animated hero counter (~0.9 s, cubic easing). Respects reduced-motion. */ function useCountUp(target: number | null | undefined, ms = 900): string | null { const [v, setV] = useState(null); useEffect(() => { if (target == null) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setV(target); return; } let raf = 0; const t0 = performance.now(); const step = (t: number) => { const p = Math.min(1, (t - t0) / ms); setV(Math.round(target * (1 - Math.pow(1 - p, 3)))); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [target, ms]); return v == null ? null : v.toLocaleString("en-US"); } /** Live "X ago" — re-rendered every second while the ts exists. */ function useAgo(ts: number | null): string | null { const [, tick] = useState(0); useEffect(() => { if (ts == null) return; const id = setInterval(() => tick((x) => x + 1), 1000); return () => clearInterval(id); }, [ts]); if (ts == null) return null; const s = Math.max(0, Math.floor(Date.now() / 1000 - ts)); if (s < 90) return `${s} s ago`; if (s < 5400) return `${Math.round(s / 60)} min ago`; return `${Math.round(s / 3600)} h ago`; } // pagination window: 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 [params, setParams] = useSearchParams(); const [page, setPage] = useState(Number(params.get("page")) || 1); // 12/page, in the URL const [facets, setFacets] = useState(null); const [cities, setCities] = useState<{ city: string; n: number }[]>([]); const [stats, setStats] = useState(null); const [error, setError] = useState(null); const [q, setQ] = useState(params.get("q") ?? ""); const [usState, setUsState] = useState(params.get("state") ?? ""); const [city, setCity] = useState(params.get("city") ?? ""); 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("beds_min") ?? ""); const [bathsMin, setBathsMin] = useState(params.get("baths_min") ?? ""); const [sqftMin, setSqftMin] = useState(params.get("sqft_min") ?? ""); const [sort, setSort] = useState(params.get("sort") ?? "recent"); const [sheetOpen, setSheetOpen] = useState(false); const [advOpen, setAdvOpen] = useState(false); // Filters bottom-sheet: background scroll lock + Escape to close useEffect(() => { if (!sheetOpen) return; document.body.style.overflow = "hidden"; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSheetOpen(false); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); }; }, [sheetOpen]); const activeFilters = [q, usState, city, ptype, source, priceMin, priceMax, bedsMin, bathsMin, sqftMin].filter(Boolean).length; const advCount = [bedsMin, bathsMin, sqftMin, source].filter(Boolean).length; const [view, setView] = useState<"list" | "map">(params.get("view") === "map" ? "map" : "list"); useEffect(() => { setView(params.get("view") === "map" ? "map" : "list"); }, [params]); // map mode (Ka Map System v2): body class — the page's filter sheet becomes // a modal ABOVE the full-viewport shell. useEffect(() => { document.body.classList.toggle("ka-map-mode", view === "map"); return () => document.body.classList.remove("ka-map-mode"); }, [view]); const filters: ListingFilters = useMemo(() => ({ q, city, state: usState, property_type: ptype, source, price_min: priceMin, price_max: priceMax, beds_min: bedsMin, baths_min: bathsMin, sqft_min: sqftMin, sort, }), [q, city, usState, ptype, source, priceMin, priceMax, bedsMin, bathsMin, sqftMin, sort]); useEffect(() => { fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {}); fetchFacets().then(setFacets).catch(() => {}); fetchStats().then(setStats).catch(() => {}); }, []); // cities narrowed by the selected state useEffect(() => { fetchFacets(usState || undefined).then((f) => setCities(f.cities)).catch(() => setCities([])); }, [usState]); // back to page 1 when filters change const firstRender = useRef(true); useEffect(() => { if (firstRender.current) { firstRender.current = false; return; } setPage(1); }, [filters]); // sync filters + page + sort + view into the URL → going back from a // detail page returns to the SAME page/filters. useEffect(() => { const p = new URLSearchParams(); const set = (k: string, v: string) => { if (v) p.set(k, v); }; set("q", q); set("state", usState); set("city", city); set("property_type", ptype); set("source", source); set("price_min", priceMin); set("price_max", priceMax); set("beds_min", bedsMin); set("baths_min", bathsMin); set("sqft_min", sqftMin); if (sort && sort !== "recent") p.set("sort", sort); if (view === "map") p.set("view", "map"); if (page > 1) p.set("page", String(page)); setParams(p, { replace: true }); }, [filters, page, view, q, usState, city, ptype, source, priceMin, priceMax, bedsMin, bathsMin, sqftMin, sort, setParams]); // load the current page (12 listings) — replaces the grid 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)); if (view === "map") return; // the map-mode panel manages its own scroll document.getElementById("results-top")?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const resetAll = () => { setQ(""); setUsState(""); setCity(""); setPtype(""); setSource(""); setPriceMin(""); setPriceMax(""); setBedsMin(""); setBathsMin(""); setSqftMin(""); }; // live hero data — real connector syncs (recent_syncs) const totalLive = useCountUp(stats?.total); const lastSync = useMemo(() => { const ss = stats?.recent_syncs ?? []; if (!ss.length) return null; const ts = Math.max(...ss.map((s) => s.ts)); return ts > 1e12 ? Math.round(ts / 1000) : ts; }, [stats]); const syncAgo = useAgo(lastSync); const newToday = useMemo(() => { const ss = stats?.recent_syncs ?? []; const now = Date.now() / 1000; return ss .filter((s) => (s.ts > 1e12 ? s.ts / 1000 : s.ts) > now - 86400) .reduce((n, s) => n + (s.added || 0), 0); }, [stats]); const pills: { label: string; clear: () => void }[] = []; if (q) pills.push({ label: `“${q}”`, clear: () => setQ("") }); if (usState) pills.push({ label: usState, clear: () => { setUsState(""); setCity(""); } }); if (city) pills.push({ label: city, clear: () => setCity("") }); 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}+ bd`, clear: () => setBedsMin("") }); if (bathsMin) pills.push({ label: `${bathsMin}+ ba`, clear: () => setBathsMin("") }); if (sqftMin) pills.push({ label: `≥ ${sqftMin} sq ft`, clear: () => setSqftMin("") }); if (source) pills.push({ label: sourceName(source), clear: () => setSource("") }); const topTypes = (facets?.property_types ?? []).slice(0, 7); return (
Aggregator — every listing source in the U.S.

Brokerages, MLS feeds and listing platforms across the United States — aggregated in one place, always current, with full photos and details and a direct link to the original listing.

live {syncAgo && synced {syncAgo}} {newToday > 0 && ( +{newToday.toLocaleString("en-US")} today )} {stats && stats.sources > 0 && ( {stats.sources} sources )}
{sheetOpen &&
setSheetOpen(false)} aria-hidden="true" />}
{/* mobile: criteria summary — opens the sheet (search included, no FAB) */}
{topTypes.map((t) => ( ))}
{pills.length > 0 && (
{pills.map((p) => ( ))}
)}

{listings ? <>{total.toLocaleString("en-US")} home{total !== 1 ? "s" : ""} : "Homes"}

{error && (

Could not load listings

{error}

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

No homes match your criteria

Try widening your search {activeFilters > 0 && <> — or }.

)} {!error && view === "map" && (
Loading the map…
}> setView("list")} onOpenFilters={() => setSheetOpen(true)} filtersCount={activeFilters} />
)} {!error && view === "list" && listings !== null && listings.length > 0 && (
{listings.map((l) => )}
{totalPages > 1 && ( )}
)}
); }