Files
BanGUI/Docs/Tasks.md

9.4 KiB
Raw Blame History

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 GeoJSON FeatureCollection.
  • @types/topojson-client — TypeScript definitions for topojson-client.
  • world-atlas — provides the countries-110m.json TopoJSON 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):

  1. Import the TopoJSON locallyimport worldData from "world-atlas/countries-110m.json" instead of fetching from a CDN URL. Use topojson-client's feature() to extract the GeoJSON FeatureCollection once (memoised).

  2. 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.

  3. Create a path generatorgeoPath().projection(projection). Memoise.

  4. Render countries — Map over the GeoJSON features and render a <path> element for each country. Use the ISO_NUMERIC_TO_ALPHA2 lookup (already exists in frontend/src/data/isoNumericToAlpha2.ts) to translate the numeric feature id to the alpha-2 code expected by the countries prop.

  5. Preserve colour coding — Continue using getBanCountColor() from frontend/src/utils/mapColors.ts to compute each country's fill colour based on its ban count and the three threshold props.

  6. Preserve ban-count labels — For every country with count > 0, compute the centroid with pathGenerator.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).

  7. Preserve country selection — Clicking a country calls onSelectCountry with the alpha-2 code (or null to deselect). The selected country must receive a distinct brand fill colour, matching the current behaviour.

  8. Preserve hover tooltip — On mouseenter / mousemove / mouseleave, show/hide a tooltip portal (createPortal into document.body) displaying the country name and ban count. Use the same Fluent UI styled tooltip div that the current implementation uses.

  9. Preserve keyboard accessibility — Each country with a known alpha-2 code must have role="button", tabIndex={0}, an aria-label ("CC: N ban(s)"), and aria-pressed when selected. Enter and Space must trigger selection/deselection.

  10. Use a viewBox-based responsive SVG — Set viewBox="0 0 {width} {height}" and style={{ 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:

  1. State: Track zoom (number, 18) and center (translate offset [x, y]).

  2. Zoom controls: Keep the three overlay buttons (Zoom In +, Zoom Out , Reset ) in the top-right corner. Each button adjusts the zoom state by ±0.5, clamped to [1, 8]. Reset sets zoom to 1 and center to [0, 0].

  3. Mouse-wheel zoom: Attach a wheel event handler to the SVG that increments/decrements zoom on scroll, zooming toward the cursor position.

  4. Click-and-drag pan: Track mousedownmousemovemouseup on the SVG to translate the center offset. Only pan when the drag exceeds a small threshold (e.g. 3 px) to avoid conflicting with country click events.

  5. Touch support (stretch goal): Optionally support pinch-to-zoom and touch-drag for tablet users.

  6. Apply transform: Wrap all <path> and <text> elements in a <g transform="translate(tx, ty) scale(zoom)"> group. Alternatively, use d3-zoom if 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 makeStyles rules) for default, hovered, and selected states.
  • Apply the correct class based on component state (isSelected, isHovered).
  • Use a CSS transition on fill and stroke for 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:

  1. Remove the vi.mock("react-simple-maps", ...) block.

  2. Mock the TopoJSON data instead. Since the new implementation imports world-atlas/countries-110m.json directly, mock that module to return a minimal TopoJSON object containing a single country feature (e.g. id "840" for the US). Use topojson-client's feature() to verify the mock produces a valid GeoJSON feature.

  3. 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).

  4. Verify zoom controls render: assert that the three zoom buttons (Zoom In, Zoom Out, Reset) are present and have the correct aria-label values.

  5. Also verify that tests in MapPage.test.tsx and HistoryPage.test.tsx still pass (they mock WorldMap at 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:

  1. Delete the GEO_URL constant.
  2. Confirm the TopoJSON file is included in the Vite bundle (imported as a JSON module).
  3. 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 MapPage still 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.