npm test # Run all Vitest unit tests (runs with TZ=UTC)
npm test -- <pattern> # Run specific test file(s) matching pattern
npm run lint -- --type-check # Run linter and tsc simultaneouslysrc/
├── api/ # SWR hooks and API functions
├── components/ # Reusable UI components
├── constants/ # App constants (routes, nav, user types)
├── entities/ # TypeScript types and Zod schemas
├── modals/ # Modal dialog components
├── pages/ # Page components
├── services/ # Business logic (AuthService)
├── state/ # Recoil atoms
├── styles/ # CSS files
├── utils/ # Utilities and custom hooks
├── App.tsx # Main app component (handles auth and routing)
├── Router.tsx # Route definitions with lazy loading
└── index.tsx # App entry point
- Routes are defined in Router.tsx using React Router v6
- Route paths are constants in src/constants/routes.ts
- All pages use lazy loading via
React.lazy()for code splitting - Routes are split into unauthenticated (login, register) and authenticated sections
- Permission checks use
canViewPage()utility before rendering routes - Many routes only render when an
activeGameis selected
- Global state uses Recoil with atoms in src/state/
- Key atoms:
userState- Current authenticated useractiveGameState- Currently selected game (persisted to localStorage)gamesState- User's games listorganisationState- Organization datadevDataState- Dev data inclusion flag
- Route-level checks with
canViewPage()in Router - Action-level checks with
canPerformAction()throughout components - Based on user type (ADMIN, OWNER, DEV)
How it works:
Runtime env vars are injected via a <script> in index.html that sets window.__ENV__. The app reads them through getEnv() in src/utils/env.ts, which:
- Checks
window.__ENV__for a substituted value - Falls back to
import.meta.envfor dev / CI builds - Detects unresolved placeholders (e.g.,
"${API_URL}") and skips them
Why not import.meta.env directly?
Vite/Rollup inlines import.meta.env values at build time. For Docker images built without knowing the user's runtime env vars, this would bake empty strings into conditionals — breaking features like hCaptcha when the env var is optional.
To add a new env var:
- Add it to
.env.productionwith placeholder syntax:VITE_NEW_VAR=${NEW_VAR} - Add it to the
window.__ENV__object inindex.html - Use
getEnv('VITE_NEW_VAR')in code instead ofimport.meta.env.VITE_NEW_VAR
The Dockerfile automatically extracts var names from .env.production and entrypoint.sh runs envsub on index.html at container startup.