// -----------------------------------------------------------------------------
// 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}
>
);
}
/** 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)}
/>
} />
);
}