Skip to content

Commit 9c3bb3b

Browse files
committed
improve map UI
1 parent 0171840 commit 9c3bb3b

File tree

2 files changed

+63
-326
lines changed

2 files changed

+63
-326
lines changed

apps/dashboard/components/analytics/map-component.tsx

Lines changed: 63 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type { Layer, Map as LeafletMap } from "leaflet";
1010
import dynamic from "next/dynamic";
1111
import { useTheme } from "next-themes";
1212
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13-
import { getCountryPopulation } from "@/lib/data";
1413
import { type Country, useCountries } from "@/lib/geo";
1514
import { 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-
4232
const mapApiToGeoJson = (code: string): string =>
4333
code === "TW" ? "CN-TW" : code;
4434
const mapGeoJsonToApi = (code: string): string => {
@@ -51,13 +41,11 @@ const mapGeoJsonToApi = (code: string): string => {
5141

5242
export 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

Comments
 (0)