diff --git a/frontend/src/components/WorldMap.tsx b/frontend/src/components/WorldMap.tsx index e102790..b5ad730 100644 --- a/frontend/src/components/WorldMap.tsx +++ b/frontend/src/components/WorldMap.tsx @@ -208,10 +208,16 @@ export function WorldMap({ [onSelectCountry, selectedCountry], ); + /** SVG-level click handler — paths never receive click when pointer capture + * is active on the SVG, so we resolve the target via the data-cc attribute. */ + const handleSvgClick = useCallback((event: React.MouseEvent) => { + const target = (event.target as Element).closest('[data-cc]'); + const cc = target?.getAttribute('data-cc') ?? null; + if (cc) handleCountrySelect(cc); + }, [handleCountrySelect]); + const handlePointerDown = useCallback((event: React.PointerEvent) => { if (event.button !== 0) return; - - event.currentTarget.setPointerCapture(event.pointerId); dragStateRef.current = { active: true, startX: event.clientX, @@ -231,6 +237,7 @@ export function WorldMap({ if (!drag.moved && Math.hypot(dx, dy) > PAN_THRESHOLD) { drag.moved = true; clickSuppressedRef.current = true; + event.currentTarget.setPointerCapture(event.pointerId); } setCenter([drag.startCenter[0] + dx, drag.startCenter[1] + dy]); @@ -332,6 +339,7 @@ export function WorldMap({ onPointerUp={handlePointerUp} onPointerLeave={handlePointerUp} onWheel={handleWheel} + onClick={handleSvgClick} > {countryFeatures.map((featureItem) => { @@ -350,6 +358,7 @@ export function WorldMap({ { - if (cc) { - handleCountrySelect(cc); - } + if (cc) handleCountrySelect(cc); }} onKeyDown={(event): void => { if (cc && (event.key === "Enter" || event.key === " ")) { diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx index 4cf0c2e..b243a11 100644 --- a/frontend/src/pages/MapPage.tsx +++ b/frontend/src/pages/MapPage.tsx @@ -113,6 +113,13 @@ export function MapPage(): React.JSX.Element { const { countries, countryNames, bans, total, loading, error, refresh } = useMapData(range, originFilter, source, selectedCountry ?? undefined); + // True after the first successful data load — keeps the map mounted + // during subsequent re-fetches so country selection gives instant feedback. + const [hasLoadedOnce, setHasLoadedOnce] = useState(false); + useEffect(() => { + if (!loading && !error) setHasLoadedOnce(true); + }, [loading, error]); + const { thresholds: mapThresholds, error: mapThresholdError, @@ -195,7 +202,8 @@ export function MapPage(): React.JSX.Element { )} - {loading && !error && ( + {/* Initial load spinner — only shown before the first data arrives. */} + {loading && !error && !hasLoadedOnce && (
@@ -203,8 +211,10 @@ export function MapPage(): React.JSX.Element { {/* ---------------------------------------------------------------- */} {/* World map */} + {/* Keep the map mounted after first load so clicking a country gives */} + {/* immediate visual feedback before the filtered data arrives. */} {/* ---------------------------------------------------------------- */} - {!loading && !error && ( + {!error && hasLoadedOnce && ( - {String(total)} total ban{total !== 1 ? "s" : ""} in the selected period - {" · "} - {String(Object.keys(countries).length)} countr{Object.keys(countries).length !== 1 ? "ies" : "y"} affected - + {!error && hasLoadedOnce && ( +
+ + {String(total)} total ban{total !== 1 ? "s" : ""} in the selected period + {" · "} + {String(Object.keys(countries).length)} countr{Object.keys(countries).length !== 1 ? "ies" : "y"} affected + + {loading && } +
)} {/* ---------------------------------------------------------------- */} {/* Companion bans table */} {/* ---------------------------------------------------------------- */} - {!loading && !error && ( -
+ {!error && hasLoadedOnce && ( +