// -----------------------------------------------------------------------------
// Author: Simon-Pierre Boucher
// Contact: contact@spboucher.ai
// Project: Groupe Ka / Ka Maps (Home-Ka integration)
// components/MapView.tsx : the Home-Ka MAP MODE (Ka Map System v2) — same
// architecture as Lou-Ka Maps, themed green/off-white/ink.
// · viewport takeover (KaMapShell): mobile edge-to-edge + 3-notch results
// bottom sheet, desktop resizable list|map split;
// · viewport-driven data: /api/listings.geojson?bbox=… (Ka Maps adapter —
// cancellable requests, never a storm);
// · unified toolbar (zoom, 3D, draw, locate), contextual "Search this
// area", drawn zone clipped CLIENT-SIDE (setClipPolygon) with a
// "N homes in this area" CTA;
// · contextual preview card v2: photo, price, swipe/chevrons between
// neighboring homes.
// -----------------------------------------------------------------------------
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 { homeKaMapTheme } from "../kamaps/theme";
import { homeKaMapAdapter } from "../kamaps/adapter";
import { MAPBOX_TOKEN } from "../kamaps/config";
// Continental US — initial camera
const US_CENTER = { lat: 39.81, lng: -98.55 };
const US_ZOOM = 4;
/** Preview card — Ka Map System ka-prevcard structure, Home-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; state?: string | null;
};
const price = p.price != null
? `$${p.price.toLocaleString("en-US", { maximumFractionDigits: 0 })}`
: extra.priceLabel || "Price on request";
const meta = [
p.propertyType,
p.bedrooms != null ? `${p.bedrooms} bd` : "",
p.bathrooms != null ? `${p.bathrooms} ba` : "",
[p.city, extra.state].filter(Boolean).join(", "),
extra.source ? sourceName(extra.source) : "",
].filter(Boolean).join(" · ");
const detail = 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 (off-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) — results panel of the shell. */
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 continental US
const initialCamera = useMemo(() => {
const cam = cameraFromParams(new URLSearchParams(window.location.search));
return cam ?? { ...US_CENTER, zoom: US_ZOOM };
}, []);
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 zone: CLIENT-SIDE clip of the displayed set (the Home-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-US")}
{" "}home{total !== 1 ? "s" : ""}
{mapCount != null && hasZone && (
{mapCount.toLocaleString("en-US")} in this area
)}
);
const listPane = (
{(listings ?? []).map((l) => (
engineRef.current?.setHovered(l.uid, "app")}
onMouseLeave={() => engineRef.current?.setHovered(null, "app")}
>
))}
{listings !== null && listings.length === 0 && (
No homes match your criteria
Try widening your search.
)}
{totalPages > 1 && (
)}
);
return (
Home·KaMap}
onExit={onExit}
exitLabel="List"
storageKey="homeka-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-US")} home${n !== 1 ? "s" : ""}`}
onClear={() => engineRef.current?.setClipPolygon(null)}
/>
} />
);
}