Skip to content

Commit

Permalink
feat: implement PopupArea component and enhance AreaBoundary with ren…
Browse files Browse the repository at this point in the history
…der prop
  • Loading branch information
fityannugroho committed Nov 29, 2024
1 parent 74dadd1 commit 24193a4
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 83 deletions.
91 changes: 9 additions & 82 deletions modules/MapDashboard/AreaBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
import { useArea } from '@/hooks/useArea'
import useBoundary from '@/hooks/useBoundary'
import { type FeatureArea, featureConfig } from '@/lib/config'
import { addDotSeparator, getAllParents, ucFirstStr } from '@/lib/utils'
import { LinkIcon, MapIcon } from 'lucide-react'
import type { GetSpecificDataReturn } from '@/lib/data'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import type { GeoJSONProps } from 'react-leaflet'
import { toast } from 'sonner'

Expand Down Expand Up @@ -40,26 +38,27 @@ const FeatureGroup = dynamic(
*/
const defaultOverlayPaneZIndex = 400

export type AreaBoundaryProps = Omit<
export type AreaBoundaryProps<A extends FeatureArea> = Omit<
GeoJSONProps,
'key' | 'data' | 'children' | 'pane'
> & {
area: FeatureArea
area: A
code: string
onLoading: (isLoading: boolean) => void
render?: (data?: GetSpecificDataReturn<A>['data']) => React.ReactNode
}

export default function AreaBoundary({
export default function AreaBoundary<A extends FeatureArea>({
area,
code,
onLoading,
pathOptions,
render,
...props
}: AreaBoundaryProps) {
}: AreaBoundaryProps<A>) {
const { order, color } = featureConfig[area]
const boundary = useBoundary(area, code)
const areaDetails = useArea(area, code)
const [latLng, setLatLng] = useState<{ lat: number; lng: number }>()

useEffect(() => {
onLoading(boundary.status === 'pending')
Expand Down Expand Up @@ -100,81 +99,9 @@ export default function AreaBoundary({
fillOpacity: 0.08,
...pathOptions,
}}
eventHandlers={{
click: (e) => {
setLatLng(e.latlng)
props.eventHandlers?.click?.(e)
},
...props.eventHandlers,
}}
/>

{/* Render Popup inside the default `popupPane`.
See https://leafletjs.com/reference.html#map-pane */}
<Popup pane="popupPane">
{areaDetails.status === 'pending' ? (
<span className="block text-gray-500">Loading...</span>
) : (
<>
<span className="block font-bold text-sm">
{areaDetails.data.name}
</span>
<span className="text-sm">
{addDotSeparator(areaDetails.data.code)}
</span>

{getAllParents(area).map((parent) => {
const parentData = areaDetails.data.parent?.[parent]

if (!parentData) return null

return (
<div key={parent} className="mt-1">
<span className="text-xs text-gray-500">
{ucFirstStr(parent)} :
</span>
<br />
<span className="text-xs">{parentData.name}</span>
</div>
)
})}

<button
type="button"
onClick={() => {
try {
navigator.clipboard.writeText(
`${window.location.origin}/${addDotSeparator(code)}`,
)
toast.success('Link copied to clipboard', {
duration: 3_000, // 3 seconds
})
} catch (error) {
toast.error('Failed to copy link to clipboard', {
closeButton: true,
})
}
}}
className="text-xs flex items-center gap-2 mt-4"
>
<LinkIcon className="h-4 w-4" />
Copy Link
</button>

<Link
href={`https://www.google.com/maps/search/${latLng?.lat},${latLng?.lng}`}
passHref
target="_blank"
rel="noopener noreferrer"
className="text-xs flex items-center gap-2 mt-2"
title={`Coordinate: ${latLng?.lat}, ${latLng?.lng}`}
>
<MapIcon className="h-4 w-4" />
See on Google Maps
</Link>
</>
)}
</Popup>
{render?.(areaDetails.data)}
</FeatureGroup>
</Pane>
)
Expand Down
10 changes: 9 additions & 1 deletion modules/MapDashboard/BoundaryLayers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import { featureConfig } from '@/lib/config'
import { getObjectKeys } from '@/lib/utils'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import AreaBoundary from './AreaBoundary'
import PopupArea from './PopupArea'
import { useMapDashboard } from './hooks/useDashboard'

export default function BoundaryLayers() {
const { boundaryVisibility, loading, selectedArea, setAreaBounds } =
useMapDashboard()
const [latLng, setLatLng] = useState<{ lat: number; lng: number }>()

return (
<>
Expand All @@ -26,6 +28,9 @@ export default function BoundaryLayers() {
area={area}
code={selected.code}
eventHandlers={{
click: (e) => {
setLatLng(e.latlng)
},
add: (e) => {
setAreaBounds(e.target.getBounds())
},
Expand All @@ -37,6 +42,9 @@ export default function BoundaryLayers() {
fillOpacity: 0,
}),
}}
render={(data) => (
<PopupArea area={area} data={data} latLng={latLng} />
)}
/>
)
}
Expand Down
97 changes: 97 additions & 0 deletions modules/MapDashboard/PopupArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client'

import type { FeatureArea } from '@/lib/config'
import type { GetSpecificDataReturn } from '@/lib/data'
import { addDotSeparator, getAllParents, ucFirstStr } from '@/lib/utils'
import { LinkIcon, MapIcon } from 'lucide-react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import type { PropsWithChildren } from 'react'
import { toast } from 'sonner'

const Popup = dynamic(() => import('react-leaflet').then((mod) => mod.Popup), {
ssr: false,
})

type PopupAreaProps<Area extends FeatureArea> = {
area: Area
data?: GetSpecificDataReturn<Area>['data']
latLng?: { lat: number; lng: number }
}

function BasePopupArea({ children }: PropsWithChildren) {
// Use `pane` property to render Popup inside the default `popupPane`.
// See https://leafletjs.com/reference.html#map-pane
return <Popup pane="popupPane">{children}</Popup>
}

export default function PopupArea<Area extends FeatureArea>({
area,
data,
latLng,
}: PopupAreaProps<Area>) {
if (!data) {
return (
<BasePopupArea>
<span className="block text-gray-500">Loading...</span>
</BasePopupArea>
)
}

return (
<BasePopupArea>
<span className="block font-bold text-sm">{data.name}</span>
<span className="text-sm">{addDotSeparator(data.code)}</span>

{getAllParents(area).map((parent) => {
const parentData = data.parent?.[parent]

if (!parentData) return null

return (
<div key={parent} className="mt-1">
<span className="text-xs text-gray-500">
{ucFirstStr(parent)} :
</span>
<br />
<span className="text-xs">{parentData.name}</span>
</div>
)
})}

<button
type="button"
onClick={() => {
try {
navigator.clipboard.writeText(
`${window.location.origin}/${addDotSeparator(data.code)}`,
)
toast.success('Link copied to clipboard', {
duration: 3_000, // 3 seconds
})
} catch (error) {
toast.error('Failed to copy link to clipboard', {
closeButton: true,
})
}
}}
className="text-xs flex items-center gap-2 mt-4"
>
<LinkIcon className="h-4 w-4" />
Copy Link
</button>

<Link
href={`https://www.google.com/maps/search/${latLng?.lat},${latLng?.lng}`}
passHref
target="_blank"
rel="noopener noreferrer"
className="text-xs flex items-center gap-2 mt-2"
title={`Coordinate: ${latLng?.lat}, ${latLng?.lng}`}
>
<MapIcon className="h-4 w-4" />
See on Google Maps
</Link>
</BasePopupArea>
)
}

0 comments on commit 24193a4

Please sign in to comment.