Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: using alertdialog versus popup #1156

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export const AreaLatLngForm: React.FC<{ initLat: number, initLng: number, uuid:
<div className='w-full h-100vh'>
<div className='h-[90vh] lg:h-[50vh] w-full'>
<CoordinatePickerMap
initialCenter={[initLng, initLat]}
onCoordinateConfirmed={() => {
setPickerSelected(false)
}}
Expand Down
154 changes: 85 additions & 69 deletions src/components/maps/CoordinatePickerMap.tsx
Original file line number Diff line number Diff line change
@@ -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<CoordinatePickerMapProps> = ({
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<string>('default')
const [center, setCenter] = useState<{ lat: number, lng: number } | null>(null)
const { isMobile } = useResponsive()
const [mapStyle, setMapStyle] = useState<string>(MAP_STYLES.light.style)
const [mapInstance, setMapInstance] = useState<MapInstance | null>(null)
const [popupOpen, setPopupOpen] = useState(false)
const initialZoom = 14
const triggerButtonRef = useRef<HTMLButtonElement>(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])
}
}

Expand All @@ -84,27 +64,46 @@ export const CoordinatePickerMap: React.FC<CoordinatePickerMapProps> = ({
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 (
<div className='relative w-full h-full'>
<Map
id='coordinate-picker-map'
onLoad={onLoad}
initialViewState={{
longitude: center?.lng ?? 0,
latitude: center?.lat ?? 0,
zoom: initialZoom
}}
onDragStart={() => {
setPopupOpen(false)
setCursor('move')
}}
onDragEnd={() => {
if (selectedCoord != null) {
setPopupOpen(true)
}
setCursor('default')
}}
onClick={onClick}
onClick={handleClick}
mapStyle={mapStyle}
cursor={cursor}
cooperativeGestures={showFullscreenControl}
Expand All @@ -115,20 +114,37 @@ export const CoordinatePickerMap: React.FC<CoordinatePickerMapProps> = ({
{showFullscreenControl && <FullscreenControl />}
<GeolocateControl position='top-left' onGeolocate={handleGeolocate} />
<NavigationControl showCompass={false} position='bottom-right' />
{(selectedCoord.lat !== 0 && selectedCoord.lng !== 0) && (
<>
<Marker longitude={selectedCoord.lng} latitude={selectedCoord.lat} draggable onDragEnd={onMarkerDragEnd} anchor='bottom'>
<MapPin size={36} weight='fill' className='text-accent' />
</Marker>
<CoordinatePickerPopup
info={{ coordinates: selectedCoord, mapInstance }}
onConfirm={confirmSelection}
onClose={() => setPopupOpen(false)}
open={popupOpen}
/>
</>
{center != null && (
<Marker longitude={center.lng} latitude={center.lat} anchor='bottom'>
<MapPin size={36} weight='fill' className='text-accent' />
</Marker>
)}
{isNewCoord() && (
<Marker
longitude={newSelectedCoord.lng}
latitude={newSelectedCoord.lat}
anchor='center'
>
<Crosshair size={36} weight='fill' className='text-accent' />
</Marker>
)}
</Map>
<AlertDialog
title='Confirm Selection'
button={<button ref={triggerButtonRef} style={{ display: 'none' }}>Open Dialog</button>} // 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}
</AlertDialog>
</div>
)
}
Expand Down
64 changes: 0 additions & 64 deletions src/components/maps/CoordinatePickerPopup.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/ui/MobileDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const DialogContent = React.forwardRef<any, Props>(
>
<div className='flex items-center px-2'>
<DialogPrimitive.Close aria-label='Close' asChild className='dialog-close-button'>
<button className='btn btn-circle btn-ghost outline-none'>
<button className='btn btn-circle btn-ghost outline-none m-1'>
<XMarkIcon size={24} />
</button>
</DialogPrimitive.Close>
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/micro/AlertDialogue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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'
)}
Expand Down
Loading