// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/NearbyPlaces.tsx : « À proximité » et « Transport » — carrousels de // cartes horizontales (transport · courses · services) construits à partir // des POI OpenStreetMap (louka/poi.py) et des grandes bannières (Mapbox), // les 4 lieux les plus pertinents en liste, puis « Voir les N lieux » // → bottom sheet groupé par catégorie. // ----------------------------------------------------------------------------- import { ReactNode, useState } from "react"; import { CommerceItem, CommercesNearby, Poi, fmtDist } from "../api"; import { IcoBaby, IcoBook, IcoBus, IcoCart, IcoCoffee, IcoDumbbell, IcoHospital, IcoMapPin, IcoPill, IcoSchool, IcoStore, IcoTrain, IcoTrees, } from "../components/Icons"; import BottomSheet from "./BottomSheet"; import { ErrorState, MoreButton, SectionCard, SkeletonLines, SourceLine, fmtMarche, NBSP } from "./ui"; import { Res } from "./useFicheData"; const POI_META: Record = { epicerie: { ico: , label: "Épicerie" }, depanneur: { ico: , label: "Dépanneur" }, pharmacie: { ico: , label: "Pharmacie" }, ecole: { ico: , label: "École" }, garderie: { ico: , label: "Garderie" }, parc: { ico: , label: "Parc" }, bus: { ico: , label: "Arrêt de bus" }, metro: { ico: , label: "Métro" }, gym: { ico: , label: "Gym" }, cafe: { ico: , label: "Café" }, clinique: { ico: , label: "Clinique" }, hopital: { ico: , label: "Hôpital" }, bibliotheque: { ico: , label: "Bibliothèque" }, }; const GROUPES: { titre: string; cats: string[] }[] = [ { titre: "Transport", cats: ["metro", "bus"] }, { titre: "Courses", cats: ["epicerie", "depanneur", "pharmacie"] }, { titre: "Études et famille", cats: ["ecole", "garderie", "bibliotheque"] }, { titre: "Santé", cats: ["clinique", "hopital"] }, { titre: "Vie de quartier", cats: ["cafe", "parc", "gym"] }, ]; // bannières : [couleur, monogramme, texte] const BANNIERE_CAT: Record = { metro_station: "Station de métro", rem_station: "Station REM", arret_bus: "Arrêt de bus", gare_train: "Gare de train", costco: "Épicerie · entrepôt", walmart: "Grande surface", metro: "Épicerie", iga: "Épicerie", maxi: "Épicerie", superc: "Épicerie", provigo: "Épicerie", canadiantire: "Quincaillerie", dollarama: "Magasin à 1 $", saq: "Alcools", pharmaprix: "Pharmacie", jeancoutu: "Pharmacie", homedepot: "Rénovation", rona: "Rénovation", }; const catDe = (c: CommerceItem) => BANNIERE_CAT[c.id] ?? c.commerce; const BANNIERES: Record = { metro_station: ["#0083C9", "M"], rem_station: ["#84BD00", "R"], arret_bus: ["#4E5357", "B"], gare_train: ["#6E5B3F", "T"], costco: ["#005DAA", "C"], walmart: ["#0071CE", "W"], metro: ["#EF3E42", "M"], iga: ["#D50032", "IGA"], maxi: ["#0079C1", "Mx"], superc: ["#E4002B", "SC"], provigo: ["#DA291C", "P"], canadiantire: ["#D6001C", "CT"], dollarama: ["#00B140", "D", "#FFDD00"], saq: ["#892034", "SAQ"], pharmaprix: ["#E11B22", "Ph"], jeancoutu: ["#003DA5", "JC"], homedepot: ["#F96302", "HD"], rona: ["#1B4298", "R"], }; export function Pastille({ id }: { id: string }) { const [bg, mono, fg] = BANNIERES[id] ?? ["#777", "•"]; const fs = mono.length >= 3 ? 9 : mono.length === 2 ? 11 : 14; const rond = ["metro_station", "rem_station", "arret_bus", "gare_train"].includes(id); return ( ); } interface Carte { key: string; cat: string; nom: string; sous?: string; dist: number; ico: ReactNode; } function CCard({ c }: { c: Carte }) { return (
{c.cat}
{fmtDist(c.dist)}
{c.nom}
≈{NBSP}{fmtMarche(c.dist)} à pied{c.sous ? ` · ${c.sous}` : ""}
); } export default function NearbyPlaces({ pois, commerces, onRetry }: { pois: Poi[]; commerces: Res; onRetry: () => void }) { const [sheet, setSheet] = useState(false); const cm = commerces.status === "ok" ? commerces.data : null; const transit: CommerceItem[] = cm?.transit ?? []; const bannieres: CommerceItem[] = cm?.commerces ?? []; // cartes « Transport » : métro/bus/REM/train (bannières transit) + POI bus/métro const transport: Carte[] = [ ...transit.map((t) => ({ key: `t-${t.id}`, cat: catDe(t), nom: t.nom, dist: t.dist_m, ico: })), ...pois.filter((p) => (p.cat === "metro" || p.cat === "bus") && !transit.some((t) => t.dist_m === p.dist_m)) .map((p) => ({ key: `p-${p.cat}`, cat: POI_META[p.cat].label, nom: p.name, dist: p.dist_m, ico: POI_META[p.cat].ico })), ].sort((a, b) => a.dist - b.dist); // cartes « Courses et services » : bannières + POI hors transport const services: Carte[] = [ ...bannieres.map((b) => ({ key: `b-${b.id}`, cat: catDe(b), nom: b.nom, dist: b.dist_m, ico: })), ...pois.filter((p) => p.cat !== "metro" && p.cat !== "bus") .map((p) => ({ key: `p-${p.cat}`, cat: POI_META[p.cat]?.label ?? p.cat, nom: p.name, dist: p.dist_m, ico: POI_META[p.cat]?.ico ?? })), ].sort((a, b) => a.dist - b.dist); const total = transport.length + services.length; const loading = commerces.status === "loading" || commerces.status === "idle"; if (total === 0 && !loading && commerces.status !== "error") return null; // « les plus pertinents » : métro/épicerie/pharmacie/parc/école les plus proches const prio = ["Station de métro", "Métro", "Épicerie", "Pharmacie", "Parc", "École", "Arrêt de bus"]; const top = [...transport, ...services] .sort((a, b) => (prio.findIndex((p) => a.cat.startsWith(p)) + 1 || 99) - (prio.findIndex((p) => b.cat.startsWith(p)) + 1 || 99) || a.dist - b.dist) .filter((c, i, arr) => arr.findIndex((x) => x.cat === c.cat) === i) .slice(0, 4); return ( <> } sub={total ? `${total} lieux repérés · temps de marche estimés` : undefined}> {loading && total === 0 && } {commerces.status === "error" && Commerces et transport temporairement indisponibles.} {top.length > 0 && (
    {top.map((c) => (
  • {c.nom}
    {c.cat}
    {fmtDist(c.dist)}
    ≈{NBSP}{fmtMarche(c.dist)}
  • ))}
)} {services.length > 0 && ( <>

Courses et services

{services.slice(0, 12).map((c) => )}
)} {total > 4 && setSheet(true)}>Voir les {total} lieux à proximité}
{transport.length > 0 && ( } sub="Stations et arrêts les plus proches">
{transport.slice(0, 10).map((c) => )}
)} setSheet(false)} title="Lieux à proximité" sub={`${total} lieux · distances à vol d'oiseau`} tall> {GROUPES.map((g) => { const items = [ ...pois.filter((p) => g.cats.includes(p.cat)).map((p) => ({ key: `p-${p.cat}`, cat: POI_META[p.cat]?.label ?? p.cat, nom: p.name, dist: p.dist_m, ico: POI_META[p.cat]?.ico })), ...(g.titre === "Transport" ? transit.map((t) => ({ key: `t-${t.id}`, cat: catDe(t), nom: t.nom, dist: t.dist_m, ico: })) : []), ...(g.titre === "Courses" ? bannieres.map((b) => ({ key: `b-${b.id}`, cat: catDe(b), nom: b.nom, dist: b.dist_m, ico: })) : []), ].sort((a, b) => a.dist - b.dist); if (items.length === 0) return null; return (

{g.titre} · {items.length}

    {items.map((c) => (
  • {c.nom}
    {c.cat}
    {fmtDist(c.dist)}
    ≈{NBSP}{fmtMarche(c.dist)}
  • ))}
); })}
); }