Skip to content

Commit

Permalink
feat: enable user to share a link to the map that will open a drawer …
Browse files Browse the repository at this point in the history
…at a specific area id (#1210)

* feat: updating url when user clicks a point
* feat: open drawer on page load when areaId provided
* refactor: add share button, add default camera position if one not provided
  • Loading branch information
clintonlunn authored Dec 4, 2024
1 parent 5cf5ee2 commit fc78a47
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 118 deletions.
4 changes: 3 additions & 1 deletion src/app/(default)/components/SharePageURLButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { ControlledTooltip } from '@/components/ui/Tooltip'
*/
export const SharePageURLButton: React.FC<{ path: string, name: string }> = ({ path, name }) => {
const slug = getFriendlySlug(name)
const url = `https://openbeta.io${path}/${slug}`
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL != null ? process.env.NEXT_PUBLIC_BASE_URL : 'http://localhost:3000'
const optionalSlug = slug !== '' ? `/${slug}` : ''
const url = `${baseUrl}${path}${optionalSlug}`

const [clicked, setClicked] = useState(false)

Expand Down
2 changes: 1 addition & 1 deletion src/app/(default)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '../global.css'
import Header from './header'
import { PageFooter } from './components/PageFooter'
import { NextAuthProvider } from '@/components/auth/NextAuthProvider'
import { ReactToastifyProvider } from './components/ReactToastifyProvider'
import { ReactToastifyProvider } from '@/components/toast/ReactToastifyProvider'
import { BlockingAlertUploadingInProgress } from './components/ui/GlobalAlerts'

export const metadata: Metadata = {
Expand Down
167 changes: 94 additions & 73 deletions src/app/(maps)/components/FullScreenMap.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,114 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { CameraInfo, GlobalMap } from '@/components/maps/GlobalMap'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useRouter } from 'next/navigation'
import { MapLayerMouseEvent } from 'maplibre-gl'
import { useUrlParams } from '@/js/hooks/useUrlParams'

export const FullScreenMap: React.FC = () => {
const [initialCenter, setInitialCenter] = useState<[number, number] | undefined>(undefined)
const [initialZoom, setInitialZoom] = useState<number | undefined>(undefined)
const router = useRouter()
const [center, setCenter] = useState<[number, number] | undefined>(undefined)
const [zoom, setZoom] = useState<number | undefined>(undefined)
const [areaId, setAreaId] = useState<string | undefined>(undefined)
const [isInitialized, setIsInitialized] = useState(false)
const DEFAULT_CENTER: [number, number] = [0, 0]
const DEFAULT_ZOOM = 2

const cameraParams = useCameraParams()
const router = useRouter()
const urlParams = useUrlParams()

// Handle initial state setup only once
useEffect(() => {
const initialStateFromUrl = cameraParams.fromUrl()
if (isInitialized) return

if (initialStateFromUrl != null) {
setInitialCenter([initialStateFromUrl.center.lng, initialStateFromUrl.center.lat])
setInitialZoom(initialStateFromUrl.zoom)
return
const { camera, areaId: urlAreaId } = urlParams.fromUrl()

if (urlAreaId != null) {
setAreaId(urlAreaId)
}

getVisitorLocation().then((visitorLocation) => {
if (visitorLocation != null) {
setInitialCenter([visitorLocation.longitude, visitorLocation.latitude])
}
}).catch(() => {
console.log('Unable to determine user\'s location')
})
}, [])
// If camera params exist in URL, use them
if (camera != null) {
setCenter([camera.center.lng, camera.center.lat])
setZoom(camera.zoom)
setIsInitialized(true)
return
}

const handleCamerMovement = useCallback((camera: CameraInfo) => {
const url = cameraParams.toUrl(camera)
// If no camera params, get visitor location and set URL
setZoom(DEFAULT_ZOOM)
getVisitorLocation()
.then((visitorLocation) => {
const newCenter: [number, number] = (visitorLocation != null)
? [visitorLocation.longitude, visitorLocation.latitude]
: DEFAULT_CENTER

setCenter(newCenter)

// Always update URL with camera position
const newCamera: CameraInfo = {
center: {
lng: newCenter[0],
lat: newCenter[1]
},
zoom: DEFAULT_ZOOM
}

const url = urlParams.toUrl({
camera: newCamera,
areaId: urlAreaId
})
router.replace(url, { scroll: false })
})
.catch(() => {
console.log('Unable to determine user\'s location')
setCenter(DEFAULT_CENTER)

// Set URL with default camera position on error
const defaultCamera: CameraInfo = {
center: {
lng: DEFAULT_CENTER[0],
lat: DEFAULT_CENTER[1]
},
zoom: DEFAULT_ZOOM
}

const url = urlParams.toUrl({
camera: defaultCamera,
areaId: urlAreaId
})
router.replace(url, { scroll: false })
})
.finally(() => {
setIsInitialized(true)
})
}, [urlParams, isInitialized, router])

const handleCameraMovement = useCallback(
(camera: CameraInfo) => {
const { areaId } = urlParams.fromUrl()
const url = urlParams.toUrl({ camera, areaId })
router.replace(url, { scroll: false })
},
[urlParams, router]
)

router.replace(url, { scroll: false })
}, [])
const handleMapClick = useCallback(
(e: MapLayerMouseEvent) => {
const areaId = e.features?.[0]?.properties?.id ?? null
const { camera } = urlParams.fromUrl()
const url = urlParams.toUrl({ camera: camera ?? null, areaId })
router.replace(url, { scroll: false })
}, [urlParams, router]
)

return (
<GlobalMap
showFullscreenControl={false}
initialCenter={initialCenter}
initialZoom={initialZoom}
onCameraMovement={handleCamerMovement}
initialAreaId={areaId}
initialCenter={center}
initialZoom={zoom}
onCameraMovement={handleCameraMovement}
handleOnClick={handleMapClick}
/>
)
}
Expand All @@ -53,51 +122,3 @@ const getVisitorLocation = async (): Promise<{ longitude: number, latitude: numb
return undefined
}
}

function useCameraParams (): { toUrl: (camera: CameraInfo) => string, fromUrl: () => CameraInfo | null } {
const pathname = usePathname()
const initialSearchParams = useSearchParams()

function toUrl (camera: CameraInfo): string {
const params = new URLSearchParams(initialSearchParams)
params.delete('camera')

const queryParams = [
params.toString(),
`camera=${cameraInfoToQuery(camera)}`
]

return `${pathname}?${queryParams.filter(Boolean).join('&')}`
}

function fromUrl (): CameraInfo | null {
const cameraParams = initialSearchParams.get('camera')
if (cameraParams == null) {
return null
}

return queryToCameraInfo(cameraParams)
}

return { toUrl, fromUrl }
}

const cameraInfoToQuery = ({ zoom, center }: CameraInfo): string => {
return `${Math.ceil(zoom)}/${center.lat.toFixed(5)}/${center.lng.toFixed(5)}`
}

const queryToCameraInfo = (cameraParam: string): CameraInfo | null => {
const [zoomRaw, latitude, longitude] = cameraParam.split('/')
const lat = parseFloat(latitude)
const lng = parseFloat(longitude)
const zoom = parseInt(zoomRaw, 10)

if ([lat, lng, zoom].some(isNaN)) {
return null
}

return {
center: { lat, lng },
zoom
}
}
Loading

0 comments on commit fc78a47

Please sign in to comment.