SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%
5.1 KB · 110 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/MapInner.tsx : partie Mapbox de la carte de la fiche (chargée5//   paresseusement) — KaSpotlightMap (Ka Maps, thème Immo-Ka) + lignes de6//   métro + couche des lieux filtrés (cercles colorés par catégorie +7//   étiquettes) + caméra qui englobe les lieux affichés.8// -----------------------------------------------------------------------------9import { useEffect, useMemo } from "react";10import "mapbox-gl/dist/mapbox-gl.css";11import "@groupe-ka/ka-maps/styles.css";12import type { MapProperty } from "@groupe-ka/ka-maps";13import { KaBrandBadge, KaSpotlightMap, useKaMap } from "@groupe-ka/ka-maps/react";14import type mapboxgl from "mapbox-gl";15import type { Listing } from "../api";16import MetroLignes from "../components/MetroLignes";17import { immoKaMapTheme } from "../kamaps/theme";18import { MAPBOX_TOKEN } from "../kamaps/config";19import type { Categorie, Lieu } from "./InteractiveMap";2021const SRC = "ik-lieux";22/** Cerise signal Immo-Ka — même langage que l'accent de marque. */23export const BUILDING_RED = "#e23744";2425function LieuxLayer({ lieux, categories, center }: { lieux: Lieu[]; categories: Categorie[]; center: [number, number] }) {26  const ka = useKaMap();27  const couleurs = useMemo(() => {28    const m: unknown[] = ["match", ["get", "cat"]];29    for (const c of categories) m.push(c.key, c.color);30    m.push("#666");31    return m;32  }, [categories]);3334  useEffect(() => {35    if (!ka) return;36    const map = (ka as unknown as { map: mapboxgl.Map }).map;37    if (!map) return;38    const data = {39      type: "FeatureCollection" as const,40      features: lieux.map((x) => ({41        type: "Feature" as const, properties: { cat: x.cat, name: x.name },42        geometry: { type: "Point" as const, coordinates: [x.lng, x.lat] },43      })),44    };45    const ensure = () => {46      const src = map.getSource(SRC) as mapboxgl.GeoJSONSource | undefined;47      if (src) { src.setData(data); return; }48      map.addSource(SRC, { type: "geojson", data });49      map.addLayer({50        id: `${SRC}-halo`, type: "circle", source: SRC,51        paint: { "circle-radius": 9, "circle-color": "#ffffff", "circle-opacity": 0.95 },52      });53      map.addLayer({54        id: `${SRC}-dot`, type: "circle", source: SRC,55        paint: { "circle-radius": 6, "circle-color": couleurs as mapboxgl.ExpressionSpecification },56      });57      map.addLayer({58        id: `${SRC}-lbl`, type: "symbol", source: SRC,59        layout: {60          "text-field": ["get", "name"], "text-size": 11, "text-offset": [0, 1.1], "text-anchor": "top",61          "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-optional": true,62        },63        paint: { "text-color": "#141814", "text-halo-color": "#ffffff", "text-halo-width": 1.4 },64      });65    };66    const apply = () => { try { ensure(); } catch { /* style pas prêt */ } };67    if (map.isStyleLoaded()) apply();68    map.on("style.load", apply);69    map.on("load", apply);70    // caméra : englober les lieux + le bâtiment ; sans lieu → retour sur le bâtiment71    if (lieux.length > 0) {72      let w = center[0], e = center[0], s = center[1], n = center[1];73      const proches = lieux.filter((x) => x.dist_m <= 2000);74      for (const x of proches.length ? proches : lieux) { w = Math.min(w, x.lng); e = Math.max(e, x.lng); s = Math.min(s, x.lat); n = Math.max(n, x.lat); }75      map.fitBounds([[w, s], [e, n]], { padding: { top: 60, bottom: 40, left: 40, right: 40 }, pitch: 30, maxZoom: 16, duration: 700 });76    } else {77      map.easeTo({ center, zoom: 17, pitch: 62, duration: 700 });78    }79    return () => { map.off("style.load", apply); map.off("load", apply); };80  }, [ka, lieux, couleurs, center]);8182  useEffect(() => () => {83    if (!ka) return;84    const map = (ka as unknown as { map: mapboxgl.Map }).map;85    try {86      for (const id of [`${SRC}-lbl`, `${SRC}-dot`, `${SRC}-halo`]) if (map.getLayer(id)) map.removeLayer(id);87      if (map.getSource(SRC)) map.removeSource(SRC);88    } catch { /* carte détruite */ }89  }, [ka]);90  return null;91}9293export default function MapInner({ l, lieux, categories, deal }: { l: Listing; lieux: Lieu[]; categories: Categorie[]; deal: boolean }) {94  const property = useMemo<MapProperty>(() => ({95    id: l.uid, appSource: "immo-ka", latitude: l.lat as number, longitude: l.lng as number,96    kind: "listing", listingType: "sale", price: l.price ?? undefined,97    propertyType: l.property_type || undefined, address: l.address || l.title || undefined,98    city: l.city || undefined, thumbnailUrl: l.images?.[0], highlight: deal,99  }), [l.uid, l.lat, l.lng, l.price, l.property_type, l.address, l.title, l.city, l.images, deal]);100  const center = useMemo<[number, number]>(() => [l.lng as number, l.lat as number], [l.lat, l.lng]);101102  return (103    <KaSpotlightMap theme={immoKaMapTheme} mapboxToken={MAPBOX_TOKEN} property={property} buildingColor={BUILDING_RED}>104      <KaBrandBadge />105      <MetroLignes />106      <LieuxLayer lieux={lieux} categories={categories} center={center} />107    </KaSpotlightMap>108  );109}110