Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

feat(Upgrade): Upgrade/rewrite to newest react - #272

Open
schinken wants to merge 28 commits into
masterfrom
upgrade
Open

feat(Upgrade): Upgrade/rewrite to newest react#272
schinken wants to merge 28 commits into
masterfrom
upgrade

Conversation

@schinken

Copy link
Copy Markdown
Contributor

No description provided.

schinken-tio and others added 28 commits May 27, 2026 21:49
Replace react-scripts/CRA with Vite 5 and migrate the Jest suite to Vitest.
Move index.html to the project root, switch REACT_APP_* env vars to
import.meta.env.VITE_*, drop the CRA service worker, and adopt an ESLint 9
flat config. Add a Playwright E2E smoke suite that drives the app against the
live demo API, plus a CI quality gate (lint/typecheck/test/build/E2E) and an
updated Node 20 release workflow. Switch the package manager from yarn to npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrade React 16 to 19 (createRoot) and migrate react-router 5 to 7. A small
compatibility shim (src/routing.tsx) reconstructs the v5-style withRouter /
RouteComponentProps / useHistory API on top of v7 hooks so consumers did not
need rewriting; routers themselves move to Routes/Route element/Navigate.
Also upgrade react-redux 9, react-intl 6, recharts 2, and RTL 16, and fix the
React 19 type changes (JSX -> React.JSX, explicit children on FC props,
NavLink activeClassName -> className function).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the Redux store to Redux Toolkit configureStore and convert the
client/UI-state reducers (loader, error, search) to createSlice. Introduce a
TanStack Query client and migrate the server reads for the metrics resources
(global and per-user) to useQuery, establishing the query pattern for server
state while UI state stays in Redux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the app-wide accessibility foundation: skip link, <main> landmark and
per-route titles (in app.tsx), :focus-visible styling, prefers-reduced-motion
support, and WCAG AA contrast fixes in the theme (nav links, balance text,
step buttons). Make the Modal a proper dialog with a focus trap and focus
restoration, label the navigation and icon-only controls (theme switcher,
text scaling, add-user), and hide decorative icons from assistive tech. Wire
automated axe scans + a keyboard skip-link check into the E2E suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve the remaining axe violations on the deeper screens: name the
accept/cancel buttons, hide the barcode scanner's capture inputs and the
user-detail focus-hack input from assistive tech, and make the global error
toast a role=alert live region. Extend the axe E2E to the user detail, edit,
send-money and article-form screens (all now violation-free) and document the
WCAG 2.1 AA conformance status and follow-ups in specs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
react-intl 7 declares React 19 support, so the workaround flags are no longer
needed: remove legacy-peer-deps from .npmrc and the @types/react override from
package.json. Installs now resolve cleanly under strict peer-dependency rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the settings Redux reducer/thunk with a useSettings() query (default
data as the synchronous fallback, error toast preserved via errorHandler).
selector-hooks re-exports it so the 25 consumer call sites are unchanged.
Move the transaction 'deletable' check to a hook (useSettings + useTransaction),
drop settings from the store/action union, and update the test render helper to
provide a QueryClient that seeds settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the article Redux reducer/thunks/selectors with a queries/articles
module: useArticles/useArticle/usePopularArticles read via useQuery, and the
mutations (add/delete article, barcode, tag, fetch-by-barcode) run as imperative
helpers that reuse errorHandler for the toast and invalidate the article caches.
selector-hooks re-exports the read hooks so consumers are unchanged; drop the
article reducer from the store/action union and its now-obsolete reducer tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the user and transaction Redux reducers/thunks with queries/users and
queries/transactions: useUsers/useUser/useFilteredUsers/useUserTransactions read
via useQuery, and createUser/updateUser/createTransaction/deleteTransaction run
as imperative helpers (errorHandler for the toast, query invalidation for cache).
UserList/UserCard now take user objects and TransactionListItem takes a
transaction object (no per-row refetch); TransactionTable is a hook-based
component. Query keys coerce ids to strings so numeric API ids and string route
params hit the same cache entry. The Redux store is now only client/UI state
(error, loader, search); delete the obsolete reducer tests and the dead
user-multi-selection stub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the Redux store is RTK-only client state, type Dispatch as the real
AppDispatch (removing the (action: any) shim), re-enable RTK's default
middleware checks, and add typed useAppDispatch/useAppSelector hooks. Replace
module-level mutable state with hooks: the idle timer uses a ref, and the
article tag filter uses a useTags() query instead of a hand-rolled cache.
Move the hardcoded accept/cancel/theme-toggle aria-labels into the i18n catalog
and stop them overriding consumer-supplied intl titles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the leftover Redux machinery with a tiny subscribable global-error
store (useSyncExternalStore) — the only remaining client-state piece. The
error and loader slices and the dead search slice are gone; errorHandler no
longer takes a dispatch and writes to the new store directly.

Move the type-only modules out of src/store/reducers/ to src/types/ (with a
barrel and DeepPartial), add src/queries/index.ts so consumers import server
hooks from one place, and repoint every '../../store' import to '../../queries'
or '../../types'. Drop the Redux Provider from app.tsx, the redux-mock-store
spec helper, and the legacy filtered-users search-query coupling (the search
slice was never written, so the filter was already a no-op).

Uninstall react-redux, @reduxjs/toolkit, redux, redux-mock-store and their
types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite services/api.ts with a typed request() helper that throws ApiError
on any non-2xx, accepts AbortSignal, and standardises mode/credentials.
get/post/restDelete are now generic <T>; all queries pass their response
type and forward the query's AbortSignal so navigations cancel inflight
requests. Free-form path/query inputs (barcode, user ids) are now
URLSearchParams/encodeURIComponent'd.

errorHandler stops resetting the global error on entry (concurrent success
no longer hides a pending failure), and the error.class lookup is exact
against both the full FQCN and its short name (was: substring match — a
namespaced superstring would have collided).

Normalize User.id (number from API → string in TS) at the API boundary so
the rest of the app and the cache keys match. Drop the redux-era
OwnProps/StateProps/ActionProps scaffolding from ErrorMessage. Move
playCashSound into the createTransaction success branch (no ka-ching on a
rejected POST). Route metrics/tags through the queryKeys factory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t while pending

Add useCreate/Update/Delete-* hooks alongside the imperative helpers and
migrate every function-component call site (transaction-button, deposit/
dispense, split-invoice, paypal, user-article, article-form barcode/tag/
article CRUD, undo, create-user, edit-user) to consume mutateAsync + isPending.
Submit buttons are now disabled while the mutation is in flight, eliminating
the double-submit hole in split-invoice and rapid-rescan in article-scanner.

The legacy class CreateUserTransactionForm gets an isSubmitting state guard
in the same shape until the full hooks conversion in a later commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tsconfig: drop useUnknownInCatchVariables: false (all catches are now bare),
flip noUnusedLocals on, remove the tsc --noEmit duplication in the build
script (CI already typechecks separately).

ESLint: stop hiding unused-directive warnings (reportUnusedDisableDirectives
back to 'warn'), promote no-unused-vars to 'error' (was 'warn'), and surface
no-explicit-any as 'warn' so new code is discouraged from using it.

Remove the orphaned 'import * as React from react' lines that the automatic
JSX runtime made obsolete; clean up the now-flagged stale eslint-disable
directives; replace the two surviving '==' / '!=' loose-equality checks with
'===' / '!==' (User.id is normalized to string at the API boundary).

Drop the _initialState parameter from renderWithContext (Redux-era vestige)
and the ~6 call sites that passed it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modal: drop the button-as-backdrop (now role=presentation; backdrops aren't
real controls — Esc, the focus trap, and click-outside still dismiss),
remove the English 'close'/'Dialog' literal defaults in favour of intl, rename
the typo'd backDropTile prop out of existence, replace keyCode === 27 with
e.key === 'Escape', and add a focus-restore fallback to #main-content when
the previously-focused element has unmounted. user-selection now passes a
real intl label.

Touch targets: bump .button to 0.75rem 1rem padding + 2.75rem min-height and
.fab to 2.75rem so all interactive controls meet WCAG 2.5.5 / kiosk 44x44 at
every breakpoint.

Dark theme contrast: brighten --textSubtile (3.1:1 → AA) so inactive nav
links read correctly; rebalance --buttonGreenFont/--buttonRedFont on the
darker step-button backgrounds.

FormField now uses React.useId() (React 19 primitive) instead of Date.now(),
and gets a real typed Props.

AlertText pairs its green/red color with a screen-reader-only positive/
negative cue so colour isn't the only signal.

RouteTitle renders a localized sr-only <h1> on every route so the heading
outline starts at level 1 (was: every page started at h2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toast, Scanner, CurrencyInput and CreateUserTransactionForm are now function
components:

- Toast: pure timer-driven visibility on useEffect.
- Scanner: keystroke buffer lives in a ref so the document keydown handler
  always reads the current value (fixes the stale-closure bug where onChange
  fired with the previous barcode).
- CurrencyInput: derived display value with explicit prop->state sync via
  useEffect; drops the @ts-expect-error ref forwarding.
- CreateUserTransactionForm: uses useRouter() + useCreateTransaction() so the
  double-submit guard becomes a one-line isPending check instead of a
  hand-rolled isSubmitting state machine; ConnectedCreateCustomTransactionForm
  is now an identity re-export.

Migrate trivial leaf consumers off the v5 routing shim to direct v7 hooks
(ScrollToTop, UserDetailsSeparator).

Delete confirmed dead code: BackButton, CreateUserTransactionLink,
UserArticleTransactionLink, useUserArray alias, the inaccessible Tag brick,
the dead common/index barrel, the unused 'first' prop on TransactionListItem
and the unused 'disabled'/'getString' props on UserSelection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wallowing

Read queries no longer wrap their queryFn in errorHandler — they let
ApiError/network failures bubble out to TanStack Query so isError, retry,
and the data placeholder behave correctly. A QueryCache.onError handler
forwards each query's meta.defaultError message id to setGlobalError, so
the user-facing toast UX is unchanged. Add a TypeScript module
augmentation so meta is typed.

Annotate every existing query (useUsers, useArticles, useArticle, useTags,
useSettings, useUserTransactions, useMetrics, useUserMetrics) with a
meta.defaultError pointing at its localized message id.

Mutations keep the imperative errorHandler path because they still map
specific error.class values to specific messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A11y E2E:
- Replace every page.waitForTimeout with deterministic waits (nav landmark
  visible, network-idle, balance heading visible, first textbox visible) so
  the suite is reliable on slow CI.
- Add a dark-theme scan (set localStorage.SELECTED_THEME='dark' via
  addInitScript before goto).
- Add a modal-open scan + focus-trap traversal: confirms Tab cycles between
  the input and submit button inside the dialog and Escape dismisses it.
- Add --greenText/--redText overrides for the dark palette (user-card
  balance was failing 4.5:1 against #2e3d4d).

Release pipeline (package.yml) now runs lint + typecheck before tests/build
so tag-pushes can't ship something the PR gate would reject.

Tooling hygiene:
- .env.example documents both demo and self-hosted VITE_API.
- engines field pins Node >= 20 to match CI.
- .gitignore picks up .vite/, *.log, *.tsbuildinfo, stats.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lockfile was generated on macOS and only recorded
@rollup/rollup-darwin-*, so 'npm ci' on the Linux CI runner failed with
'Cannot find module @rollup/rollup-linux-x64-gnu' (npm bug npm/cli#4828:
optional deps for other platforms aren't recorded in package-lock.json
unless they're declared explicitly).

Declare the common platform natives as optionalDependencies so they
land in the lockfile for every platform. npm still installs only the one
matching the current OS/CPU; the rest are skipped at install time but
resolved correctly under 'npm ci' on any of those platforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…warnings

Type ButtonProps without the orphan 'ref?: any' (forwardRef provides it),
Tab as Omit<NavLinkProps,...> with explicit className/style/active(Class)Name
fields, Plus as React.SVGProps<SVGSVGElement>, SearchIcon with a typed style
prop, FlexProps.grow as number | string, and the metrics resource's local
Article precursor as Article (was any).

Recharts' Tooltip is generic-typed and trips React 19's JSX checker; assert
a narrower ComponentType<{ contentStyle?: CSSProperties }> instead of any.

Drop the now-stale eslint-disable comments in modal.tsx, nav-tab-menu.tsx
and services/sound.ts that the tighter ESLint config surfaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- delete the now-stale docs/review.md snapshot (action items it raised are
  either addressed or live in docs/review-followup.md).
- fix useTags meta key (ARTICLES_COULD_NOT_BE_LOADED -> TAGS_COULD_NOT_BE_LOADED)
  and add missing locale strings for tag-load / barcode-delete / tag-delete /
  article-delete failure paths so the toast doesn't lie about which entity
  failed.
- drop Tab.active dead-on-arrival prop (no callers pass it).
- inline the ConnectedCreateCustomTransactionForm identity re-export at its
  single consumer and remove the alias.
- key RouteTitle's document-title effect on pathname only (title/match are
  derived, no need to retrigger on identity changes).
- clarify Scanner's barcode regex limitation (-, . dropped — affects ISBN-10).
- comment invalidateUsers' double prefix so the second invalidate doesn't
  read as a typo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two patterns were both burning the same way: Toast and Scanner ran their
effect with the callback (onFadeOut / onChange) in the deps array, so every
parent render created a fresh closure that tore down and re-added the
setTimeout / document keydown listener. The class-component versions
explicitly bound once in componentDidMount; restore that semantic by
holding the callback in a ref and dropping it from the deps array.

useModal: the eslint-disable directive for exhaustive-deps had been removed
in the lint pass, but handleHide / handlePopState were still captured by
the effect without being in the deps. Wrap handleHide/handleShow in
useCallback so they're stable identities and add handleHide to the deps —
now the dep array is honest and consumers can also rely on stable function
references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before, the imperative mutation helpers (createUser, createTransaction,
addArticle, …) called errorHandler() internally and swallowed every
failure into 'return undefined', so useMutation's native isError/error/
onError were permanently dead — every call site had to truthy-check the
return value and the global toast was the only surface that knew anything
went wrong.

Now:
- ApiError gains an optional errorClass field so the body-level 'error.class'
  case (HTTP 200 + { error: { class } } envelope) can carry the FQCN.
- A small throwOnBodyError(data) helper turns that body shape into a thrown
  ApiError; HTTP errors already throw from request<T>.
- query-client.ts registers MutationCache.onError, reads meta.errors /
  meta.defaultError per mutation, and routes the right localized message
  through setGlobalError — mirroring the QueryCache.onError bridge so reads
  and writes use a single funnel.
- Every imperative helper now throws on failure; the useXxx hooks moved their
  errors/defaultError onto useMutation's meta. mutationKey decorations that
  nothing read were removed.
- Consumers that used to 'if (result)' switch to try/catch on mutateAsync
  (or .catch on the promise in the paypal-transaction effect). isError and
  onError now actually work for any future caller that wants per-site error
  UX.

The deprecated errorHandler() function is gone; pickErrorMessage,
shortClass and ErrorClassMap remain (they're pure and shared with the
mutation cache).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ArticleDetails: re-seed form state on article.id change, not on the
  article reference identity — keeps unsaved edits from being blown away
  when the cache invalidates after another write.
- CurrencyInput: explicit controlled / uncontrolled discipline (mirror the
  prop directly when value is given, hold internal state when not). Drops
  the prop-sync effect that round-tripped value→displayValue on every parent
  re-render; the round trip happened to be clean but the contract was vague.
- SplitInvoiceForm: validation is a pure derivation of (participants, amount,
  recipient, settings) — replaced the useEffect+useState dance with
  useMemo so the validation object identity is only fresh when an input
  actually changes.
- ArticleScanner: hold an AbortController ref and abort the in-flight
  fetchArticleByBarcode when a new scan arrives (or on unmount); ignore the
  AbortError. Prevents a slow earlier response from overwriting state for
  a newer scan. fetchArticleByBarcode now accepts an optional AbortSignal.
- Modal: a MutationObserver watches dialog content for async-rendered
  controls (loading → form pattern) and moves focus to the first focusable
  as soon as it appears, instead of leaving focus on the dialog wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four blockers from the follow-up audit:

- AlertText was labeling balance=0 as 'positive balance' (SC 1.1.1/1.3.1).
  Three-way sign cue now — negative / zero / positive — and the prefixes
  are i18n keys instead of hardcoded English strings.

- Transaction success played a ka-ching with no way to silence it
  (SC 1.4.2 Audio Control). Added a persistent localStorage-backed user
  preference (services/sound-preference.ts), a SoundToggle button beside
  ScalingButtons / ThemeSwitcher in the nav, and playCashSound now also
  respects prefers-reduced-motion. New SoundOnIcon / SoundOffIcon bricks.

- Heading hierarchy gaps (SC 1.3.1 / 2.4.6): demoted in-view <h1>s on
  /split-invoice and /user/:id/metrics to <h2> (RouteTitle injects the
  sr-only <h1>); added sr-only <h2>s on /user/active, /user/inactive,
  /articles/active, /articles/inactive, /search-results, and the send-money
  view so screen-reader heading nav has something between the page title
  and the list/form.

- Programmatic focus on route change (SC 2.4.3): a small
  FocusMainOnRouteChange component focuses #main-content on every
  pathname change after the initial mount, so keyboard/AT users land
  on the new view's landmark instead of a stale or unmounted element.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Important findings from the audit:

- useModal: track open modals in a module-level stack; Esc and popstate
  handlers now only fire for the topmost open dialog. Previously two
  nested modals both closed (and history popped twice) on a single Esc.

- ErrorMessage: drop redundant aria-live='assertive' next to role='alert'
  (the role implies it). Avoids double-announce on some AT clients.

- Transaction success was silent to AT (the ka-ching sound is not an AT
  signal). New global-status singleton + role='status' polite live region
  mounted in Layout, fired from createTransaction with TRANSACTION_SUCCESS.
  Auto-clears after 4s.

- Placeholder-as-only-label fixed on seven inputs (custom-tx comment,
  split-invoice comment, edit-user name/email, create-user name, search-list
  search, article-form item row, article-selection-bubbles): each gets an
  explicit aria-label matching the placeholder. CurrencyInput grows an
  optional aria-label prop with a sane default (the placeholder).

- Programmatic focus on route change is now gated on navigationType==='PUSH'
  so initial loads, root→user/active redirect (REPLACE), and browser
  back/forward (POP) leave focus alone — the skip link stays reachable as
  the natural first Tab target.

- Footer GitHub SVG: aria-hidden + focusable=false, matching every other
  decorative icon in the app.

- Consolidate visually-hidden styles: drop bricks/text/text.module.css's
  duplicate .srOnly in favour of the global .sr-only utility in
  bricks/theme/theme.css.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ans, doc

- Unit tests for shortClass / pickErrorMessage / throwOnBodyError covering
  FQCN vs short-class fallback and the body-error ApiError wrapping.
- Unit tests for the queryClient bridges: QueryCache.onError routes
  meta.defaultError to setGlobalError on read failures; MutationCache.onError
  prefers meta.errors[short class] over meta.defaultError on ApiError; the
  bridge is a no-op when a mutation has no meta.
- getGlobalError(): non-hook accessor so tests (and non-component callers)
  can peek the current snapshot without spinning up a renderer.
- New E2E tests: dark theme axe scan extended to /articles/active and
  /metrics; every scanned route asserts exactly one <h1>.
- specs/accessibility-conformance.md rewritten: dark-theme scan moved out
  of 'known follow-ups' into the verified column; new sections for
  route-change focus, modal stack, sound preference, three-way sign cue,
  status live region, placeholder-as-label fix list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two operator-facing changes:

- The backend base-URL env var is now plain `API` (was `VITE_API`). Vite's
  envPrefix is overridden to `'API'` in vite.config.ts so Vite still
  exposes the value to the client bundle at `import.meta.env.API`. Updated
  every consumer: .env.development / .env.production / .env.example, the
  ImportMetaEnv ambient type, services/api.ts, playwright.config.ts
  (webServer.env), and the two doc references. Verified by inspecting the
  built bundle: '/api/' inlined for production, the demo URL for dev.

- CI runners and `engines.node` move from Node 20 to Node 26 (the current
  release as of April 2026; becomes LTS in October). Updates ci.yml,
  package.yml, package.json. Local dev still works on older Node since
  engine-strict isn't set in .npmrc — engines is a hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@m-ober

m-ober commented May 28, 2026

Copy link
Copy Markdown
Contributor

Would be really cool to modernize the frontend a bit (also to get rid of stuff like --openssl-legacy-provider). Will try to test this a bit if I have time :)

@schinken

Copy link
Copy Markdown
Contributor Author

Im currently trying with Claude to donit with symfony ux without Javascript:D

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants