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%
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 Ka</b></div>90 </div>91 );92}93