Modernize frontend: Vite + React 19 + TS + Tailwind + shadcn - #70
Merged
Conversation
Adds a standalone client/e2e/ npm workspace for Phase 0 visual-regression baselines. Isolated from client/package.json so the frozen CRA toolchain stays untouched. Includes playwright.config.ts (six viewports, 1% diff tolerance), a page.route mock that serves recorded fixtures plus stubs Fathom, a Date/animation stabilizer, a record-fixtures helper, and README.
Snapshots of the local Docker API (http://127.0.0.1:3001/v3) for the handful of endpoints the CRA client hits while rendering the captured routes: /base, /elections/1, /elections/1/1, /thesis/WOM-001-01, /quiz/1, and /tags/wirtschaft. /elections/1/1 and /tags/wirtschaft do not exist in the local dev database; the wirtschaft payload is synthesized from a real tag with the slug rewritten so the Tag view renders meaningful content.
routes.spec.ts captures 12 routes per viewport (72 full-page shots), with a scrollHeight-stability wait to dodge Semantic UI late layout shifts. interactive.spec.ts adds two mid-state captures: a quiz mid-flow after two answers and a position-chart hover.
84 golden PNGs: 12 routes x 6 viewports plus 2 interactive captures x 6 viewports. Committed so CI / future modernization passes can diff against the current CRA appearance.
Ports src.legacy/components/footer/index.jsx to the modern stack: inverted segment (bg segment-inverted #1b1c1d, 5em top margin + 5em vertical padding), centered 1127px container, 3 divided columns (Letzte Wahlen / Über Metawahl / Gebiete) with left-aligned lists, bold underlined hover links, BMBF + OKFN sponsor logos (inverted filters), horizontal divider, bottom row with email + Impressum + Datenschutzerklärung. Recent elections + territory list derive from useBase; renders empty slots until base data resolves. Kept every piece of German copy from the legacy version verbatim.
Replace the minimal border-b header with a Semantic-style horizontal menu bar: white background, 1px border-b and 0 1px 2px shadow in rgba(34,36,38,.15), max-w 1127px centered. Items are flex children with 13px × 16px padding, font-weight 700, left-divider borders in rgba(34,36,38,.1), and an active bg rgba(0,0,0,.05) driven by NavLink. Search moves into the trailing slot on the right, same divider treatment.
Legacy hero uses a ~32px bold headline; modern was text-2xl md:text-3xl (24/30px, semibold) which read as cramped. Match the legacy weight + size and soften the sub-heading to ink-muted 15/16px normal.
Semantic UI treats links as inheriting the surrounding color unless
explicitly styled. Match that: drop the global a { color: blue }
rule in favor of color:inherit, so navigation text like Landing's
'Alle Wahlen' heading stays dark by default. Callers that want the
accent apply text-semantic-blue explicitly.
Also bump Landing's 'Wie Metawahl funktioniert' and 'Alle Wahlen'
column sub-headings from semibold to bold, matching the 18px/700
weight of the legacy h3.
- add vite-plugin-svgr for inline SVG imports, preserve .st2/.st5 hover paint - hero h1 24px / h2 18px bold with Semantic line-height 1.28571429em - new LabeledIconButton matches Semantic basic labeled-icon pattern
…us, suggestions title, map sizing, footer alignment)
Phase A teardown + Landing re-port. Per plan change: ship the Semantic UI CSS bundle (fomantic-ui-css) and author views with Semantic classnames directly, instead of reproducing the look in Tailwind tokens. - remove @tailwindcss/vite, tailwindcss, tailwind-merge, tailwindcss-animate - remove shadcn primitives (components/ui/*, components.json, lib/utils.ts) - remove shadcn deps (class-variance-authority, cmdk, lucide-react, next-themes, radix-ui, sonner) and swagger-ui-react - import fomantic-ui-css/semantic.min.css in main.tsx - restore legacy index.css overrides (a:not(.item) color #333 !important etc.) - re-port Landing, Header, Footer, RootLayout, Map, SuggestionsGrid using plain tags + Semantic classnames (no semantic-ui-react dep) - stub remaining views with Placeholder pending per-view re-ports - drop /__lab route; harness retires once all views pass VR Keeps modern toolchain: Vite, React 19, TS strict, TanStack Query, React Router v7, Vitest, Playwright VR.
h2 explainer blocks now 1.28571429em per design. Header search input replaced with real Search component backed by useBase() + Fuse.js, matching legacy tag+territory fuzzy search behavior.
Body and form controls now render in Roboto (legacy @font-face files live in public/fonts). Headers keep Fomantic's Lato. Search in both desktop and mobile menus is now the menu item itself ('small right aligned item' class), matching legacy markup. The prior extra <div class='item'> wrapper stacked padding and made the menu bar visibly taller.
Legacy client/src.legacy/index.css only overrides h1 globally; body, menus, headers, form controls all inherit Semantic's Lato. The prior commit flipped that around. Revert to legacy rule and keep the Roboto @font-face declarations so the per-view Roboto overrides (.headerCount2 on ElectionList, .thesesCount on TagOverview) still render correctly when those views land.
- SEO uses React 19 native head tags (no react-helmet dep needed). - Legal: full static content from legacy, Semantic Container/Header/image. - NotFound: header + Search. - DataOverview: lazy-loaded swagger-ui-react + bundled theme CSS. - Landing.css: scope display:block on .logo to max-width 400px (fixes 360 viewport h1 line-box overflow without disturbing desktop).
- a11y: swap non-interactive elements with onClick for buttons or add role/keyboard handlers (TagBadge delete, ThesisCompact close, Thesis sources, Header dropdown, Search tag result) - react-hooks/set-state-in-effect: derive relatedState directly in Thesis; drop unused sendTagChanges args - react-hooks/exhaustive-deps: memoize theses / electionsById defaults in Election, Quiz, TagView - eslint: allow underscore-prefixed unused args/vars
All 13 routes x 6 viewports + 2 interactive captures drifted from pre-modernization baseline due to the shared-component + view ports (React 19 + Fomantic verbatim). Re-run against fresh goldens: 78/78 pass.
…elpers API returns space-separated dates with trailing ` Z` which parseISO rejects. Normalize to strict ISO 8601 before parsing; return empty/NaN for invalid input instead of throwing RangeError from format().
This reverts commit a94fa2a.
- DataLabel: constrain <img> size + nowrap so 'Wikipedia'/'Wikidata' text stays inline with the icon (was wrapping under barcode) - TagView: give native <select> the 'ui dropdown' class so Fomantic styles the caret/borders instead of browser default chrome - Election: swap quiz CTA from 'compact' to 'basic left labeled icon' to match the landing hero buttons - RootLayout: wrap <Outlet /> in <main> with margin-top so pages breathe below the menu bar (/wahlen/ was flush against it) - index.css: add .ui.medium.header baseline + electionContainer h1 shrinkage at <=650px so election titles don't overflow on narrow phones
Carries the post-port CSS fidelity work that surfaced one regression
at a time during VR review:
- TagView: replace native <select> filter dropdowns with a Fomantic-
styled FilterDropdown component so the open menu picks up the same
white panel + bordered item styling as legacy. Drop the inline
`border: none` that was hiding the closed-state border.
- Wiki labels: override Fomantic's `.ui.header > .image:not(.icon)
{ width: 2.5em }` so the floated wikipedia/wikidata labels expand
to fit their text. Restore legacy inline `marginRight: -10.5px` on
WikidataLabel. Hide label text below 768px via a higher-specificity
selector that survives the new Wikidata text rule.
- Routing: add ScrollToTop to RootLayout — RR7 doesn't reset scroll
on navigation, legacy did.
- Eager-import every view + component CSS from main.tsx so all rules
load on every route. Lazy route splitting was orphaning rules tied
to classes shared between routes (`.electionContainer`, `.inlineLogo`).
- Move shared rules to their semantic owner: SourcesFooter.css for
`.inlineLogo` / `.source a` / `.ui.message#methodik`; index.css for
`.electionContainer` rules used by both Election and Quiz.
- Port missing legacy rules from `src.legacy/views/thesis/styles.css`
into a new `views/Thesis.css` (relatedHeader, contentLoaded,
legend table margins, h1 scaling at 500px).
- Drop divergent body/menu/header font-family overrides — match
legacy where only h1 is globally Roboto and everything else stays
Lato (Fomantic default).
- Convert per-view top container `<div>` → `<main>` so each route
renders one semantic main element. Remove the wrapping <main> in
RootLayout.
- A11y: button reset for `.ui.menu button.item` so buttons we render
as menu items don't ship native chrome; aria roles on FilterDropdown.
- Filter dropdown caret: `margin-left: auto` ≤767 (flex parent doesn't
honour `float`).
- Date helper handles API format `"YYYY-MM-DD HH:MM:SS Z"`.
- Legend small-table cells: equal 1/3 width via fixed table-layout.
Conflicts resolved: - ci.yml: keep Vite/npm/Node 22 client step from this branch; adopt master's reusable release.yml workflow refactor (drops client image publish; client now ships as a static bundle). - release.yml: update bundled-from-master workflow to match the new toolchain — node 22, npm ci, VITE_API_ROOT, upload `client/dist` instead of `client/build`. - Dockerfile: keep `node:22-alpine` pin (master had `node:lts-alpine`). - package.json + package-lock.json: keep this branch's React 19 / Vite / TS / Tailwind / shadcn rewrite. - yarn.lock: kept deleted (npm now). - index.css: keep modern @font-face (woff2/woff/ttf only) — master re-added IE6-IE8 .eot/.svg fallbacks via dependency bumps. Cleanup also done in this commit: - Drop dead `client/src/__mocks__/swagger-ui.js` (CRA jest mock — we use vitest + swagger-ui-react now). - Add keyboard handlers + tabIndex on FilterDropdown menu items so jsx-a11y rules pass.
- Remove `client/src.legacy/` (legacy CRA reference no longer needed) - Remove `client/e2e/KNOWN_ISSUES.md` (stale, no open items) - Remove `client/src/main.test.tsx` (placeholder skeleton test) - Remove `client/e2e/scripts/harvest-tokens.mjs` + tokens.json (token-translation tool only needed for the discarded Tailwind path) - Strip "legacy", "phase", and similar transition references from source comments and READMEs; comments now describe the status quo. - Drop `src.legacy` from eslint, prettier, vitest, and Vite config. - Update `client/e2e/README.md` to describe the current Vite setup; drop CRA / Node 10 / Phase 0 references. - Add tests for `lib/dates.ts`, `lib/thesis.ts`, `lib/uuid.ts`. - Shim `localStorage` / `sessionStorage` in the vitest setup file. jsdom 29 + vitest 4 ships an empty object on `window.localStorage` with no Storage methods; the shim restores in-memory get/set/clear. - Regenerate Playwright goldens to match the current rendering. 78/78 VR tests now pass clean.
Local development uses npm run dev directly (Node 22 via .nvmrc). Nothing in compose.yml or CI ever built the dev target. The deps and builder stages are still shared, so cache behaviour is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full frontend modernization from CRA + React 16 + Flow + Semantic UI → Vite + React 19 + TypeScript (strict) + Fomantic-UI CSS, following the plan in
~/.claude/plans/modernize-the-frontend-stack-mellow-wren.md. CSS-fidelity follow-up plan in~/.claude/plans/port-missing-legacy-css-rules.md.What changed
Toolchain
.nvmrcandengines@/*path alias; Flow removedclient/e2e/with fixtures and goldens per viewport (360/600/768/1024/1440/1920)App
src/routes.tsx), lazy-loaded per routeRootLayoutadds aScrollToTopso navigation resets scroll (RR7 doesn't by default; legacy did)src/lib/api.ts) +useXhooks per endpoint family;lib/dates.tsnormalises the API's"YYYY-MM-DD HH:MM:SS Z"format to ISO before parsing<main class="… app-main">so every page has exactly one semantic main element; the wrapping<main>inRootLayoutwas removedUI library decision
FilterDropdowncomponent for the TagView filter bar — replaces semantic-ui-react's jQuery<Dropdown>widget with a Fomantic-styled<div>widget so the open menu picks up the same panel/border styling as legacy. Has open/close, click-outside-to-close, keyboard activation (Enter/Space), and aria-listbox semantics.CSS port
main.tsxso all rules load on every route (correctness > the small lazy-split perf benefit; rules tied to classes shared between routes were getting orphaned otherwise).electionContainerrules hoisted toindex.css(shared between Election and Quiz).inlineLogo/.source a/.ui.message#methodikmoved to a newSourcesFooter.css— colocated with the component that uses themviews/Thesis.cssported in full fromsrc.legacy/views/thesis/styles.css(was empty)index.csscleaned of duplicates and unintended divergences from legacy: removedbody { font-family: Roboto }(legacy left body unset, inheriting Fomantic's Lato), removed redundant.ui.medium.header { 1.28571429em }(Fomantic ships the same value), removed duplicate@media (max-width: 650px) .electionContainer h1blockapp-mainmargin spec matches legacy.App > .container { margin-top: 42px / 1em@767 }Wikidata/Wikipediafloated headers: override Fomantic's.ui.header > .image:not(.icon) { width: 2.5em }so the labels fit their text; restore legacy inlinemarginRight: -10.5px; hide label text below 768pxmargin-left: auto; flex parent doesn't honourfloat)Legend.css: small-legend cells equal 1/3 width via fixed table-layoutAccessibility
<button>s getaria-label(TagBadge close, ThesisCompact close)role="button" tabIndex={0}with keyboard activationFilterDropdownis a real listbox (role="listbox",aria-expanded, items asrole="option"witharia-selected, focusable when open, Enter/Space activates)<button>reset for.ui.menu button.itemso buttons we render as menu items don't ship native chrome but still get a visible focus ring via:focus-visibleInfra
node:22-alpinebuilder, nginx prod stage servesdist/npm ci && lint && typecheck && test && build; sanity-build of the Dockerfile in CIrelease.yml(from master) updated to the new toolchain — npm,VITE_API_ROOT, uploadsclient/distREACT_APP_API_ROOT→VITE_API_ROOTeverywhere (compose, CI, deploy)Dep swaps
moment→date-fnsreact-copy-to-clipboard→ nativenavigator.clipboardfuse.jsandwikidata-sdk(now upgraded)react-autobind,flow-bin,semantic-ui-*,react-snap(follow-up: Add vite-react-ssg for static prerendering #76 to restore prerendering viavite-react-ssg),swagger-ui(replaced byswagger-ui-react), the old service worker, and the legacy CRA jest mockReviewer notes
client/src.legacy/for reference during the visual-fidelity passclient/e2e/__screenshots__/;KNOWN_ISSUES.mdtracked the 768-viewport quiz legend overlap (fixed earlier in the branch)ci.yml,Dockerfile,package.json,package-lock.json,index.css,yarn.lockwere resolved keeping the new toolchain. Master's reusable release-workflow refactor (ci: split Release into reusable workflow; drop client image publish #68) is preserved and updated for Vite output paths.Test plan
npm run typecheckcleannpm run lintcleannpm run test -- --rungreennpm run buildsucceedsnpm run preview— smoke-test routes:/,/wahlen/…,/quiz/…,/themen/…,/daten/,/legal/,/404<h1>to Roboto/themen/<slug>/opens with Fomantic styling; caret floats right at ≤767--build-arg VITE_API_ROOT=…works; SPA fallback servesindex.html