SPB Git forge

spb/lou-ka

Public

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

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.4 KB · 106 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// fiche/KaScoresCard.tsx : « KA Scores » — 5 cercles compacts (Marche,5//   Transport, Vélo, Calme, Services), score personnalisé selon les6//   pondérations de l'utilisateur, détail honnête en accordéon.7// -----------------------------------------------------------------------------8import { Link } from "react-router-dom";9import { KaScores, KA_DEFAULT_WEIGHTS, fmtDist, kaGlobal, kaLabel, kaWeights } from "../api";10import { kaTint } from "../components/KaScoreBadge";11import { IcoLayers } from "../components/Icons";12import { Accordion, SectionCard, SourceLine } from "./ui";1314const CAT: Record<string, string> = {15  epicerie: "Épicerie", pharmacie: "Pharmacie", parc: "Parc", cafe: "Café", ecole: "École",16  clinique: "Clinique", garderie: "Garderie", depanneur: "Dépanneur", gym: "Gym", bibliotheque: "Bibliothèque",17};18const FAM: Record<string, string> = { commerces: "commerces", sante: "santé", education: "éducation", loisirs: "loisirs" };1920function Cercle({ score, nom, note }: { score: number | null; nom: string; note?: string }) {21  const r = 22, c = 2 * Math.PI * r;22  const part = score == null ? 0 : Math.max(0, Math.min(1, score / 100));23  return (24    <div className={`lk-ks-c ${kaTint(score)}`} role="img"25         aria-label={`${nom} : ${score == null ? note ?? "données insuffisantes" : `${Math.round(score)} sur 100`}`}>26      <div className="lk-ks-wrap">27        <svg viewBox="0 0 52 52" aria-hidden="true">28          <circle className="bg" cx="26" cy="26" r={r} />29          <circle className="arc" cx="26" cy="26" r={r} strokeDasharray={`${c * part} ${c}`} />30        </svg>31        <div className="lk-ks-v">{score == null ? "—" : Math.round(score)}</div>32      </div>33      <div className="lk-ks-n">{nom}</div>34      <div className="lk-ks-l">{score == null ? (note ?? "n/d") : kaLabel(score)}</div>35    </div>36  );37}3839export default function KaScoresCard({ ks }: { ks: KaScores }) {40  const weights = kaWeights();41  const custom = JSON.stringify(weights) !== JSON.stringify(KA_DEFAULT_WEIGHTS);42  const perso = custom ? kaGlobal(ks, weights) : null;43  const d = ks.details ?? {};44  const walkCats = (d.walk?.cats ?? []).filter((c) => c.dist_m != null).slice(0, 6);4546  return (47    <SectionCard id="ka-scores" title="KA Scores" icon={<IcoLayers size={18} />}48                 sub={ks.global != null ? `Global ${Math.round(ks.global)} · ${kaLabel(ks.global)}${perso != null ? ` · selon vos priorités : ${Math.round(perso)}` : ""}` : undefined}>49      <div className="lk-ks">50        <Cercle score={ks.walk} nom="Marche" />51        <Cercle score={ks.transit} nom="Transport" note={ks.transit == null ? "Non desservi" : undefined} />52        <Cercle score={ks.bike} nom="Vélo" />53        <Cercle score={ks.calme} nom="Calme" />54        <Cercle score={ks.services} nom="Services" />55      </div>56      <Accordion title="Le détail des scores de ce secteur" small>57        <div className="lk-ks-detail">58          {walkCats.length > 0 && (59            <div>60              <h4>Marche</h4>61              <ul>{walkCats.map((c) => <li key={c.cat}>{CAT[c.cat] ?? c.cat} : {fmtDist(c.dist_m as number)}</li>)}</ul>62            </div>63          )}64          <div>65            <h4>Transport</h4>66            <ul>67              {d.transit?.arret_bus_m != null && <li>Arrêt de bus à {fmtDist(d.transit.arret_bus_m)}</li>}68              {d.transit?.station_metro_m != null && <li>Station de métro à {fmtDist(d.transit.station_metro_m)}</li>}69              {d.transit?.pmd_percentile != null && <li>Desserte : {d.transit.pmd_percentile}ᵉ percentile canadien (StatCan)</li>}70              {d.transit?.raison && <li>{d.transit.raison}</li>}71            </ul>72            <h4>Vélo</h4>73            <ul>74              {d.bike?.km_cyclables_1km != null && <li>{d.bike.km_cyclables_1km.toLocaleString("fr-CA")} km de voies cyclables à moins de 1 km</li>}75              {d.bike?.raison && <li>{d.bike.raison}</li>}76              {d.bike?.note && <li>{d.bike.note}</li>}77            </ul>78          </div>79          <div>80            <h4>Calme</h4>81            <ul>82              {(d.calme?.sources_bruit ?? []).length === 0 && <li>Aucune source de bruit majeure détectée à proximité</li>}83              {(d.calme?.sources_bruit ?? []).map((s) => <li key={s.source}>{s.source}{s.dist_m != null ? ` à ${fmtDist(s.dist_m)}` : ""}</li>)}84              {(d.calme?.bonus_parc ?? 0) > 0 && <li>Parc à proximité</li>}85              {d.calme?.note && <li>{d.calme.note}</li>}86            </ul>87            {d.services?.familles && (88              <>89                <h4>Services</h4>90                <ul>{Object.entries(d.services.familles).map(([f, n]) => <li key={f}>{n} {FAM[f] ?? f} dans le secteur</li>)}</ul>91              </>92            )}93          </div>94        </div>95        <p>96          Scores 0-100 calculés depuis OpenStreetMap et les mesures de proximité de Statistique Canada — le Calme97          est une estimation d'environnement, pas une mesure sonore. Barème {ks.version}, calculé le{" "}98          {new Date(ks.computed_at * 1000).toLocaleDateString("fr-CA")}.{" "}99          <Link to="/ka-scores">Méthodologie et réglage de vos priorités</Link>100        </p>101      </Accordion>102      <SourceLine name="OpenStreetMap · Statistique Canada" date={`barème ${ks.version}`} />103    </SectionCard>104  );105}106