create-win-project 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/ci/expo.yml +28 -0
- package/ci/laravel.yml +49 -0
- package/ci/nextjs.yml +29 -0
- package/ci/springboot.yml +33 -0
- package/ci/vite.yml +36 -0
- package/compatibility/profiles.json +237 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/CONTENT_MODEL.md +36 -0
- package/docs/DEPENDENCY_MAINTENANCE.md +31 -0
- package/index.js +431 -0
- package/lib/application-shapes.js +95 -0
- package/lib/banner.js +45 -0
- package/lib/catalog.js +343 -0
- package/lib/compatibility.js +116 -0
- package/lib/constants.js +3 -0
- package/lib/doctor.js +45 -0
- package/lib/files.js +158 -0
- package/lib/generator.js +366 -0
- package/lib/interview.js +77 -0
- package/lib/laravel-scaffold.js +3 -0
- package/lib/playbooks.js +172 -0
- package/lib/project-location.js +18 -0
- package/lib/scaffold.js +467 -0
- package/lib/stacks/context.js +27 -0
- package/lib/stacks/contract.js +142 -0
- package/lib/stacks/index.js +8 -0
- package/lib/stacks/laravel/architecture.js +27 -0
- package/lib/stacks/laravel/auth/oidc.js +13 -0
- package/lib/stacks/laravel/auth/public.js +28 -0
- package/lib/stacks/laravel/auth/sanctum.js +14 -0
- package/lib/stacks/laravel/auth/session.js +17 -0
- package/lib/stacks/laravel/composer.js +50 -0
- package/lib/stacks/laravel/generate.js +362 -0
- package/lib/stacks/laravel/index.js +18 -0
- package/lib/stacks/laravel/ui/blade.js +15 -0
- package/lib/stacks/laravel/ui/index.js +29 -0
- package/lib/stacks/laravel/ui/inertia-react.js +35 -0
- package/lib/stacks/laravel/ui/livewire.js +39 -0
- package/lib/stacks/laravel/ui/shared.js +14 -0
- package/lib/stacks/registry.js +42 -0
- package/lib/stacks/shared/contributions.js +25 -0
- package/lib/template.js +62 -0
- package/package.json +49 -0
- package/playbooks/INDEX.md +58 -0
- package/playbooks/capabilities/auth/oidc-resource-server.md +15 -0
- package/playbooks/capabilities/auth/spring-session.md +15 -0
- package/playbooks/capabilities/ci/github-actions.manifest.json +10 -0
- package/playbooks/capabilities/ci/github-actions.md +61 -0
- package/playbooks/capabilities/docker/docker.manifest.json +11 -0
- package/playbooks/capabilities/docker/overview.md +55 -0
- package/playbooks/capabilities/flyway/environments.md +5 -0
- package/playbooks/capabilities/flyway/flyway.manifest.json +12 -0
- package/playbooks/capabilities/flyway/migrations.md +7 -0
- package/playbooks/capabilities/flyway/testing.md +5 -0
- package/playbooks/capabilities/laravel/database.md +8 -0
- package/playbooks/capabilities/laravel/migrations.md +15 -0
- package/playbooks/capabilities/laravel/observability.md +8 -0
- package/playbooks/capabilities/laravel/oidc-resource-server.md +10 -0
- package/playbooks/capabilities/laravel/queues.md +8 -0
- package/playbooks/capabilities/laravel/sanctum-spa.md +8 -0
- package/playbooks/capabilities/laravel/scheduler.md +7 -0
- package/playbooks/capabilities/laravel/session-auth.md +8 -0
- package/playbooks/capabilities/laravel/storage-uploads.md +7 -0
- package/playbooks/capabilities/postgresql/architecture.md +5 -0
- package/playbooks/capabilities/postgresql/migrations.md +5 -0
- package/playbooks/capabilities/postgresql/postgresql.manifest.json +15 -0
- package/playbooks/capabilities/postgresql/schema-design.md +7 -0
- package/playbooks/capabilities/postgresql/security.md +5 -0
- package/playbooks/capabilities/postgresql/testing.md +5 -0
- package/playbooks/capabilities/prisma/architecture.md +5 -0
- package/playbooks/capabilities/prisma/migrations.md +11 -0
- package/playbooks/capabilities/prisma/prisma.manifest.json +14 -0
- package/playbooks/capabilities/prisma/runtime.md +5 -0
- package/playbooks/capabilities/prisma/schema.md +5 -0
- package/playbooks/capabilities/prisma/testing.md +5 -0
- package/playbooks/capabilities/supabase/architecture.md +5 -0
- package/playbooks/capabilities/supabase/authentication.md +5 -0
- package/playbooks/capabilities/supabase/expo.md +16 -0
- package/playbooks/capabilities/supabase/migrations.md +7 -0
- package/playbooks/capabilities/supabase/nextjs.md +24 -0
- package/playbooks/capabilities/supabase/rls.md +7 -0
- package/playbooks/capabilities/supabase/supabase.manifest.json +32 -0
- package/playbooks/capabilities/supabase/testing.md +5 -0
- package/playbooks/capabilities/supabase/vite.md +14 -0
- package/playbooks/concerns/axios.md +123 -0
- package/playbooks/concerns/next-safe-action.md +66 -0
- package/playbooks/concerns/next-themes.md +87 -0
- package/playbooks/concerns/nuqs.md +59 -0
- package/playbooks/concerns/t3-env.md +48 -0
- package/playbooks/concerns/tanstack-query.md +145 -0
- package/playbooks/concerns/zod.md +174 -0
- package/playbooks/concerns/zustand.md +93 -0
- package/playbooks/devops/makefile.manifest.json +10 -0
- package/playbooks/devops/makefile.md +556 -0
- package/playbooks/devops/pr-template.manifest.json +10 -0
- package/playbooks/devops/pr-template.md +106 -0
- package/playbooks/platform/laravel-ui/blade/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/blade/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/blade/security.md +5 -0
- package/playbooks/platform/laravel-ui/blade/structure.md +5 -0
- package/playbooks/platform/laravel-ui/blade/testing.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/security.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/structure.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/testing.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/security.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/structure.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/testing.md +5 -0
- package/playbooks/platform/mobile.manifest.json +17 -0
- package/playbooks/platform/mobile.md +15 -0
- package/playbooks/platform/web.manifest.json +17 -0
- package/playbooks/platform/web.md +17 -0
- package/playbooks/stack/expo/architecture.md +27 -0
- package/playbooks/stack/expo/runtime.md +14 -0
- package/playbooks/stack/expo/security.md +18 -0
- package/playbooks/stack/expo/structure.md +26 -0
- package/playbooks/stack/expo/testing.md +13 -0
- package/playbooks/stack/laravel/architecture.md +34 -0
- package/playbooks/stack/laravel/runtime.md +27 -0
- package/playbooks/stack/laravel/security.md +23 -0
- package/playbooks/stack/laravel/structure.md +29 -0
- package/playbooks/stack/laravel/testing.md +22 -0
- package/playbooks/stack/laravel-ui.manifest.json +42 -0
- package/playbooks/stack/laravel.manifest.json +35 -0
- package/playbooks/stack/nextjs/architecture.md +62 -0
- package/playbooks/stack/nextjs/runtime.md +31 -0
- package/playbooks/stack/nextjs/security.md +32 -0
- package/playbooks/stack/nextjs/structure.md +50 -0
- package/playbooks/stack/nextjs/testing.md +20 -0
- package/playbooks/stack/nextjs.manifest.json +49 -0
- package/playbooks/stack/no-frontend.manifest.json +25 -0
- package/playbooks/stack/none.manifest.json +21 -0
- package/playbooks/stack/react-native.manifest.json +49 -0
- package/playbooks/stack/react-vite/architecture.md +33 -0
- package/playbooks/stack/react-vite/runtime.md +13 -0
- package/playbooks/stack/react-vite/security.md +16 -0
- package/playbooks/stack/react-vite/structure.md +23 -0
- package/playbooks/stack/react-vite/testing.md +12 -0
- package/playbooks/stack/react-vite.manifest.json +46 -0
- package/playbooks/stack/springboot/architecture.md +40 -0
- package/playbooks/stack/springboot/runtime.md +28 -0
- package/playbooks/stack/springboot/security.md +28 -0
- package/playbooks/stack/springboot/structure.md +30 -0
- package/playbooks/stack/springboot/testing.md +22 -0
- package/playbooks/stack/springboot.manifest.json +38 -0
- package/playbooks/styling/css-modules-extensions.md +267 -0
- package/playbooks/styling/css-modules.manifest.json +11 -0
- package/playbooks/styling/native-styles.manifest.json +9 -0
- package/playbooks/styling/native-styles.md +29 -0
- package/playbooks/styling/tailwind-extensions.md +182 -0
- package/playbooks/styling/tailwind.manifest.json +11 -0
- package/playbooks/universal/accessibility.manifest.json +11 -0
- package/playbooks/universal/accessibility.md +45 -0
- package/playbooks/universal/coding-rules.manifest.json +12 -0
- package/playbooks/universal/coding-rules.md +281 -0
- package/playbooks/universal/error-handling.manifest.json +10 -0
- package/playbooks/universal/error-handling.md +21 -0
- package/playbooks/universal/git-conventions.manifest.json +10 -0
- package/playbooks/universal/git-conventions.md +186 -0
- package/playbooks/universal/observability.manifest.json +10 -0
- package/playbooks/universal/observability.md +29 -0
- package/playbooks/universal/security.manifest.json +11 -0
- package/playbooks/universal/security.md +30 -0
- package/playbooks/universal/typescript.manifest.json +11 -0
- package/playbooks/universal/typescript.md +272 -0
- package/scripts/compatibility-matrix.mjs +48 -0
- package/scripts/validate-content.mjs +76 -0
- package/scripts/verify-generated.mjs +153 -0
- package/templates/agents/nextjs.md +47 -0
- package/templates/agents/react-native.md +48 -0
- package/templates/agents/react-vite.md +48 -0
- package/templates/docker/compose/postgres.yml +29 -0
- package/templates/docker/compose/springboot.yml +69 -0
- package/templates/docker/compose/supabase.yml +18 -0
- package/templates/docker/compose-prod/springboot.yml +64 -0
- package/templates/docker/dockerfile/laravel-inertia.dev.dockerfile +21 -0
- package/templates/docker/dockerfile/laravel.dev.dockerfile +12 -0
- package/templates/docker/dockerfile/laravel.prod.dockerfile +13 -0
- package/templates/docker/dockerfile/nextjs.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/nextjs.prod.dockerfile +18 -0
- package/templates/docker/dockerfile/springboot.dev.dockerfile +5 -0
- package/templates/docker/dockerfile/springboot.prod.dockerfile +10 -0
- package/templates/docker/dockerfile/vite.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/vite.prod.dockerfile +12 -0
- package/templates/gitignore/base.gitignore +32 -0
- package/templates/gitignore/nextjs.gitignore +40 -0
- package/templates/gitignore/react-native.gitignore +43 -0
- package/templates/gitignore/react-vite.gitignore +39 -0
- package/templates/makefile/frontend.mk +31 -0
- package/templates/makefile/laravel.mk +47 -0
- package/templates/makefile/postgres.mk +40 -0
- package/templates/makefile/springboot.mk +125 -0
- package/templates/makefile/supabase.mk +42 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# t3-env for Next.js
|
|
2
|
+
|
|
3
|
+
> Optional: adopt this when a Next.js project needs one typed, startup/build-time environment boundary. Install `@t3-oss/env-nextjs` and `zod` in the same change.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Declare only values the selected stack actually consumes:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createEnv } from '@t3-oss/env-nextjs'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
|
|
13
|
+
export const env = createEnv({
|
|
14
|
+
server: {
|
|
15
|
+
DATABASE_URL: z.string().min(1).optional(),
|
|
16
|
+
},
|
|
17
|
+
client: {
|
|
18
|
+
NEXT_PUBLIC_API_URL: z.string().url().optional(),
|
|
19
|
+
NEXT_PUBLIC_SUPABASE_URL: z.string().url().optional(),
|
|
20
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: z.string().min(1).optional(),
|
|
21
|
+
},
|
|
22
|
+
runtimeEnv: {
|
|
23
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
24
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
25
|
+
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
26
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Make a value required only for a stack that cannot run without it. Client-prefixed values are bundled and are never secrets.
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- Server-only values belong in `server`; client-visible values require `NEXT_PUBLIC_` and belong in `client`.
|
|
36
|
+
- Add each new value to the schema, runtime mapping, `.env.example`, deployment configuration, and generated environment documentation.
|
|
37
|
+
- Keep secrets out of logs, client schemas, public prefixes, and committed environment files.
|
|
38
|
+
- Do not copy speculative JWT, administrator, or provider secrets into every project.
|
|
39
|
+
|
|
40
|
+
## Agent Quick Reference
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
New value? → identify its runtime owner and whether the browser needs it
|
|
44
|
+
Server secret? → server schema, no public prefix
|
|
45
|
+
Browser value? → client schema with NEXT_PUBLIC_; treat as public
|
|
46
|
+
Required value? → fail validation before serving traffic
|
|
47
|
+
Unused value? → remove it from schema, examples, CI, and deployment config
|
|
48
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# TanStack Query — Server State
|
|
2
|
+
|
|
3
|
+
> **When to use:** Any data fetched from a server or API that needs caching, background refetch,
|
|
4
|
+
> loading/error states, or invalidation after mutations.
|
|
5
|
+
> Required for React Native. Optional for web stacks.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 1. The Mental Model
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
TanStack Query owns: fetch, cache, loading, error, background refetch, invalidation
|
|
13
|
+
Your code owns: queryFn (calls a service), queryKey (cache identity)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Never manage loading/error state manually when TanStack Query can do it.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# 2. Query Key Convention
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
Resource list: ['products']
|
|
24
|
+
Resource by ID: ['products', id]
|
|
25
|
+
Resource with filters: ['products', { category, page }]
|
|
26
|
+
Nested resource: ['products', productId, 'reviews']
|
|
27
|
+
Non-resource (dashboard): ['dashboard', 'stats']
|
|
28
|
+
Auth session: ['auth', 'session']
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Keys are the cache identity. Same key = same cache entry. Be consistent.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# 3. Read Hook
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// features/products/hooks/useProducts.ts
|
|
39
|
+
import { useQuery } from '@tanstack/react-query'
|
|
40
|
+
import { productService } from '../services/productService'
|
|
41
|
+
|
|
42
|
+
export const productKeys = {
|
|
43
|
+
all: ['products'] as const,
|
|
44
|
+
byId: (id: string) => ['products', id] as const,
|
|
45
|
+
filtered: (filters: ProductFilters) => ['products', 'filtered', filters] as const,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useProducts(filters?: ProductFilters) {
|
|
49
|
+
return useQuery({
|
|
50
|
+
queryKey: filters ? productKeys.filtered(filters) : productKeys.all,
|
|
51
|
+
queryFn: () => productService.getAll(filters),
|
|
52
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useProduct(id: string) {
|
|
57
|
+
return useQuery({
|
|
58
|
+
queryKey: productKeys.byId(id),
|
|
59
|
+
queryFn: () => productService.getById(id),
|
|
60
|
+
enabled: !!id,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# 4. Mutation Hook
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// features/products/hooks/useCreateProduct.ts
|
|
71
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
72
|
+
|
|
73
|
+
export function useCreateProduct() {
|
|
74
|
+
const queryClient = useQueryClient()
|
|
75
|
+
|
|
76
|
+
return useMutation({
|
|
77
|
+
mutationFn: (input: CreateProductInput) => productService.create(input),
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: productKeys.all })
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# 5. QueryClient Setup
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// lib/queryClient.ts (web: src/lib/queryClient.ts)
|
|
91
|
+
// lib/queryClient.ts (RN: lib/queryClient.ts)
|
|
92
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
93
|
+
|
|
94
|
+
export const queryClient = new QueryClient({
|
|
95
|
+
defaultOptions: {
|
|
96
|
+
queries: {
|
|
97
|
+
staleTime: 1000 * 60 * 5, // 5min — most data
|
|
98
|
+
retry: 1,
|
|
99
|
+
refetchOnWindowFocus: false, // web only; ignored on RN
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Stale time guide:
|
|
106
|
+
```text
|
|
107
|
+
Real-time (notifications, live status) → staleTime: 0
|
|
108
|
+
Normal data (products, users, orders) → staleTime: 1000 * 60 * 5 (5min)
|
|
109
|
+
Slow-changing (profile, settings) → staleTime: 1000 * 60 * 30 (30min)
|
|
110
|
+
Static (categories, countries) → staleTime: Infinity
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
# 6. Rules
|
|
116
|
+
|
|
117
|
+
- Hooks call services. Services call the API client. Never call `fetch` or `axios` directly in `queryFn`.
|
|
118
|
+
- Always define query keys in a `[resource]Keys` object — never inline magic strings.
|
|
119
|
+
- Always invalidate related query keys after a mutation succeeds.
|
|
120
|
+
- `enabled: !!id` — guard queries that depend on a param that may not exist yet.
|
|
121
|
+
- Never put query results in Zustand — TanStack Query IS your cache.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# 7. Platform Notes
|
|
126
|
+
|
|
127
|
+
> **React Native** — works identically. `refetchOnWindowFocus` is ignored (no window).
|
|
128
|
+
> Wrap the root component in `<QueryClientProvider client={queryClient}>` in your root layout.
|
|
129
|
+
|
|
130
|
+
> **Next.js** — Server Components do NOT use TanStack Query.
|
|
131
|
+
> TanStack Query is for Client Components that need cached/refetched server data.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
# 8. Agent Quick Reference
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
New data fetch? → useQuery hook → features/[name]/hooks/
|
|
139
|
+
New mutation? → useMutation hook → features/[name]/hooks/
|
|
140
|
+
→ invalidateQueries on success
|
|
141
|
+
QueryKey for a list? → ['resource']
|
|
142
|
+
QueryKey for a single item? → ['resource', id]
|
|
143
|
+
Query depends on a param? → enabled: !!param
|
|
144
|
+
Server data in Zustand? → move to useQuery instead
|
|
145
|
+
```
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Zod + React Hook Form — Validation
|
|
2
|
+
|
|
3
|
+
> **When to use:** Any project with forms or untrusted input (API responses, URL params, env vars).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 1. Schema First, Always
|
|
8
|
+
|
|
9
|
+
Write the Zod schema before writing the form component or service. The schema is the source of truth.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// features/auth/schemas/login.schema.ts
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
export const loginSchema = z.object({
|
|
16
|
+
email: z.string().email('Invalid email address'),
|
|
17
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type LoginInput = z.infer<typeof loginSchema>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Never write a separate TypeScript type for form data — infer it from the schema.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# 2. Schema Placement
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
Feature-specific form? → features/[name]/schemas/[name].schema.ts
|
|
31
|
+
Shared across multiple features? → src/schemas/[name].schema.ts (web)
|
|
32
|
+
schemas/[name].schema.ts (RN)
|
|
33
|
+
API response shape? → features/[name]/types.ts (type only, no runtime parse needed)
|
|
34
|
+
OR parse with z.safeParse() at the API boundary if strict
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# 3. Web Form (React Hook Form)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// features/auth/components/LoginForm.tsx
|
|
43
|
+
'use client' // Next.js only — omit for React Vite
|
|
44
|
+
import { useForm } from 'react-hook-form'
|
|
45
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
46
|
+
import { loginSchema, type LoginInput } from '../schemas/login.schema'
|
|
47
|
+
|
|
48
|
+
export function LoginForm({ onSubmit }: { onSubmit: (data: LoginInput) => void }) {
|
|
49
|
+
const {
|
|
50
|
+
register,
|
|
51
|
+
handleSubmit,
|
|
52
|
+
formState: { errors, isSubmitting },
|
|
53
|
+
} = useForm<LoginInput>({ resolver: zodResolver(loginSchema) })
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
57
|
+
<input {...register('email')} placeholder="Email" />
|
|
58
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
59
|
+
|
|
60
|
+
<input type="password" {...register('password')} placeholder="Password" />
|
|
61
|
+
{errors.password && <span>{errors.password.message}</span>}
|
|
62
|
+
|
|
63
|
+
<button type="submit" disabled={isSubmitting}>
|
|
64
|
+
{isSubmitting ? 'Submitting...' : 'Login'}
|
|
65
|
+
</button>
|
|
66
|
+
</form>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
# 4. React Native Form (React Hook Form + Controller)
|
|
74
|
+
|
|
75
|
+
React Native has no native `<input>` — use `Controller` to bridge RHF with RN's `TextInput`.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// features/auth/components/LoginForm.tsx (React Native)
|
|
79
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
80
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
81
|
+
import { loginSchema, type LoginInput } from '../schemas/login.schema'
|
|
82
|
+
|
|
83
|
+
export function LoginForm({ onSubmit }: { onSubmit: (data: LoginInput) => void }) {
|
|
84
|
+
const {
|
|
85
|
+
control,
|
|
86
|
+
handleSubmit,
|
|
87
|
+
formState: { errors, isSubmitting },
|
|
88
|
+
} = useForm<LoginInput>({ resolver: zodResolver(loginSchema) })
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View className="gap-4">
|
|
92
|
+
<Controller
|
|
93
|
+
control={control}
|
|
94
|
+
name="email"
|
|
95
|
+
render={({ field }) => (
|
|
96
|
+
<TextInput
|
|
97
|
+
value={field.value}
|
|
98
|
+
onChangeText={field.onChange}
|
|
99
|
+
placeholder="Email"
|
|
100
|
+
keyboardType="email-address"
|
|
101
|
+
autoCapitalize="none"
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
/>
|
|
105
|
+
{errors.email && <Text>{errors.email.message}</Text>}
|
|
106
|
+
|
|
107
|
+
<Controller
|
|
108
|
+
control={control}
|
|
109
|
+
name="password"
|
|
110
|
+
render={({ field }) => (
|
|
111
|
+
<TextInput
|
|
112
|
+
value={field.value}
|
|
113
|
+
onChangeText={field.onChange}
|
|
114
|
+
placeholder="Password"
|
|
115
|
+
secureTextEntry
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
/>
|
|
119
|
+
{errors.password && <Text>{errors.password.message}</Text>}
|
|
120
|
+
|
|
121
|
+
<PrimaryButton
|
|
122
|
+
title={isSubmitting ? 'Submitting...' : 'Login'}
|
|
123
|
+
onPress={handleSubmit(onSubmit)}
|
|
124
|
+
disabled={isSubmitting}
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
# 5. Validating API Responses
|
|
134
|
+
|
|
135
|
+
Parse untrusted data at the boundary, not deep in hooks or components.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// features/products/services/productService.ts
|
|
139
|
+
import { z } from 'zod'
|
|
140
|
+
|
|
141
|
+
const productSchema = z.object({
|
|
142
|
+
id: z.string(),
|
|
143
|
+
name: z.string(),
|
|
144
|
+
price: z.number(),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
export async function getProduct(id: string) {
|
|
148
|
+
const raw = await apiClient.get(`/products/${id}`)
|
|
149
|
+
return productSchema.parse(raw) // throws if shape is wrong
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
# 6. Rules
|
|
156
|
+
|
|
157
|
+
- Schema first — always write the schema before the form.
|
|
158
|
+
- Never write a separate TypeScript type for form input — use `z.infer<typeof schema>`.
|
|
159
|
+
- Validate on the server too (or in the service) — never trust client-only validation.
|
|
160
|
+
- Use `z.string().min(1)` not `z.string().nonempty()` — better cross-platform compatibility.
|
|
161
|
+
- Route on `error.code` after submission, never on `error.message`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
# 7. Agent Quick Reference
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
New form? → schema first in features/[name]/schemas/
|
|
169
|
+
→ useForm + zodResolver
|
|
170
|
+
→ web: register(); RN: Controller + TextInput
|
|
171
|
+
New type for form data? → z.infer<typeof schema> instead
|
|
172
|
+
Validate API response? → z.safeParse() or schema.parse() in service
|
|
173
|
+
Shared schema? → src/schemas/ or schemas/ (not in features/)
|
|
174
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Zustand — Client State Management
|
|
2
|
+
|
|
3
|
+
> **When to use:** Shared UI state that persists across components or screens but is NOT server data.
|
|
4
|
+
> If the project has no such need, this concern does not apply.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 1. The One Rule
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
Zustand → sidebar, active tab, auth user, wizard steps, user preferences, modal state
|
|
12
|
+
TanStack Query → anything fetched from a server or API (users, posts, orders, products)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Never put server data in Zustand. Never put UI state in TanStack Query.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# 2. Store Setup
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/stores/uiStore.ts (web)
|
|
23
|
+
// stores/uiStore.ts (React Native)
|
|
24
|
+
import { create } from 'zustand'
|
|
25
|
+
|
|
26
|
+
type UIState = {
|
|
27
|
+
sidebarOpen: boolean
|
|
28
|
+
toggleSidebar: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
32
|
+
sidebarOpen: false,
|
|
33
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
34
|
+
}))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# 3. Auth Store Pattern
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// stores/authStore.ts
|
|
43
|
+
import { create } from 'zustand'
|
|
44
|
+
|
|
45
|
+
type AuthState = {
|
|
46
|
+
user: User | null
|
|
47
|
+
setUser: (user: User | null) => void
|
|
48
|
+
logout: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
52
|
+
user: null,
|
|
53
|
+
setUser: (user) => set({ user }),
|
|
54
|
+
logout: () => set({ user: null }),
|
|
55
|
+
}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# 4. Rules
|
|
61
|
+
|
|
62
|
+
- One store per domain concern: `authStore`, `cartStore`, `settingsStore` — not one giant global store.
|
|
63
|
+
- Do NOT store server data (fetched from API) in Zustand — use TanStack Query.
|
|
64
|
+
- Do NOT store local component-only state in Zustand — use `useState`.
|
|
65
|
+
- Keep stores narrow. If a store grows beyond 5–6 fields, split it.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
# 5. What Belongs Where
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
useState / useReducer → one component, resets on unmount
|
|
73
|
+
Zustand → multiple components/screens, survives navigation
|
|
74
|
+
TanStack Query → anything from a server, needs caching or refetch
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
# 6. Platform Notes
|
|
80
|
+
|
|
81
|
+
> **React Native** — works identically. Import from `zustand` the same way.
|
|
82
|
+
> Zustand has no dependency on the DOM or browser APIs.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
# 7. Agent Quick Reference
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
New shared UI state? → stores/[name]Store.ts
|
|
90
|
+
State only one component needs → useState instead
|
|
91
|
+
State fetched from server? → TanStack Query instead
|
|
92
|
+
Store getting large? → split into focused stores
|
|
93
|
+
```
|