spb/immo-ka Public
Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 66.4%
TypeScript 19.9%
CSS 13.2%
HTML 0.5%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// pages/Sources.tsx : registre des agences — éclaté par bannière → sous-agence5// -----------------------------------------------------------------------------6import { useEffect, useState } from "react";7import { Link } from "react-router-dom";8import { Franchise, fetchAgencies } from "../api";910export default function SourcesPage() {11 const [data, setData] = useState<Franchise[] | null>(null);12 const [error, setError] = useState<string | null>(null);13 const [open, setOpen] = useState<Record<string, boolean>>({});1415 useEffect(() => {16 fetchAgencies().then((r) => setData(r.franchises)).catch((e) => setError(String(e)));17 }, []);1819 const totalProps = (data ?? []).reduce((s, f) => s + f.total, 0);20 const totalSub = (data ?? []).reduce((s, f) => s + f.sub_agencies, 0);2122 return (23 <div className="container sources">24 <span className="kicker">Registre — agences & sous-agences</span>25 <h1>Sources par sous-agence</h1>26 <p className="sub">27 Chaque bannière immobilière du Québec est éclatée par sous-agence (bureau).28 Chaque annonce est synchronisée par un connecteur dédié et dédoublonnée par29 numéro Centris. Cliquez un compte pour voir les propriétés.30 </p>3132 {error && <div className="notice">⚠️ {error}</div>}33 {!data && !error && <div className="notice">Chargement…</div>}3435 {data && (36 <>37 <p className="stats-foot" style={{ marginTop: 0 }}>38 <b>{totalProps.toLocaleString("fr-CA")}</b> propriétés ·{" "}39 <b>{data.length}</b> bannières ·{" "}40 <b>{totalSub.toLocaleString("fr-CA")}</b> sous-agences41 </p>4243 <div className="franchise-list">44 {data.map((f) => {45 const isOpen = open[f.franchise] ?? false;46 return (47 <div className="franchise" key={f.franchise}>48 <button49 className="franchise-head"50 onClick={() => setOpen({ ...open, [f.franchise]: !isOpen })}51 aria-expanded={isOpen}52 >53 <span className="fh-caret">{isOpen ? "▾" : "▸"}</span>54 <span className="fh-name">{f.franchise}</span>55 <span className="fh-sub">56 {f.sub_agencies} sous-agence{f.sub_agencies > 1 ? "s" : ""}57 </span>58 <span className="fh-count">{f.total.toLocaleString("fr-CA")}</span>59 </button>6061 {isOpen && (62 <ul className="subagency-list">63 {f.agencies.map((a) => (64 <li key={a.name}>65 <span className="sa-name">{a.name}</span>66 <Link67 className="count-pill"68 to={`/?source=${encodeURIComponent(a.sources[0])}`}69 >70 {a.count.toLocaleString("fr-CA")}71 </Link>72 </li>73 ))}74 </ul>75 )}76 </div>77 );78 })}79 </div>8081 <p className="stats-foot">82 Les sous-agences des grandes bannières (RE/MAX, Via Capitale, Century 21…)83 servent aussi de source de secours, dédoublonnées par n° Centris — aucun84 double-comptage.85 </p>86 </>87 )}88 </div>89 );90}91