mihretue 4edce751ee feat(exam): candidate exam-taking experience (Phase 4)
New portal feature apps/portal/src/app/features/exam-attempt/, following
the repo's existing folder convention (pages/<Page>/index.tsx, components/,
types/, hooks/ — matches the payments feature's hooks/ precedent):

- types/exam-attempt.ts — response/local-state shapes
- hooks/useExamAttempt.ts — all API orchestration: load (registration +
  attempt), start/resume, per-question autosave (immediate on MCQ select,
  debounced+flushed-on-navigation for essay text), local countdown seeded
  from the server's serverTime/remainingSeconds, submit, and resync from
  the server whenever a write is refused as expired/already-submitted
- components/ExamInstructions, ExamTimer, ExamQuestionNav,
  ExamQuestionDisplay, ExamCompletion — one concern per file, not a single
  page dump
- pages/ExamAttemptPage — thin view layer over the hook

Flow: /exams (existing) gets a 'Take exam' action on eligible registration
rows → /exams/:examId/take, which shows instructions before an attempt
exists, the live exam screen while IN_PROGRESS, and a no-score completion
screen once SUBMITTED/EXPIRED (never fabricates a result — grading
doesn't exist yet).

Security note: RequirePermission on the route and disabled inputs after
local timeout are UI conveniences only. Every save/submit is independently
re-checked by the backend's ownership + applyExpiry() on each call: a
client that skipped the UI entirely and hit the API directly would be
bound by exactly the same rules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 09:17:09 +00:00
2026-06-12 11:46:25 +03:00
2026-06-09 10:37:47 +03:00
2026-06-08 13:17:29 +03:00
2026-06-15 10:29:28 +00:00
2026-06-15 10:29:28 +00:00
2026-07-28 11:07:38 +03:00
2026-06-11 06:55:40 +00:00
2026-06-02 09:23:01 +03:00
2026-06-11 12:04:21 +00:00

EMA Platform — Nx Monorepo Frontend

A fully scaffolded Nx monorepo housing two Vite + React 19 SPAs (Backoffice and Portal) with shared libraries for API integration, UI components, and theming.


Tech Stack

Tool Version
React 19
Nx 22
Vite 7
TypeScript 5.9
Redux Toolkit 2.11
Mantine 8.3
React Router 7
TanStack Query 5
React Hook Form 7
Zod 4
Tailwind CSS 3.4
Vitest 4

Monorepo Structure

emaui/
├── apps/
│   ├── backoffice/   # Admin/operator SPA — port 4201
│   └── portal/       # End-user SPA — port 4200
└── libs/
    ├── api/          # RTK Query baseApi, session resolution, generic query/mutation hooks
    ├── ui/           # Shared Mantine components (ConfirmModal, ApiErrorAlert, notify)
    └── shared/       # Mantine theme (emaTheme), design tokens

libs/api

  • base-api/ — RTK Query createApi instance with prepareHeaders that injects the Bearer token from Redux state or storage.
  • session/resolveTokenFromStorage() reads the auth-token cookie first, falling back to localStorage for legacy pre-migration sessions. resolveSessionContext() merges Redux state token with storage fallback.
  • query-and-mutation/ — Generic useApiQuery / useApiMutation wrappers for one-off API calls without defining a dedicated endpoint file.

libs/ui

  • ConfirmModal — Reusable Mantine modal for destructive-action confirmation.
  • ApiErrorAlert — Extracts a human-readable message from RTK Query error shapes or Error objects.
  • notify — Thin wrapper around @mantine/notifications with .success, .error, .info, .warning helpers.

libs/shared

  • ema-theme — Mantine v8 createTheme() with emaPrimary (blue) and emaSecondary (warm) color tuples, Inter font, and custom shadow scale.

Auth Flow

  1. User submits the login form (LoginForm / LoginPage).
  2. The form calls the login RTK Query mutation (backoffice) or a plain fetch (portal).
  3. On success, loginSuccess action is dispatched → Redux auth slice stores token and user; authStorage.setToken() persists the token to a cookie (both apps call configureAuthStorage(prefix, true)).
  4. baseApi's prepareHeaders reads the token via resolveSessionContext(getState()) and attaches Authorization: Bearer <token> to every RTK Query request.
  5. ProtectedRoute checks authStorage/the token cookie on every navigation — if absent, redirects to /login.
  6. logout action clears Redux state and calls authStorage.clear() to remove all auth cookies.

Local Setup

# 1. Install dependencies
npm install

# 2. Copy environment config
cp .env.example .env
# Edit VITE_BASE_API_URL to point at your running backend

# 3. Start the backoffice (port 4201)
npm run backoffice

# 4. Start the portal (port 4200)
npm run portal

# 5. Or start both in parallel
npm run dev:all

Environment Variables

Variable Required Default Description
VITE_BASE_API_URL Yes http://localhost:3000 Base URL for all API requests
VITE_ENABLE_DEVELOPER_TOOLS No true Toggle Redux DevTools
VITE_PAYMENT_API_URL Yes (portal) Base URL for the payment service (portal payment feature). Can be a same-origin path if a backend proxy is later placed in front of it
VITE_PAYMENT_SERVICE_TOKEN Yes (portal) Sent as x-service-token on every payment request. Note: bundled VITE_* values are public in the built app, not secret
PORTAL_PORT No 4200 Docker host port for portal
BACKOFFICE_PORT No 4201 Docker host port for backoffice

Docker

# Build and run both apps via Docker Compose
cp .env.example .env
docker compose up --build

The Dockerfile uses multi-stage builds with named targets (portal / backoffice). Each stage produces an nginx image serving the built SPA.


Adding a New Feature

  1. Create the feature folder under the relevant app:

    apps/backoffice/src/app/features/<feature-name>/
    ├── types/       # TypeScript interfaces
    ├── api/         # RTK Query injectEndpoints
    ├── store/       # Redux slice (if local state needed)
    ├── hooks/       # Custom hooks wrapping store/api
    ├── components/  # Presentational React components
    └── pages/       # Route-level components
    
  2. Wire up the API endpoint in <feature-name>/api/<feature>-api.ts using baseApi.injectEndpoints(...).

  3. Add a route in apps/<app>/src/app/router/index.tsx (backoffice) or apps/<app>/src/app/router.tsx (portal).

  4. Add a sidebar entry in AppSidebar.tsx (backoffice only) for the new route.

  5. If the feature needs shared UI, add components to libs/ui/ and export from libs/ui/src/index.ts.

Description
No description provided
Readme 219 MiB
Languages
TypeScript 97.9%
CSS 1.3%
HTML 0.4%
Shell 0.3%