spb/ora-ka Public
Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)
Python 80%
TypeScript 12.9%
CSS 6.8%
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 { Ico } from "../components/Icons";7import { useEffect, useState } from "react";8import { Link } from "react-router-dom";9import { Franchise, fetchAgencies } from "../api";1011export default function SourcesPage() {12 const [data, setData] = useState<Franchise[] | null>(null);13 const [error, setError] = useState<string | null>(null);14 const [open, setOpen] = useState<Record<string, boolean>>({});1516 useEffect(() => {17 fetchAgencies().then((r) => setData(r.franchises)).catch((e) => setError(String(e)));18 }, []);1920 const totalProps = (data ?? []).reduce((s, f) => s + f.total, 0);21 const totalSub = (data ?? []).reduce((s, f) => s + f.sub_agencies, 0);2223 return (24 <div className="container sources">25 <span className="kicker">Registre — agences & sous-agences</span>26 <h1>Sources par sous-agence</h1>27 <p className="sub">28 Chaque bannière immobilière du Québec est éclatée par sous-agence (bureau).29 Chaque annonce est synchronisée par un connecteur dédié et dédoublonnée par30 numéro Centris. Cliquez un compte pour voir les propriétés.31 </p>3233 {error && <div className="notice"> {error}</div>}34 {!data && !error && <div className="notice">Chargement…</div>}3536 {data && (37 <>38 <p className="stats-foot" style={{ marginTop: 0 }}>39 <b>{totalProps.toLocaleString("fr-CA")}</b> propriétés ·{" "}40 <b>{data.length}</b> bannières ·{" "}41 <b>{totalSub.toLocaleString("fr-CA")}</b> sous-agences42 </p>4344 <div className="franchise-list">45 {data.map((f) => {46 const isOpen = open[f.franchise] ?? false;47 return (48 <div className="franchise" key={f.franchise}>49 <button50 className="franchise-head"51 onClick={() => setOpen({ ...open, [f.franchise]: !isOpen })}52 aria-expanded={isOpen}53 >54 <span className="fh-caret">{isOpen ? "▾" : "▸"}</span>55 <span className="fh-name">{f.franchise}</span>56 <span className="fh-sub">57 {f.sub_agencies} sous-agence{f.sub_agencies > 1 ? "s" : ""}58 </span>59 <span className="fh-count">{f.total.toLocaleString("fr-CA")}</span>60 </button>6162 {isOpen && (63 <ul className="subagency-list">64 {f.agencies.map((a) => (65 <li key={a.name}>66 <span className="sa-name">{a.name}</span>67 <Link68 className="count-pill"69 to={`/?source=${encodeURIComponent(a.sources[0])}`}70 >71 {a.count.toLocaleString("fr-CA")}72 </Link>73 </li>74 ))}75 </ul>76 )}77 </div>78 );79 })}80 </div>8182 <p className="stats-foot">83 Les sous-agences des grandes bannières (RE/MAX, Via Capitale, Century 21…)84 servent aussi de source de secours, dédoublonnées par n° Centris — aucun85 double-comptage.86 </p>87 </>88 )}89 </div>90 );91}92