LDS Stake Portal

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/refresh to 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.

NeedUse
Buttons, badges, inputs, checkboxes, selects, textareasshadcn (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, Accordionshadcn (components/ui/)
Cards, tabs, tablesshadcn (components/ui/)
Toasts / notificationsSonner
Layout, spacing, custom positioningRaw 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 like bg-blue-500 or text-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-4 for inline icons and size-5 for 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. Use unknown and narrow explicitly.
  • Path aliases: @/client/src/, @shared/shared/, @assets/assets/.
  • Run the type checker with npm run check from frontend/.

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.

On this page