9.4 KiB
BanGUI — Task List
This document breaks the entire BanGUI project into development stages, ordered so that each stage builds on the previous one. Every task is described in prose with enough detail for a developer to begin work. References point to the relevant documentation.
Reference: Docs/Refactoring.md for full analysis of each issue.
Open Issues
Replace react-simple-maps with d3-geo in WorldMap
The current WorldMap component (frontend/src/components/WorldMap.tsx) uses the react-simple-maps library (ComposableMap, ZoomableGroup, Geography, useGeographies). This library wraps d3-geo but adds a heavy abstraction layer and fetches the TopoJSON geography file from a remote CDN at runtime. Replace it with direct d3-geo rendering, following the pattern demonstrated in the reference project at /media/lukas/Volume/repo/worldmaptest/.
Reference: Docs/Features.md §4 (World Map View) for the full feature specification.
All existing features must be preserved. The component's public API (WorldMapProps) and behaviour must remain identical so that MapPage.tsx, HistoryPage.tsx, and the existing unit test continue to work after the migration.
Task 1 — Swap npm dependencies [DONE]
Remove react-simple-maps and @types/react-simple-maps from frontend/package.json. Add the following packages that the new implementation requires:
d3-geo— geographic projection and SVG path generation.@types/d3-geo— TypeScript definitions for d3-geo.topojson-client— converts TopoJSON to GeoJSONFeatureCollection.@types/topojson-client— TypeScript definitions for topojson-client.world-atlas— provides thecountries-110m.jsonTopoJSON file as a local npm asset (no more CDN fetch at runtime).
Run npm install and verify the lock file updates cleanly.
Task 2 — Rewrite WorldMap.tsx to use d3-geo directly [DONE]
Rewrite the component so that it renders a plain <svg> with <path> elements generated by d3-geo instead of the react-simple-maps wrappers. The implementation should follow this approach (as seen in the reference project):
-
Import the TopoJSON locally —
import worldData from "world-atlas/countries-110m.json"instead of fetching from a CDN URL. Usetopojson-client'sfeature()to extract the GeoJSONFeatureCollectiononce (memoised). -
Create a projection — Use
geoMercator()from d3-geo (matching the current Mercator projection) with.fitSize([width, height], featureCollection)to auto-scale. Memoise the projection so it is only recomputed when the geometry changes. -
Create a path generator —
geoPath().projection(projection). Memoise. -
Render countries — Map over the GeoJSON features and render a
<path>element for each country. Use theISO_NUMERIC_TO_ALPHA2lookup (already exists infrontend/src/data/isoNumericToAlpha2.ts) to translate the numeric feature id to the alpha-2 code expected by thecountriesprop. -
Preserve colour coding — Continue using
getBanCountColor()fromfrontend/src/utils/mapColors.tsto compute each country's fill colour based on its ban count and the three threshold props. -
Preserve ban-count labels — For every country with
count > 0, compute the centroid withpathGenerator.centroid(feature)and render a<text>element at that position showing the count. Countries with zero bans must remain blank and transparent (no fill, no label). -
Preserve country selection — Clicking a country calls
onSelectCountrywith the alpha-2 code (ornullto deselect). The selected country must receive a distinct brand fill colour, matching the current behaviour. -
Preserve hover tooltip — On
mouseenter/mousemove/mouseleave, show/hide a tooltip portal (createPortalintodocument.body) displaying the country name and ban count. Use the same Fluent UI styled tooltip div that the current implementation uses. -
Preserve keyboard accessibility — Each country with a known alpha-2 code must have
role="button",tabIndex={0}, anaria-label("CC: N ban(s)"), andaria-pressedwhen selected.EnterandSpacemust trigger selection/deselection. -
Use a
viewBox-based responsive SVG — SetviewBox="0 0 {width} {height}"andstyle={{ width: "100%", height: "auto" }}so the map scales with its container, matching the reference project's approach.
Task 3 — Implement zoom and pan without react-simple-maps [DONE]
The current implementation relies on ZoomableGroup from react-simple-maps for zoom/pan. Reimplement this using a <g> wrapper with an SVG transform attribute driven by React state:
-
State: Track
zoom(number, 1–8) andcenter(translate offset[x, y]). -
Zoom controls: Keep the three overlay buttons (Zoom In
+, Zoom Out−, Reset⟲) in the top-right corner. Each button adjusts thezoomstate by ±0.5, clamped to[1, 8]. Reset sets zoom to 1 and center to[0, 0]. -
Mouse-wheel zoom: Attach a
wheelevent handler to the SVG that increments/decrements zoom on scroll, zooming toward the cursor position. -
Click-and-drag pan: Track
mousedown→mousemove→mouseupon the SVG to translate thecenteroffset. Only pan when the drag exceeds a small threshold (e.g. 3 px) to avoid conflicting with country click events. -
Touch support (stretch goal): Optionally support pinch-to-zoom and touch-drag for tablet users.
-
Apply transform: Wrap all
<path>and<text>elements in a<g transform="translate(tx, ty) scale(zoom)">group. Alternatively, used3-zoomif a more robust implementation is preferred, but keep React as the rendering layer (no d3 DOM manipulation).
Task 4 — Update hover and selection styles to use CSS transitions [DONE]
The reference project applies hover highlights via CSS classes (.country, .country.hovered) with CSS transition instead of the react-simple-maps style={{ default, hover, pressed }} object. Adopt the same approach:
- Define CSS classes (or Fluent UI
makeStylesrules) for default, hovered, and selected states. - Apply the correct class based on component state (
isSelected,isHovered). - Use a CSS
transitiononfillandstrokefor a smooth 150 ms highlight effect. - This avoids the react-simple-maps per-geography style object entirely.
Ensure the selected state still uses tokens.colorBrandBackground / tokens.colorBrandBackgroundHover / tokens.colorBrandBackgroundPressed from Fluent UI so the map integrates visually with the rest of the application.
Task 5 — Update the WorldMap unit test [DONE]
The existing test at frontend/src/components/__tests__/WorldMap.test.tsx mocks react-simple-maps. After the migration those mocks are invalid. Update the test:
-
Remove the
vi.mock("react-simple-maps", ...)block. -
Mock the TopoJSON data instead. Since the new implementation imports
world-atlas/countries-110m.jsondirectly, mock that module to return a minimal TopoJSON object containing a single country feature (e.g. id"840"for the US). Usetopojson-client'sfeature()to verify the mock produces a valid GeoJSON feature. -
Keep the same assertions: tooltip appears on hover with country name and ban count, tooltip disappears on mouse leave, country element has correct ARIA attributes (
role="button",aria-label,aria-pressed). -
Verify zoom controls render: assert that the three zoom buttons (Zoom In, Zoom Out, Reset) are present and have the correct
aria-labelvalues. -
Also verify that tests in
MapPage.test.tsxandHistoryPage.test.tsxstill pass (they mockWorldMapat the component level so they should be unaffected, but confirm).
Task 6 — Remove CDN dependency and verify offline capability [DONE]
The old implementation fetched geography data from https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json at runtime. The new implementation bundles the data via the world-atlas npm package, so:
- Delete the
GEO_URLconstant. - Confirm the TopoJSON file is included in the Vite bundle (imported as a JSON module).
- Verify the map renders correctly without any network request for geography data (check the browser network tab or write a test that asserts no fetch calls are made for the old CDN URL).
Task 7 — Final integration smoke test [DONE]
After all changes, manually verify the following against the feature specification in Docs/Features.md §4:
- Countries are colour-coded by ban count (transparent → green → yellow → red) using smooth interpolation.
- Ban count numbers are displayed centred inside each country that has bans.
- Countries with zero bans are transparent with no label.
- Clicking a country filters the companion ban table below.
- Clicking the same country again deselects it.
- Zoom in / zoom out / reset buttons work correctly (range 1×–8×).
- Mouse-wheel zoom and click-drag pan work.
- Tooltip appears on hover showing country name and localised ban count.
- Keyboard navigation works (Tab to focus, Enter/Space to toggle selection).
- The map is responsive and scales with the container width.
- Time-range selector on
MapPagestill updates the map data correctly. - Colour thresholds from settings are applied (thresholdLow, thresholdMedium, thresholdHigh props).
- Run
npm run test— all existing tests pass. - Run
npm run build— production build succeeds with no errors or warnings.