Files
gig-admin/CLAUDE.md
T
kongkxandClaude Opus 5 5b3f5f7197 docs: add CLAUDE.md, expand AGENTS.md, correct stale README
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
2026-09-06 21:16:29 +08:00

6.7 KiB

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 SPAindex.htmlmain.tsxRouterProvider; @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.