1- import { useEffect } from 'react' ;
1+ /* eslint-disable react/no-array-index-key */
2+ import { useEffect , useMemo } from 'react' ;
23import L from 'leaflet' ;
34import { MapContainer , TileLayer , useMap , CircleMarker } from 'react-leaflet' ;
45import 'leaflet/dist/leaflet.css' ;
56import 'leaflet.heat' ;
7+
68import Loading from '~/components/common/Loading' ;
79
810const volunteerColors = {
@@ -13,86 +15,138 @@ function HeatMap({ points }) {
1315 const map = useMap ( ) ;
1416
1517 useEffect ( ( ) => {
16- if ( points . length > 0 ) {
17- const heat = L . heatLayer ( points , {
18- radius : 20 ,
19- blur : 20 ,
20- maxZoom : 2 ,
21- gradient : {
22- 0.4 : '#00f' ,
23- 0.6 : '#0f0' ,
24- 0.7 : '#ff0' ,
25- 0.8 : '#ffa500' ,
26- 1.0 : '#f00' ,
27- } ,
28- } ) . addTo ( map ) ;
29-
30- return ( ) => {
31- map . removeLayer ( heat ) ;
32- } ;
33- }
34- return undefined ;
35- } , [ points , map ] ) ;
18+ if ( ! map || ! points . length ) return undefined ;
19+
20+ const heatLayer = L . heatLayer ( points , {
21+ radius : 20 ,
22+ blur : 20 ,
23+ maxZoom : 4 ,
24+ gradient : {
25+ 0.2 : '#00f' ,
26+ 0.4 : '#0f0' ,
27+ 0.6 : '#ff0' ,
28+ 0.8 : '#ffa500' ,
29+ 1.0 : '#f00' ,
30+ } ,
31+ } ) ;
32+
33+ heatLayer . addTo ( map ) ;
34+
35+ return ( ) => {
36+ map . removeLayer ( heatLayer ) ;
37+ } ;
38+ } , [ map , points ] ) ;
3639
3740 return null ;
3841}
3942
40- function MapComponent ( { locations = [ ] , isLoading, error } ) {
41- const heatMapPoints = ( locations || [ ] ) . map ( location => [
42- location . _id . lat ,
43- location . _id . lng ,
44- location . count ,
45- ] ) ;
43+ function GlobalVolunteerMap ( { locations = [ ] , isLoading, error } ) {
44+ /**
45+ * Normalize backend response safely
46+ */
47+ const normalizedLocations = useMemo ( ( ) => {
48+ if ( ! Array . isArray ( locations ) ) return [ ] ;
49+
50+ return locations
51+ . map ( location => {
52+ const lat = location ?. _id ?. lat ?? location ?. lat ?? location ?. latitude ;
53+
54+ const lng = location ?. _id ?. lng ?? location ?. lng ?? location ?. longitude ;
4655
47- const activeVolunteers = ( locations || [ ] ) . filter ( v => v . status === 'active' ) ;
56+ const count = Number ( location ?. count || 1 ) ;
57+
58+ return {
59+ lat : Number ( lat ) ,
60+ lng : Number ( lng ) ,
61+ count,
62+ } ;
63+ } )
64+ . filter (
65+ item =>
66+ ! Number . isNaN ( item . lat ) &&
67+ ! Number . isNaN ( item . lng ) &&
68+ item . lat >= - 90 &&
69+ item . lat <= 90 &&
70+ item . lng >= - 180 &&
71+ item . lng <= 180 ,
72+ ) ;
73+ } , [ locations ] ) ;
74+
75+ /**
76+ * Build heatmap points
77+ */
78+ const heatMapPoints = useMemo (
79+ ( ) => normalizedLocations . map ( location => [ location . lat , location . lng , location . count ] ) ,
80+ [ normalizedLocations ] ,
81+ ) ;
4882
4983 if ( isLoading ) {
5084 return (
5185 < div className = "d-flex justify-content-center align-items-center" >
52- < div className = "w-100vh " >
86+ < div className = "w-100 " >
5387 < Loading />
5488 </ div >
5589 </ div >
5690 ) ;
5791 }
5892
93+ if ( error ) {
94+ return (
95+ < div className = "error-container text-center p-4" >
96+ < p > Error loading map data</ p >
97+ </ div >
98+ ) ;
99+ }
100+
101+ if ( ! normalizedLocations . length ) {
102+ return (
103+ < div
104+ className = "d-flex justify-content-center align-items-center"
105+ style = { {
106+ height : '500px' ,
107+ border : '1px solid #ddd' ,
108+ borderRadius : '8px' ,
109+ } }
110+ >
111+ < p > No volunteer location data available</ p >
112+ </ div >
113+ ) ;
114+ }
115+
59116 return (
60117 < div className = "map-container" style = { { marginTop : '20px' } } >
61- { error && (
62- < div className = "error-container" >
63- < p > Error: { error } </ p >
64- < button
65- type = "button"
66- onClick = { ( ) => {
67- /* Add retry logic here */
68- } }
69- >
70- Retry
71- </ button >
72- </ div >
73- ) }
74- < MapContainer center = { [ 20 , 0 ] } zoom = { 2 } style = { { height : '500px' , width : '100%' } } >
118+ < MapContainer
119+ center = { [ 20 , 0 ] }
120+ zoom = { 2 }
121+ scrollWheelZoom
122+ style = { {
123+ height : '500px' ,
124+ width : '100%' ,
125+ borderRadius : '8px' ,
126+ } }
127+ >
75128 < TileLayer
76129 url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
77- attribution = ' © <a href="https://www.openstreetmap.org/copyright"> OpenStreetMap</a> contributors'
130+ attribution = " © OpenStreetMap contributors"
78131 />
132+
79133 < HeatMap points = { heatMapPoints } />
80- { activeVolunteers . length > 0 &&
81- activeVolunteers . map ( volunteer => (
82- < CircleMarker
83- key = { `active- ${ volunteer . _id . lat } -${ volunteer . _id . lng } ` }
84- center = { [ volunteer . _id . lat , volunteer . _id . lng ] }
85- radius = { 8 }
86- pathOptions = { {
87- color : volunteerColors . active ,
88- fillColor : volunteerColors . active ,
89- fillOpacity : 0.8 ,
90- } }
91- />
92- ) ) }
134+
135+ { normalizedLocations . map ( ( location , index ) => (
136+ < CircleMarker
137+ key = { `${ location . lat } -${ location . lng } - ${ index } ` }
138+ center = { [ location . lat , location . lng ] }
139+ radius = { Math . min ( 10 , Math . max ( 4 , location . count ) ) }
140+ pathOptions = { {
141+ color : volunteerColors . active ,
142+ fillColor : volunteerColors . active ,
143+ fillOpacity : 0.7 ,
144+ } }
145+ />
146+ ) ) }
93147 </ MapContainer >
94148 </ div >
95149 ) ;
96150}
97151
98- export default MapComponent ;
152+ export default GlobalVolunteerMap ;
0 commit comments