Frontend
React 19 SPA conventions: routing, data fetching, auth state, and component library rules.
The frontend is a React 19 single-page application built with Vite. It runs behind an Express proxy that forwards /api/* requests to FastAPI. All source lives under frontend/client/src/.
Directory structure
frontend/
├── client/src/
│ ├── App.tsx # All Wouter routes + AuthSync component
│ ├── pages/ # One file per route, named to match the path
│ ├── components/
│ │ ├── layout/ # Navbar, Footer, ProtectedRoute
│ │ └── ui/ # shadcn/ui generated — do not edit manually
│ ├── lib/
│ │ ├── queryClient.ts # React Query setup, apiRequest(), token store
│ │ └── utils.ts # cn() utility
│ ├── hooks/ # Custom React hooks
│ ├── stores/ # Zustand stores (auth.ts)
│ └── data/ # Static JSON (GeoJSON ward boundaries, etc.)
├── server/
│ ├── routes.ts # Single http-proxy-middleware catch-all → FastAPI
│ └── index.ts # Express entry point
└── shared/
└── schema.ts # Drizzle stub schema (not operationally used)pages/ convention
Each file in pages/ maps to exactly one Wouter route. The file path mirrors the URL path — for example, /leader/assignments lives at pages/leader/assignments.tsx. All routes are declared in App.tsx; do not define routes elsewhere.
components/layout/ vs components/ui/
layout/— hand-authored components:Navbar,Footer,ProtectedRoute. These are safe to edit.ui/— generated by the shadcn/ui CLI. Do not edit these files manually; re-run the CLI to update them.
Routing
Routing uses Wouter only. Do not introduce React Router.
All routes are declared in App.tsx. Routes under /leader/* are wrapped with ProtectedRoute, which redirects unauthenticated users to /login.
// App.tsx — simplified example
<Route path="/leader/assignments" component={ProtectedRoute(AssignmentsPage)} />Data fetching
Use React Query for all server state. Do not use useState + useEffect to fetch data.
useQuery — reading data
The queryClient is configured with a default queryFn that treats queryKey[0] as the URL to fetch. This means the query key doubles as the cache key and the request URL:
import { useQuery } from "@tanstack/react-query";
const { data, isLoading, error } = useQuery({
queryKey: ["/api/users"],
});For typed responses that need explicit .json() parsing:
const { data } = useQuery({
queryKey: ["/api/users"],
queryFn: () => apiRequest("GET", "/api/users").then(r => r.json()),
});apiRequest() — mutations and auth-protected fetches
apiRequest(method, url, body?) is defined in lib/queryClient.ts. Use it for all useMutation calls and any fetch that needs authentication:
- Injects the
Authorization: Bearer <token>header automatically. - On a 401 response, calls
GET /api/auth/refreshto get a fresh access token, then retries the original request transparently.
import { apiRequest } from "@/lib/queryClient";
const mutation = useMutation({
mutationFn: (payload: CreateUserPayload) =>
apiRequest("POST", "/api/users", payload).then(r => r.json()),
});Never call fetch() directly for authenticated endpoints. Always use apiRequest() so that token refresh is handled automatically.
Auth state
Auth state is managed by Zustand in stores/auth.ts.
interface AuthStore {
user: AuthUser | null;
isLoading: boolean;
}Access it anywhere with useAuthStore():
import { useAuthStore } from "@/stores/auth";
const { user, isLoading } = useAuthStore();Use user to check if the current user is logged in and to read identity fields (user.id, user.fname, etc.). Use isLoading to gate rendering until the session restore check is complete.
Component library
shadcn/ui and Tailwind CSS are the only UI systems. Do not use DaisyUI. Never build a custom version of something shadcn already provides.
| Need | Use |
|---|---|
| Buttons, badges, inputs, checkboxes, selects, textareas | shadcn (components/ui/) |
| Loading skeletons | <Skeleton> from components/ui/skeleton.tsx |
| Spinners / full-page loading | <Spinner> from components/ui/spinner.tsx |
| Dialog, Dropdown, Popover, Command, Sheet, Tooltip, Accordion | shadcn (components/ui/) |
| Cards, tabs, tables | shadcn (components/ui/) |
| Toasts / notifications | Sonner |
| Layout, spacing, custom positioning | Raw Tailwind |
Add new shadcn components via the CLI — do not hand-write files into components/ui/:
npx shadcn@latest add <component-name>Styling rules
- Use semantic tokens only:
primary,secondary,accent,muted,background,card,destructive. Do not use raw Tailwind color classes likebg-blue-500ortext-gray-700. - Max content width:
max-w-6xl mx-auto. Page padding:px-4 sm:px-6 lg:px-8. Section spacing:py-12. - Typography: Plus Jakarta Sans for body text, Fraunces (
font-serif) for hero and section headings. - Icons: Lucide React only. Use
size-4for inline icons andsize-5for standalone icons. - Dark mode is class-based and works automatically when semantic tokens are used — no manual dark: variants needed.
TypeScript
- Do not use
any. Useunknownand narrow explicitly. - Path aliases:
@/→client/src/,@shared/→shared/,@assets/→assets/. - Run the type checker with
npm run checkfromfrontend/.
Forms
Use React Hook Form + Zod for all user input. Show inline field errors — not toasts. Use Sonner toasts only for async success or failure notifications.