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:
parent
3ee89bc440
commit
d9a6d58d93
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue