Added a temporary marker when the user flies to a location. It will disappear when the add map marker button is clicked.
This commit is contained in:
parent
d9a6d58d93
commit
b4d23bf91b
|
|
@ -88,9 +88,11 @@ export default function MapComponent({
|
|||
}: MapComponentProps) {
|
||||
const mapRef = useRef<MapRef>(null)
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
const handledMapCommandIdRef = useRef<number | null>(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<number | null>(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 && (
|
||||
<Marker longitude={targetIndicator.lng} latitude={targetIndicator.lat} anchor="center">
|
||||
<div
|
||||
className="pointer-events-none flex h-9 w-9 items-center justify-center rounded-full border-2 border-desert-orange bg-surface-primary/70 shadow-lg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="relative h-5 w-5">
|
||||
<div className="absolute left-1/2 top-0 h-full w-[2px] -translate-x-1/2 bg-desert-orange" />
|
||||
<div className="absolute left-0 top-1/2 h-[2px] w-full -translate-y-1/2 bg-desert-orange" />
|
||||
</div>
|
||||
</div>
|
||||
</Marker>
|
||||
)}
|
||||
|
||||
<ScaleUnitToggle
|
||||
scaleUnit={scaleUnit}
|
||||
onChange={handleScaleUnitChange}
|
||||
|
|
@ -342,6 +370,7 @@ export default function MapComponent({
|
|||
setPlacingMarker(null)
|
||||
setEditingMarkerId(null)
|
||||
setHasUnsavedMarkerChanges(false)
|
||||
setTargetIndicator(null)
|
||||
}}
|
||||
>
|
||||
<MarkerPin
|
||||
|
|
@ -360,6 +389,7 @@ export default function MapComponent({
|
|||
await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined)
|
||||
setPlacingMarker(null)
|
||||
setHasUnsavedMarkerChanges(false)
|
||||
setTargetIndicator(null)
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (!confirmDiscardMarkerChanges()) return
|
||||
|
|
@ -367,6 +397,7 @@ export default function MapComponent({
|
|||
setPlacingMarker(null)
|
||||
setEditingMarkerId(null)
|
||||
setHasUnsavedMarkerChanges(false)
|
||||
setTargetIndicator(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -376,6 +407,7 @@ export default function MapComponent({
|
|||
marker={selectedMarker}
|
||||
onClose={() => setSelectedMarkerId(null)}
|
||||
onEdit={() => setEditingMarkerId(selectedMarker.id)}
|
||||
onMouseEnter={hideCoordinates}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -385,6 +417,7 @@ export default function MapComponent({
|
|||
latitude={selectedMarker.latitude}
|
||||
initialMarker={selectedMarker}
|
||||
onDirtyChange={setHasUnsavedMarkerChanges}
|
||||
onMouseEnter={hideCoordinates}
|
||||
onSave={async ({ id, name, notes, color }) => {
|
||||
if (!id) return
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type MapMarkerFormPopupProps = {
|
|||
}) => Promise<void> | void
|
||||
onCancel: () => void
|
||||
onDirtyChange?: (dirty: boolean) => void
|
||||
onMouseEnter?: () => void
|
||||
}
|
||||
|
||||
export default function MapMarkerFormPopup({
|
||||
|
|
@ -30,6 +31,7 @@ export default function MapMarkerFormPopup({
|
|||
onSave,
|
||||
onCancel,
|
||||
onDirtyChange,
|
||||
onMouseEnter,
|
||||
}: MapMarkerFormPopupProps) {
|
||||
const [name, setName] = useState(initialMarker?.name ?? '')
|
||||
const [notes, setNotes] = useState(initialMarker?.notes ?? '')
|
||||
|
|
@ -91,12 +93,14 @@ export default function MapMarkerFormPopup({
|
|||
offset={[0, -36] as [number, number]}
|
||||
onClose={onCancel}
|
||||
closeOnClick={false}
|
||||
closeButton={false}
|
||||
>
|
||||
<div
|
||||
className="p-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
|
|
@ -153,7 +157,7 @@ export default function MapMarkerFormPopup({
|
|||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={isSaving}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors disabled:opacity-40"
|
||||
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -8,59 +8,76 @@ type ViewMapMarkerPopupProps = {
|
|||
marker: MapMarker
|
||||
onClose: () => void
|
||||
onEdit: () => void
|
||||
onMouseEnter?: () => void
|
||||
}
|
||||
|
||||
export default function ViewMapMarkerPopup({
|
||||
marker,
|
||||
onClose,
|
||||
onEdit,
|
||||
onMouseEnter,
|
||||
}: ViewMapMarkerPopupProps) {
|
||||
return (
|
||||
<div className="max-w-[260px]">
|
||||
<Popup
|
||||
longitude={marker.longitude}
|
||||
latitude={marker.latitude}
|
||||
anchor="bottom"
|
||||
offset={[0, -36] as [number, number]}
|
||||
onClose={onClose}
|
||||
closeOnClick={false}
|
||||
>
|
||||
<div className="text-sm font-medium">{marker.name}</div>
|
||||
<Popup
|
||||
longitude={marker.longitude}
|
||||
latitude={marker.latitude}
|
||||
anchor="bottom"
|
||||
offset={[0, -36] as [number, number]}
|
||||
onClose={onClose}
|
||||
closeOnClick={false}
|
||||
closeButton={false}
|
||||
>
|
||||
<div
|
||||
className="max-w-[260px]"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-sm font-medium break-words">{marker.name}</div>
|
||||
|
||||
{marker.notes && (
|
||||
<div className="mt-1 max-w-[240px] break-all whitespace-pre-wrap text-xs text-gray-500">
|
||||
{/* react-markdown is intentionally used without rehypeRaw.
|
||||
Do not enable raw HTML rendering unless notes are sanitized first. */}
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({href, children}) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{marker.notes}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="rounded bg-[#424420] px-2.5 py-1 text-xs text-white hover:bg-[#525530]"
|
||||
{marker.notes && (
|
||||
<div className="mt-1 max-w-[240px] break-all whitespace-pre-wrap text-xs text-gray-500">
|
||||
{/* react-markdown is intentionally used without rehypeRaw.
|
||||
Do not enable raw HTML rendering unless notes are sanitized first. */}
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-600 underline hover:text-blue-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</Popup>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{marker.notes}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="w-16 rounded bg-[#424420] px-2.5 py-1 text-xs text-white hover:bg-[#525530] border-none outline-none"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="w-16 rounded bg-[#424420] px-2.5 py-1 text-xs text-white hover:bg-[#525530] border-none outline-none"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import StyledButton from '~/components/StyledButton'
|
|||
import { IconArrowLeft, IconMapPin, IconPlaneTilt } from '@tabler/icons-react'
|
||||
import { FileEntry } from '../../types/files'
|
||||
import Alert from '~/components/Alert'
|
||||
import { IconCrosshair } from '@tabler/icons-react'
|
||||
|
||||
type MapCommand = {
|
||||
id: number
|
||||
|
|
@ -19,6 +20,7 @@ export default function Maps(props: {
|
|||
}) {
|
||||
const [coordinateSearch, setCoordinateSearch] = useState('')
|
||||
const [mapCommand, setMapCommand] = useState<MapCommand | null>(null)
|
||||
const [showCoordinatesEnabled, setShowCoordinatesEnabled] = useState(true)
|
||||
|
||||
const parseCoordinates = () => {
|
||||
const [latRaw, lngRaw] = coordinateSearch.split(',').map((value) => value.trim())
|
||||
|
|
@ -84,7 +86,7 @@ export default function Maps(props: {
|
|||
className="rounded border border-border-default bg-surface-primary p-2 text-text-secondary hover:bg-surface-secondary"
|
||||
title="Fly to coordinates"
|
||||
>
|
||||
<IconPlaneTilt size={18} />
|
||||
<IconPlaneTilt size={18}/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -93,7 +95,20 @@ export default function Maps(props: {
|
|||
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} />
|
||||
<IconMapPin size={18}/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCoordinatesEnabled((prev) => !prev)}
|
||||
className={`rounded border border-border-default p-2 transition-colors ${
|
||||
showCoordinatesEnabled
|
||||
? 'bg-desert-green text-white'
|
||||
: 'bg-surface-primary text-text-secondary hover:bg-surface-secondary'
|
||||
}`}
|
||||
title={showCoordinatesEnabled ? 'Hide coordinates' : 'Show coordinates'}
|
||||
>
|
||||
<IconCrosshair size={18}/>
|
||||
</button>
|
||||
|
||||
<Link href="/settings/maps" className="mr-4">
|
||||
|
|
@ -122,7 +137,10 @@ export default function Maps(props: {
|
|||
)}
|
||||
|
||||
<div className="absolute inset-0">
|
||||
<MapComponent mapCommand={mapCommand} />
|
||||
<MapComponent
|
||||
mapCommand={mapCommand}
|
||||
showCoordinatesEnabled={showCoordinatesEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MapsLayout>
|
||||
|
|
|
|||
Loading…
Reference in New Issue