Web app changes
Conventions for screens, strings, money and data fetching in msaving/web.
Layout
src/
config.ts build-time settings, all optional (VITE_API_URL defaults to the same origin)
auth/ session token store, useSession, signOut, new-account forms
lib/api.ts fetch wrapper (bearer token, ApiError with the code) and API types
lib/queries.ts TanStack Query hooks and keys
lib/money.ts format and parse amounts in minor units
components/ Shell (sidebar, book switcher, admin link), AccountForm, AmountField, pickers, dialogs
i18n/ vi.json (default), en.json, strings.test.ts
pages/ one file per screen; App.tsx routes (setup, sign-in, join, reset, the app)
Stack: React, Vite, TypeScript, TanStack Query, React Router, i18next, Material UI (default theme, follows the device’s light/dark), Vitest, oxlint. Forms: plain inputs and FormData (no form library decided).
Rules
- Same origin: the server serves this app, so calls go to
/v1on the same address and the API refuses other origins. In development, Vite’s proxy keeps it that way. Links (invites, resets) are built fromwindow.location.origin. - Strings: every visible string is a key in both
vi.jsonanden.json. Vietnamese first. The test fails if keys differ. Never translate what the user typed. - Error codes from the API are translated here (
components/errors.tsand the string files). - Money: integers from the API; format with
lib/money.ts(Intl.NumberFormat). Never float math. In Vietnamese.groups thousands, so typing “1.250” for VND means 1250. - Concurrency: send the
versionyou loaded; on 409 reload and tell the user. Make a transaction’s id when the user clicks Save and reuse it on retry. - Desktop first: sidebar from the
mdbreakpoint, tables for lists. Phones still get a working layout. - Nothing in localStorage but the session token and the language choice. No analytics or tracking.
- Fast refresh:
pages/andcomponents/export components only; shared functions go inauth/orlib/.
Gotchas
- i18next’s
<Trans>drops tags named like HTML void elements (<link>,<br>,<img>): use names like<policy>. - Queries that need a session pass
enabled; the cache is cleared on sign-out. - The CSP comes from the server (
backend/app/api/main.py): scripts only from itself,data:images (QR codes). A new external origin is a security decision first.
Checks
npm run lint && npm test && npm run build