// Auteur : Simon-Pierre Boucher — contact@spboucher.ai "use client"; import { useState } from "react"; import { CountUp } from "./MetricViz"; import { useLang } from "./LangContext"; import type { TKey } from "@/lib/i18n"; export interface ProvStats { generated: string; unites: number; valeur_totale_2026: number; valeur_role_totale: number; valeur_mediane_2026: number; croissance_2021_2026_pct: number; logements: number; aire_etages_km2: number; terrain_km2: number; municipalites: number; totaux_annee: { year: number; total: number; n: number }[]; par_type: { type: string; n: number; total: number; mediane: number | null }[]; par_ville: { ville: string; n: number; total: number; mediane: number | null }[]; } const POP_QC = 9_111_000; // population du Québec (2026, ISQ) const BUDGET_QC = 165_800_000_000; // dépenses budget QC 2026-2027 (approx.) const PIB_QC = 640_000_000_000; // PIB nominal QC (approx. 2025) const money = (v: number, lang: string, frac = 0) => new Intl.NumberFormat(lang === "fr" ? "fr-CA" : "en-CA", { style: "currency", currency: "CAD", maximumFractionDigits: frac, }).format(v); const compact = (v: number, lang: string) => { const fr = lang === "fr"; if (v >= 1e12) return `${(v / 1e12).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 2 })} ${fr ? "billions $" : "T$"}`; if (v >= 1e9) return `${(v / 1e9).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 1 })} G$`; if (v >= 1e6) return `${(v / 1e6).toLocaleString(fr ? "fr-CA" : "en-CA", { maximumFractionDigits: 1 })} M$`; return money(v, lang); }; const num = (v: number, lang: string) => v.toLocaleString(lang === "fr" ? "fr-CA" : "en-CA"); export default function StatsView({ s }: { s: ProvStats }) { const { lang, t } = useLang(); const fr = lang === "fr"; const [q, setQ] = useState(""); const maxYear = Math.max(...s.totaux_annee.map((x) => x.total)); const maxType = Math.max(...s.par_type.map((x) => x.total)); const top10 = s.par_ville.slice(0, 10); const maxVille = Math.max(...top10.map((x) => x.total)); const villes = s.par_ville.filter((v) => v.ville.toLowerCase().includes(q.trim().toLowerCase()) ); return (
{/* ---- LE CHIFFRE ---- */}
{fr ? "Juste pour le show" : "Just for the show"}

{fr ? ( <> Tout l'immobilier du Québec vaut,
au dollar près… ) : ( <> All Québec real estate is worth,
to the dollar… )}

money(v, lang)} />

{fr ? `≈ ${compact(s.valeur_totale_2026, lang)} · ${num(s.unites, lang)} propriétés · ${num(s.municipalites, lang)} municipalités · millésime 2026` : `≈ ${compact(s.valeur_totale_2026, lang)} · ${num(s.unites, lang)} properties · ${num(s.municipalites, lang)} municipalities · 2026 vintage`}

{( [ [fr ? "par Québécois·e" : "per Quebecer", money(s.valeur_totale_2026 / POP_QC, lang)], [fr ? "budgets annuels du Québec" : "annual Québec budgets", `× ${(s.valeur_totale_2026 / BUDGET_QC).toFixed(1)}`], [fr ? "fois le PIB du Québec" : "× Québec GDP", `× ${(s.valeur_totale_2026 / PIB_QC).toFixed(1)}`], [fr ? "depuis 2021 (périmètre constant)" : "since 2021 (constant perimeter)", `+${s.croissance_2021_2026_pct.toLocaleString(fr ? "fr-CA" : "en-CA")} %`], ] as [string, string][] ).map(([k, v]) => ( {v} {k} ))}

{fr ? "Somme des 3 747 008 estimations Vrai-Prix (modèle hédonique, rôle 2026). Évaluation municipale totale correspondante : " : "Sum of 3,747,008 Vrai-Prix estimates (hedonic model, 2026 roll). Corresponding total municipal assessment: "} {compact(s.valeur_role_totale, lang)}.

{fr ? "Télécharger le rapport statistique provincial (PDF)" : "Download the provincial statistics report (PDF)"}{" "} ↓
{/* ---- tuiles physiques ---- */}
{( [ [fr ? "Valeur médiane" : "Median value", money(s.valeur_mediane_2026, lang), true], [fr ? "Logements" : "Dwellings", num(s.logements, lang), false], [fr ? "Plancher bâti" : "Built floor area", `${num(s.aire_etages_km2, lang)} km²`, false], [fr ? "Terrains privés" : "Private land", `${num(s.terrain_km2, lang)} km²`, false], ] as [string, string, boolean][] ).map(([k, v, hero], i) => (

{v}

{k}

))}
{/* ---- valeur provinciale par année ---- */}
2021 → 2026

{fr ? "La province, année par année" : "The province, year by year"}

{s.totaux_annee.map((h, i) => { const prev = i > 0 ? s.totaux_annee[i - 1].total : null; const yoy = prev ? ((h.total / prev - 1) * 100) : null; return (
{h.year} {compact(h.total, lang)} {yoy != null && ( = 0 ? "bg-lime-soft text-green-deep" : "bg-[var(--danger-soft)] text-[var(--danger)]"}`}> {yoy >= 0 ? "+" : ""}{yoy.toFixed(1)} % )}
); })}

{fr ? "Sommes des unités estimées chaque millésime — le parc s'agrandit d'année en année (nouvelles constructions incluses)." : "Sums of units estimated each vintage — the stock grows every year (new construction included)."}

{/* ---- par type ---- */}
{fr ? "Répartition" : "Breakdown"}

{fr ? "Valeur par type de propriété" : "Value by property type"}

{s.par_type.map((tp, i) => (
{t(tp.type as TKey)} {compact(tp.total, lang)} {num(tp.n, lang)} · {fr ? "méd." : "med."} {tp.mediane ? compact(tp.mediane, lang) : "—"}
))}
{/* ---- par ville ---- */}
{fr ? "Palmarès" : "Ranking"}

{fr ? "Valeur par municipalité" : "Value by municipality"}

{/* top 10 en barres */}
{top10.map((v, i) => (
{i + 1} {v.ville} {compact(v.total, lang)}
))}
{/* table complète filtrable */}
setQ(e.target.value)} placeholder={fr ? "Filtrer parmi les 200 plus grandes municipalités…" : "Filter the 200 largest municipalities…"} className="vp-input max-w-md" />
{villes.map((v) => { const rank = s.par_ville.indexOf(v) + 1; return ( ); })}
# {fr ? "Municipalité" : "Municipality"} {fr ? "Propriétés" : "Properties"} {fr ? "Valeur totale" : "Total value"} {fr ? "Valeur médiane" : "Median value"}
{rank} {v.ville} {num(v.n, lang)} {compact(v.total, lang)} {v.mediane ? money(v.mediane, lang) : "—"}

{fr ? `Top 200 des ${num(s.municipalites, lang)} municipalités, par valeur totale estimée (millésime 2026). Généré le ${s.generated}.` : `Top 200 of ${num(s.municipalites, lang)} municipalities by total estimated value (2026 vintage). Generated ${s.generated}.`}

); }