Skip to main content

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

소스 정보

저장소
fullstackhero/dotnet-starter-kit
최근 소스 활동
2026년 7월 13일 13:28
감지된 SKILL.md 언어
영어
스타
6,818
포크
2,003

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
add-react-page
description
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/.
argument-hint
[admin|dashboard] [Area] [Resource]
# 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 params | PascalCase (`PageNumber`, `Search`) | camelCase (`pageNumber`, `search`) | | `PagedResponse<T>` | import from `@/lib/api-types` | re-declare inline in the api module | | Path constant | `const BASE = "/api/v1/..."` | inline the full path per call | | Forms | **react-hook-form + zod** | **hand-rolled** controlled inputs (no RHF/zod) | | List + create | separate routed pages (`list.tsx`, `create.tsx`) | one file with `<Dialog>` editors | | Route wrapper | `<RouteGuard perms={[…]}>` | `withSuspense(<X/>)` (no permission gate) | | Permissions | mirror in `src/lib/permissions.ts` | fetched 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. ```ts // 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) ```tsx 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. ```tsx 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`) ```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`) ```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 ```bash 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
GitHub에서 보기