1// -----------------------------------------------------------------------------2// House-Ka — Homes-for-sale aggregator (Canada outside Québec, Ontario first)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/NearbyPlaces.tsx : “Shops and transit” block (listing page)5// Distance to the closest location of each big banner (Costco, Walmart,6// Canadian Tire… via the Mapbox Search Box API) + nearest transit stops.7// SVG monogram chips in the banners' colours (no registered logos).8// -----------------------------------------------------------------------------9import { useEffect, useState } from "react";10import { CommercesNearby, fetchCommerces, fmtDist } from "../api";1112// id -> [background colour, monogram, text colour]13const ICONS: Record<string, [string, string, string?]> = {14 metro_station: ["#0083C9", "M"],15 rem_station: ["#84BD00", "R"],16 arret_bus: ["#4E5357", "B"],17 gare_train: ["#6E5B3F", "T"],18 costco: ["#005DAA", "C"],19 walmart: ["#0071CE", "W"],20 metro: ["#EF3E42", "M"],21 iga: ["#D50032", "IGA"],22 maxi: ["#0079C1", "Mx"],23 superc: ["#E4002B", "SC"],24 provigo: ["#DA291C", "P"],25 canadiantire: ["#D6001C", "CT"],26 dollarama: ["#00B140", "D", "#FFDD00"],27 saq: ["#892034", "SAQ"],28 pharmaprix: ["#E11B22", "Ph"],29 jeancoutu: ["#003DA5", "JC"],30 homedepot: ["#F96302", "HD"],31 rona: ["#1B4298", "R"],32};3334function Chip({ id }: { id: string }) {35 const [bg, mono, fg] = ICONS[id] ?? ["#777", "•"];36 const fs = mono.length >= 3 ? 9 : mono.length === 2 ? 11 : 14;37 return (38 <svg className="cm-ico" viewBox="0 0 28 28" width="28" height="28"39 aria-hidden="true">40 {["metro_station", "rem_station", "arret_bus", "gare_train"].includes(id)41 ? <circle cx="14" cy="14" r="13" fill={bg} />42 : <rect x="1" y="1" width="26" height="26" rx="7" fill={bg} />}43 <text x="14" y="14" textAnchor="middle" dominantBaseline="central"44 fontSize={fs} fontWeight="800" fontFamily="inherit"45 fill={fg ?? "#fff"}>{mono}</text>46 </svg>47 );48}4950export default function NearbyPlaces({ lat, lng }:51 { lat: number | null; lng: number | null }) {52 const [d, setD] = useState<CommercesNearby | null>(null);53 useEffect(() => {54 setD(null);55 if (lat == null || lng == null) return;56 fetchCommerces(lat, lng).then(setD).catch(() => setD(null));57 }, [lat, lng]);58 if (lat == null || lng == null || !d) return null;59 const all = [...(d.transit ?? []), ...(d.commerces ?? [])];60 if (all.length === 0) return null;6162 return (63 <section className="f-bloc f-commerces" id="nearby">64 <h2>Shops and transit</h2>65 <ul className="cm-grille">66 {all.map((c) => (67 <li key={c.id} className="cm-item"68 title={c.adresse || undefined}>69 <Chip id={c.id} />70 <span className="cm-txt">71 <span className="cm-nom">{c.commerce}</span>72 <span className="cm-poi">{c.nom}</span>73 </span>74 <span className="cm-dist">{fmtDist(c.dist_m)}</span>75 </li>76 ))}77 </ul>78 <p className="fine">79 Closest location of each banner — straight-line distances (Mapbox80 search; transit: OpenStreetMap).81 </p>82 </section>83 );84}85