@@ -10,7 +10,6 @@ import type { Layer, Map as LeafletMap } from "leaflet";
1010import dynamic from "next/dynamic" ;
1111import { useTheme } from "next-themes" ;
1212import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
13- import { getCountryPopulation } from "@/lib/data" ;
1413import { type Country , useCountries } from "@/lib/geo" ;
1514import { CountryFlag } from "./icons/CountryFlag" ;
1615
@@ -28,17 +27,8 @@ type TooltipContent = {
2827 code : string ;
2928 count : number ;
3029 percentage : number ;
31- perCapita ?: number ;
3230} ;
3331
34- type TooltipPosition = {
35- x : number ;
36- y : number ;
37- } ;
38-
39- const roundToTwo = ( num : number ) : number =>
40- Math . round ( ( num + Number . EPSILON ) * 100 ) / 100 ;
41-
4232const mapApiToGeoJson = ( code : string ) : string =>
4333 code === "TW" ? "CN-TW" : code ;
4434const mapGeoJsonToApi = ( code : string ) : string => {
@@ -51,13 +41,11 @@ const mapGeoJsonToApi = (code: string): string => {
5141
5242export function MapComponent ( {
5343 height,
54- mode = "total" ,
5544 locationData,
5645 isLoading : passedIsLoading = false ,
5746 selectedCountry,
5847} : {
5948 height : number | string ;
60- mode ?: "total" | "perCapita" ;
6149 locationData ?: LocationData ;
6250 isLoading ?: boolean ;
6351 selectedCountry ?: string | null ;
@@ -109,40 +97,18 @@ export function MapComponent({
10997 const [ tooltipContent , setTooltipContent ] = useState < TooltipContent | null > (
11098 null
11199 ) ;
112- const [ tooltipPosition , setTooltipPosition ] = useState < TooltipPosition > ( {
113- x : 0 ,
114- y : 0 ,
115- } ) ;
116100 const [ mapView ] = useState < "countries" | "subdivisions" > ( "countries" ) ;
117101 const [ hoveredId , setHoveredId ] = useState < string | null > ( null ) ;
118102
119- const processedCountryData = useMemo ( ( ) => {
120- if ( ! countryData ?. data ) {
121- return null ;
122- }
123-
124- return countryData . data . map (
125- ( item : { value : string ; count : number ; percentage : number } ) => {
126- const population = getCountryPopulation ( item . value ) ;
127- const perCapitaValue = population > 0 ? item . count / population : 0 ;
128- return {
129- ...item ,
130- perCapita : perCapitaValue ,
131- } ;
132- }
133- ) ;
134- } , [ countryData ?. data ] ) ;
135-
136103 const colorScale = useMemo ( ( ) => {
137- if ( ! processedCountryData ) {
104+ if ( ! countryData ?. data ) {
138105 return ( ) =>
139106 resolvedTheme === "dark" ? "hsl(240 3.7% 15.9%)" : "hsl(210 40% 92%)" ;
140107 }
141108
142- const metricToUse = mode === "perCapita" ? "perCapita" : "count" ;
143- const values = processedCountryData ?. map (
144- ( d : { count : number ; perCapita : number } ) => d [ metricToUse ]
145- ) || [ 0 ] ;
109+ const values = countryData . data ?. map ( ( d : { count : number } ) => d . count ) || [
110+ 0 ,
111+ ] ;
146112 const maxValue = Math . max ( ...values ) ;
147113 const nonZeroValues = values . filter ( ( v : number ) => v > 0 ) ;
148114 const minValue = nonZeroValues . length > 0 ? Math . min ( ...nonZeroValues ) : 0 ;
@@ -171,7 +137,7 @@ export function MapComponent({
171137 }
172138 return `rgba(${ baseBlue } , ${ 0.8 + intensity * 0.2 } )` ;
173139 } ;
174- } , [ processedCountryData , mode , resolvedTheme ] ) ;
140+ } , [ countryData ?. data , resolvedTheme ] ) ;
175141
176142 const { data : countriesGeoData } = useCountries ( ) ;
177143
@@ -209,20 +175,17 @@ export function MapComponent({
209175 const dataKey = feature ?. properties ?. ISO_A2 ;
210176 // Convert GeoJSON code to API code for data lookup
211177 const apiCode = mapGeoJsonToApi ( dataKey ?? "" ) ;
212- const foundData = processedCountryData ?. find (
178+ const foundData = countryData ?. data ?. find (
213179 ( { value } : { value : string } ) => value === apiCode
214180 ) ;
215181
216- const metricValue =
217- mode === "perCapita"
218- ? foundData ?. perCapita || 0
219- : foundData ?. count || 0 ;
182+ const metricValue = foundData ?. count || 0 ;
220183 const isHovered = hoveredId === dataKey ?. toString ( ) ;
221184 const hasData = metricValue > 0 ;
222185
223186 return { dataKey, foundData, metricValue, isHovered, hasData } ;
224187 } ,
225- [ processedCountryData , mode , hoveredId ]
188+ [ countryData ?. data , hoveredId ]
226189 ) ;
227190
228191 const getStyleWeights = useCallback (
@@ -292,19 +255,17 @@ export function MapComponent({
292255 const name = feature . properties ?. ADMIN ;
293256 // Convert GeoJSON code to API code for data lookup
294257 const apiCode = mapGeoJsonToApi ( code ?? "" ) ;
295- const foundData = processedCountryData ?. find (
258+ const foundData = countryData ?. data ?. find (
296259 ( { value } ) => value === apiCode
297260 ) ;
298261 const count = foundData ?. count || 0 ;
299262 const percentage = foundData ?. percentage || 0 ;
300- const perCapita = foundData ?. perCapita || 0 ;
301263
302264 setTooltipContent ( {
303265 name,
304266 code : apiCode , // Use API code for flag display
305267 count,
306268 percentage,
307- perCapita,
308269 } ) ;
309270 } ,
310271 mouseout : ( ) => {
@@ -313,24 +274,18 @@ export function MapComponent({
313274 } ,
314275 click : ( e ) => {
315276 if ( mapRef . current ) {
316- mapRef . current . flyTo (
277+ mapRef . current . setView (
317278 e . latlng ,
318- Math . min ( mapRef . current . getZoom ( ) + 2 , 12 ) ,
319- {
320- animate : true ,
321- duration : 1.2 ,
322- easeLinearity : 0.5 ,
323- }
279+ Math . min ( mapRef . current . getZoom ( ) + 1 , 12 )
324280 ) ;
325281 }
326282 } ,
327283 } ) ;
328284 } ,
329- [ processedCountryData ]
285+ [ countryData ?. data ]
330286 ) ;
331287
332- const containerRef = useRef < HTMLDivElement > ( null ) ;
333- const zoom = 1.5 ;
288+ const zoom = 1.0 ;
334289
335290 useEffect ( ( ) => {
336291 if ( mapRef . current ) {
@@ -401,27 +356,13 @@ export function MapComponent({
401356
402357 const centroid = calculateCountryCentroid ( countryFeature . geometry ) ;
403358 if ( centroid ) {
404- mapRef . current . flyTo ( [ centroid . lat , centroid . lng ] , 7 , {
405- animate : true ,
406- duration : 1.5 ,
407- easeLinearity : 0.5 ,
408- } ) ;
359+ mapRef . current . setView ( [ centroid . lat , centroid . lng ] , 5 ) ;
409360 }
410361 } , [ selectedCountry , countriesGeoData , calculateCountryCentroid ] ) ;
411362
412363 return (
413364 < div
414- className = "relative cursor-pointer"
415- onMouseMove = { ( e ) => {
416- if ( tooltipContent ) {
417- setTooltipPosition ( {
418- x : e . clientX ,
419- y : e . clientY ,
420- } ) ;
421- }
422- } }
423- ref = { containerRef }
424- role = "tablist"
365+ className = "relative flex h-full w-full flex-col overflow-hidden rounded border bg-card"
425366 style = { { height } }
426367 >
427368 { passedIsLoading && (
@@ -456,7 +397,7 @@ export function MapComponent({
456397 outline : "none" ,
457398 zIndex : "1" ,
458399 } }
459- wheelPxPerZoomLevel = { 60 }
400+ wheelPxPerZoomLevel = { 120 }
460401 zoom = { zoom }
461402 zoomControl = { false }
462403 zoomDelta = { 0.5 }
@@ -465,53 +406,64 @@ export function MapComponent({
465406 { mapView === "countries" && countriesGeoData && (
466407 < GeoJSON
467408 data = { countriesGeoData as GeoJsonObject }
468- key = { `countries-${ mode } - ${ locationData ?. countries ?. length || 0 } ` }
409+ key = { `countries-${ locationData ?. countries ?. length || 0 } ` }
469410 onEachFeature = { handleEachFeature }
470411 style = { handleStyle }
471412 />
472413 ) }
473414 </ MapContainer >
474415 ) }
475416
476- { tooltipContent && (
477- < div
478- className = "pointer-events-none fixed z-50 rounded border bg-popover p-3 text-popover-foreground text-sm shadow-xl backdrop-blur-sm"
479- style = { {
480- left : tooltipPosition . x ,
481- top : tooltipPosition . y - 10 ,
482- transform : "translate(-50%, -100%)" ,
483- boxShadow :
484- resolvedTheme === "dark"
485- ? "0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1)"
486- : "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)" ,
487- } }
488- >
489- < div className = "mb-1 flex items-center gap-2 font-medium" >
490- { tooltipContent . code && (
491- < CountryFlag country = { tooltipContent . code } />
492- ) }
493- < span className = "text-foreground" > { tooltipContent . name } </ span >
494- </ div >
495- < div className = "space-y-1" >
417+ { ! passedIsLoading &&
418+ ( ! locationData ?. countries || locationData . countries . length === 0 ) && (
419+ < div className = "pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-background/70 text-center text-muted-foreground text-sm" >
496420 < div >
497- < span className = "font-bold text-foreground" >
498- { tooltipContent . count . toLocaleString ( ) }
499- </ span > { " " }
500- < span className = "text-muted-foreground" >
501- ({ tooltipContent . percentage . toFixed ( 1 ) } %) visitors
502- </ span >
421+ < p className = "font-semibold text-foreground" > No map data yet</ p >
422+ < p > Visitors will appear as soon as traffic flows in.</ p >
503423 </ div >
504- { mode === "perCapita" && (
505- < div className = "text-muted-foreground text-sm" >
506- < span className = "font-bold text-foreground" >
507- { roundToTwo ( tooltipContent . perCapita ?? 0 ) }
508- </ span > { " " }
509- per million people
510- </ div >
511- ) }
512424 </ div >
425+ ) }
426+
427+ < div className = "pointer-events-none absolute top-3 left-3 z-20 flex max-w-[240px] flex-col gap-2 rounded border bg-card p-3 text-sm shadow-sm" >
428+ < div className = "flex items-center gap-2 font-semibold text-foreground" >
429+ { tooltipContent ?. code ? (
430+ < >
431+ < CountryFlag country = { tooltipContent . code } />
432+ < span > { tooltipContent . name } </ span >
433+ </ >
434+ ) : (
435+ < span > Move over a country</ span >
436+ ) }
513437 </ div >
514- ) }
438+ < div className = "text-muted-foreground text-xs" >
439+ { tooltipContent ? (
440+ < >
441+ < span className = "font-semibold text-foreground" >
442+ { tooltipContent . count . toLocaleString ( ) }
443+ </ span > { " " }
444+ visitors ({ tooltipContent . percentage . toFixed ( 1 ) } %)
445+ </ >
446+ ) : (
447+ "Hover to explore visitor share"
448+ ) }
449+ </ div >
450+ </ div >
451+
452+ < div className = "pointer-events-none absolute bottom-3 left-3 z-20 flex w-[210px] flex-col gap-2 rounded border bg-card p-3 text-muted-foreground text-xs shadow-sm" >
453+ < div className = "flex items-center justify-between" >
454+ < span > Lower share</ span >
455+ < span > Higher share</ span >
456+ </ div >
457+ < div
458+ className = "h-2 rounded-full"
459+ style = { {
460+ background :
461+ resolvedTheme === "dark"
462+ ? "linear-gradient(90deg, rgba(96,165,250,0.4) 0%, rgba(59,130,246,0.95) 100%)"
463+ : "linear-gradient(90deg, rgba(147,197,253,0.4) 0%, rgba(37,99,235,0.95) 100%)" ,
464+ } }
465+ />
466+ </ div >
515467 </ div >
516468 ) ;
517469}
0 commit comments