SPB Git

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%
3.7 KB · 91 lines tsx
Raw Blame History
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