fitse-yotor 659e954306 refactor(theme): share one base theme between backoffice and portal
The two apps had drifted into unrelated themes. The portal's carried a full
type scale, radius scale, shadow ramp and component defaults; the backoffice's
had none of them — 55 lines defining two colour ramps and little else. With
nothing to inherit, its 23 features each invented their own sizing, which is
the real source of the inconsistency the UI reads with.

Promote the portal's structure to `libs/shared` as `baseTheme`, and reduce both
themes to what they should differ on: brand. The backoffice keeps #1e40af and
the portal keeps Coastal Modern — a distinct accent tells an officer which of
the two systems they are in, and the ramps are not interchangeable in contrast.

Both export names are preserved, so no consumer import changes.

Two properties are deliberately held back rather than shared:
- `colors.gray`: the portal's blue-tinted neutrals retint every dimmed label,
  neutral badge and table border. The backoffice adopts them as its own
  reviewed change, not as a side effect of sharing a base.
- `primaryShade.dark`: moves every filled control in dark mode; waits until
  dark mode is verified end to end.

Also fixes a live bug: PageLoader coloured its primary label `navy.9`, which is
defined in neither theme. Mantine drops unresolved colour keys silently, so the
label in a component used by 20 files had been rendering an inherited colour.

Adds a visual-regression harness to make all of this reviewable. It runs
against a static gallery route rather than real pages, so it needs no API,
database or auth — a theme diff cannot be masked by a migration or an expired
token. The portal is the control group: it is pixel-identical across all four
light/dark × desktop/tablet baselines, which is what makes the refactor
provably lossless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:25:26 +03: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-08-20 15:30:59 +03:00
2026-08-20 15:30:59 +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%