// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/EnvironmentCards.tsx : cartes « Risques » et « Environnement » — // · FloodRiskCard : statut clair (BDZI), explication courte, carte officielle, // méthodologie et portée juridique intactes dans « En savoir plus » ; // · AirQualityCard : verdict + 3 tuiles polluants (couleur sémantique // discrète vs repère OMS / RAA), station et année, méthodologie repliée ; // · GasNearbyCard : 3 statistiques compactes, 3 stations, « Voir les N ». // ----------------------------------------------------------------------------- import { useState } from "react"; import { AirNearby, GazNearby, Inondation, fmtDist } from "../api"; import { IcoDroplets, IcoFuel, IcoWind } from "../components/Icons"; import BottomSheet from "./BottomSheet"; import { Accordion, ErrorState, MoreButton, SectionCard, SkeletonLines, SourceLine, StatTile, StatusBadge, Tone, NBSP } from "./ui"; import { Res } from "./useFicheData"; /* --- Risque d'inondation ------------------------------------------------------ */ const ZI_URL = "https://www.quebec.ca/agriculture-environnement-et-ressources-naturelles/eau/zones-inondables-mobilite-rives-littoral/cartographies"; export function FloodRiskCard({ r, onRetry }: { r: Res; onRetry: () => void }) { if (r.status === "na") return null; const d = r.status === "ok" ? r.data : null; let tone: Tone = "neutral", titre = "Données insuffisantes", texte = ""; if (d) { const z = d.zones[0]; if (d.statut === "en_zone") { tone = d.severite === "eleve" ? "bad" : "warn"; titre = d.severite === "eleve" ? "Risque élevé" : d.severite === "modere" ? "Risque modéré" : "Zone inondable"; texte = `L'adresse se trouve dans une ${z?.type.toLowerCase() ?? "zone inondable"}${z?.recurrence ? ` (${z.recurrence})` : ""}.`; } else if (d.statut === "a_proximite") { tone = "warn"; titre = "Zone inondable à proximité"; texte = `Une ${z?.type.toLowerCase() ?? "zone inondable"} se trouve à environ ${z?.distance_m ?? "—"} m${z?.recurrence ? ` (${z.recurrence})` : ""}.`; } else if (d.statut === "hors_zone") { tone = "good"; titre = "Hors zone inondable"; texte = "L'adresse est à l'extérieur des zones inondables cartographiées de ce secteur."; } else { tone = "neutral"; titre = "Données insuffisantes"; texte = "Cette adresse n'est pas couverte par la cartographie officielle consultée."; } } return ( }> {r.status === "loading" && } {r.status === "error" && Base des zones inondables temporairement indisponible.} {d && ( <>
{titre}

{texte}

{d.zones.length > 1 && (
    {d.zones.slice(1, 4).map((z, i) => (
  • {z.type}{z.recurrence ? ` (${z.recurrence})` : ""}
    {z.distance_m === 0 ? "à l'adresse" : `~${z.distance_m} m`}
  • ))}
)} Voir la carte officielle

Base de données des zones à risque d'inondation (BDZI), gouvernement du Québec — indicatif seulement, selon la position géocodée de l'adresse. L'absence de zone dans un secteur non cartographié ne signifie pas une absence de risque.{" "} La cartographie officielle fait foi.

)}
); } /* --- Qualité de l'air ------------------------------------------------------- */ const ORDRE = ["PM2.5", "NO2", "O3", "PST", "PM10", "SO2", "CO"]; const NOMS: Record = { "PM2.5": "PM2,5", PST: "PST", PM10: "PM10", NO2: "NO₂", O3: "O₃", SO2: "SO₂", CO: "CO" }; const LONG: Record = { "PM2.5": "Particules fines", PST: "Particules totales", PM10: "Particules PM10", NO2: "Dioxyde d'azote", O3: "Ozone", SO2: "Dioxyde de soufre", CO: "Monoxyde de carbone", }; export function AirQualityCard({ r, onRetry }: { r: Res; onRetry: () => void }) { if (r.status === "na") return null; const d = r.status === "ok" ? r.data : null; const pols = d ? ORDRE.filter((p) => d.mesures[p]) : []; if (d && (!d.station || pols.length === 0)) return null; let tone: Tone = "neutral", verdict = "Mesures disponibles"; if (d) { const pm = d.mesures["PM2.5"]; if (pm?.ref) { const rr = pm.moyenne / pm.ref; [tone, verdict] = rr <= 1 ? ["good", "Très bonne"] : rr <= 2 ? ["good", "Bonne"] : rr <= 3 ? ["warn", "Passable"] : ["bad", "Particules élevées"]; } else if (d.mesures.PST?.ref) { [tone, verdict] = d.mesures.PST.moyenne <= d.mesures.PST.ref ? ["good", "Sous la norme"] : ["bad", "Au-dessus de la norme"]; } } const annee = d ? Object.values(d.mesures)[0]?.annee : null; return ( } aside={d && {verdict}}> {r.status === "loading" && } {r.status === "error" && Données de qualité de l'air temporairement indisponibles.} {d && ( <>
{pols.slice(0, 3).map((p) => { const m = d.mesures[p]; const ratio = m.ref ? m.moyenne / m.ref : null; const t: Tone = ratio == null ? "neutral" : ratio <= 1 ? "good" : ratio <= 2 ? "warn" : "bad"; const s = ratio == null ? "sans repère annuel" : ratio <= 1 ? "sous le repère" : ratio <= 1.5 ? "légèrement au-dessus du repère" : `${ratio.toFixed(1).replace(".", ",")}× le repère`; return (
{NOMS[p] ?? p}
{m.moyenne.toLocaleString("fr-CA")}{m.unite}
{s}{m.ref_nom && ratio != null ? ` ${m.ref_nom}` : ""}
); })}
{pols.length > 3 && (
{pols.slice(3).map((p) => { const m = d.mesures[p]; return (
{LONG[p] ?? p} {m.moyenne.toLocaleString("fr-CA")} {m.unite} {m.ref != null ? `repère ${m.ref}` : ""}
); })}
)} station {d.station}{d.distance_km != null ? ` · ${d.distance_km.toLocaleString("fr-CA")} km` : ""}{annee ? ` · données ${annee}` : ""}} />

Moyennes annuelles {annee} mesurées à la station {d.station} ({d.ville}, à {d.distance_km} km) du Réseau de surveillance de la qualité de l'air du Québec (MELCCFP, données ouvertes). Chaque mesure est située par rapport à son repère annuel (lignes directrices OMS 2021, ou norme québécoise RAA pour les PST). L'air à l'adresse peut différer localement.

)}
); } /* --- Essence ------------------------------------------------------------------ */ const cents = (v: number | null | undefined) => (v == null ? "—" : `${v.toLocaleString("fr-CA", { minimumFractionDigits: 1 })}${NBSP}¢`); export function GasNearbyCard({ r, onRetry }: { r: Res; onRetry: () => void }) { const [sheet, setSheet] = useState(false); if (r.status === "na" || r.status === "idle") return null; const d = r.status === "ok" ? r.data : null; if (d && d.stations.length === 0) return null; const Row = ({ s, full = false }: { s: GazNearby["stations"][number]; full?: boolean }) => (
  • {s.nom}{s.moins_chere && la moins chère}
    {fmtDist(s.dist_m)}{full && s.adresse ? ` · ${s.adresse}` : ""}{full ? ` · super ${cents(s.super)} · diesel ${cents(s.diesel)}` : ""}
    {cents(s.regulier)}/L
    régulier
  • ); return ( }> {r.status === "loading" && } {r.status === "error" && Prix de l'essence temporairement indisponibles.} {d && ( <>
      {d.stations.slice(0, 3).map((s, i) => )}
    {d.stations.length > 3 && setSheet(true)}>Voir les {d.n} stations} setSheet(false)} title="Stations-service" sub={`${d.n} stations à moins de ${fmtDist(d.rayon_m)} · prix en ¢/L`} tall>
      {d.stations.map((s, i) => )}
    )}
    ); }