Skip to content

Commit 3762d9b

Browse files
feat: add islands filtering controls and show/hide markers (#77)
1 parent 5697469 commit 3762d9b

File tree

5 files changed

+140
-18
lines changed

5 files changed

+140
-18
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ The map of Indonesia's administrative areas.
1919
- [x] Button to see the area on Google Maps by coordinates
2020
- [x] Dark mode
2121
- [x] Responsive design (works on mobile)
22+
- [x] **NEW!** Toggle the visibility of island markers
23+
- [x] **NEW!** Filter islands by their attributes: `populated`, `outermostSmall`
2224

2325
> Suggestions and contributions are welcome!
2426

modules/MapDashboard/Dashboard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import DashboardLayout from '@/components/DashboardLayout'
22
import MapDashboardProvider from './DashboardProvider'
33
import type { SelectedArea } from './hooks/useDashboard'
4+
import IslandsFilterProvider from './IslandsFilterProvider'
45
import MapView from './MapView'
56
import Sidebar from './Sidebar'
67

@@ -9,7 +10,9 @@ type Props = { defaultSelected?: SelectedArea }
910
export default function MapDashboard({ defaultSelected }: Props) {
1011
return (
1112
<MapDashboardProvider defaultSelected={defaultSelected}>
12-
<DashboardLayout Sidebar={Sidebar} MapView={MapView} />
13+
<IslandsFilterProvider>
14+
<DashboardLayout Sidebar={Sidebar} MapView={MapView} />
15+
</IslandsFilterProvider>
1316
</MapDashboardProvider>
1417
)
1518
}

modules/MapDashboard/IslandMarkers.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { config } from '@/lib/config'
2020
import { Area } from '@/lib/const'
2121
import { useMapDashboard } from './hooks/useDashboard'
22-
import { useIslands } from './hooks/useIslands'
22+
import { useIslandsFilter } from './IslandsFilterProvider'
2323

2424
const MarkerClusterGroup = dynamic(
2525
() => import('@/components/MapMarkerClusterGroup'),
@@ -32,7 +32,9 @@ const MapMarker = dynamic(() => import('@/components/MapMarker'), {
3232

3333
export default function IslandMarkers() {
3434
const { loading } = useMapDashboard()
35-
const { data: islands = [] } = useIslands()
35+
const { filteredIslands: islands = [], showMarkers } = useIslandsFilter()
36+
37+
if (!showMarkers) return null
3638

3739
return (
3840
<MarkerClusterGroup
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use client'
2+
3+
import { createContext, useContext, useMemo, useState } from 'react'
4+
import type { Island } from '@/lib/const'
5+
import { useIslands } from './hooks/useIslands'
6+
7+
type FilterState = {
8+
populated: boolean
9+
outermostSmall: boolean
10+
}
11+
12+
type ContextValue = {
13+
filter: FilterState
14+
setFilter: (f: Partial<FilterState>) => void
15+
filteredIslands: Island[]
16+
counts: {
17+
total: number
18+
shown: number
19+
populated: number
20+
outermostSmall: number
21+
}
22+
isLoading: boolean
23+
showMarkers: boolean
24+
setShowMarkers: (v: boolean) => void
25+
}
26+
27+
const IslandsFilterContext = createContext<ContextValue | null>(null)
28+
29+
export function IslandsFilterProvider({
30+
children,
31+
}: {
32+
children: React.ReactNode
33+
}) {
34+
const { data: islands = [], isLoading } = useIslands()
35+
const [filter, setFilterState] = useState<FilterState>({
36+
populated: false,
37+
outermostSmall: false,
38+
})
39+
const [showMarkers, setShowMarkers] = useState<boolean>(true)
40+
41+
const setFilter = (f: Partial<FilterState>) => {
42+
setFilterState((s) => ({ ...s, ...f }))
43+
}
44+
45+
const counts = useMemo(() => {
46+
const total = islands.length
47+
const populated = islands.filter((i) => i.isPopulated).length
48+
const outermostSmall = islands.filter((i) => i.isOutermostSmall).length
49+
// compute shown using OR semantics when both toggles true
50+
const shown = islands.filter((i) => {
51+
if (!filter.populated && !filter.outermostSmall) return true
52+
return (
53+
(filter.populated && i.isPopulated) ||
54+
(filter.outermostSmall && i.isOutermostSmall)
55+
)
56+
}).length
57+
58+
return { total, shown, populated, outermostSmall }
59+
}, [islands, filter])
60+
61+
const filteredIslands = useMemo(() => {
62+
if (!filter.populated && !filter.outermostSmall) return islands
63+
return islands.filter(
64+
(i) =>
65+
(filter.populated && i.isPopulated) ||
66+
(filter.outermostSmall && i.isOutermostSmall),
67+
)
68+
}, [islands, filter])
69+
70+
const value: ContextValue = {
71+
filter,
72+
setFilter,
73+
showMarkers,
74+
setShowMarkers,
75+
filteredIslands,
76+
counts,
77+
isLoading,
78+
}
79+
80+
return (
81+
<IslandsFilterContext.Provider value={value}>
82+
{children}
83+
</IslandsFilterContext.Provider>
84+
)
85+
}
86+
87+
export function useIslandsFilter() {
88+
const ctx = useContext(IslandsFilterContext)
89+
if (!ctx)
90+
throw new Error(
91+
'useIslandsFilter must be used within IslandsFilterProvider',
92+
)
93+
return ctx
94+
}
95+
96+
export default IslandsFilterProvider

modules/MapDashboard/IslandsInfo.tsx

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
'use client'
22

3-
import { LoaderCircleIcon } from 'lucide-react'
3+
import { EyeClosedIcon, EyeIcon, LoaderCircleIcon } from 'lucide-react'
4+
import { Button } from '@/components/ui/button'
45
import { useMapDashboard } from './hooks/useDashboard'
5-
import { useIslands } from './hooks/useIslands'
6+
import { useIslandsFilter } from './IslandsFilterProvider'
67

78
export default function IslandsInfo() {
89
const { selectedArea } = useMapDashboard()
9-
const { isLoading, data: islands = [] } = useIslands()
10-
11-
const populatedIslands = islands.filter((i) => i.isPopulated)
12-
const outermostSmallIslands = islands.filter((i) => i.isOutermostSmall)
10+
const { filter, setFilter, counts, isLoading } = useIslandsFilter()
11+
const { showMarkers, setShowMarkers } = useIslandsFilter()
1312

1413
return (
1514
<div className="w-full p-2 border rounded flex justify-center items-center text-center">
@@ -22,15 +21,35 @@ export default function IslandsInfo() {
2221
</div>
2322
) : (
2423
<>
25-
<span className="text-sm">{islands.length} islands found</span>
26-
<span className="text-sm text-gray-500">
27-
{populatedIslands.length} populated island
28-
{populatedIslands.length !== 1 ? 's' : ''}
29-
</span>
30-
<span className="text-sm text-gray-500">
31-
{outermostSmallIslands.length} outermost-small island
32-
{outermostSmallIslands.length !== 1 ? 's' : ''}
33-
</span>
24+
<Button
25+
variant={showMarkers ? 'outline' : 'secondary'}
26+
size="sm"
27+
onClick={() => setShowMarkers(!showMarkers)}
28+
title={showMarkers ? 'Hide islands' : 'Show islands'}
29+
>
30+
{counts.total} island{counts.total === 1 ? '' : 's'} found
31+
{showMarkers ? <EyeIcon /> : <EyeClosedIcon />}
32+
</Button>
33+
34+
<div className="flex flex-wrap gap-2 w-full justify-center items-center mt-2">
35+
<Button
36+
variant={filter.populated ? 'secondary' : 'outline'}
37+
size="sm"
38+
onClick={() => setFilter({ populated: !filter.populated })}
39+
>
40+
Populated ({counts.populated})
41+
</Button>
42+
43+
<Button
44+
variant={filter.outermostSmall ? 'secondary' : 'outline'}
45+
size="sm"
46+
onClick={() =>
47+
setFilter({ outermostSmall: !filter.outermostSmall })
48+
}
49+
>
50+
Outermost-small ({counts.outermostSmall})
51+
</Button>
52+
</div>
3453
</>
3554
)}
3655
</div>

0 commit comments

Comments
 (0)