# frontend Vite + React + TypeScript SPA for the SAR Link portal. No Node at runtime: production is a static bundle served by nginx. | | | |---|---| | Build | Vite 8 | | UI | React 19, Tailwind CSS v4 | | Routing | React Router 7 (SPA, `BrowserRouter`) | | Auth | knox token in `localStorage`, `AuthProvider` in `src/lib/auth.tsx` | ## Layout ``` src/lib/api.ts fetch wrapper, typed endpoints, ApiError src/lib/auth.tsx AuthProvider (session bootstrap, sign in/out) src/lib/auth-context.ts AuthContext + useAuth src/lib/date.ts local-time YYYY-MM-DD formatting src/components/ ui primitives, Select, DateField, AppLayout, RequireAuth src/pages/ Login (two-step), Register (form), Dashboard, NotFound ``` ## Run it From the repo root (starts the API and database too): ```sh docker compose up --build ``` Or on the host, against a backend on `localhost:8000`: ```sh npm install npm run dev ``` Then `http://localhost:5173`. ## Talking to the API The app always calls a relative `/api/...`. Nothing hardcodes a backend URL: - **dev** — `vite.config.ts` proxies `/api`, `/admin`, `/static` and `/media` to `VITE_API_PROXY_TARGET` (`http://backend:8000` in compose). - **prod** — nginx serves `dist/` and proxies the same prefixes to gunicorn. So there is no CORS in production, and no API origin to configure at build time. ## Sign-in and registration `src/pages/Login.tsx` is one form whose second step the API chooses: 1. Mobile number -> `POST /api/auth/start/` 2. The response's `next` decides what appears under it: - `password` -> password field -> `POST /api/auth/login/password/` - `otp` -> 6-digit code, with a resend cooldown -> `POST /api/auth/verify/` 3. `verify/` answers with `next`: `dashboard` (token stored, on to the portal) or `register` (the returned ticket goes to `/register` in router state). The code step looks the same whether or not the number has an account - the UI has no idea until the code is confirmed, which is the point. `src/pages/Register.tsx` requires that ticket (no ticket -> back to `/login`), shows the verified number read-only, and collects name, ID card/passport/work permit number, date of birth (`DateField`, a react-day-picker popover with month and year dropdowns), atoll + island (from `GET /api/locations/atolls/`, island list filtered by the chosen atoll) and the two agreement checkboxes linking to sarlink.net/terms and /policy. Submitting shows a "pending approval" panel - it does not sign the applicant in. A pending or rejected account that signs in later sees a status banner on the dashboard instead of services. On reload `AuthProvider` calls `GET /api/auth/me/` to turn the stored token back into a user, and clears it if the API rejects it. ## Adding a dependency `node_modules` lives in a Docker volume that outlives image rebuilds, so a new entry in `package.json` isn't in the container until it's installed there. The dev service runs `npm install` on every start, so: ```sh npm install # updates package.json + lock on the host docker compose restart frontend # installs it in the container ``` If it still can't resolve the import, the volume is stale — recreate it with `docker compose up -d -V frontend`. ## Scripts ```sh npm run dev # dev server on :5173 npm run build # tsc -b && vite build -> dist/ npm run preview # serve dist/ locally npm run lint # oxlint ```