spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Toit-Ka5// components/PropertyMap.tsx : mini-carte d'une annonce (fiche détail).6// Fond Ka Maps (Mapbox Standard 3D), anneaux de marchabilité 5/15 min et7// marqueur-prix pulsant — couleur d'anneaux selon l'univers.8// -----------------------------------------------------------------------------9import { useEffect, useRef } from "react";10import mapboxgl from "mapbox-gl";11import "mapbox-gl/dist/mapbox-gl.css";12import { applyKaBasemapConfig, KA_STYLE_URL } from "@groupe-ka/ka-maps";13import { formatCompactPrice } from "@groupe-ka/ka-maps";14import { MAPBOX_TOKEN } from "../kamaps/config";1516/** Cercle géodésique approché (72 côtés) pour les anneaux de marche. */17function circle(lng: number, lat: number, radiusM: number): GeoJSON.Feature {18 const pts: [number, number][] = [];19 for (let i = 0; i <= 72; i++) {20 const a = (i / 72) * 2 * Math.PI;21 const dLat = (radiusM * Math.cos(a)) / 111_000;22 const dLng = (radiusM * Math.sin(a)) / (111_000 * Math.cos((lat * Math.PI) / 180));23 pts.push([lng + dLng, lat + dLat]);24 }25 return { type: "Feature", geometry: { type: "Polygon", coordinates: [pts] }, properties: {} };26}2728const RING_COLORS: Record<string, [string, string]> = {29 louer: ["#1c5c41", "#123f2e"],30 acheter: ["#e23744", "#b3202b"],31};3233export default function PropertyMap({ lat, lng, price, tx }: {34 lat: number;35 lng: number;36 price?: number | null;37 tx: "louer" | "acheter";38}) {39 const div = useRef<HTMLDivElement>(null);4041 useEffect(() => {42 if (!div.current) return;43 const [fill, line] = RING_COLORS[tx] ?? RING_COLORS.acheter;44 const map = new mapboxgl.Map({45 container: div.current,46 accessToken: MAPBOX_TOKEN,47 style: KA_STYLE_URL,48 center: [lng, lat],49 zoom: 15.2,50 pitch: 45,51 scrollZoom: false,52 dragRotate: false,53 attributionControl: false,54 });55 map.addControl(new mapboxgl.AttributionControl({ compact: true }), "bottom-right");56 map.addControl(new mapboxgl.NavigationControl({ showCompass: false }), "top-right");57 map.touchZoomRotate.disableRotation();5859 map.once("load", () => {60 applyKaBasemapConfig(map, "light");61 map.addSource("rings", {62 type: "geojson",63 data: {64 type: "FeatureCollection",65 features: [circle(lng, lat, 400), circle(lng, lat, 1200)],66 },67 });68 map.addLayer({69 id: "rings-fill", type: "fill", source: "rings", slot: "top",70 paint: { "fill-color": fill, "fill-opacity": 0.035 },71 });72 map.addLayer({73 id: "rings-line", type: "line", source: "rings", slot: "top",74 paint: {75 "line-color": line, "line-opacity": 0.55,76 "line-width": 1.4, "line-dasharray": [3, 3],77 },78 });79 });8081 const el = document.createElement("div");82 el.className = "pm-marker";83 el.innerHTML =84 `<div class="pm-pill">${price != null ? formatCompactPrice(price) : "•"}</div>` +85 `<div class="pm-pulse"></div><div class="pm-dot"></div>`;86 const marker = new mapboxgl.Marker({ element: el, anchor: "bottom" })87 .setLngLat([lng, lat])88 .addTo(map);8990 return () => {91 marker.remove();92 map.remove();93 };94 }, [lat, lng, price, tx]);9596 return (97 <div className="pm-wrap">98 <div ref={div} style={{ position: "absolute", inset: 0 }} role="img" aria-label="Carte de l'annonce" />99 <div className="pm-rings-legend" aria-hidden="true">100 <span>◌ 5 min à pied</span>101 <span>◌ 15 min à pied</span>102 </div>103 </div>104 );105}106