From f1e442fc98335f3be1e0186ec7c0bfdfe6398e06 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Sun, 30 Aug 2026 15:09:26 -0700 Subject: [PATCH] feat(maps): confirm before deleting a pin, after flying to it Deleting a pin was a single click on a trash icon with no confirmation and no undo. Pin names are short and repetitive by nature ("Well", "Cache", "Fishing bridge", "Fishing bridge west"), so the name in the row is not enough to tell the user which place they are about to lose. The trash icon now arms rather than deletes: it selects the pin and flies the map to it, then the row offers Delete or Cancel. The user sees the actual location before confirming. Escape cancels. The armed state also clears if that pin stops being listed -- deleted from elsewhere, or filtered out by a search -- so a stray confirm can never end up pointing at a different pin. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01F8Mgg1vd2eKSs8StZiuyMV --- admin/inertia/components/maps/MarkerPanel.tsx | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/admin/inertia/components/maps/MarkerPanel.tsx b/admin/inertia/components/maps/MarkerPanel.tsx index 0a0856f7..c96683aa 100644 --- a/admin/inertia/components/maps/MarkerPanel.tsx +++ b/admin/inertia/components/maps/MarkerPanel.tsx @@ -1,5 +1,5 @@ import { resolveMarkerIcon } from './marker_icons' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { IconEye, IconEyeOff, @@ -123,6 +123,9 @@ export default function MarkerPanel({ }: MarkerPanelProps) { const [open, setOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') + // The pin whose delete is armed. Deleting a place is not undoable and the + // names are short and similar, so the trash icon arms rather than deletes. + const [pendingDeleteId, setPendingDeleteId] = useState(null) const [sortField, setSortField] = useState('name') const [sortDirection, setSortDirection] = useState('asc') @@ -187,8 +190,27 @@ export default function MarkerPanel({ }) } + useEffect(() => { + if (pendingDeleteId === null) return + + const stillListed = markers.some((marker) => marker.id === pendingDeleteId) + if (!stillListed) setPendingDeleteId(null) + }, [markers, pendingDeleteId]) + + useEffect(() => { + if (pendingDeleteId === null) return + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setPendingDeleteId(null) + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [pendingDeleteId]) + const renderMarkerRow = (marker: MapMarker) => { const MarkerIcon = resolveMarkerIcon(marker.icon) + const confirmingDelete = pendingDeleteId === marker.id return (
  • : } - + {confirmingDelete ? ( + + + + + + ) : ( + + )}
  • ) }