Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// pages/Bannieres.tsx : annuaire des bannières (agences) immobilières du Québec4// issu du registre OACIQ — ~690 bannières, 17 357 courtiers. Filtrable par5// région et par texte ; chaque ligne se déplie pour lister les courtiers de la6// bannière (/api/bannieres puis /api/bannieres/{id} à la demande).7// -----------------------------------------------------------------------------8import { Fragment, useEffect, useMemo, useState } from "react";910type Banniere = {11 id: string;12 oaciq_id: string | null;13 name: string;14 regions: string[];15 brokers_n: number;16 covered_by: string | null;17 site: string | null;18};1920type Broker = { name: string; city: string; region: string };2122type Doc = {23 generated: string | null;24 total: number;25 brokers_total: number;26 count: number;27 regions: { region: string; n: number }[];28 bannieres: Banniere[];29};3031const host = (u: string) => {32 try {33 return new URL(u).hostname.replace(/^www\./, "");34 } catch {35 return u;36 }37};3839export default function BannieresPage() {40 const [doc, setDoc] = useState<Doc | null>(null);41 const [error, setError] = useState<string | null>(null);42 const [q, setQ] = useState("");43 const [region, setRegion] = useState("");44 const [open, setOpen] = useState<string | null>(null);45 const [brokers, setBrokers] = useState<Record<string, Broker[]>>({});4647 useEffect(() => {48 fetch("/api/bannieres")49 .then((r) => {50 if (!r.ok) throw new Error("HTTP " + r.status);51 return r.json();52 })53 .then(setDoc)54 .catch((e) => setError(String(e)));55 }, []);5657 const filtered = useMemo(() => {58 if (!doc) return [];59 const needle = q.trim().toLowerCase();60 return doc.bannieres.filter(61 (b) =>62 (!region || b.regions.includes(region)) &&63 (!needle || b.name.toLowerCase().includes(needle)),64 );65 }, [doc, q, region]);6667 const toggle = (id: string) => {68 if (open === id) {69 setOpen(null);70 return;71 }72 setOpen(id);73 if (!brokers[id]) {74 fetch(`/api/bannieres/${id}`)75 .then((r) => {76 if (!r.ok) throw new Error("HTTP " + r.status);77 return r.json();78 })79 .then((b) => setBrokers((prev) => ({ ...prev, [id]: b.brokers ?? [] })))80 .catch(() => setBrokers((prev) => ({ ...prev, [id]: [] })));81 }82 };8384 const filtering = q.trim() !== "" || region !== "";8586 return (87 <div className="container sources">88 <span className="kicker">Annuaire — courtage immobilier</span>89 <h1>Bannières & courtiers du Québec</h1>90 <p className="sub">91 Toutes les bannières de courtage immobilier de la province selon le92 registre public de l'OACIQ : <b>{doc ? doc.total : "…"}</b> bannières93 et <b>{doc ? doc.brokers_total.toLocaleString("fr-CA") : "…"}</b>{" "}94 courtiers titulaires d'un permis, dans les 17 régions administratives.95 Cliquez sur une bannière pour voir la liste de ses courtiers96 {doc?.generated ? ` — recensement du ${doc.generated}` : ""}.97 </p>9899 {error && <div className="notice">⚠️ {error}</div>}100 {!doc && !error && <div className="notice">Chargement…</div>}101102 {doc && (103 <>104 <div105 style={{106 display: "flex",107 flexWrap: "wrap",108 gap: 10,109 alignItems: "center",110 margin: "6px 0 10px",111 }}112 >113 <input114 type="search"115 value={q}116 onChange={(e) => setQ(e.target.value)}117 placeholder="Rechercher une bannière…"118 aria-label="Rechercher une bannière"119 style={{120 flex: "1 1 240px",121 maxWidth: 380,122 padding: "10px 14px",123 border: "1.5px solid var(--line)",124 borderRadius: 10,125 font: "inherit",126 background: "var(--surface)",127 color: "inherit",128 }}129 />130 <select131 value={region}132 onChange={(e) => setRegion(e.target.value)}133 aria-label="Filtrer par région administrative"134 style={{135 padding: "10px 12px",136 border: "1.5px solid var(--line)",137 borderRadius: 10,138 font: "inherit",139 background: "var(--surface)",140 color: "inherit",141 maxWidth: "100%",142 }}143 >144 <option value="">Toutes les régions ({doc.total})</option>145 {doc.regions.map((r) => (146 <option key={r.region} value={r.region}>147 {r.region} ({r.n})148 </option>149 ))}150 </select>151 {filtering && (152 <span style={{ color: "var(--ink-2)", fontSize: 14 }}>153 {filtered.length} bannière{filtered.length > 1 ? "s" : ""}154 </span>155 )}156 </div>157158 {filtered.length === 0 && (159 <div className="notice">160 Aucune bannière ne correspond à cette recherche.161 </div>162 )}163164 <div className="src-wrap" style={{ marginTop: 12 }}>165 <table className="src-table">166 <thead>167 <tr>168 <th></th>169 <th>Bannière</th>170 <th>Courtiers</th>171 <th>Régions</th>172 <th>Site web</th>173 <th>Suivi Immo-Ka</th>174 </tr>175 </thead>176 <tbody>177 {filtered.map((b) => (178 <Fragment key={b.id}>179 <tr180 onClick={() => toggle(b.id)}181 style={{ cursor: "pointer" }}182 >183 <td style={{ width: 24, color: "var(--ink-2)" }}>184 {open === b.id ? "▾" : "▸"}185 </td>186 <td>187 <b>{b.name}</b>188 </td>189 <td style={{ whiteSpace: "nowrap" }}>{b.brokers_n}</td>190 <td style={{ fontSize: 13 }}>191 {b.regions.length > 3192 ? `${b.regions.slice(0, 3).join(", ")} +${b.regions.length - 3}`193 : b.regions.join(", ")}194 </td>195 <td>196 {b.site ? (197 <a198 href={b.site}199 target="_blank"200 rel="noopener noreferrer nofollow"201 onClick={(e) => e.stopPropagation()}202 >203 {host(b.site)}204 </a>205 ) : (206 "—"207 )}208 </td>209 <td style={{ whiteSpace: "nowrap" }}>210 {b.covered_by ? (211 <span title={`Connecteur : ${b.covered_by}`}>212 ✓ annonces suivies213 </span>214 ) : (215 "—"216 )}217 </td>218 </tr>219 {open === b.id && (220 <tr>221 <td222 colSpan={6}223 style={{ background: "var(--surface-2)" }}224 >225 {!brokers[b.id] ? (226 <span style={{ color: "var(--ink-2)" }}>227 Chargement des courtiers…228 </span>229 ) : brokers[b.id].length === 0 ? (230 <span style={{ color: "var(--ink-2)" }}>231 Aucun courtier trouvé.232 </span>233 ) : (234 <div235 style={{236 display: "grid",237 gridTemplateColumns:238 "repeat(auto-fill, minmax(230px, 1fr))",239 gap: "6px 18px",240 padding: "6px 0",241 }}242 >243 {brokers[b.id].map((c, i) => (244 <div key={i} style={{ fontSize: 13 }}>245 <b>{c.name}</b>246 <span style={{ color: "var(--ink-2)" }}>247 {" "}248 — {c.city}249 </span>250 </div>251 ))}252 </div>253 )}254 </td>255 </tr>256 )}257 </Fragment>258 ))}259 </tbody>260 </table>261 </div>262263 <p264 className="sub"265 style={{ marginTop: 34, fontSize: 13, maxWidth: 720 }}266 >267 Annuaire informatif compilé à partir du registre public des268 titulaires de permis de l'OACIQ (Organisme d'autoréglementation du269 courtage immobilier du Québec). Pour vérifier le dossier270 professionnel d'un courtier (permis, mesures disciplinaires),271 consultez directement le registre officiel sur oaciq.com. « Annonces272 suivies » indique que les inscriptions de la bannière sont agrégées273 sur Immo-Ka. Le Groupe KA n'a aucun lien d'affaires avec les274 bannières listées.275 </p>276 </>277 )}278 </div>279 );280}281