The repo had no agent-facing project guidance: AGENTS.md contained only the generated TanStack Intent skill pointers, and README.md described a stack this app does not use. - CLAUDE.md (new): API client contract (envelope, ApiError, single-flight 401 refresh, the FormData Content-Type trap), the PERM catalog mirroring the backend permission classes, the route + NAV_GROUPS + PERM triad that must be edited together to add a page, and the useListPage/DataTable pattern. - AGENTS.md: gotchas above the generated block, which is left untouched — global query retry policy, can() vs useCan() reactivity, the gig-admin-auth persist key, and that jsdom is opted into per file via a `// @vitest-environment jsdom` pragma since there is no vitest config. - README.md: @tanstack/react-start is not a dependency (this is a plain Vite SPA, no SSR); cf:deploy no longer copies a shell to index.html; the feature list was a milestone-1 snapshot that omitted five shipped nav groups and still called moderation reports a future milestone. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013MADG54Y51wZkU3ViRjeSi
73 lines
6.7 KiB
Markdown
73 lines
6.7 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
Standalone admin dashboard for the **gig-platform** Laravel backend, consuming `/api/admin/v1/*`. React 19, TanStack Router (+ Query + Table), shadcn/ui on Radix, Tailwind v4, TypeScript, Vite. It is a **plain client-rendered SPA** — `index.html` → `main.tsx` → `RouterProvider`; `@tanstack/react-start` is not a dependency, so there is no SSR and no server functions. Package manager is **pnpm**. UI copy is hardcoded 简体中文 — `i18next`/`react-i18next` are in `package.json` but nothing imports them yet, so don't reach for translation hooks.
|
|
|
|
The backend repo is expected as a sibling checkout at `../gig-platform` (see `pnpm gen:api`).
|
|
|
|
## Commands
|
|
|
|
| Command | Purpose |
|
|
| --- | --- |
|
|
| `pnpm dev` | dev server on :3000 |
|
|
| `pnpm build` / `pnpm preview` | production build / preview |
|
|
| `pnpm typecheck` | `tsc --noEmit` |
|
|
| `pnpm test` | vitest (all) |
|
|
| `pnpm test src/api/client.test.ts` | single test file |
|
|
| `pnpm generate-routes` | `tsr generate` — regenerate `routeTree.gen.ts` (the Vite plugin also does this on dev/build) |
|
|
| `pnpm gen:api` | regenerate `src/api/types.gen.ts` from `../gig-platform/storage/app/api-docs/admin/openapi.yaml` |
|
|
| `pnpm cf:deploy` | build + `wrangler deploy` |
|
|
|
|
There is no lint script and no ESLint config — `typecheck` + `test` are the gate (and what CI runs, in `.github/workflows/ci.yml` and the mirrored `.gitea/workflows/ci.yml`).
|
|
|
|
`pnpm gen:api` requires the backend to have generated its docs first: run `ddev artisan api-docs:generate` in `../gig-platform`.
|
|
|
|
## Architecture
|
|
|
|
### API layer (`src/api/`)
|
|
|
|
`client.ts` is the only place that talks to the network. Everything else calls the thin `api.{get,post,put,patch,delete,upload,text}` helpers.
|
|
|
|
- **Envelope**: the backend returns `{ success, data, message }`; paginated lists add `pagination` (`current_page`, `next_page`, `page_size`, `prev_page`, `total_count`, `total_pages`). Call sites type responses as `ApiResponse<T>` / `PaginatedResponse<T>` and read `.data`.
|
|
- **Errors**: non-2xx throws `ApiError(status, code, message, errors?)` built from the backend's `{ success: false, code, message, data.errors }` payload. Catch `ApiError`, don't inspect raw responses.
|
|
- **Auth**: Bearer token from the zustand store; on a 401 the client does a **single-flight** refresh (`POST /api/v1/auth/refresh-token` with `client_machine_name=admin-api-client` — note this is the *public* `v1` prefix, not `admin/v1`) and retries the request once, then clears the session. Login and refresh pass `anonymous: true` to opt out of this.
|
|
- **Headers**: `X-Language-Locale: zh-CN` on every request. A `FormData` body deliberately gets **no** `Content-Type` — the browser must set it so the multipart boundary survives; setting it by hand makes the server see an empty upload (422 instead of 202). `fetch` gives no upload progress, so upload UIs stay indeterminate.
|
|
- **Endpoint modules**: one file per backend domain in `src/api/modules/` (`studio.ts`, `exc.ts`, `categories.ts`, …), each with a `const BASE = '/api/admin/v1/{module}'` and exported `fetchX` / mutation functions. Add new endpoints there, not inline in components.
|
|
- **Types**: `src/api/types.ts` is hand-written and authoritative. `types.gen.ts` is `@ts-nocheck` reference-only — scribe emits duplicate operationIds so the generated file doesn't typecheck.
|
|
|
|
### Auth & permissions (`src/auth/`)
|
|
|
|
- `store.ts` — zustand + persist (localStorage) holding tokens, user, roles, permissions. `can(key)` / `useCan(key)` return true for anyone holding the key **or** the `super-admin` role.
|
|
- `permissions.ts` — the `PERM` catalog mirroring the backend's `Modules/{Module}/app/Permissions/{Module}Permissions.php`. Keys are `admin:{area}:{action}`; a few operator-only ones use the `auth:` prefix (role management, db-studio, robot tokens) precisely because the `admin` role is synced to `admin:%` only. **When the backend adds a permission, add it to `PERM` here** — routes and nav reference the constants, never raw strings.
|
|
- `requirePermission(PERM.X)` is the route `beforeLoad` guard. It redirects to the first accessible nav route only once a session is loaded; an empty store falls through and the API's 403 is the backstop.
|
|
- `GET /auth/me` refreshes `meta.roles` / `meta.permissions` on mount and window focus (`src/routes/_authed.tsx`); an account with neither is logged out with a toast.
|
|
|
|
### Routing (`src/routes/`)
|
|
|
|
File-based TanStack Router with `autoCodeSplitting`. `routeTree.gen.ts` is generated — never edit it. `_authed.tsx` is the authenticated shell (sidebar + header + `<Outlet/>`); everything under `_authed/` is behind the session check. Public routes: `login.tsx`, `api-docs.tsx`.
|
|
|
|
Adding a page means three coordinated edits: the route file (with its `requirePermission` guard), an entry in `NAV_GROUPS` in `src/components/layout/nav.ts` (with `permission`, so the sidebar self-filters), and the `PERM` key if it's new.
|
|
|
|
### List pages
|
|
|
|
`useListPage(key, fetcher, filters)` in `src/lib/list-page.ts` is the standard pattern: it owns `page` / `page_size` state, keys the query on the serialized filters, uses `keepPreviousData`, and snaps back to page 1 when filters change. Pair it with `<DataTable>` (`src/components/data-table/`) and `<PageHeader>`. Follow this rather than hand-rolling `useQuery` for a table.
|
|
|
|
### Layout of the rest
|
|
|
|
- `src/components/ui/` — shadcn/ui primitives (`components.json` config); regenerate rather than hand-editing where possible
|
|
- `src/components/` — shared app-level pieces (`data-table/`, `page-header`, `status-pill`, `confirm-dialog`, `show-once-token-dialog` for one-time token reveals)
|
|
- `src/features/{domain}/` — domain-specific composed UI (detail sheets, edit sheets) pulled into route files
|
|
- Path aliases: both `@/*` and `#/*` map to `./src/*`; existing code consistently uses `@/`
|
|
|
|
## Deployment
|
|
|
|
Client-rendered Vite SPA served as static assets by a Cloudflare Worker (`wrangler.jsonc`, `not_found_handling: single-page-application`, so `dist/index.html` answers every client route). `cf:deploy` is just `vite build && wrangler deploy`. `VITE_API_BASE_URL` is **baked in at build time** — it is a CI variable, not a runtime setting. Pushes to `main` typecheck + test + build + deploy; `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` are repo secrets.
|
|
|
|
## Notes
|
|
|
|
- `AGENTS.md` holds gotchas and non-obvious conventions that complement this file; everything below its `intent-skills` markers is generated by TanStack Intent — edit only above them.
|
|
- Local backend defaults to `https://gig-platform.ddev.site` (`.env.example`). Seed a local admin from the backend repo with `ddev exec php artisan tinker storage/app/e2e-admin-seed.php` → `[email protected] / Admin@12345`.
|