// ----------------------------------------------------------------------------- // 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}
{ e.preventDefault(); navigate(detail); }} > View details →
); } /** 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)} /> } /> ); }