SPB Git

spb/ora-ka Public

Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)

Python 80% TypeScript 12.9% CSS 6.8%
3.6 KB · 93 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// components/PropertyMap.tsx : carte d'emplacement de la fiche propriété5//   · centrée et zoomée sur LA propriété (fond « Groupe Ka » maison)6//   · marqueur pulsant + pastille de prix, rouge si sous l'estimation Vrai-Prix7//   · anneaux « 5 min / 15 min à pied » — signature visuelle Immo-Ka8// -----------------------------------------------------------------------------9import { useEffect, useRef } from "react";10import maplibregl from "maplibre-gl";11import "maplibre-gl/dist/maplibre-gl.css";12import immokaStyle from "../map/immokaStyle";13import { shortPrice } from "../map/pills";1415/** Cercle géodésique approx. (polygone 72 côtés) autour d'un point. */16function circle(lng: number, lat: number, radiusM: number): GeoJSON.Feature {17  const pts: [number, number][] = [];18  const dLat = radiusM / 111_000;19  const dLng = radiusM / (111_000 * Math.cos((lat * Math.PI) / 180));20  for (let i = 0; i <= 72; i++) {21    const a = (i / 72) * 2 * Math.PI;22    pts.push([lng + dLng * Math.cos(a), lat + dLat * Math.sin(a)]);23  }24  return { type: "Feature", properties: {},25           geometry: { type: "Polygon", coordinates: [pts] } };26}2728interface Props {29  lat: number;30  lng: number;31  price?: number | null;32  deal?: boolean;          // prix sous l'estimation Vrai-Prix33}3435export default function PropertyMap({ lat, lng, price, deal }: Props) {36  const div = useRef<HTMLDivElement>(null);3738  useEffect(() => {39    if (!div.current) return;40    const map = new maplibregl.Map({41      container: div.current,42      style: immokaStyle,43      center: [lng, lat],44      zoom: 14.6,45      attributionControl: false,46      scrollZoom: false,          // ne pas piéger le défilement de la page47      dragRotate: false,48    });49    map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");50    map.touchZoomRotate.disableRotation();5152    map.on("load", () => {53      // anneaux piéton — 400 m ≈ 5 min, 1,2 km ≈ 15 min54      map.addSource("rings", {55        type: "geojson",56        data: { type: "FeatureCollection",57                features: [circle(lng, lat, 400), circle(lng, lat, 1200)] },58      });59      map.addLayer({60        id: "rings-fill", type: "fill", source: "rings",61        paint: { "fill-color": "#e23744", "fill-opacity": 0.035 },62      });63      map.addLayer({64        id: "rings-line", type: "line", source: "rings",65        paint: { "line-color": "#b3202b", "line-opacity": 0.55,66                 "line-width": 1.4, "line-dasharray": [3, 3] },67      });68    });6970    // marqueur maison : halo pulsant + pastille de prix71    const el = document.createElement("div");72    el.className = "pm-marker";73    el.innerHTML = `74      <div class="pm-pill${deal ? " pm-pill-deal" : ""}">${shortPrice(price)}${deal ? " ↓" : ""}</div>75      <div class="pm-pulse"></div>76      <div class="pm-dot"></div>`;77    const marker = new maplibregl.Marker({ element: el, anchor: "bottom" })78      .setLngLat([lng, lat]).addTo(map);7980    return () => { marker.remove(); map.remove(); };81  }, [lat, lng, price, deal]);8283  return (84    <div className="pm-wrap">85      <div ref={div} className="mapview" role="img" aria-label="Emplacement de la propriété" />86      <div className="pm-rings-legend" aria-hidden="true">87        <span>◌ 5 min à pied</span><span>◌ 15 min à pied</span>88      </div>89      <div className="mv-credit" aria-hidden="true">Cartographie <b>Groupe&nbsp;Ka</b></div>90    </div>91  );92}93