spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// components/QuartierBlock.tsx : section « Le quartier » de la fiche —5// démographie du recensement 2021 (aire de diffusion ~500 hab.), scores de6// proximité StatCan, îlot de chaleur/fraîcheur INSPQ, criminalité.7// -----------------------------------------------------------------------------8import { Quartier } from "../api";910const fmtMoney = (v: number | null | undefined) =>11 v == null ? null : v.toLocaleString("fr-CA", { maximumFractionDigits: 0 }) + " $";12const fmtPct = (v: number | null | undefined) =>13 v == null ? null : Math.round(v) + " %";1415// scores PMD affichés (clé backend -> libellé)16const PROX_LABELS: [string, string][] = [17 ["prox_epicerie", "Épiceries"],18 ["prox_transport", "Transport en commun"],19 ["prox_parc", "Parcs"],20 ["prox_ecole_prim", "Écoles primaires"],21 ["prox_sante", "Soins de santé"],22 ["prox_pharmacie", "Pharmacies"],23];2425function chaleurBadge(classe: number, ecart: number | null) {26 if (classe <= 3)27 return { txt: "Îlot de fraîcheur", cls: "q-badge cool", ico: "🌿" };28 if (classe >= 7)29 return {30 txt: `Îlot de chaleur${ecart != null ? ` (+${ecart.toFixed(1)} °C)` : ""}`,31 cls: "q-badge hot", ico: "🌡",32 };33 return { txt: "Température de quartier moyenne", cls: "q-badge neutral", ico: "🌤" };34}3536export default function QuartierBlock({ q }: { q: Quartier }) {37 const d = q.demographie;38 const stats: [string, string | null][] = d39 ? [40 ["Revenu médian des ménages", fmtMoney(d.revenu_median)],41 ["Ménages locataires", fmtPct(d.pct_locataires)],42 ["Loyer moyen du secteur", fmtMoney(d.loyer_moyen)],43 ["Âge médian", d.age_median != null ? `${Math.round(d.age_median)} ans` : null],44 ["Français à la maison", fmtPct(d.pct_francais)],45 ["Diplôme universitaire", fmtPct(d.pct_univ)],46 ]47 : [];48 const statsOk = stats.filter(([, v]) => v != null) as [string, string][];49 const prox = q.proximite ?? {};50 const proxOk = PROX_LABELS.filter(([k]) => typeof prox[k] === "number");5152 if (statsOk.length === 0 && proxOk.length === 0 && !q.chaleur && !q.crime)53 return null;5455 return (56 <section className="quartier">57 <h2>Le quartier</h2>58 <p className="q-sub">59 Secteur immédiat de l'immeuble (aire de diffusion du recensement, ± 500 habitants).60 </p>6162 {statsOk.length > 0 && (63 <div className="q-grid">64 {statsOk.map(([label, val]) => (65 <div className="q-cell" key={label}>66 <div className="q-val">{val}</div>67 <div className="q-label">{label}</div>68 </div>69 ))}70 </div>71 )}7273 {proxOk.length > 0 && (74 <div className="q-prox">75 {proxOk.map(([k, label]) => {76 const v = Math.max(0, Math.min(1, prox[k]));77 return (78 <div className="q-bar" key={k}>79 <span className="q-bar-label">{label}</span>80 <span className="q-bar-track">81 <span className="q-bar-fill" style={{ width: `${Math.round(v * 100)}%` }} />82 </span>83 <span className="q-bar-num">{Math.round(v * 100)}</span>84 </div>85 );86 })}87 <div className="fine">Accessibilité 0–100 — mesures de proximité de Statistique Canada.</div>88 </div>89 )}9091 <div className="q-badges">92 {q.chaleur && (() => {93 const b = chaleurBadge(q.chaleur.classe, q.chaleur.ecart);94 return <span className={b.cls}>{b.ico} {b.txt}</span>;95 })()}96 {q.crime?.type === "points" && (97 <span className="q-badge neutral">98 🛡 {q.crime.douze_mois} acte{q.crime.douze_mois > 1 ? "s" : ""} criminel{q.crime.douze_mois > 1 ? "s" : ""} à99 moins de 500 m (12 mois)100 {q.crime.douze_mois_precedents > 0 && (101 q.crime.douze_mois <= q.crime.douze_mois_precedents102 ? ` · en baisse (${q.crime.douze_mois_precedents} l'année d'avant)`103 : ` · en hausse (${q.crime.douze_mois_precedents} l'année d'avant)`104 )}105 </span>106 )}107 {q.crime?.type === "igc" && (() => {108 const c = q.crime;109 if (c.indice_canada != null && c.indice_canada > 0) {110 const delta = Math.round(100 * (c.indice - c.indice_canada) / c.indice_canada);111 const sous = delta <= 0;112 return (113 <span className={`q-badge ${sous ? "cool" : "neutral"}`}>114 🛡 Criminalité {Math.abs(delta)} % {sous ? "sous" : "au-dessus de"} la115 moyenne canadienne{sous ? " ✅" : ""}116 <small className="q-badge-sub">({c.ville} {c.annee} : {c.indice} · Canada : {c.indice_canada})</small>117 </span>118 );119 }120 return (121 <span className="q-badge neutral">122 🛡 Gravité de la criminalité ({c.ville}, {c.annee}) : <b>{c.indice}</b>123 </span>124 );125 })()}126 </div>127128 <div className="fine">129 Sources : Statistique Canada (Recensement 2021, licence ouverte), INSPQ130 (CC-BY 4.0){q.crime?.type === "points" ? ", Ville de Montréal (CC-BY 4.0)" : ""}.131 Statistiques du secteur, pas de l'immeuble.132 </div>133 </section>134 );135}136