Skip to main content

Frontend

A single React SPA that serves three experiences — the authenticated ERP app, the public marketing site, and the customer portal storefront (see ../architecture.md §"One frontend, three experiences"). This page is the map; the deep dives are:

DocWhat it covers
routing.mdThe three route trees, lazy loading, guards
state.mdRedux (auth) / React Query (server) / Zustand (islands) split
api-layer.mdaxios client, interceptors, silent refresh
auth-and-permissions.mduseAuth, active role, permission gating
ag-grid.mdThe enterprise grid subsystem
forms.mdReact Hook Form + Zod patterns
styling.mdChakra v3 theme, dark mode, conventions

Stack

Versions are ranges from frontend/package.json; check there for the authoritative list.

LibraryVersionRole
React + ReactDOM^19.2UI runtime
TypeScript~6.0Language (tsc -b runs before every build)
Vite^8.0Dev server + bundler (vite.config.ts: @src/ alias, port 5173)
Chakra UI^3.36Component system + theming (styling.md)
AG Grid Community + ag-grid-react^33.3Every data table, via the in-house wrapper (ag-grid.md)
Redux Toolkit + react-redux^2.12 / ^9.3Auth state only (state.md)
TanStack React Query^5.101All other server state
Zustand^5.0Small client-state islands (portal auth/cart/wishlist, sidebar, breadcrumb)
react-router^7.17Routing — the unified react-router package, not react-router-dom (routing.md)
axios^1.17HTTP, with auth interceptors (api-layer.md)
react-hook-form + zod + @hookform/resolvers^7.79 / ^3.25 / ^3.10Forms + validation (forms.md)
recharts^3.8Analytics charts
three + @react-three/fiber^0.185 / ^9.63D scenes (marketing pages, HR analytics), always code-split
altcha^3.2Proof-of-work CAPTCHA widget on login (altcha-captcha.tsx)
@stomp/stompjs + sockjs-client^7.3 / ^1.6WebSocket clients for notifications/messages/audit-log live updates
next-themes^0.4Light/dark color mode
vitest^3.0Unit tests — currently exactly one test file (see ../testing/README.md)

Also in the tree: TipTap (rich text), FullCalendar (holiday calendar), @pdfme + jspdf/jspdf-autotable (payslip + grid PDF export), dnd-kit (Kanban), html5-qrcode (inventory scan station), motion, react-icons/lucide-react.

src/ directory map

frontend/src/
├── api/ ~60 thin axios modules, one per backend domain (employees.ts, payroll.ts, …)
│ plus client.ts / portalClient.ts / errors.ts / types.ts → api-layer.md
├── hooks/ Cross-cutting hooks: useAuth, the three STOMP WebSocket hooks, SEO, scanner
│ └── api/ ~52 React Query hooks wrapping api/ modules (query keys + invalidation) → state.md
├── pages/ Route-level components, one folder per domain: auth, billing, crm, hr,
│ inventory, marketing, payroll, portal, recruitment, self-service
├── components/ Feature components grouped per domain (~27 folders) — form drawers,
│ detail drawers, cards, import modals → forms.md
├── ui/ The design-system layer:
│ ├── ag-grid/ Enterprise grid subsystem (ServerDataGrid, useEnterpriseGrid, …) → ag-grid.md
│ ├── components/ Shared primitives: route guards, toaster, color mode, ALTCHA, rich text
│ ├── layout/ AppLayout / PublicLayout / PortalLayout, nav-config, header, profile menu
│ └── theme.ts Chakra v3 system (brand tokens, recipes) → styling.md
├── redux/ The auth slice + store — the ONLY Redux state → state.md
├── store/ Five Zustand stores (portal auth/cart/wishlist, sidebar, breadcrumb)
├── constants/ routes.ts (path constants), permissions.ts (backend permission mirror),
│ role-permissions.ts (static role → permission map) → auth-and-permissions.md
├── lib/ payslip-template/ — @pdfme template registry + builders for payslip PDFs
├── utils/ currency.ts, date.ts formatters
└── assets/ Static assets

Build commands

All from frontend/ (scripts in package.json):

CommandWhat it does
npm run devVite dev server with HMR (see the caveat below before using it)
npm run buildtsc -b && vite build — type-check then bundle to dist/
npm run lintESLint (flat config, typescript-eslint + react-hooks v7)
npm testVitest, single run
npm run test:watchVitest in watch mode
npm run previewServe the production dist/ build locally

The static-Docker-build reality

In the Docker stack, the frontend container is an nginx image serving a static production build — there is no Vite process and no hot reload. Two consequences:

  1. VITE_API_URL is baked in at image build time. It defaults to empty, which means relative URLs proxied by nginx to the backend on the same origin (api-layer.md). Changing it requires rebuilding the image, not restarting the container.
  2. Editing source does nothing to a running stack. To see a change at http://localhost:5173 you must rebuild: docker compose up -d --build frontend.

For day-to-day work use the HMR loop instead: keep the stack running for the backend/DB, then run npm run dev with VITE_API_URL=http://localhost:8081 in frontend/.env.local and that origin CORS-whitelisted. The exact recipe is in ../onboarding.md §4 "Pick your dev loop".