From ff764e2b2ddacd0c2f87a71c56f07c730fdc8fd3 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Wed, 29 Apr 2026 10:22:18 -0400 Subject: [PATCH 1/5] Addressed further pr comments about the api, onSave async, and adding a comment about rehypeRaw --- .../inertia/components/maps/MapComponent.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index bac307d..d9e6c15 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -46,6 +46,29 @@ export default function MapComponent() { } }, []) + useEffect(() => { + const params = new URLSearchParams(window.location.search) + + const lat = Number(params.get('lat')) + const lng = Number(params.get('lng')) + const zoom = Number(params.get('zoom') ?? 12) + + if ( + Number.isFinite(lat) && + Number.isFinite(lng) && + lat >= -90 && + lat <= 90 && + lng >= -180 && + lng <= 180 + ) { + mapRef.current?.flyTo({ + center: [lng, lat], + zoom: Number.isFinite(zoom) ? zoom : 12, + duration: 1500, + }) + } + }, []) + const handleMapClick = useCallback((e: MapLayerMouseEvent) => { setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) setMarkerName('') From b0e62ebae1b3b5d0de516d7fdf30ffacc1fe47a4 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Wed, 29 Apr 2026 11:11:09 -0400 Subject: [PATCH 2/5] removed redundant code --- .../inertia/components/maps/MapComponent.tsx | 133 ++++++++++++------ 1 file changed, 91 insertions(+), 42 deletions(-) diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index d9e6c15..59bcb7b 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -12,23 +12,82 @@ import 'maplibre-gl/dist/maplibre-gl.css' import { Protocol } from 'pmtiles' import { useEffect, useRef, useState, useCallback } from 'react' -type ScaleUnit = 'imperial' | 'metric' import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers' import type { PinColorId } from '~/hooks/useMapMarkers' import MarkerPin from './MarkerPin' import MarkerPanel from './MarkerPanel' +type ScaleUnit = 'imperial' | 'metric' + +type MapLocationParams = { + lat: number + lng: number + zoom: number +} + +const getMapLocationParams = (): MapLocationParams | null => { + const params = new URLSearchParams(window.location.search) + + const lat = Number(params.get('lat')) + const lngParam = params.get('lng') + const longParam = params.get('long') + const lng = Number(lngParam ?? longParam) + const zoom = Number(params.get('zoom') ?? 12) + + if ( + !Number.isFinite(lat) || + !Number.isFinite(lng) || + lat < -90 || + lat > 90 || + lng < -180 || + lng > 180 + ) { + return null + } + + if (!lngParam && longParam) { + params.set('lng', longParam) + params.delete('long') + + const query = params.toString() + window.history.replaceState( + null, + '', + `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}` + ) + } + + return { + lat, + lng, + zoom: Number.isFinite(zoom) ? zoom : 12, + } +} + export default function MapComponent() { const mapRef = useRef(null) const { markers, addMarker, deleteMarker } = useMapMarkers() + const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [markerName, setMarkerName] = useState('') const [markerColor, setMarkerColor] = useState('orange') const [selectedMarkerId, setSelectedMarkerId] = useState(null) + const [scaleUnit, setScaleUnit] = useState( () => (localStorage.getItem('nomad:map-scale-unit') as ScaleUnit) || 'metric' ) + const flyToLocationParams = useCallback(() => { + const location = getMapLocationParams() + if (!location) return + + mapRef.current?.flyTo({ + center: [location.lng, location.lat], + zoom: location.zoom, + duration: 1500, + }) + }, []) + const toggleScaleUnit = useCallback(() => { setScaleUnit((prev) => { const next = prev === 'metric' ? 'imperial' : 'metric' @@ -37,37 +96,18 @@ export default function MapComponent() { }) }, []) - // Add the PMTiles protocol to maplibre-gl useEffect(() => { - let protocol = new Protocol() + const protocol = new Protocol() maplibregl.addProtocol('pmtiles', protocol.tile) + return () => { maplibregl.removeProtocol('pmtiles') } }, []) - useEffect(() => { - const params = new URLSearchParams(window.location.search) - - const lat = Number(params.get('lat')) - const lng = Number(params.get('lng')) - const zoom = Number(params.get('zoom') ?? 12) - - if ( - Number.isFinite(lat) && - Number.isFinite(lng) && - lat >= -90 && - lat <= 90 && - lng >= -180 && - lng <= 180 - ) { - mapRef.current?.flyTo({ - center: [lng, lat], - zoom: Number.isFinite(zoom) ? zoom : 12, - duration: 1500, - }) - } - }, []) + const handleMapLoad = useCallback(() => { + flyToLocationParams() + }, [flyToLocationParams]) const handleMapClick = useCallback((e: MapLayerMouseEvent) => { setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) @@ -97,7 +137,7 @@ export default function MapComponent() { [selectedMarkerId, deleteMarker] ) - const selectedMarker = selectedMarkerId ? markers.find((m) => m.id === selectedMarkerId) : null + const selectedMarker = selectedMarkerId ? markers.find((marker) => marker.id === selectedMarkerId) : null return ( @@ -115,11 +155,13 @@ export default function MapComponent() { latitude: 40, zoom: 3.5, }} + onLoad={handleMapLoad} onClick={handleMapClick} > +
+
- {/* Existing markers */} {markers.map((marker) => ( c.id === marker.color)?.hex} + color={PIN_COLORS.find((color) => color.id === marker.color)?.hex} active={marker.id === selectedMarkerId} /> ))} - {/* Popup for selected marker */} {selectedMarker && ( )} - {/* Popup for placing a new marker */} {placingMarker && ( +
- {PIN_COLORS.map((c) => ( + {PIN_COLORS.map((color) => ( ))}
+
+ ))}
From d9a6d58d9321eaf94978f2cc315c12384b01e808 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Wed, 29 Apr 2026 13:48:15 -0400 Subject: [PATCH 4/5] Added search box. Allows the user to fly to the location or fly to and add a marker. There are some bugs that need to be fixed. --- .../inertia/components/maps/MapComponent.tsx | 56 +++++++++- .../components/maps/MapMarkerFormPopup.tsx | 17 ++- admin/inertia/components/maps/MarkerPin.tsx | 68 ++++++++++-- admin/inertia/pages/maps.tsx | 103 ++++++++++++------ 4 files changed, 200 insertions(+), 44 deletions(-) diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index d8d6e24..201bc8e 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -23,9 +23,17 @@ import MapMarkerFormPopup from './MapMarkerFormPopup' type ScaleUnit = 'imperial' | 'metric' +type MapCommand = { + id: number + lat: number + lng: number + action: 'fly' | 'marker' +} + type MapComponentProps = { - isHoveringUI: boolean - showCoordinatesEnabled: boolean + mapCommand?: MapCommand | null + isHoveringUI?: boolean + showCoordinatesEnabled?: boolean } type MapLocationParams = { @@ -74,8 +82,9 @@ const getMapLocationParams = (): MapLocationParams | null => { } export default function MapComponent({ - isHoveringUI, - showCoordinatesEnabled, + mapCommand, + isHoveringUI = false, + showCoordinatesEnabled = true, }: MapComponentProps) { const mapRef = useRef(null) const animationFrameRef = useRef(null) @@ -138,6 +147,45 @@ export default function MapComponent({ } }, []) + useEffect(() => { + if (!mapCommand) return + + if (mapCommand.action === 'fly') { + const currentZoom = mapRef.current?.getZoom() ?? 12 + + mapRef.current?.flyTo({ + center: [mapCommand.lng, mapCommand.lat], + zoom: currentZoom, + duration: 1500, + }) + + return + } + + if (mapCommand.action === 'marker') { + if (!confirmDiscardMarkerChanges()) return + + const currentZoom = mapRef.current?.getZoom() ?? 12 + + mapRef.current?.flyTo({ + center: [mapCommand.lng, mapCommand.lat], + zoom: currentZoom, + duration: 750, + }) + + window.setTimeout(() => { + setPlacingMarker({ + lng: mapCommand.lng, + lat: mapCommand.lat, + }) + + setSelectedMarkerId(null) + setEditingMarkerId(null) + setHasUnsavedMarkerChanges(false) + }, 750) + } + }, [mapCommand, confirmDiscardMarkerChanges]) + const handleScaleUnitChange = useCallback((unit: ScaleUnit) => { setScaleUnit(unit) localStorage.setItem('nomad:map-scale-unit', unit) diff --git a/admin/inertia/components/maps/MapMarkerFormPopup.tsx b/admin/inertia/components/maps/MapMarkerFormPopup.tsx index 94ff34b..1a9c32a 100644 --- a/admin/inertia/components/maps/MapMarkerFormPopup.tsx +++ b/admin/inertia/components/maps/MapMarkerFormPopup.tsx @@ -50,6 +50,13 @@ export default function MapMarkerFormPopup({ resizeTextarea() }, [resizeTextarea]) + const nameInputRef = useRef(null) + + useLayoutEffect(() => { + nameInputRef.current?.focus() + nameInputRef.current?.select() + }, []) + const isDirty = name !== (initialMarker?.name ?? '') || notes !== (initialMarker?.notes ?? '') || @@ -85,8 +92,14 @@ export default function MapMarkerFormPopup({ onClose={onCancel} closeOnClick={false} > -
+
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > -
+
))}
diff --git a/admin/inertia/components/maps/MarkerPin.tsx b/admin/inertia/components/maps/MarkerPin.tsx index 94c86af..c7ff026 100644 --- a/admin/inertia/components/maps/MarkerPin.tsx +++ b/admin/inertia/components/maps/MarkerPin.tsx @@ -1,17 +1,71 @@ -import { IconMapPinFilled } from '@tabler/icons-react' +import {IconCircleFilled} from '@tabler/icons-react' +import type { ComponentType, CSSProperties } from 'react' + +type MarkerIconProps = { + size?: number + color?: string + style?: CSSProperties + className?: string +} interface MarkerPinProps { color?: string active?: boolean + Icon?: ComponentType + iconColor?: string } -export default function MarkerPin({ color = '#a84a12', active = false }: MarkerPinProps) { +export default function MarkerPin({ + color = '#a84a12', + active = false, + Icon = IconCircleFilled, + iconColor = '#ffffff', + }: MarkerPinProps) { + const width = active ? 42 : 36 + const height = active ? 52 : 46 + const iconSize = active ? 18 : 16 + return ( -
- +
+ + +
+ +
) } diff --git a/admin/inertia/pages/maps.tsx b/admin/inertia/pages/maps.tsx index a1d8df1..fedbdbb 100644 --- a/admin/inertia/pages/maps.tsx +++ b/admin/inertia/pages/maps.tsx @@ -1,52 +1,102 @@ import { useState } from 'react' -import { Head, Link, router } from '@inertiajs/react' -import { IconArrowLeft } from '@tabler/icons-react' - import MapsLayout from '~/layouts/MapsLayout' +import { Head, Link, router } from '@inertiajs/react' import MapComponent from '~/components/maps/MapComponent' import StyledButton from '~/components/StyledButton' +import { IconArrowLeft, IconMapPin, IconPlaneTilt } from '@tabler/icons-react' +import { FileEntry } from '../../types/files' import Alert from '~/components/Alert' -import { FileEntry } from '../../types/files' +type MapCommand = { + id: number + lat: number + lng: number + action: 'fly' | 'marker' +} export default function Maps(props: { maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] } }) { - const [isHoveringUI, setIsHoveringUI] = useState(false) - const [showMapCoordinates, setShowMapCoordinates] = useState(true) + const [coordinateSearch, setCoordinateSearch] = useState('') + const [mapCommand, setMapCommand] = useState(null) + + const parseCoordinates = () => { + const [latRaw, lngRaw] = coordinateSearch.split(',').map((value) => value.trim()) + const lat = Number(latRaw) + const lng = Number(lngRaw) + + if ( + !Number.isFinite(lat) || + !Number.isFinite(lng) || + lat < -90 || + lat > 90 || + lng < -180 || + lng > 180 + ) { + return null + } + + return { lat, lng } + } + + const handleCoordinateAction = (action: 'fly' | 'marker') => { + const coordinates = parseCoordinates() + if (!coordinates) return + + setMapCommand({ + id: Date.now(), + ...coordinates, + action, + }) + } const alertMessage = !props.maps.baseAssetsExist ? 'The base map assets have not been installed. Please download them first to enable map functionality.' : props.maps.regionFiles.length === 0 - ? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.' - : null + ? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.' + : null return ( -
- {/* Navbar */} -
setIsHoveringUI(true)} - onMouseLeave={() => setIsHoveringUI(false)} - > +

Back to Home

-
+
+ setCoordinateSearch(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') handleCoordinateAction('fly') + }} + className="w-52 rounded border border-border-default bg-surface-primary px-2 py-1 text-sm text-text-primary placeholder:text-text-muted focus:border-desert-green focus:outline-none" + /> + - + + + Manage Map Regions @@ -54,13 +104,8 @@ export default function Maps(props: {
- {/* Alert */} {alertMessage && ( -
setIsHoveringUI(true)} - onMouseLeave={() => setIsHoveringUI(false)} - > +
)} - {/* Map */}
- +
From b4d23bf91bacf467ccea3068a6f6f01d993977a1 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Thu, 30 Apr 2026 03:21:40 -0400 Subject: [PATCH 5/5] Added a temporary marker when the user flies to a location. It will disappear when the add map marker button is clicked. --- .../inertia/components/maps/MapComponent.tsx | 33 ++++++ .../components/maps/MapMarkerFormPopup.tsx | 6 +- .../components/maps/ViewMapMarkerPopup.tsx | 107 ++++++++++-------- admin/inertia/pages/maps.tsx | 24 +++- 4 files changed, 121 insertions(+), 49 deletions(-) diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index 201bc8e..922713d 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -88,9 +88,11 @@ export default function MapComponent({ }: MapComponentProps) { const mapRef = useRef(null) const animationFrameRef = useRef(null) + const handledMapCommandIdRef = useRef(null) const { markers, addMarker, updateMarker, deleteMarker } = useMapMarkers() + const [targetIndicator, setTargetIndicator] = useState<{ lng: number; lat: number } | null>(null) const [isDraggingMap, setIsDraggingMap] = useState(false) const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [selectedMarkerId, setSelectedMarkerId] = useState(null) @@ -149,10 +151,18 @@ export default function MapComponent({ useEffect(() => { if (!mapCommand) return + if (handledMapCommandIdRef.current === mapCommand.id) return + + handledMapCommandIdRef.current = mapCommand.id if (mapCommand.action === 'fly') { const currentZoom = mapRef.current?.getZoom() ?? 12 + setTargetIndicator({ + lng: mapCommand.lng, + lat: mapCommand.lat, + }) + mapRef.current?.flyTo({ center: [mapCommand.lng, mapCommand.lat], zoom: currentZoom, @@ -165,6 +175,8 @@ export default function MapComponent({ if (mapCommand.action === 'marker') { if (!confirmDiscardMarkerChanges()) return + setTargetIndicator(null) + const currentZoom = mapRef.current?.getZoom() ?? 12 mapRef.current?.flyTo({ @@ -203,6 +215,7 @@ export default function MapComponent({ setSelectedMarkerId(null) setEditingMarkerId(null) setHasUnsavedMarkerChanges(false) + setTargetIndicator(null) }, [confirmDiscardMarkerChanges] ) @@ -239,6 +252,7 @@ export default function MapComponent({ ) const handleFlyTo = useCallback((longitude: number, latitude: number) => { + setTargetIndicator(null) mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 }) }, []) @@ -321,6 +335,20 @@ export default function MapComponent({ /> )} + {targetIndicator && ( + +