Skip to content

The frontend monorepo

The frontend/ repo is an Nx 21 monorepo (nx.json, nx@21.0.3 in package.json) that houses four separate React 18 single-page apps — one per persona — plus five shared libraries they all import through @orascom/* TypeScript path aliases. Every app is its own deployable and every app talks to the same Laravel backend repo; what differs per app is an env-driven endpoint prefix and its own base-host env var, so one API host can serve all four personas without the browser ever calling a CRM or middleware directly.

Stack (from package.json): React 18.3.1, Vite 6.3.5, TypeScript 5.7.3, Yarn 1 (yarn@1.22.22, classic — not pnpm/npm), TanStack React Query 5.84.1, react-router-dom 6.11.2 (createBrowserRouter), @sentry/react 7.112.2, i18next 21.9.2 + react-i18next 11.18.6, and Formik 2.2.9 + Yup 0.32.11 for forms. The build graph is driven by the @nx/vite, @nx/eslint, and @nx/cypress plugins (nx.json plugins).

Each app lives under apps/, has its own router basename env var (the “path” var below), its own API base-host env var, its own localStorage token key, and its own Sentry DSN. Follow the link for the per-app deep-dive.

App (deep-dive) One-line role Router basename env API base-host env
Shopperapps/orascom-shopper-app Customer-facing portal: browse, reserve & pay online, EOI payment, post-purchase Pre-Delivery owner area NX_SHOPPER_PATH NX__API_ENDPOINT
Brokerapps/orascom-broker-app External brokers selling ODH units on commission; the same build also serves sales-managers by role NX_BROKER_PATH NX__BROKER_API_ENDPOINT
Sales-manapps/orascom-sales-man-app Internal Orascom sales-staff CRM: leads → pipelines → customers → sales, plus EOI and Taskeen NX_SALES_MAN_PATH NX__SALESMAN_API_ENDPOINT
Shopper Analyticsapps/orascom-shopper-analytics-app Internal BI dashboards over shopper-portal data (sales, leads, engagement, demographics, funnel) NX_SHOPPER_ANALYTICS_PATH NX__SHOPPER_ANALYTICS_API_ENDPOINT

All four share one architecture: src/api/routes.tsx (route tree) → src/utils/routes-mapper.ts (path constants) → src/pages/* (screens), with a thin Network.fetch HTTP layer (src/api/network.ts) over a flat ENDPOINTS map (src/api/endpoints.ts).

Five Nx libraries live under libs/, imported by the apps via the @orascom/* aliases configured in tsconfig.base.json. Always import through the alias and the lib’s src/index.ts barrel — never deep-import ../../../libs/... (frontend/CLAUDE.md § 4).

  • @orascom/api-interfaces (libs/api-interfaces) — Shared TypeScript types/interfaces for the whole platform: the HTTP Payload / Route shapes (api-interfaces.ts, route.interface.ts) plus common, unit, destination, country, reservation, user, eoi, and oman-registration interfaces. Pure types, no runtime code. When a change represents an API shape, the type goes here.
  • @orascom/common-components (libs/common-components) — The cross-app React UI kit used by all four apps: primitives (button, card, select / filter-select, modal / generic-modal, loader / spinner, tooltip, accordion, pagination, range-slider / price-range-slider, cards-slider, thumbs-gallery, video, language-dropdown); auth UI (login-component, set-password, forget-password, auth-wrapper); analytics UI (analytics-calendar / analytics-filter / analytics-period-filter + the use-analytics-* hooks); the sales-offer calculators (sales-offer-payment-calculator, -installment-plan, -floor-plan, plus Montenegro variants); the walkThrough guided-tour system; and shared contexts (user-context, reservation.context).
  • @orascom/broker-sales-man-common-components (libs/broker-sales-man-common-components) — Components shared specifically between the Broker and Sales-man CRM portals: the full EOI UI (eoi-listing, eoi-details, eoi-card, eoi-add-units, eoi-add-preference, create-eoi-component); destination detail UI (destination-details-ui and its -facilities / -location / -projects / -masterplan / -video parts); unit UI (unit-details, unit-payment-calculator / -plan, unit-specs, similar-units, property-card); compare (compare-button, compare-properties); Taskeen (taskeen-card, taskeen-table, taskeen-details-modal); plus launch-details, header, notifications-wrapper, change-logo-modal (whitelabel), and the compare-units / country-selected contexts.
  • @orascom/common-styles (libs/common-styles) — Shared SCSS: styles.scss plus a styles/ folder of design tokens, variables, mixins, and breakpoints, imported by each app’s own styles.scss. Consumed as an SCSS import rather than a JS symbol.
  • @orascom/utils (libs/utils) — Shared runtime utilities, hooks and contexts: the React Query wiring (query-clientqueryClient / mutationCache, used in each app’s main.tsx); constructURL / errorMessagesHandler and type-guards (utils.ts); the currency system (currency.context + CurrencyContextProvider, use-currency-context / -converter, exchange.rates.utils, use-restrict-currency-to-gouna); i18n changeLanguage; filter helpers (get-filters-query-string, use-unit-filters, use-price-slider); Taskeen helpers; and generic hooks (use-window-dimensions, use-media-query, use-disclosure, use-download, payment-plan-calculator).

The four apps deliberately look almost identical inside; the differences are all pushed to environment variables so nothing persona-specific is hard-coded.

  • Env-driven, per-persona endpoint prefixes. Every request is built as base-host + endpoint-prefix + path. The base host is the app’s own *_API_ENDPOINT; the prefix is its own *_ENDPOINT_PREFIX. Shopper uses NX__SHOPPER_ENDPOINT_PREFIX (plus NX__PRE_DELIVERY_ENDPOINT_PREFIX for the owner area); Broker uses two prefixes in one build — NX__BROKER_ENDPOINT_PREFIX and NX__SALES_MANAGER_ENDPOINT_PREFIX; Sales-man uses NX__SALESMAN_ENDPOINT_PREFIX; Analytics uses NX__SHOPPER_ANALYTICS_ENDPOINT_PREFIX. A few endpoints (currency, /user, some cross-portal /shopper/* lookups) are deliberately prefix-less. Persona endpoints are mutually exclusive — don’t copy a call between apps; lift the shared shape into @orascom/api-interfaces (frontend/CLAUDE.md § 2.6).
  • No refresh tokens anywhere. No app implements refresh-token rotation. Each app’s userDataLoader fetches /me (or /user); on a missing or expired token it removes the token from localStorage and redirects to that app’s /login. Token keys are persona-scoped (NX_SHOPPER_ACCESS_TOKEN_KEY, NX_BROKER_ACCESS_TOKEN_KEY, NX_SALESMAN_ACCESS_TOKEN_KEY, NX_SHOPPER_ANALYTICS_ACCESS_TOKEN_KEY) so two personas open in the same browser profile can’t read each other’s tokens — don’t collapse them. See the auth model for the four distinct login flows (phone+OTP, email+password, Microsoft SSO, email+password).
  • React Query in three of four apps — not analytics. Shopper, Broker, and Sales-man each wrap their tree in QueryClientProvider (the queryClient comes from @orascom/utils). Shopper Analytics does NOT use React Query — its filter state lives in a plain FILTER_CONTEXT and data is fetched through service classes directly.
  • ProtectedRoutes + userDataLoader gating. Every non-auth route group is wrapped in <ProtectedRoutes> (re-checks the token per navigation) and runs a route-loader userDataLoader (loads the current user and redirects to /login when unauthenticated). NotFoundPage is the errorElement for each group.
  • Per-app Sentry DSN. Each src/main.tsx inits @sentry/react (reactRouterV6 tracing + replay) with its own DSN env var — NX_SENTRY_SHOPPER_DSN, NX_SENTRY_BROKER_DSN, NX_SENTRY_SALESMAN_DSN, NX_SENTRY_ANALYTICS_DSN. Source-map upload at build time is handled by tools/orascom-sentry-vite-plugin.ts.

Nx targets are exposed both as package.json shortcuts and directly via yarn nx <target> <project>. Use the Nx graph (don’t invoke Vite directly) so caching and the affected-graph behave (frontend/CLAUDE.md § 2).

Terminal window
# install (Yarn 1 classic only — never npm/pnpm)
yarn install
# per-app dev servers
yarn start # shopper (port 4200)
yarn start:broker # broker (port 4201)
yarn start:sales-man
yarn start:shopper-analytics
# equivalently:
yarn nx serve orascom-shopper-app
# per-app production builds
yarn build # shopper
yarn build:broker
yarn build:sales-man
yarn build:shopper-analytics
# equivalently:
yarn nx build orascom-broker-app
# lint / test / CI-equivalent local check
yarn nx lint orascom-shopper-app
yarn nx test orascom-shopper-app
yarn nx affected -t lint test build # what changed vs. main
# inspect the workspace
yarn nx graph
yarn nx show project orascom-shopper-app --json
  • README.md is wrong about the app count (says 2, there are 4). Trust apps/ and package.json.
  • Yarn 1 only. The lockfile is yarn.lock; do not run npm install or migrate to pnpm, and don’t add a non-Nx-aware bundler (Bun, standalone Turbopack) — Nx caching depends on its own runners (frontend/CLAUDE.md § 2, § 12).
  • Vite inlines every NX_* / VITE_* var into the JS bundle. A secret placed in a populated .env ships to every browser — never commit one, and never make a direct browser call to a CRM or middleware.
  • The NX_CURRENY_LAYER_* misspelling is intentional. CURRENY (missing the second C) is baked into every populated env; “fixing” the spelling silently breaks currency config everywhere. Rename only as one coordinated change across .env.example, all envs, and code.
  • @types/react is 19.x while the runtime React is 18.3.1. This mismatch is intentional and fragile (frontend/CLAUDE.md § 12) — don’t “align” the types to 19 or bump React to 19 without a coordinated check.
  • tsconfig.base.json has a dangling @orascom/api alias pointing at a non-existent libs/api. Ignore it.
  • The Nx Cloud accessToken in nx.json is a committed read-write token (frontend/CLAUDE.md § 12, and PROJECT-OVERVIEW.md § 9). Treat it as leaked — it needs rotating out of the repo into a CI-only env var.
  • No SSR / Next.js. The apps are SPA + Vite by design; moving to SSR would require coordinated backend caching work.