'use client'; /** * `MapEditLayer` — the EDITABLE inner layer of the NotionEditor `mapBlock` * NodeView. Rendered as a child of `LazyMapContainer` (so it sits inside the * `MapProvider` and can use the map's hooks). Composes the Map tool's existing * `DraggableMarkers` + `useMapEvents`; it does NOT rebuild map internals. * * Two-way binding: every edit (drag a pin, add a pin, remove a pin) is mapped * back to the node's `MapMarkerPayload[]` and committed through the * `onMarkersChange` callback, which the NodeView wires to * `useNodeAttrs().patch({ markers })` → the ```map JSON updates. * * Affordances (editable only; keyboard-reachable, semantic tokens): * - "Add pin" toggles a click-to-place mode; the next map click appends a * marker at the clicked lng/lat. * - "Remove" toggles a delete mode; a small ✕ badge appears on each pin and * clicking it drops that marker. * The basemap switch is handled by `LazyMapContainer`'s own switcher (the * NodeView wires `onBasemapChange`), so it isn't duplicated here. */ import { useCallback, useMemo, useState, type ReactNode } from 'react'; import { MapPin, Plus, Trash2, X } from 'lucide-react'; import { DraggableMarkers, useMapEvents, type DraggablePoint, type MarkerData, } from '../../dev/Map/lazy'; import type { MapMarkerPayload } from '../../../common/blocks'; export interface MapEditLayerProps { /** Current markers (the node's committed payload). */ markers: MapMarkerPayload[]; /** Commit a new marker array back to the node (→ markdown). */ onMarkersChange: (next: MapMarkerPayload[]) => void; } /** Generate a stable-ish unique marker id. */ function nextMarkerId(existing: MapMarkerPayload[]): string { let n = existing.length + 1; const ids = new Set(existing.map((m) => m.id)); let id = `m${n}`; while (ids.has(id)) { n += 1; id = `m${n}`; } return id; } export function MapEditLayer({ markers, onMarkersChange }: MapEditLayerProps) { // Mutually-exclusive edit modes. `null` = plain drag-only editing. const [mode, setMode] = useState<'add' | 'delete' | null>(null); // ── Map → payload: DraggablePoint {id,lng,lat} ⇄ MapMarkerPayload ────────── // DraggableMarkers is controlled by `positions`; on drag-END it hands back // the full updated array. We map each point back onto its source payload so // labels / colours / icons / cards are PRESERVED (DraggablePoint only carries // id/lng/lat/label). const positions = useMemo( () => markers.map((m) => ({ id: m.id, lng: m.lng, lat: m.lat, ...(m.label !== undefined ? { label: m.label } : {}), })), [markers], ); const handleDragChange = useCallback( (next: DraggablePoint[]) => { const byId = new Map(markers.map((m) => [m.id, m])); const merged: MapMarkerPayload[] = next.map((p) => { const prev = byId.get(p.id); // Preserve everything but the moved coordinates. return prev ? { ...prev, lat: p.lat, lng: p.lng } : { id: p.id, lat: p.lat, lng: p.lng, ...(p.label ? { label: p.label } : {}) }; }); onMarkersChange(merged); }, [markers, onMarkersChange], ); // ── Click-to-place (add mode) ────────────────────────────────────────────── useMapEvents({ onClick: (event) => { if (mode !== 'add') return; const { lng, lat } = event.lngLat; const id = nextMarkerId(markers); onMarkersChange([...markers, { id, lat, lng }]); }, }); // ── Remove (delete mode): a ✕ badge per pin ──────────────────────────────── const removeMarker = useCallback( (id: string) => { onMarkersChange(markers.filter((m) => m.id !== id)); }, [markers, onMarkersChange], ); // Only supplied to DraggableMarkers in delete mode (see render below), so it // always renders the ✕ badge that replaces the default pin. const renderMarker = useCallback( (marker: MarkerData) => { return ( ); }, [removeMarker], ); return ( <> {/* Edit control overlay — top-left so it clears the map toolbar chips. */}
setMode((m) => (m === 'add' ? null : 'add'))} > setMode((m) => (m === 'delete' ? null : 'delete'))} >
{mode ? (
) : null}
); } function ControlButton({ active, disabled, label, onClick, children, }: { active: boolean; disabled?: boolean; label: string; onClick: () => void; children: ReactNode; }) { return ( ); }