Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Groupe Ka / Ka Maps (Rent-Ka integration)5// components/MapView.tsx: Rent-Ka Maps — the product map, powered by6// le framework partagé Ka Maps (moteur MapLibre, pastilles de prix GPU,7// clustering natif, « Rechercher dans cette zone », aperçu React).8// · chargée paresseusement (React.lazy) comme avant ;9// · données pilotées par le viewport : /api/listings.geojson?bbox=…10// (requêtes annulables, cache, jamais de tempête pendant le pan) ;11// · caméra partageable dans l'URL (?lat=…&lng=…&zoom=…).12// -----------------------------------------------------------------------------13import { useCallback, useEffect, useMemo } from "react";14import { useNavigate } from "react-router-dom";15import "mapbox-gl/dist/mapbox-gl.css";16import "@groupe-ka/ka-maps/styles.css";17import type { MapProperty } from "@groupe-ka/ka-maps";18import { cameraFromParams, cameraToParams } from "@groupe-ka/ka-maps";19import {20 KaBrandBadge,21 KaMapView,22 LoadingIndicator,23 PropertyPreview,24 ResultCount,25 SearchAreaControl,26 Tilt3DControl,27 useKaMap,28} from "@groupe-ka/ka-maps/react";29import { ListingFilters, sourceName } from "../api";30import { louKaMapTheme } from "../kamaps/theme";31import { louKaMapAdapter } from "../kamaps/adapter";32import { MAPBOX_TOKEN } from "../kamaps/config";3334const CANADA_CENTER = { lat: 47.5, lng: -84.5 };3536const fmtDispo = (iso: string | null | undefined): string =>37 !iso ? "" : iso === "now" ? "Available now"38 : `Available ${new Date(iso + "T12:00:00").toLocaleDateString("en-CA", { day: "numeric", month: "short", year: "numeric" })}`;3940/** Compact rental preview — same visual language as ListingCard. */41function PreviewCard({ p }: { p: MapProperty }) {42 const navigate = useNavigate();43 const extra = (p.extra ?? {}) as {44 title?: string | null; priceLabel?: string | null;45 availabilityDate?: string | null; source?: string; areaSqft?: number | null;46 };47 const prix = p.price != null48 ? `$${p.price.toLocaleString("en-CA", { maximumFractionDigits: 0 })}`49 : extra.priceLabel || "Price on request";50 const meta = [51 p.propertyType,52 extra.areaSqft ? `${Math.round(extra.areaSqft)} sq ft` : "",53 fmtDispo(extra.availabilityDate),54 ].filter(Boolean).join(" · ");55 return (56 <div className="mv-pop">57 {p.thumbnailUrl ? (58 <img src={p.thumbnailUrl} alt="" loading="lazy" />59 ) : (60 <div className="mv-noimg" aria-hidden="true">⌂</div>61 )}62 <div className="mv-pop-body">63 <div className="mv-pop-price">64 {prix}65 <small>{p.price != null ? " /month" : ""}</small>66 </div>67 <div className="mv-pop-title">{extra.title ?? p.address ?? ""}</div>68 <div className="mv-pop-meta">{meta}</div>69 <div className="mv-pop-src">{sourceName(extra.source ?? "")}</div>70 <a71 className="mv-pop-cta"72 href={p.originalUrl}73 onClick={(e) => {74 e.preventDefault();75 navigate(p.originalUrl ?? "/");76 }}77 >78 See the listing →79 </a>80 </div>81 </div>82 );83}8485/** Pont déclaratif : reflète la sélection/le survol venus de la liste. */86function SelectionBridge({ selectedUid, hoveredUid }: {87 selectedUid: string | null;88 hoveredUid: string | null;89}) {90 const map = useKaMap();91 useEffect(() => { map?.select(selectedUid, "app"); }, [map, selectedUid]);92 useEffect(() => { map?.setHovered(hoveredUid, "app"); }, [map, hoveredUid]);93 return null;94}9596export interface MapViewProps {97 filters: ListingFilters;98 selectedUid?: string | null;99 hoveredUid?: string | null;100 onSelect?: (uid: string | null) => void;101}102103export default function MapView({104 filters, selectedUid = null, hoveredUid = null, onSelect,105}: MapViewProps) {106 // initial camera: shared URL (?lat&lng&zoom) else Canada107 const initialCamera = useMemo(() => {108 const cam = cameraFromParams(new URLSearchParams(window.location.search));109 return cam ?? { ...CANADA_CENTER, zoom: 4.4 };110 }, []);111112 // caméra → URL (replaceState : pas de re-render du routeur)113 const onMoveEnd = useCallback((center: { lat: number; lng: number }, zoom: number) => {114 const url = new URL(window.location.href);115 url.search = cameraToParams({ ...center, zoom }, url.searchParams).toString();116 window.history.replaceState(null, "", url);117 }, []);118119 const mapFilters = useMemo(120 () => Object.fromEntries(Object.entries(filters).filter(([, v]) => v)),121 [filters],122 );123124 return (125 <div className="mapview" role="application" aria-label="Rentals map">126 <KaMapView127 theme={louKaMapTheme}128 adapter={louKaMapAdapter}129 mapboxToken={MAPBOX_TOKEN}130 filters={mapFilters}131 center={initialCamera}132 zoom={initialCamera.zoom}133 pitch={50}134 // Rendu réaliste : Standard pleine couleur + repères 3D (stades,135 // ponts, églises) — les pastilles encre/blanc gardent leur contraste.136 basemap={{ theme: "default", showLandmarks: true }}137 cluster={{ maxZoom: 15, valueClamp: [250, 8000] }}138 searchMode="manual"139 onMoveEnd={onMoveEnd}140 onSelect={(p) => onSelect?.(p?.id ?? null)}141 >142 <SelectionBridge selectedUid={selectedUid} hoveredUid={hoveredUid} />143 <KaBrandBadge />144 <Tilt3DControl />145 <SearchAreaControl />146 <LoadingIndicator label="Updating rentals…" />147 <ResultCount148 emptyTitle="No rentals found in this area."149 emptyHint="Widen the map or change your filters."150 />151 <PropertyPreview render={(p) => <PreviewCard p={p} />} />152 </KaMapView>153 </div>154 );155}156