From 924bfdd8f6a1c1a723e5fd073955b8317682a234 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 13 Jul 2026 17:47:05 -0400 Subject: [PATCH] feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep) --- web/package-lock.json | 30 +++++++++++++++++++++++++ web/package.json | 2 ++ web/src/domain/README.md | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 web/src/domain/README.md diff --git a/web/package-lock.json b/web/package-lock.json index 5d716c3..686754c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "@fontsource/dm-sans": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/montserrat": "^5.2.8", + "@hookform/resolvers": "^5.4.0", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.1.1", "@proposal-system/api-contracts": "file:../shared/api-contracts", @@ -22,6 +23,7 @@ "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-hook-form": "^7.81.0", "react-redux": "^9.2.0", "react-router-dom": "^7.18.1", "react-toastify": "^11.0.5", @@ -625,6 +627,18 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@hookform/resolvers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", + "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3309,6 +3323,22 @@ "react": "^19.2.7" } }, + "node_modules/react-hook-form": { + "version": "7.81.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz", + "integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", diff --git a/web/package.json b/web/package.json index 321cf87..8a4317c 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "@fontsource/dm-sans": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/montserrat": "^5.2.8", + "@hookform/resolvers": "^5.4.0", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.1.1", "@proposal-system/api-contracts": "file:../shared/api-contracts", @@ -25,6 +26,7 @@ "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-hook-form": "^7.81.0", "react-redux": "^9.2.0", "react-router-dom": "^7.18.1", "react-toastify": "^11.0.5", diff --git a/web/src/domain/README.md b/web/src/domain/README.md new file mode 100644 index 0000000..79266db --- /dev/null +++ b/web/src/domain/README.md @@ -0,0 +1,47 @@ +# Frontend domain layer — conventions + +SHOC-alignment Phase 4 (mirrors shoc-frontend-new dev's `src/domain//` layering). +Every agent/contributor working in this tree builds to THIS spec. + +## Structure + +``` +src/domain// +├── api.ts # HTTP calls only: axios via lib/api/client, path constants, no hooks +├── types.ts # re-exports from @proposal-system/api-contracts + view-only types +├── schemas.ts # re-exports from @proposal-system/api-contracts/schemas + form schemas +└── use-cases.ts # TanStack Query hooks + this domain's query keys (the ONLY public surface) +``` + +Domains: `proposals`, `lineItems`, `customers`, `pricingLibrary`, `admin`, `sites`. +(`auth` arrives with the separate auth-storage PR — do NOT create it here.) + +## Rules + +1. **Pages import ONLY from `domain//use-cases` (and `types`)** — never from + `lib/api/*`, never `apiClient` directly, never `useQuery`/`useMutation` inline + in a page. Pages are thin callers. +2. **Query keys live in the domain**: each `use-cases.ts` exports + `export const Keys = { all: [''] as const, detail: (id: string) => ['', id] as const, ... }` + (TanStack hierarchical-key convention). `src/constants/queryKeys.ts` is + deleted at integration — do not add imports of it. +3. **Invalidation** uses the domain key objects across domains where needed + (e.g. approving a proposal invalidates `proposalsKeys.all`). +4. **Forms**: react-hook-form + `zodResolver` (from `@hookform/resolvers/zod`). + Form schemas live in the domain's `schemas.ts`, derived from the shared + contract schemas (`@proposal-system/api-contracts/schemas`) via + `.pick/.extend/.omit` — never hand-written duplicates. Form field state maps + to the request type at submit (a `toCreateRequest(formValues)` mapper in + `schemas.ts` when non-trivial). +5. **`lib/api/client.ts` stays** — the single axios instance (interceptors, + 401 handling). Domain `api.ts` files import it. The old `lib/api/.ts` + modules are deleted once no page imports them (integration step) — + `lib/api/auth.ts` stays until the auth PR. +6. **Mutations**: toast on error stays in the hook (matching current UX), + success invalidation in the hook; page-specific side effects (navigate, + dialog close) via the mutation's callbacks at the call site. +7. **No new state managers, no context** — server state = TanStack Query, + existing Redux auth/ui slices untouched (auth refactor is a separate PR). +8. **Styling**: tokens only (`var(--...)`, theme) — no hardcoded hexes. +9. Verify before returning/committing: `npx tsc --noEmit`, `npm test -- --run`, + `npm run build` — all green, no skipped tests, no `@ts-ignore`.