Skip to content

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_POLICY in authz.py if 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 and require_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