SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
5.4 KB · 136 lines tsx
Raw Blame History
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