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/MapView.tsx : Toit-Ka Maps — carte du produit propulsée par le6// framework partagé Ka Maps (Mapbox Standard 3D, pastilles de prix par7// univers, « Rechercher dans cette zone », aperçu React).8// -----------------------------------------------------------------------------9import { useCallback, useEffect, useMemo } from "react";10import { useNavigate } from "react-router-dom";11import "mapbox-gl/dist/mapbox-gl.css";12import "@groupe-ka/ka-maps/styles.css";13import type { MapProperty } from "@groupe-ka/ka-maps";14import { cameraFromParams, cameraToParams } from "@groupe-ka/ka-maps";15import {16 KaBrandBadge,17 KaMapView,18 LoadingIndicator,19 PropertyPreview,20 ResultCount,21 SearchAreaControl,22 Tilt3DControl,23 useKaMap,24} from "@groupe-ka/ka-maps/react";25import { ListingFilters, sourceName } from "../api";26import { Ico } from "./Icons";27import { toitKaMapTheme } from "../kamaps/theme";28import { toitKaMapAdapter } from "../kamaps/adapter";29import { MAPBOX_TOKEN } from "../kamaps/config";3031const QUEBEC_CENTER = { lat: 46.4, lng: -72.5 };3233/** Aperçu compact d'une annonce — même langage visuel que ListingCard. */34function PreviewCard({ p }: { p: MapProperty }) {35 const navigate = useNavigate();36 const extra = (p.extra ?? {}) as {37 tx?: string; title?: string | null; priceLabel?: string | null; source?: string;38 };39 const louer = extra.tx === "louer";40 const prix = p.price != null41 ? `${p.price.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $${louer ? " /mois" : ""}`42 : extra.priceLabel || "Prix sur demande";43 const meta = [44 louer ? "À louer" : "À vendre",45 p.propertyType,46 p.bedrooms != null ? `${p.bedrooms} ch.` : "",47 ].filter(Boolean).join(" · ");48 return (49 <div className="mv-pop">50 {p.thumbnailUrl ? (51 <img src={p.thumbnailUrl} alt="" loading="lazy" />52 ) : (53 <div className="mv-noimg" aria-hidden="true"><Ico name="home" size={30} /></div>54 )}55 <div className="mv-pop-body">56 <div className="mv-pop-price">{prix}</div>57 <div className="mv-pop-title">{p.address ?? extra.title ?? ""}</div>58 <div className="mv-pop-meta">{meta}</div>59 <div className="mv-pop-src">{sourceName(extra.source ?? "")}</div>60 <a61 className="mv-pop-cta"62 href={p.originalUrl}63 onClick={(e) => {64 e.preventDefault();65 navigate(p.originalUrl ?? "/");66 }}67 >68 Voir la fiche →69 </a>70 </div>71 </div>72 );73}7475/** Pont déclaratif : sélection/survol venus de la liste de résultats. */76function SelectionBridge({ selectedUid, hoveredUid }: {77 selectedUid: string | null;78 hoveredUid: string | null;79}) {80 const map = useKaMap();81 useEffect(() => { map?.select(selectedUid, "app"); }, [map, selectedUid]);82 useEffect(() => { map?.setHovered(hoveredUid, "app"); }, [map, hoveredUid]);83 return null;84}8586export interface MapViewProps {87 filters: ListingFilters;88 selectedUid?: string | null;89 hoveredUid?: string | null;90 onSelect?: (uid: string | null) => void;91}9293export default function MapView({94 filters, selectedUid = null, hoveredUid = null, onSelect,95}: MapViewProps) {96 const initialCamera = useMemo(() => {97 const cam = cameraFromParams(new URLSearchParams(window.location.search));98 return cam ?? { ...QUEBEC_CENTER, zoom: 6.2 };99 }, []);100101 const onMoveEnd = useCallback((center: { lat: number; lng: number }, zoom: number) => {102 const url = new URL(window.location.href);103 url.search = cameraToParams({ ...center, zoom }, url.searchParams).toString();104 window.history.replaceState(null, "", url);105 }, []);106107 const mapFilters = useMemo(108 () => Object.fromEntries(109 Object.entries(filters).filter(([k, v]) => v && k !== "sort"),110 ),111 [filters],112 );113114 return (115 <div className="mapview" role="application" aria-label="Carte des annonces">116 <KaMapView117 theme={toitKaMapTheme}118 adapter={toitKaMapAdapter}119 mapboxToken={MAPBOX_TOKEN}120 filters={mapFilters}121 center={initialCamera}122 zoom={initialCamera.zoom}123 pitch={50}124 cluster={{ maxZoom: 15, valueClamp: [500, 3_000_000] }}125 searchMode="manual"126 onMoveEnd={onMoveEnd}127 onSelect={(p) => onSelect?.(p?.id ?? null)}128 >129 <SelectionBridge selectedUid={selectedUid} hoveredUid={hoveredUid} />130 <KaBrandBadge />131 <Tilt3DControl />132 <SearchAreaControl />133 <LoadingIndicator label="Mise à jour des annonces…" />134 <ResultCount135 emptyTitle="Aucune annonce trouvée dans cette zone."136 emptyHint="Élargissez la carte ou modifiez vos filtres."137 />138 <PropertyPreview render={(p) => <PreviewCard p={p} />} />139 <div className="mv-legend" aria-hidden="true">140 <span><i className="mv-dot mv-dot-louer" /> À louer</span>141 <span><i className="mv-dot mv-dot-acheter" /> À vendre</span>142 </div>143 </KaMapView>144 </div>145 );146}147