CtrlK
BlogDocsLog inGet started
Tessl Logo

add-react-page

Add a list+create page to a React app (clients/admin or clients/dashboard) — API module, page, lazy route, (admin) permission gate, Playwright test. Use when adding any frontend screen. See .agents/rules/frontend/.

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Add React Page

The frontend slice. Read .agents/rules/frontend/shared.md plus the app file (frontend/admin.md / frontend/dashboard.md) — the two apps deliberately diverge:

admin (operator)dashboard (tenant)
Query paramsPascalCase (PageNumber, Search)camelCase (pageNumber, search)
PagedResponse<T>import from @/lib/api-typesre-declare inline in the api module
Path constantconst BASE = "/api/v1/..."inline the full path per call
Formsreact-hook-form + zodhand-rolled controlled inputs (no RHF/zod)
List + createseparate routed pages (list.tsx, create.tsx)one file with <Dialog> editors
Route wrapper<RouteGuard perms={[…]}>withSuspense(<X/>) (no permission gate)
Permissionsmirror in src/lib/permissions.tsfetched from GET /identity/permissions (not JWT); nav gating via perm/anyPerm in nav-data.ts; no route guard — server 403 backstops

Shared everywhere: types are hand-written (no codegen); apiFetch<T> from @/lib/api-client; cn() from @/lib/cn; env.apiBase from runtime /config.json; CVA components/ui + components/list primitives; Tailwind v4 CSS-first (tokens in src/styles/globals.css); toast from sonner; pages are named exports; placeholderData: keepPreviousData (v5).

Step 1 — API module (src/api/{resource}.ts)

Hand-write the DTO/param/input types and thin apiFetch functions.

// admin
import { apiFetch } from "@/lib/api-client";
import type { PagedResponse } from "@/lib/api-types";
const BASE = "/api/v1/{module}/{resources}";

export type {Resource}Dto = { id: string; name: string; /* … */ };

export async function search{Resources}(p: { pageNumber?: number; search?: string } = {}) {
  const q = new URLSearchParams();
  q.set("PageNumber", String(p.pageNumber ?? 1));
  q.set("PageSize", "10");
  if (p.search?.trim()) q.set("Search", p.search.trim());
  return apiFetch<PagedResponse<{Resource}Dto>>(`${BASE}/search?${q}`);
}
export async function create{Resource}(input: Create{Resource}Input) {
  return apiFetch<{ id: string }>(BASE, { method: "POST", body: JSON.stringify(input) });
}

(dashboard: inline type PagedResponse<T> = …, inline the path, camelCase params, mutations often return Promise<string>.)

Step 2 — Page (src/pages/{area}/...tsx, named export)

export function {Resource}ListPage() {
  const [search, setSearch] = useState("");           // debounce → reset page to 1 on change
  const [pageNumber, setPage] = useState(1);
  const query = useQuery({
    queryKey: ["{resources}", { pageNumber, search }],   // hierarchical; params object last
    queryFn: () => search{Resources}({ pageNumber, search: search || undefined }),
    placeholderData: keepPreviousData,
  });
  // render with components/ui/* + components/list/* (admin: PageHeader/Field…; dashboard: Entity* family)
}

Step 3 — Mutation (race-safe mutate(arg))

Pass per-call data through mutate(arg); read it from the callback variables — never from a closed-over render variable.

const qc = useQueryClient();
const createMut = useMutation({
  mutationFn: (input: Create{Resource}Input) => create{Resource}(input),
  onSuccess: () => { toast.success("Created"); qc.invalidateQueries({ queryKey: ["{resources}"] }); },
  onError: (e) => toast.error(e instanceof ApiRequestError ? e.message : "Failed"),
});
// admin: const form = useForm({ resolver: zodResolver(schema) });  form.handleSubmit(v => createMut.mutate(v))
// dashboard: controlled useState fields; onSubmit(e){ e.preventDefault(); createMut.mutate(payload); }

If you need to track the in-flight item (e.g. a per-row busy state), use onMutate: (arg) => setBusyId(arg) reading the mutate(arg) value (pattern: admin/src/pages/settings/sessions.tsx).

Step 4 — Register the route (routes.tsx)

const {Resource}ListPage = lazyNamed(() => import("@/pages/{area}/list"), "{Resource}ListPage");
// admin — under AppShell.children, gated:
{ path: "{resources}", element: <RouteGuard perms={[{Module}Permissions.{Resources}.View]}><{Resource}ListPage /></RouteGuard> },
// dashboard — under AppShell.children, suspense only:
{ path: "{area}/{resources}", element: withSuspense(<{Resource}ListPage />) },

Step 5 — (admin only) mirror the permission

Add the constant to src/lib/permissions.ts ({Module}Permissions.{Resources}.View = "Permissions.{Resources}.View"), and a PERMISSION_CATALOG entry if it belongs in the Role editor. See add-permission.

Step 6 — Playwright test (tests/{area}/{resource}.spec.ts)

test.beforeEach(async ({ page }) => {
  // admin: seedAuthedSession(page, { ...TEST_USER, permissions: [...ADMIN_PERMS] }); await installAdminShellMocks(page);
  // dashboard: await seedAuthedSession(page, TEST_USER); await installShellMocks(page);
  await mockJsonResponse(page, "**/api/v1/{module}/{resources}**", paged([SAMPLE]));   // page mocks AFTER shell mocks
});

Use mockProblemDetails(...) for error states. Dashboard: scope row assertions with .last() / dialog scoping (lists render mobile + desktop copies → strict-mode double match).

Step 7 — Verify

cd clients/{app} && npm run lint && npm run test:e2e

Checklist

  • API module: hand-written types, apiFetch, correct param casing per app (Pascal=admin, camel=dashboard)
  • Page is a named export; useQuery key hierarchical + placeholderData: keepPreviousData
  • Mutation passes data via mutate(arg), invalidates in onSuccess
  • Route via lazyNamed; admin wraps in <RouteGuard perms>, dashboard in withSuspense
  • (admin) permission mirrored in lib/permissions.ts
  • Playwright test: seed + shell mocks + page mocks; lint + test:e2e green
Repository
fullstackhero/dotnet-starter-kit
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.