# 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 `` with `` 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 locally** — `import 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 generator** — `geoPath().projection(projection)`. Memoise. 4. **Render countries** — Map over the GeoJSON features and render a `` 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 `` 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 `` wrapper with an SVG `transform` attribute driven by React state: 1. **State:** Track `zoom` (number, 1–8) 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 `mousedown` → `mousemove` → `mouseup` 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 `` and `` elements in a `` 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.