Skip to content

Frontend

Joël Deffner edited this page Jul 17, 2026 · 1 revision

Frontend

The frontend is a React 19 single-page application in frontend/, built with Vite and TypeScript. It renders all UI; the backend only supplies JSON.

Tech stack

Concern Library
Framework React 19 (with the React Compiler babel plugin)
Build Vite 8, TypeScript 6
Routing react-router-dom v7
Styling Tailwind CSS 4
Components shadcn components on Base UI (@base-ui/react, not Radix; props differ)
Icons Phosphor Icons
Data grid TanStack Table (admin dashboard)
Animations GSAP + @gsap/react (landing page scroll animations)
Maps Leaflet + react-leaflet + leaflet.markercluster
Fonts Bricolage Grotesque (display), Instrument Sans (body), self-hosted woff2

Routing (src/App.tsx)

BrowserRouter runs with a basename derived from import.meta.env.BASE_URL (/public in production builds, empty in dev).

Route Page Guard
/ home/HomePage (standalone marketing landing, own layout) public
/meets meets/MeetsPage public
/meets/:id meets/MeetDetailPage public
/meets/new meets/MeetCreatePage RequireAuth
/meets/:id/edit meets/MeetEditPage RequireAuth
/groups groups/GroupsPage public
/groups/:id groups/GroupDetailPage public
/chat ChatPage (3s polling) RequireAuth
/weather WeatherPage public
/pilots/:username pilots/PilotProfilePage public
/login, /register LoginPage, RegisterPage public
/profile ProfilePage RequireAuth
/admin/dashboard AdminDashboardPage RequireAdmin
* redirect to /

Every route except the landing page renders inside AppChrome: the floating NavIsland navigation pill, a centered main column, and the SiteFooter (hills illustration). The ChatLauncher floating bubble mounts globally and hides itself on /chat, /login, /register, and when logged out.

API access (src/lib/api.ts)

All server communication goes through the api<T>(path, options) helper. It:

  1. Prefixes paths with BASE_URL (so /api/... becomes /public/api/... in production; without the prefix the root .htaccess redirect would turn a POST into a GET).
  2. Sends credentials: 'same-origin' so the Shield session cookie travels along.
  3. Attaches the CSRF token (kept in module memory) as a header on every non-GET request.
  4. Captures a fresh CSRF token from any response that carries a csrf field (login regenerates the session).
  5. Throws ApiError on non-2xx, exposing status and parsed fieldErrors from 422 responses so forms can show per-field messages.

Never call fetch directly; always go through this wrapper.

Auth state (src/lib/auth.tsx)

AuthProvider wraps the app and exposes useAuth():

  • On mount it calls /api/auth/me to restore the session (and pick up the CSRF token); loading is true until that resolves.
  • login(login, password, remember) and logout() update the context. Logout re-fetches /me to get a fresh CSRF token for the new anonymous session.
  • isAdmin is derived from user.permissions['admin.access'].

RequireAuth and RequireAdmin (in src/components/) redirect unauthenticated/unauthorized visitors; they are UX only, the backend filters remain authoritative.

Page and component map (src/)

pages/
├── home/            # GSAP landing: Hero, Story, Features, Meets, Gallery,
│                    # LiveFeed (activity), Testimonials, Steps, Cta, ...
├── meets/           # MeetsPage (cards + search/filters), MeetDetailPage,
│                    # MeetCreatePage / MeetEditPage (shared MeetForm with
│                    # LocationPickerMap), MeetWeatherPanel
├── groups/          # GroupsPage, GroupDetailPage, group-image mapping
├── pilots/          # PilotProfilePage (public profile)
├── weather/         # ForecastStrip, ReportDetail, WindArrow, WMO code map,
│                    # wind/format helpers (used by WeatherPage)
├── ChatPage.tsx     # full-page chat, 3s polling with the `after` cursor
├── LoginPage / RegisterPage / ProfilePage / AdminDashboardPage / WeatherPage
components/
├── chrome/          # NavIsland (floating nav pill), SiteFooter
├── chat/            # ChatPanel (shared message list + composer), ChatLauncher
├── map/             # BaseMap, LocationPickerMap, MarkerClusterLayer, markers
├── ui/              # shadcn-on-Base-UI primitives (button, dialog, select, ...)
├── users/           # UserFormDialog (admin create/edit)
└── RequireAuth / RequireAdmin / UserMenu
lib/
├── api.ts           # fetch wrapper (CSRF, base path, ApiError)
├── auth.tsx         # AuthProvider / useAuth
├── types.ts         # TypeScript mirrors of the API shapes
└── utils.ts         # cn() etc.

Interaction principles (from the spec)

  • Search and filters combine (AND) and update the list immediately, client-side.
  • Join/leave/create/send show instant feedback; mutating responses return the fresh resource, which replaces local state (no page reloads).
  • User-facing success/error messages appear for every state-changing action.

Build configuration (vite.config.ts)

  • base is /public/ for builds, / for the dev server.
  • build.outDir is ../public with emptyOutDir: false (so CodeIgniter's index.php survives).
  • Dev server proxies /api and /media to CI_BACKEND_URL (default http://localhost:8080).
  • @ aliases src/.

Gotchas

  • Base UI, not Radix: the shadcn components in src/components/ui/ sit on Base UI; prop names and composition differ from Radix-based shadcn. The Base UI Select needs an items prop.
  • Dialog exit animations: Base UI dialogs wait for the exit animation before unmounting. In a backgrounded/headless browser tab animations are paused, so dialogs can look "stuck". Not a bug in foreground browsers.
  • pnpm build is required before a frontend change shows up in the production (public/) app.

Clone this wiki locally