Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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