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%