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.

This commit is contained in:
Kenneth Brewer 2026-04-29 13:48:15 -04:00
parent 3ee89bc440
commit d9a6d58d93
4 changed files with 200 additions and 44 deletions

View File

@ -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<MapRef>(null)
const animationFrameRef = useRef<number | null>(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)

View File

@ -50,6 +50,13 @@ export default function MapMarkerFormPopup({
resizeTextarea()
}, [resizeTextarea])
const nameInputRef = useRef<HTMLInputElement | null>(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}
>
<div className="p-1">
<div
className="p-1"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<input
ref={nameInputRef}
autoFocus
type="text"
placeholder="Name this location"
@ -130,7 +143,7 @@ export default function MapMarkerFormPopup({
outlineOffset: '1px',
}}
>
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: pinColor.hex }} />
<div className="w-4 h-4 rounded-full" style={{backgroundColor: pinColor.hex}}/>
</button>
))}
</div>

View File

@ -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<MarkerIconProps>
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 (
<div className="cursor-pointer" style={{ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.4))' }}>
<IconMapPinFilled
size={active ? 36 : 32}
style={{ color }}
/>
<div
className="cursor-pointer"
style={{
width,
height,
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.4))',
}}
>
<svg
width={width}
height={height}
viewBox="0 0 36 46"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{/* Pin body: circular head + precise pointed tip */}
<path
d="M18 45 C18 45 4 27.5 4 16.5 C4 7.4 10.3 1 18 1 C25.7 1 32 7.4 32 16.5 C32 27.5 18 45 18 45 Z"
fill={color}
stroke="rgba(0,0,0,0.25)"
strokeWidth="1.5"
/>
{/* Inner icon circle */}
<circle cx="18" cy="16.5" r="10.5" fill="rgba(255,255,255,0.18)" />
</svg>
<div
className="pointer-events-none absolute flex items-center justify-center"
style={{
left: '50%',
top: active ? 17 : 15,
transform: 'translate(-50%, -50%)',
width: iconSize,
height: iconSize,
}}
>
<Icon size={iconSize} color={iconColor} />
</div>
</div>
)
}

View File

@ -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<MapCommand | null>(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 (
<MapsLayout>
<Head title="Maps" />
<div className="relative w-full h-screen overflow-hidden">
{/* Navbar */}
<div
className="absolute top-0 left-0 right-0 z-50 flex justify-between p-4 bg-surface-secondary backdrop-blur-sm shadow-sm"
onMouseEnter={() => setIsHoveringUI(true)}
onMouseLeave={() => setIsHoveringUI(false)}
>
<div className="absolute top-0 left-0 right-0 z-50 flex items-center justify-between gap-4 p-4 bg-surface-secondary backdrop-blur-sm shadow-sm">
<Link href="/home" className="flex items-center">
<IconArrowLeft className="mr-2" size={24} />
<p className="text-lg text-text-secondary">Back to Home</p>
</Link>
<div className="flex items-center gap-3 mr-4">
<div className="flex items-center gap-2">
<input
type="text"
placeholder="lat,lng"
value={coordinateSearch}
onChange={(event) => 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"
/>
<button
type="button"
onClick={() => setShowMapCoordinates((prev) => !prev)}
className="rounded px-3 py-2 text-sm bg-surface-primary text-text-secondary hover:opacity-80 transition"
onClick={() => handleCoordinateAction('fly')}
className="rounded border border-border-default bg-surface-primary p-2 text-text-secondary hover:bg-surface-secondary"
title="Fly to coordinates"
>
{showMapCoordinates ? 'Hide Coordinates' : 'Show Coordinates'}
<IconPlaneTilt size={18} />
</button>
<Link href="/settings/maps">
<button
type="button"
onClick={() => handleCoordinateAction('marker')}
className="rounded border border-border-default bg-surface-primary p-2 text-text-secondary hover:bg-surface-secondary"
title="Add marker at coordinates"
>
<IconMapPin size={18} />
</button>
<Link href="/settings/maps" className="mr-4">
<StyledButton variant="primary" icon="IconSettings">
Manage Map Regions
</StyledButton>
@ -54,13 +104,8 @@ export default function Maps(props: {
</div>
</div>
{/* Alert */}
{alertMessage && (
<div
className="absolute top-20 left-4 right-4 z-50"
onMouseEnter={() => setIsHoveringUI(true)}
onMouseLeave={() => setIsHoveringUI(false)}
>
<div className="absolute top-20 left-4 right-4 z-50">
<Alert
title={alertMessage}
type="warning"
@ -76,12 +121,8 @@ export default function Maps(props: {
</div>
)}
{/* Map */}
<div className="absolute inset-0">
<MapComponent
isHoveringUI={isHoveringUI}
showCoordinatesEnabled={showMapCoordinates}
/>
<MapComponent mapCommand={mapCommand} />
</div>
</div>
</MapsLayout>