Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/ImmeubleBloc.tsx: "Building passport" block (detail page)5// Everything is computed from what Rent-Ka actually observes (active AND6// historical listings of the same building): units, median rents, turnover,7// rent pressure. An indicator without a sufficient sample is shown as8// "insufficient data" — never invented.9// -----------------------------------------------------------------------------10import { Immeuble, fmtPrice } from "../api";1112const NBSP = " ";1314const fmtTs = (ts: number | null | undefined): string =>15 ts16 ? new Date(ts * 1000).toLocaleDateString("en-CA", { month: "long", year: "numeric" })17 : "—";1819const ROTATION_CLS: Record<string, string> = {20 // backend classes (bilingual for migration safety)21 "low": "zi-ok", "normal": "zi-nc", "high": "zi-modere", "very high": "zi-eleve",22 "faible": "zi-ok", "normale": "zi-nc", "élevée": "zi-modere", "très élevée": "zi-eleve",23};2425export default function ImmeubleBloc({ im }: { im: Immeuble }) {26 // at least 2 grouped listings, otherwise the "passport" adds nothing27 if (!im || im.annonces_total < 2) return null;28 const rot = im.rotation;29 const pres = im.pression_loyers;30 const parCc = im.loyer_median_par_cc;3132 return (33 <section className="f-bloc f-immeuble" id="immeuble">34 <h2>Building passport</h2>35 <div className="kv">36 <div className="cell">37 <div className="k">Listings observed</div>38 <div className="v">{im.annonces_total} <small>incl. {im.annonces_actives} active</small></div>39 </div>40 <div className="cell">41 <div className="k">Estimated units</div>42 <div className="v">≥{NBSP}{im.unites_estimees}</div>43 </div>44 {im.loyer_median != null && (45 <div className="cell">46 <div className="k">Median rent (active)</div>47 <div className="v">{fmtPrice(im.loyer_median)}</div>48 </div>49 )}50 {im.pi2_median != null && (51 <div className="cell">52 <div className="k">Median $/sq ft</div>53 <div className="v">${im.pi2_median.toFixed(2)}</div>54 </div>55 )}56 </div>5758 {parCc && Object.keys(parCc).length > 0 && (59 <p className="im-parcc">60 By bedroom count:{" "}61 {Object.entries(parCc)62 .map(([cc, v]) => `${cc}${NBSP}bd ${fmtPrice(v)}`)63 .join(" · ")}64 </p>65 )}6667 <div className="im-indicateurs">68 <div className="im-indic">69 <span className="k">Unit turnover</span>{" "}70 {rot.statut === "calculated" ? (71 <>72 <span className={`zi-badge ${ROTATION_CLS[rot.classe ?? ""] ?? "zi-nc"}`}>73 {rot.classe}74 </span>{" "}75 <span className="im-detail">76 {rot.annonces_12m} listing{(rot.annonces_12m ?? 0) > 1 ? "s" : ""} over77 12 months for ≥{NBSP}{rot.unites_estimees} units78 </span>79 </>80 ) : (81 <span className="zi-badge zi-nc">insufficient data</span>82 )}83 </div>84 <div className="im-indic">85 <span className="k">Rent pressure</span>{" "}86 {pres ? (87 <>88 <span className={`zi-badge ${pres.variation_12m > 0.05 ? "zi-modere" : pres.variation_12m < -0.02 ? "zi-ok" : "zi-nc"}`}>89 {pres.variation_12m > 0 ? "+" : "−"}90 {Math.abs(Math.round(pres.variation_12m * 100))}{NBSP}% over 12 months91 </span>{" "}92 <span className="im-detail">93 median asking price: {fmtPrice(pres.mediane_12_24m)} →{" "}94 {fmtPrice(pres.mediane_12m)} ({pres.n_12_24m} vs {pres.n_12m} listings)95 </span>96 </>97 ) : (98 <span className="zi-badge zi-nc">insufficient sample</span>99 )}100 </div>101 </div>102103 <p className="fine">104 Building tracked by Rent-Ka since {fmtTs(im.premiere_observation)}105 {rot.statut === "calculated" && rot.methode ? ` — ${rot.methode}` : (106 " — indicators computed only from listings observed by Rent-Ka " +107 "syncs (not a census of the building)."108 )}109 </p>110 </section>111 );112}113