React TypeScript Frontend
Single Vite app under frontend/, built into frontend/dist and served by the Flask container in production (no Static Web Apps). React 19, TypeScript ~5.9, Mantine 8.3.x, React Router 7.
Stack reality check
From frontend/package.json:
react/react-dom19.2.x@mantine/core8.3.x +@mantine/hooks,@mantine/dates,@mantine/dropzone,@mantine/modals,@mantine/notifications,@mantine/tiptap@tabler/icons-react3.xreact-router-dom7.10.xaxios1.13.xvite7.x,typescript~5.9,eslint9 +typescript-eslint8
There is no path-alias setup (no @/ imports), and no frontend/src/api/ directory. API calls use axios directly with relative URLs (the Vite dev server proxies /api to the Flask backend; production serves both from the same origin).
Bootstrap layout
frontend/src/
main.tsx # createRoot, BrowserRouter, MantineProvider styles imports
App.tsx # Auth state, AppShell, route table, permission gating
App.css # Global app-shell layout
index.css # Resets / base typography
theme.ts # Mantine theme (violet primary, system font stack)
contexts/
UserContext.tsx # Provides { user, permissions, hasPermission }
hooks/
useUserCache.ts # localStorage scoped per user_email
components/
Footer.tsx
GrammarLearning.tsx
pages/ # All pages — see route map below
BrowserRouter lives in main.tsx, not in App.tsx. Mantine CSS imports must come before index.css to keep cascade correct.
Route map (current)
From App.tsx:
| Path | Component | Permission |
|---|---|---|
/ |
Home |
home |
/lookup |
Lookup |
lookup |
/translate |
Translate |
translate |
/sentences |
Sentences |
sentences |
/article-analysis |
ArticleAnalysis |
article_analysis |
/verbs |
Verbs |
verbs |
/database |
Database |
database |
/publications |
Publications |
publications |
/publications/new |
NewPublication |
publications |
/publications/:id |
PublicationDetail |
publications |
/translation-game |
TranslationGame |
translation_game |
/grammar |
Grammar |
grammar |
/compose |
Compose |
compose |
/admin/users |
AdminUsers |
admin |
/login |
Login |
(public) |
When adding a page: add the file under frontend/src/pages/, add the import + <Route> in App.tsx, add a <NavLink> wrapped with hasPermission(...), and add the corresponding key to the backend ROLE_PERMISSIONS map.
Auth wiring
App.tsx calls GET /api/auth/status on mount (App.tsx) and stores { user, permissions } in local state, broadcasting via UserContext. Login flow:
// Login.tsx posts to /api/auth/login or /api/auth/magic-link
onLoginSuccess({ user, permissions }) // sets state, navigate('/')
The contract { user: User, permissions: string[] } is fixed — keep it stable when changing the login page.
Page-view tracking is fire-and-forget: every location.pathname change posts to /api/auth/track-page (App.tsx).
Mantine usage
Theme: theme.ts sets primaryColor: 'violet', system-font stack, defaultRadius: 'md', autoContrast: true. The app is light-oriented by default — there's no dark-mode toggle wired up despite the loading screen using #1a1b1e.
Use Mantine props (p="md", c="violet.7", etc.) over hand-written CSS when possible. Page-specific styles go in CSS Modules co-located with the page (e.g. Compose.module.css, Sentences.module.css). Global app-shell styling lives in App.css.
<MantineProvider theme={chuukTheme}> wraps everything in App.tsx and is also wrapped around the loading + login states — don't drop it from those branches.
Async / data patterns
- Use raw
axioswith relative paths. The session cookie is sent automatically (same-origin). - For SSE (e.g. publication processing), use
EventSourcedirectly — seePublicationDetail.tsx. - For per-user persistence (UI state, recent searches), prefer the
useUserCachehook — it namespaceslocalStoragebyuser_emailso two accounts on the same device don't collide. - No global state library; lift state into
Appor use React context. Don't introduce Redux/Zustand without a real reason. - File uploads use
@mantine/dropzone. Theacceptlist must align with backendALLOWED_EXTENSIONS(app.py).
Build & dev
cd frontend
npm install
npm run dev # Vite on :5173, proxies /api → :5000
npm run build # tsc -b && vite build → frontend/dist
npm run lint
The full-stack dev script dev-start.sh starts both Flask and Vite.
TypeScript tips
tsconfig.app.jsonisstrict. Don't widen it.- API response shapes are usually inlined as local
interfaces in the consuming page — there's no sharedtypes/directory yet. If a shape is reused in 3+ places, factor it to afrontend/src/types.ts. react-router-dom7 still re-exports the v6 hooks (useNavigate,useLocation,Routes,Route). Don't reach for the data-router APIs — the app uses the classic<Routes>declaration inApp.tsx.
Pitfalls
- The loading-state background is hard-coded
#1a1b1e— visible flash on first paint. If this matters, move it into the theme. axios1.13 defaultswithCredentialsto false, but cookies work because we're same-origin in prod. If you ever introduce a separate frontend host, you must enableaxios.defaults.withCredentials = trueAND configure CORS on Flask.- Don't
import 'axios'and add interceptors that transform error shapes globally — pages currentlytry/catchtheir own errors and any rewrite would have to be applied site-wide. - Mantine v8 changed several prop names from v7 (e.g.
position→justify). When porting external snippets, double-check.