From c36d151ff2519b656acf79f9f32ce21b2d2eb2da Mon Sep 17 00:00:00 2001 From: Clinton Lunn Date: Tue, 15 Oct 2024 14:34:37 -0600 Subject: [PATCH] refactor: using alertdialog versus popup (#1156) --- .../general/components/AreaLatLngForm.tsx | 1 - src/components/maps/CoordinatePickerMap.tsx | 168 ++++++++++-------- src/components/maps/CoordinatePickerPopup.tsx | 64 ------- src/components/ui/MobileDialog.tsx | 2 +- src/components/ui/micro/AlertDialogue.tsx | 4 +- 5 files changed, 96 insertions(+), 143 deletions(-) delete mode 100644 src/components/maps/CoordinatePickerPopup.tsx 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..a58833133 100644 --- a/src/components/maps/CoordinatePickerMap.tsx +++ b/src/components/maps/CoordinatePickerMap.tsx @@ -1,78 +1,66 @@ '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, ScaleControl, NavigationControl, Marker, GeolocateControl, GeolocateResultEvent, MapLayerMouseEvent, MapEvent } from 'react-map-gl/maplibre' import dynamic from 'next/dynamic' import { useDebouncedCallback } from 'use-debounce' -import { MAP_STYLES, type MapStyles } from './MapSelector' +import { MAP_STYLES, type MapStyles } from '@/components/maps/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 MapLayersSelector from '@/components/maps/MapLayersSelector' +import AlertDialog from '@/components/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 + onCoordinateConfirmed: () => void name?: string } -export const CoordinatePickerMap: React.FC = ({ - showFullscreenControl = true, initialCenter, onCoordinateConfirmed -}) => { - const [selectedCoord, setSelectedCoord] = useState({ lng: 0, lat: 0 }) +interface Coordinate { + lat: number + lng: number +} + +interface Coord { + initialCoordinate: Coordinate | null + newSelectedCoordinate: Coordinate | null +} + +export const CoordinatePickerMap: React.FC = ({ onCoordinateConfirmed }) => { + const initialZoom = 14 const [cursor, setCursor] = useState('default') + const [coord, setCoord] = useState({ + initialCoordinate: null, + newSelectedCoordinate: null + }) + const { initialCoordinate, newSelectedCoordinate } = coord + 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) + setCoord({ initialCoordinate: { lat, lng }, newSelectedCoordinate }) } - }, [initialCenter]) + }, [watchedCoords, newSelectedCoordinate]) + + const onLoad = useCallback((e: MapEvent) => { + if (e.target == null || initialCoordinate == null) return + e.target.jumpTo({ center: { lat: initialCoordinate.lat, lng: initialCoordinate.lng }, zoom: initialZoom }) + }, [initialCoordinate]) const updateCoordinates = useDebouncedCallback((lng, lat) => { - setSelectedCoord({ lng, lat }) - setPopupOpen(true) + setCoord((prev) => ({ initialCoordinate: prev.initialCoordinate, newSelectedCoordinate: { lat, lng } })) }, 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) + if (newSelectedCoordinate !== null) { + setValue('latlngStr', `${newSelectedCoordinate.lat.toFixed(5)},${newSelectedCoordinate.lng.toFixed(5)}`, { shouldDirty: true, shouldValidate: true }) + onCoordinateConfirmed() } } @@ -84,51 +72,79 @@ 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: MapLayerMouseEvent): void => { + const { lng, lat } = event.lngLat + updateCoordinates(lng, lat) + if (triggerButtonRef.current != null) { + triggerButtonRef.current.click() + } + } + + const anchorClass = isMobile + ? 'fixed bottom-2 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} > - {showFullscreenControl && } - {(selectedCoord.lat !== 0 && selectedCoord.lng !== 0) && ( - <> - - - - setPopupOpen(false)} - open={popupOpen} - /> - + {initialCoordinate !== null && ( + + + )} + {newSelectedCoordinate !== null && ( + + + + )} + Open Dialog} // Hidden button as trigger + confirmText='Confirm' + cancelText='Cancel' + onConfirm={confirmSelection} + onCancel={() => { + setCoord((prev) => ({ initialCoordinate: prev.initialCoordinate, newSelectedCoordinate: null })) + }} + hideCancel={false} + hideConfirm={false} + hideTitle + customPositionClasses={anchorClass} + > + Coordinates: {newSelectedCoordinate !== null ? `${newSelectedCoordinate.lat.toFixed(5)}, ${newSelectedCoordinate.lng.toFixed(5)}` : ''} + +
) } 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' )}