// ----------------------------------------------------------------------------- // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: Groupe Ka / Ka Maps (House-Ka integration) // components/MapView.tsx : the House-Ka MAP MODE (Ka Map System v2) — same // architecture as Lou-Ka/Immo-Ka Maps, themed pine/cream/ink. // · viewport takeover (KaMapShell): mobile edge-to-edge + 3-notch results // bottom sheet, desktop resizable list|map split with near-fullscreen map; // · viewport-driven data: /api/listings.geojson?bbox=… // (Ka Maps adapter — cancellable requests, never a request storm); // · unified toolbar (zoom, 3D, draw, locate me), contextual "Search this // area", drawn area clipped CLIENT-SIDE (setClipPolygon) with the // "N homes in this area" CTA; // · contextual preview card v2: photo, price, swipe/chevrons between // neighbouring properties. // ----------------------------------------------------------------------------- import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import "mapbox-gl/dist/mapbox-gl.css"; import "@groupe-ka/ka-maps/styles.css"; import type { KaMap, MapProperty } from "@groupe-ka/ka-maps"; import { cameraFromParams, cameraToParams } from "@groupe-ka/ka-maps"; import { KaBrandBadge, KaDrawAreaMode, KaMapShell, KaMapToolbar, KaMapView, KaPropertyPreview, KaToolbar3D, KaToolbarDraw, KaToolbarGroup, KaToolbarLocate, KaToolbarZoom, LoadingIndicator, SearchAreaControl, useKaMap, useKaShell, } from "@groupe-ka/ka-maps/react"; import { Listing, ListingFilters, sourceName } from "../api"; import ListingCard from "./ListingCard"; import { houseKaMapTheme } from "../kamaps/theme"; import { houseKaMapAdapter } from "../kamaps/adapter"; import { MAPBOX_TOKEN } from "../kamaps/config"; // Ontario first: open on Toronto (shared URLs override via ?lat&lng&zoom) const DEFAULT_CENTER = { lat: 43.68, lng: -79.4 }; /** Preview card — Ka Map System ka-prevcard structure, House-Ka content * (the adapter already carries photo/price/traits: no extra fetch). */ function PreviewCard({ p }: { p: MapProperty }) { const navigate = useNavigate(); const extra = (p.extra ?? {}) as { title?: string | null; priceLabel?: string | null; source?: string; }; const price = p.price != null ? "$" + p.price.toLocaleString("en-CA", { maximumFractionDigits: 0 }) : extra.priceLabel || "Price on request"; const meta = [ p.propertyType, p.bedrooms != null ? `${p.bedrooms} bed` : "", p.bathrooms != null ? `${p.bathrooms} bath` : "", extra.source ? sourceName(extra.source) : "", ].filter(Boolean).join(" · "); const fiche = p.originalUrl ?? "/"; return ( <>
{p.thumbnailUrl ? ( { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : ( )}
{price}
{p.address ?? extra.title ?? ""}
{meta}
{ e.preventDefault(); navigate(fiche); }} > See the listing →
); } /** Bridge: exposes the KaMap engine to the parent component (outside canvas). */ function EngineBridge({ onEngine }: { onEngine: (m: KaMap | null) => void }) { const map = useKaMap(); useEffect(() => { onEngine(map); return () => onEngine(null); }, [map, onEngine]); return null; } /** Mobile: selecting a marker collapses the sheet to mini. */ function SheetAutoCollapse({ selectedUid }: { selectedUid: string | null }) { const shell = useKaShell(); const shellRef = useRef(shell); shellRef.current = shell; useEffect(() => { const s = shellRef.current; if (selectedUid && s?.isMobile) s.setSheet("mini"); }, [selectedUid]); return null; } export interface MapViewProps { filters: ListingFilters; /** Current list page (12 listings) — the shell's results pane. */ listings: Listing[] | null; total: number; page: number; totalPages: number; onPage: (p: number) => void; sort: string; onSort: (s: string) => void; onExit?: () => void; onOpenFilters?: () => void; filtersCount?: number; } export default function MapView({ filters, listings, total, page, totalPages, onPage, sort, onSort, onExit, onOpenFilters, filtersCount = 0, }: MapViewProps) { // initial camera: shared URL (?lat&lng&zoom) otherwise Toronto const initialCamera = useMemo(() => { const cam = cameraFromParams(new URLSearchParams(window.location.search)); return cam ?? { ...DEFAULT_CENTER, zoom: 10 }; }, []); const [selectedUid, setSelectedUid] = useState(null); const [mapCount, setMapCount] = useState(null); const [hasZone, setHasZone] = useState(false); const [isMobile, setIsMobile] = useState( () => window.matchMedia("(max-width: 780px)").matches); const engineRef = useRef(null); const listRef = useRef(null); useEffect(() => { const mq = window.matchMedia("(max-width: 780px)"); const update = () => setIsMobile(mq.matches); mq.addEventListener("change", update); return () => mq.removeEventListener("change", update); }, []); // camera → URL (replaceState: no router re-render) const onMoveEnd = useCallback((center: { lat: number; lng: number }, zoom: number) => { const url = new URL(window.location.href); url.search = cameraToParams({ ...center, zoom }, url.searchParams).toString(); window.history.replaceState(null, "", url); }, []); const mapFilters = useMemo( () => Object.fromEntries(Object.entries(filters).filter(([, v]) => v)), [filters], ); const handleEngine = useCallback((m: KaMap | null) => { engineRef.current = m; }, []); // map selection: preview card + list card scrolled into view const onSelect = useCallback((p: MapProperty | null) => { const uid = p?.id ?? null; setSelectedUid(uid); if (!uid) return; const card = listRef.current?.querySelector( `[data-uid="${CSS.escape(uid)}"]`); card?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, []); // drawn area: CLIENT-SIDE clip of the displayed set (the House-Ka API does // not filter by polygon) — the CTA count comes from the data event. const onDraw = useCallback((polygon: [number, number][] | null, drawing: boolean) => { if (drawing) return; setHasZone(polygon !== null); engineRef.current?.setClipPolygon(polygon); }, []); const listHeader = (
{total.toLocaleString("en-CA")} {" "}home{total > 1 ? "s" : ""} {mapCount != null && hasZone && ( {mapCount.toLocaleString("en-CA")} in the area )}
); const listPane = (
{(listings ?? []).map((l) => (
engineRef.current?.setHovered(l.uid, "app")} onMouseLeave={() => engineRef.current?.setHovered(null, "app")} >
))} {listings !== null && listings.length === 0 && (

No home matches

Try widening your criteria.

)} {totalPages > 1 && ( )}
); return ( House·KaMap} onExit={onExit} exitLabel="List" storageKey="houseka-map-split" listHeader={listHeader} list={listPane} topExtras={ onOpenFilters ? ( ) : null } > setMapCount(count)} onDraw={onDraw} > {/* THE control cluster — zoom (desktop), 3D, draw, locate */} {!isMobile && ( )} {/* draw mode: temporary banner + "N homes" CTA */} `${n.toLocaleString("en-CA")} home${n > 1 ? "s" : ""}`} onClear={() => engineRef.current?.setClipPolygon(null)} /> } /> ); }