Appearance
Adding a Page
This is the practical path for adding a new dashboard page, following the existing patterns. Read Application structure first.
1. Add the page component
Create src/pages/<Name>.tsx following the existing page shape:
tsx
import { useEffect, useState } from "react";
import { usePageTitle } from "@/lib/usePageTitle";
import { LoadingState, ErrorState } from "@/components/DataState";
import { apiFetch } from "@/lib/api/client";
interface MyResourceResponse { success: boolean; items: string[] }
export default function MyPage() {
usePageTitle("My Page");
const [items, setItems] = useState<string[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
apiFetch<MyResourceResponse>("/my-resource")
.then((data) => { if (!cancelled) setItems(data.items ?? []); })
.catch((e) => { if (!cancelled) setError(e.message); });
return () => { cancelled = true; };
}, []);
if (error) return <ErrorState message={error} onRetry={() => window.location.reload()} />;
if (items === null) return <LoadingState label="Loading…" />;
return (
<div className="space-y-4">
<h1 className="text-xl font-semibold">My Page</h1>
{items.map((item) => <div key={item}>{item}</div>)}
</div>
);
}Conventions: usePageTitle, DataState components, no inline fetch — use the typed wrappers for real endpoints (the example uses apiFetch only to illustrate the shape; add a wrapper in src/lib/api/ instead).
2. Add a typed wrapper (when the page calls a new endpoint)
In src/lib/api/<domain>.ts, add a typed function mirroring the endpoint contract, and the response type in src/lib/api/types.ts if it is new. Follow the existing wrappers (reports.ts, admin.ts, punishments.ts).
3. Register the route
In src/App.tsx:
tsx
import MyPage from "@/pages/MyPage";
// inside <Route element={<AppLayout />}>:
<Route path="/my-page" element={<MyPage />} />Placement matters: AppLayout provides the sidebar/topbar, so the route must be a child of the AppLayout route. Add the navigation entry in components/layout/AppLayout.tsx with the required permission (or adminOnly for admin pages) so the link renders only for capable agents.
4. Gate it
- Add the capability to
CLEARANCE_POLICYinauthz.pyif the page's data requires a new permission; otherwise reuse an existing one. - Mirror the gate in the API route (authorization is server-side; the nav flag is advisory).
- If the page is admin-only, gate with
canAdmin("admin_panel")in the nav andrequire_admin(...)in the API.
5. Verify
bash
cd ecc-dps-dashboard
npm run build # tsc -b && vite build (typecheck included)
npm run lint # ESLint (note: the repo has pre-existing lint debt)Manual check against the demo environment: navigate, confirm loading/error states, permission filtering, and mobile layout.
Checklist
- [ ] Page uses
usePageTitle, DataState, and design tokens - [ ] Data access goes through
src/lib/api/*wrappers - [ ] Route registered under
AppLayout; nav entry permission-gated - [ ] Server route enforces the same authorization independently
- [ ] Media/URL values sanitized through
urlSafety.ts - [ ] Typecheck + build pass