diff --git a/src/app/(default)/editArea/[slug]/general/components/AreaLatLngForm.tsx b/src/app/(default)/editArea/[slug]/general/components/AreaLatLngForm.tsx index b0f9e0436..40a638e28 100644 --- a/src/app/(default)/editArea/[slug]/general/components/AreaLatLngForm.tsx +++ b/src/app/(default)/editArea/[slug]/general/components/AreaLatLngForm.tsx @@ -56,7 +56,6 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid:
{ setPickerSelected(false) }} diff --git a/src/components/maps/CoordinatePickerMap.tsx b/src/components/maps/CoordinatePickerMap.tsx index bc9b3875e..ca93c6cfc 100644 --- a/src/components/maps/CoordinatePickerMap.tsx +++ b/src/components/maps/CoordinatePickerMap.tsx @@ -1,78 +1,58 @@ -'use client' -import { useCallback, useState } from 'react' -import { Map, FullscreenControl, ScaleControl, NavigationControl, MapLayerMouseEvent, Marker, MapInstance, MarkerDragEvent, GeolocateControl, GeolocateResultEvent } from 'react-map-gl/maplibre' -import maplibregl, { MapLibreEvent } from 'maplibre-gl' +import React, { useCallback, useState, useRef, useEffect } from 'react' +import { Map, FullscreenControl, ScaleControl, NavigationControl, Marker, GeolocateControl, GeolocateResultEvent } from 'react-map-gl/maplibre' +import { MapLibreEvent } from 'maplibre-gl' import dynamic from 'next/dynamic' import { useDebouncedCallback } from 'use-debounce' import { MAP_STYLES, type MapStyles } from './MapSelector' import { useFormContext } from 'react-hook-form' import MapLayersSelector from './MapLayersSelector' -import { MapPin } from '@phosphor-icons/react/dist/ssr' -import { CoordinatePickerPopup } from './CoordinatePickerPopup' - -export interface CameraInfo { - center: { - lng: number - lat: number - } - zoom: number -} +import AlertDialog from '../ui/micro/AlertDialogue' +import useResponsive from '@/js/hooks/useResponsive' +import { MapPin, Crosshair } from '@phosphor-icons/react' interface CoordinatePickerMapProps { showFullscreenControl?: boolean - initialCenter?: [number, number] - initialViewState?: { - bounds: maplibregl.LngLatBoundsLike - fitBoundsOptions: maplibregl.FitBoundsOptions - } onCoordinateConfirmed?: (coordinates: [number, number] | null) => void name?: string } export const CoordinatePickerMap: React.FC = ({ - showFullscreenControl = true, initialCenter, onCoordinateConfirmed + showFullscreenControl = true, onCoordinateConfirmed }) => { - const [selectedCoord, setSelectedCoord] = useState({ lng: 0, lat: 0 }) + const initialZoom = 14 + const defaultCoords = { lng: 0, lat: 0 } + const [newSelectedCoord, setNewSelectedCoord] = useState<{ lng: number, lat: number }>(defaultCoords) const [cursor, setCursor] = useState('default') + const [center, setCenter] = useState<{ lat: number, lng: number } | null>(null) + const { isMobile } = useResponsive() const [mapStyle, setMapStyle] = useState(MAP_STYLES.light.style) - const [mapInstance, setMapInstance] = useState(null) - const [popupOpen, setPopupOpen] = useState(false) - const initialZoom = 14 + const triggerButtonRef = useRef(null) + const { watch, setValue } = useFormContext() - const { setValue } = useFormContext() + // Watch the 'latlngStr' value from form context + const watchedCoords = watch('latlngStr') as string - const onLoad = useCallback((e: MapLibreEvent) => { - if (e.target == null) return - setMapInstance(e.target) - if (initialCenter != null) { - e.target.jumpTo({ center: initialCenter, zoom: initialZoom ?? 6 }) + useEffect(() => { + if (watchedCoords != null) { + const [lat, lng] = watchedCoords.split(',').map(Number) + setCenter({ lat, lng }) + setNewSelectedCoord({ lat, lng }) } - }, [initialCenter]) + }, [watchedCoords]) + + const onLoad = useCallback((e: MapLibreEvent) => { + if (e.target == null || center == null) return + e.target.jumpTo({ center: { lat: center.lat, lng: center.lng }, zoom: initialZoom }) + }, [center]) const updateCoordinates = useDebouncedCallback((lng, lat) => { - setSelectedCoord({ lng, lat }) - setPopupOpen(true) + setNewSelectedCoord({ lng, lat }) }, 100) - const onClick = useCallback((event: MapLayerMouseEvent): void => { - const { lngLat } = event - setPopupOpen(false) - updateCoordinates(lngLat.lng, lngLat.lat) - }, [updateCoordinates]) - - const onMarkerDragEnd = (event: MarkerDragEvent): void => { - const { lngLat } = event - setPopupOpen(false) - updateCoordinates(lngLat.lng, lngLat.lat) - } - const confirmSelection = (): void => { - if (selectedCoord != null) { - setValue('latlngStr', `${selectedCoord.lat.toFixed(5)},${selectedCoord.lng.toFixed(5)}`, { shouldDirty: true, shouldValidate: true }) - if (onCoordinateConfirmed != null) { - onCoordinateConfirmed([selectedCoord.lng, selectedCoord.lat]) - } - setPopupOpen(false) + setValue('latlngStr', `${newSelectedCoord.lat?.toFixed(5) ?? 0},${newSelectedCoord.lng?.toFixed(5) ?? 0}`, { shouldDirty: true, shouldValidate: true }) + if (onCoordinateConfirmed != null) { + onCoordinateConfirmed([newSelectedCoord.lng ?? 0, newSelectedCoord.lat ?? 0]) } } @@ -84,27 +64,46 @@ export const CoordinatePickerMap: React.FC = ({ const handleGeolocate = useCallback((e: GeolocateResultEvent) => { const { coords } = e if (coords != null) { - setPopupOpen(false) updateCoordinates(coords.longitude, coords.latitude) } }, [updateCoordinates]) + const handleClick = (event: any): void => { + const { lng, lat } = event.lngLat + updateCoordinates(lng, lat) + if (triggerButtonRef.current != null) { + triggerButtonRef.current.click() + } + } + + // Compare newSelectedCoord with watchedCoords to decide whether to show the crosshair + const isNewCoord = (): boolean => { + if (watchedCoords === null) return false + const [lat, lng] = watchedCoords.split(',').map(Number) + return !(newSelectedCoord.lat === lat && newSelectedCoord.lng === lng) + } + + const anchorClass = isMobile + ? 'fixed bottom-1/4 left-1/2 transform -translate-x-1/2' + : 'fixed bottom-1/4 left-1/2 transform -translate-x-1/2' + return (
{ - setPopupOpen(false) setCursor('move') }} onDragEnd={() => { - if (selectedCoord != null) { - setPopupOpen(true) - } setCursor('default') }} - onClick={onClick} + onClick={handleClick} mapStyle={mapStyle} cursor={cursor} cooperativeGestures={showFullscreenControl} @@ -115,20 +114,37 @@ export const CoordinatePickerMap: React.FC = ({ {showFullscreenControl && } - {(selectedCoord.lat !== 0 && selectedCoord.lng !== 0) && ( - <> - - - - setPopupOpen(false)} - open={popupOpen} - /> - + {center != null && ( + + + + )} + {isNewCoord() && ( + + + )} + Open Dialog} // Hidden button as trigger + confirmText='Confirm' + cancelText='Cancel' + onConfirm={confirmSelection} + onCancel={() => { + setNewSelectedCoord({ lng: 0, lat: 0 }) + }} + hideCancel={false} + hideConfirm={false} + hideTitle + customPositionClasses={anchorClass} + > + Coordinates: {newSelectedCoord.lat?.toFixed(5) ?? 0}, {newSelectedCoord.lng?.toFixed(5) ?? 0} +
) } diff --git a/src/components/maps/CoordinatePickerPopup.tsx b/src/components/maps/CoordinatePickerPopup.tsx deleted file mode 100644 index dd4555691..000000000 --- a/src/components/maps/CoordinatePickerPopup.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useResponsive } from '@/js/hooks' -import * as Popover from '@radix-ui/react-popover' -import { useCallback } from 'react' -import { MapInstance } from 'react-map-gl' - -interface CoordinatePickerPopupProps { - info: { - coordinates: { lng: number, lat: number } - mapInstance: MapInstance | null - } - onConfirm: () => void - onClose: () => void - open: boolean -} - -export const CoordinatePickerPopup: React.FC = ({ info, onConfirm, onClose, open }) => { - const { coordinates, mapInstance } = info - const { lng: longitude, lat: latitude } = coordinates - const screenXY = mapInstance?.project(coordinates) - const { isMobile } = useResponsive() - - const handleConfirmClick = useCallback((e: React.MouseEvent) => { - e.stopPropagation() - onConfirm() - }, [onConfirm]) - - if (screenXY == null) return null - - const anchorClass = isMobile - ? 'fixed top-15 left-1/2 transform -translate-x-1/2' - : 'fixed top-1/4 left-1/2 transform -translate-x-1/2' - - return ( - - - e.stopPropagation()} - > -
-

Coordinates: {latitude.toFixed(5)}, {longitude.toFixed(5)}

-
- - -
-
-
-
- ) -} diff --git a/src/components/ui/MobileDialog.tsx b/src/components/ui/MobileDialog.tsx index e16cbb99e..583a6bab7 100644 --- a/src/components/ui/MobileDialog.tsx +++ b/src/components/ui/MobileDialog.tsx @@ -24,7 +24,7 @@ export const DialogContent = React.forwardRef( >
- diff --git a/src/components/ui/micro/AlertDialogue.tsx b/src/components/ui/micro/AlertDialogue.tsx index 57cc33772..5fcfc60e2 100644 --- a/src/components/ui/micro/AlertDialogue.tsx +++ b/src/components/ui/micro/AlertDialogue.tsx @@ -25,6 +25,8 @@ interface Props { hideConfirm?: boolean /** if set, confirm button is not shown */ hideTitle?: boolean + /** pass in additional position classes if needed */ + customPositionClasses?: string } /** @@ -79,7 +81,7 @@ export default function AlertDialog (props: Props): JSX.Element { className={cx( 'fixed z-50', 'w-[95vw] max-w-md rounded-lg p-4 md:w-full', - 'top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%]', + props.customPositionClasses ?? 'top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%]', 'bg-white dark:bg-gray-800', 'focus:outline-none focus-visible:ring focus-visible:ring-ob-primary focus-visible:ring-opacity-75' )}