wcz-layout 8.1.0 → 8.2.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/dist/components.js +2 -2
- package/dist/components.js.map +1 -1
- package/dist/data/client.d.ts +1 -1
- package/dist/data/server.d.ts +1 -1
- package/dist/models.d.ts +4 -4
- package/dist/{peoplesoft-CFgBFvG-.d.ts → peoplesoft-kaMETchL.d.ts} +5 -5
- package/package.json +154 -154
- package/skills/client-db/SKILL.md +111 -0
- package/skills/db-schema/SKILL.md +69 -0
- package/skills/forms/SKILL.md +85 -0
- package/skills/routes/SKILL.md +127 -0
- package/skills/server/SKILL.md +114 -0
- package/skills/start/SKILL.md +46 -0
- package/skills/start/steps/01-git-setup.md +10 -0
- package/skills/start/steps/02-dependency-check.md +11 -0
- package/skills/start/steps/03-env-setup.md +16 -0
- package/skills/start/steps/04-project-name-setup.md +14 -0
- package/skills/start/steps/05-database-setup.md +37 -0
- package/skills/start/steps/06-entra-setup.md +59 -0
- package/skills/start/steps/07-vault-setup.md +56 -0
- package/skills/start/steps/08-generate-favicon.md +15 -0
- package/skills/start/steps/09-commit.md +15 -0
- package/skills/tables/SKILL.md +184 -0
- package/skills/api-routes/SKILL.md +0 -251
- package/skills/auth/SKILL.md +0 -268
- package/skills/data-grid/SKILL.md +0 -229
- package/skills/database-schema/SKILL.md +0 -182
- package/skills/dialogs-notifications/SKILL.md +0 -241
- package/skills/forms-validation/SKILL.md +0 -331
- package/skills/forms-validation/references/field-components.md +0 -212
- package/skills/layout-navigation/SKILL.md +0 -259
- package/skills/project-initialization/SKILL.md +0 -181
- package/skills/project-structure/SKILL.md +0 -157
- package/skills/tanstack-db-collections/SKILL.md +0 -270
- package/skills/template-init.md +0 -146
- package/skills/ui-pages/SKILL.md +0 -278
package/package.json
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "wcz-layout",
|
|
3
|
-
"version": "8.
|
|
4
|
-
"private": false,
|
|
5
|
-
"keywords": [
|
|
6
|
-
"tanstack-intent"
|
|
7
|
-
],
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"skills",
|
|
11
|
-
"!skills/_artifacts"
|
|
12
|
-
],
|
|
13
|
-
"type": "module",
|
|
14
|
-
"sideEffects": false,
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"typesVersions": {
|
|
17
|
-
"*": {
|
|
18
|
-
"components": [
|
|
19
|
-
"./dist/components.d.ts"
|
|
20
|
-
],
|
|
21
|
-
"hooks": [
|
|
22
|
-
"./dist/hooks.d.ts"
|
|
23
|
-
],
|
|
24
|
-
"middleware": [
|
|
25
|
-
"./dist/middleware.d.ts"
|
|
26
|
-
],
|
|
27
|
-
"models": [
|
|
28
|
-
"./dist/models.d.ts"
|
|
29
|
-
],
|
|
30
|
-
"data": [
|
|
31
|
-
"./dist/data.d.ts"
|
|
32
|
-
],
|
|
33
|
-
"data/client": [
|
|
34
|
-
"./dist/data/client.d.ts"
|
|
35
|
-
],
|
|
36
|
-
"data/server": [
|
|
37
|
-
"./dist/data/server.d.ts"
|
|
38
|
-
],
|
|
39
|
-
"utils": [
|
|
40
|
-
"./dist/utils.d.ts"
|
|
41
|
-
],
|
|
42
|
-
"vite": [
|
|
43
|
-
"./dist/vite.d.ts"
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"exports": {
|
|
48
|
-
".": {
|
|
49
|
-
"types": "./dist/index.d.ts",
|
|
50
|
-
"import": "./dist/index.js"
|
|
51
|
-
},
|
|
52
|
-
"./components": {
|
|
53
|
-
"types": "./dist/components.d.ts",
|
|
54
|
-
"import": "./dist/components.js"
|
|
55
|
-
},
|
|
56
|
-
"./hooks": {
|
|
57
|
-
"types": "./dist/hooks.d.ts",
|
|
58
|
-
"import": "./dist/hooks.js"
|
|
59
|
-
},
|
|
60
|
-
"./middleware": {
|
|
61
|
-
"types": "./dist/middleware.d.ts",
|
|
62
|
-
"import": "./dist/middleware.js"
|
|
63
|
-
},
|
|
64
|
-
"./models": {
|
|
65
|
-
"types": "./dist/models.d.ts",
|
|
66
|
-
"import": "./dist/models.js"
|
|
67
|
-
},
|
|
68
|
-
"./data": {
|
|
69
|
-
"types": "./dist/data.d.ts",
|
|
70
|
-
"import": "./dist/data.js"
|
|
71
|
-
},
|
|
72
|
-
"./data/client": {
|
|
73
|
-
"types": "./dist/data/client.d.ts",
|
|
74
|
-
"import": "./dist/data/client.js"
|
|
75
|
-
},
|
|
76
|
-
"./data/server": {
|
|
77
|
-
"types": "./dist/data/server.d.ts",
|
|
78
|
-
"import": "./dist/data/server.js"
|
|
79
|
-
},
|
|
80
|
-
"./utils": {
|
|
81
|
-
"types": "./dist/utils.d.ts",
|
|
82
|
-
"import": "./dist/utils.js"
|
|
83
|
-
},
|
|
84
|
-
"./vite": {
|
|
85
|
-
"types": "./dist/vite.d.ts",
|
|
86
|
-
"import": "./dist/vite.js"
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
"scripts": {
|
|
90
|
-
"dev": "vp dev",
|
|
91
|
-
"vp:install": "vp install",
|
|
92
|
-
"vp:update": "vp update",
|
|
93
|
-
"ncu:install": "npx npm-check-updates -i",
|
|
94
|
-
"lint:package": "publint",
|
|
95
|
-
"build": "vp pack",
|
|
96
|
-
"prepublishOnly": "npm run build"
|
|
97
|
-
},
|
|
98
|
-
"dependencies": {
|
|
99
|
-
"@azure/msal-browser": "^5.
|
|
100
|
-
"@azure/msal-node": "^5.1
|
|
101
|
-
"@azure/msal-react": "^5.
|
|
102
|
-
"@t3-oss/env-core": "^0.13.11",
|
|
103
|
-
"file-saver": "^2.0.5",
|
|
104
|
-
"i18next": "^26.0
|
|
105
|
-
"i18next-browser-languagedetector": "^8.2.1",
|
|
106
|
-
"jose": "^6.2.3",
|
|
107
|
-
"react-dropzone": "^15.0.0",
|
|
108
|
-
"react-i18next": "^17.0.
|
|
109
|
-
"react-intersection-observer": "^10.0.3",
|
|
110
|
-
"react-number-format": "^5.4.5",
|
|
111
|
-
"tus-js-client": "^4.3.1",
|
|
112
|
-
"uuidv7": "^1.2.1"
|
|
113
|
-
},
|
|
114
|
-
"devDependencies": {
|
|
115
|
-
"@rolldown/plugin-babel": "^0.2.3",
|
|
116
|
-
"@tanstack/intent": "^0.0.
|
|
117
|
-
"@types/file-saver": "^2.0.7",
|
|
118
|
-
"@types/node": "^24.10.13",
|
|
119
|
-
"@types/react": "^19.2.14",
|
|
120
|
-
"@types/react-dom": "^19.2.3",
|
|
121
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
122
|
-
"babel-plugin-react-compiler": "^1.0.0",
|
|
123
|
-
"nitro": "npm:nitro-nightly@latest",
|
|
124
|
-
"publint": "^0.3.
|
|
125
|
-
"typescript": "^6.0.3",
|
|
126
|
-
"vite-plugin-checker": "^0.13.0"
|
|
127
|
-
},
|
|
128
|
-
"peerDependencies": {
|
|
129
|
-
"@emotion/react": "11.x",
|
|
130
|
-
"@emotion/styled": "11.x",
|
|
131
|
-
"@mui/icons-material": "9.x",
|
|
132
|
-
"@mui/material": "9.x",
|
|
133
|
-
"@mui/x-data-grid-premium": "9.x",
|
|
134
|
-
"@mui/x-date-pickers-pro": "9.x",
|
|
135
|
-
"@tanstack/query-db-collection": "1.x",
|
|
136
|
-
"@tanstack/react-db": "0.x",
|
|
137
|
-
"@tanstack/react-form": "1.x",
|
|
138
|
-
"@tanstack/react-query": "5.x",
|
|
139
|
-
"@tanstack/react-router": "1.x",
|
|
140
|
-
"@tanstack/react-router-ssr-query": "1.x",
|
|
141
|
-
"@tanstack/react-start": "1.x",
|
|
142
|
-
"axios": "1.x",
|
|
143
|
-
"dayjs": "1.x",
|
|
144
|
-
"react": "19.x",
|
|
145
|
-
"react-dom": "19.x",
|
|
146
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
147
|
-
"vite-plus": "latest",
|
|
148
|
-
"zod": "4.x"
|
|
149
|
-
},
|
|
150
|
-
"overrides": {
|
|
151
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@latest"
|
|
152
|
-
},
|
|
153
|
-
"packageManager": "npm@11.
|
|
154
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "wcz-layout",
|
|
3
|
+
"version": "8.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tanstack-intent"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"skills",
|
|
11
|
+
"!skills/_artifacts"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"typesVersions": {
|
|
17
|
+
"*": {
|
|
18
|
+
"components": [
|
|
19
|
+
"./dist/components.d.ts"
|
|
20
|
+
],
|
|
21
|
+
"hooks": [
|
|
22
|
+
"./dist/hooks.d.ts"
|
|
23
|
+
],
|
|
24
|
+
"middleware": [
|
|
25
|
+
"./dist/middleware.d.ts"
|
|
26
|
+
],
|
|
27
|
+
"models": [
|
|
28
|
+
"./dist/models.d.ts"
|
|
29
|
+
],
|
|
30
|
+
"data": [
|
|
31
|
+
"./dist/data.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"data/client": [
|
|
34
|
+
"./dist/data/client.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"data/server": [
|
|
37
|
+
"./dist/data/server.d.ts"
|
|
38
|
+
],
|
|
39
|
+
"utils": [
|
|
40
|
+
"./dist/utils.d.ts"
|
|
41
|
+
],
|
|
42
|
+
"vite": [
|
|
43
|
+
"./dist/vite.d.ts"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"exports": {
|
|
48
|
+
".": {
|
|
49
|
+
"types": "./dist/index.d.ts",
|
|
50
|
+
"import": "./dist/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./components": {
|
|
53
|
+
"types": "./dist/components.d.ts",
|
|
54
|
+
"import": "./dist/components.js"
|
|
55
|
+
},
|
|
56
|
+
"./hooks": {
|
|
57
|
+
"types": "./dist/hooks.d.ts",
|
|
58
|
+
"import": "./dist/hooks.js"
|
|
59
|
+
},
|
|
60
|
+
"./middleware": {
|
|
61
|
+
"types": "./dist/middleware.d.ts",
|
|
62
|
+
"import": "./dist/middleware.js"
|
|
63
|
+
},
|
|
64
|
+
"./models": {
|
|
65
|
+
"types": "./dist/models.d.ts",
|
|
66
|
+
"import": "./dist/models.js"
|
|
67
|
+
},
|
|
68
|
+
"./data": {
|
|
69
|
+
"types": "./dist/data.d.ts",
|
|
70
|
+
"import": "./dist/data.js"
|
|
71
|
+
},
|
|
72
|
+
"./data/client": {
|
|
73
|
+
"types": "./dist/data/client.d.ts",
|
|
74
|
+
"import": "./dist/data/client.js"
|
|
75
|
+
},
|
|
76
|
+
"./data/server": {
|
|
77
|
+
"types": "./dist/data/server.d.ts",
|
|
78
|
+
"import": "./dist/data/server.js"
|
|
79
|
+
},
|
|
80
|
+
"./utils": {
|
|
81
|
+
"types": "./dist/utils.d.ts",
|
|
82
|
+
"import": "./dist/utils.js"
|
|
83
|
+
},
|
|
84
|
+
"./vite": {
|
|
85
|
+
"types": "./dist/vite.d.ts",
|
|
86
|
+
"import": "./dist/vite.js"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"dev": "vp dev",
|
|
91
|
+
"vp:install": "vp install",
|
|
92
|
+
"vp:update": "vp update",
|
|
93
|
+
"ncu:install": "npx npm-check-updates -i",
|
|
94
|
+
"lint:package": "publint",
|
|
95
|
+
"build": "vp pack",
|
|
96
|
+
"prepublishOnly": "npm run build"
|
|
97
|
+
},
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"@azure/msal-browser": "^5.10.1",
|
|
100
|
+
"@azure/msal-node": "^5.2.1",
|
|
101
|
+
"@azure/msal-react": "^5.4.1",
|
|
102
|
+
"@t3-oss/env-core": "^0.13.11",
|
|
103
|
+
"file-saver": "^2.0.5",
|
|
104
|
+
"i18next": "^26.1.0",
|
|
105
|
+
"i18next-browser-languagedetector": "^8.2.1",
|
|
106
|
+
"jose": "^6.2.3",
|
|
107
|
+
"react-dropzone": "^15.0.0",
|
|
108
|
+
"react-i18next": "^17.0.7",
|
|
109
|
+
"react-intersection-observer": "^10.0.3",
|
|
110
|
+
"react-number-format": "^5.4.5",
|
|
111
|
+
"tus-js-client": "^4.3.1",
|
|
112
|
+
"uuidv7": "^1.2.1"
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
116
|
+
"@tanstack/intent": "^0.0.41",
|
|
117
|
+
"@types/file-saver": "^2.0.7",
|
|
118
|
+
"@types/node": "^24.10.13",
|
|
119
|
+
"@types/react": "^19.2.14",
|
|
120
|
+
"@types/react-dom": "^19.2.3",
|
|
121
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
122
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
123
|
+
"nitro": "npm:nitro-nightly@latest",
|
|
124
|
+
"publint": "^0.3.20",
|
|
125
|
+
"typescript": "^6.0.3",
|
|
126
|
+
"vite-plugin-checker": "^0.13.0"
|
|
127
|
+
},
|
|
128
|
+
"peerDependencies": {
|
|
129
|
+
"@emotion/react": "11.x",
|
|
130
|
+
"@emotion/styled": "11.x",
|
|
131
|
+
"@mui/icons-material": "9.x",
|
|
132
|
+
"@mui/material": "9.x",
|
|
133
|
+
"@mui/x-data-grid-premium": "9.x",
|
|
134
|
+
"@mui/x-date-pickers-pro": "9.x",
|
|
135
|
+
"@tanstack/query-db-collection": "1.x",
|
|
136
|
+
"@tanstack/react-db": "0.x",
|
|
137
|
+
"@tanstack/react-form": "1.x",
|
|
138
|
+
"@tanstack/react-query": "5.x",
|
|
139
|
+
"@tanstack/react-router": "1.x",
|
|
140
|
+
"@tanstack/react-router-ssr-query": "1.x",
|
|
141
|
+
"@tanstack/react-start": "1.x",
|
|
142
|
+
"axios": "1.x",
|
|
143
|
+
"dayjs": "1.x",
|
|
144
|
+
"react": "19.x",
|
|
145
|
+
"react-dom": "19.x",
|
|
146
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
147
|
+
"vite-plus": "latest",
|
|
148
|
+
"zod": "4.x"
|
|
149
|
+
},
|
|
150
|
+
"overrides": {
|
|
151
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest"
|
|
152
|
+
},
|
|
153
|
+
"packageManager": "npm@11.14.1"
|
|
154
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: client-db
|
|
3
|
+
description: Querying data on client by creating reactive TanStack DB collections.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
|
|
8
|
+
- Use `eager` syncMode for top-level collections, `on-demand` for child/relational data.
|
|
9
|
+
- Use `useLiveQuery` on list pages (provides `isLoading` for DataGrid), `useLiveSuspenseQuery` on detail/edit pages or components (wrap in `<Suspense>` with loading fallback).
|
|
10
|
+
- For mutations use `createOptimisticAction` — mirror the server mutation in `onMutate`, then call the server function and `collection.utils.refetch()` in `mutationFn`.
|
|
11
|
+
- Use `meta.loadSubsetOptions` for relational subset loading.
|
|
12
|
+
- Use Axios with auth interceptor `getAccessToken` but only for public REST APIs; use default `api` as a scope key.
|
|
13
|
+
|
|
14
|
+
## File Placement
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/db-collections/ — DB collections
|
|
18
|
+
src/server/actions/ — server functions (select/update/insert/delete)
|
|
19
|
+
src/lib/schemas/ — Zod schemas
|
|
20
|
+
src/lib/auth/scopes.ts — API scope keys
|
|
21
|
+
wcz-layout/data — shared data utilities and server functions from npm package
|
|
22
|
+
wcz-layout/utils — shared utils from npm package
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// imports
|
|
29
|
+
import { queryClient } from "wcz-layout/data";
|
|
30
|
+
import { getAccessToken } from "wcz-layout/utils";
|
|
31
|
+
|
|
32
|
+
// src/db-collections/library.ts
|
|
33
|
+
export const api = axios.create({
|
|
34
|
+
baseURL: "/api/libraries",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
api.interceptors.request.use(async (config) => {
|
|
38
|
+
const accessToken = await getAccessToken("api");
|
|
39
|
+
config.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
40
|
+
return config;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const librariesCollection = createCollection(
|
|
44
|
+
queryCollectionOptions({
|
|
45
|
+
queryKey: ["libraries"],
|
|
46
|
+
queryFn: () => selectLibraries(),
|
|
47
|
+
getKey: ({ id }) => id,
|
|
48
|
+
schema: LibrarySchema,
|
|
49
|
+
queryClient: queryClient,
|
|
50
|
+
syncMode: "eager",
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// src/db-collections/book.ts
|
|
55
|
+
export const bookCollection = createCollection(
|
|
56
|
+
queryCollectionOptions({
|
|
57
|
+
queryKey: ["books"],
|
|
58
|
+
queryFn: ({ meta }) => selectBooks({ data: meta?.loadSubsetOptions }),
|
|
59
|
+
getKey: ({ id }) => id,
|
|
60
|
+
schema: BookSchema,
|
|
61
|
+
queryClient: queryClient,
|
|
62
|
+
syncMode: "on-demand",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// src/routes/libraries/edit/$id.tsx
|
|
67
|
+
const { data, isLoading } = useLiveQuery((q) =>
|
|
68
|
+
q.from({ library: libraryCollection }).orderBy(({ library }) => library.name, "asc"),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { data } = useLiveQuery((q) =>
|
|
72
|
+
q
|
|
73
|
+
.from({ library: libraryCollection })
|
|
74
|
+
.where(({ library }) => eq(library.id, id))
|
|
75
|
+
.findOne()
|
|
76
|
+
.select(({ library }) => ({
|
|
77
|
+
...library,
|
|
78
|
+
books: toArray(
|
|
79
|
+
q
|
|
80
|
+
.from({ book: bookCollection })
|
|
81
|
+
.where(({ book }) => eq(book.libraryId, library.id))
|
|
82
|
+
.orderBy(({ book }) => book.title, "asc")
|
|
83
|
+
.select(({ book }) => ({
|
|
84
|
+
id: book.id,
|
|
85
|
+
title: book.title,
|
|
86
|
+
})),
|
|
87
|
+
),
|
|
88
|
+
})),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const handleOnSubmit = createOptimisticAction<Library>({
|
|
92
|
+
onMutate: (formValues) => {
|
|
93
|
+
libraryCollection.update(id, (prev) => Object.assign(prev, formValues));
|
|
94
|
+
},
|
|
95
|
+
mutationFn: async (formValues) => {
|
|
96
|
+
await updateLibrary({ data: formValues });
|
|
97
|
+
await libraryCollection.utils.refetch();
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const transaction = handleOnSubmit(formValues);
|
|
103
|
+
await transaction.isPersisted.promise;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof Error) alert(error.message);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Next Step (ask user after completion)
|
|
110
|
+
|
|
111
|
+
- Create a new Route
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-schema
|
|
3
|
+
description: Define PostgreSQL database schemas with Drizzle ORM.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
|
|
8
|
+
- Always use `uuid` for primary keys.
|
|
9
|
+
- Define enums with `pgEnum`; they can be shared across schema files.
|
|
10
|
+
- Omit column name strings; Drizzle auto-maps camelCase to snake_case via `casing: "snake_case"`.
|
|
11
|
+
- Use `onDelete: "cascade"` on foreign keys for child/dependent tables.
|
|
12
|
+
- Define relations into `relations.ts` file.
|
|
13
|
+
- Derive form schemas with `.extend()` from the base schema; use `.transform()` to set parent IDs on children.
|
|
14
|
+
- Always add `withTimezone: true` to timestamp columns.
|
|
15
|
+
- Generate Zod schemas with `createSelectSchema`; override fields with stricter rules (trim, min/max) where needed.
|
|
16
|
+
- Don't auto-generate migrations.
|
|
17
|
+
|
|
18
|
+
## File Placement
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
src/server/db/migrations/ — migrations
|
|
22
|
+
src/server/db/schemas/ — tables, enums, relations
|
|
23
|
+
src/lib/schemas/ — Zod schemas
|
|
24
|
+
wcz-layout/utils — shared utils from npm package
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// imports
|
|
31
|
+
import { t } from "wcz-layout/utils";
|
|
32
|
+
|
|
33
|
+
// src/server/db/schemas/book.ts
|
|
34
|
+
export const bookTable = pgTable("books", {
|
|
35
|
+
id: uuid().primaryKey(),
|
|
36
|
+
title: text().notNull(),
|
|
37
|
+
libraryId: uuid()
|
|
38
|
+
.notNull()
|
|
39
|
+
.references(() => libraryTable.id, { onDelete: "cascade" }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// src/server/db/schemas/relations.ts
|
|
43
|
+
export const relations = defineRelations(
|
|
44
|
+
{ libraryTable, bookTable },
|
|
45
|
+
({ one, many, libraryTable: library, bookTable: book }) => ({
|
|
46
|
+
libraryTable: {
|
|
47
|
+
books: many.bookTable(),
|
|
48
|
+
},
|
|
49
|
+
bookTable: {
|
|
50
|
+
library: one.libraryTable({ from: book.libraryId, to: library.id }),
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// src/lib/schemas/book.ts
|
|
56
|
+
export const BookSchema = createSelectSchema(bookTable, {
|
|
57
|
+
title: (schema) =>
|
|
58
|
+
schema
|
|
59
|
+
.trim()
|
|
60
|
+
.min(1, t("Validation.Required"))
|
|
61
|
+
.max(255, t("Validation.MaxLength", { length: 255 })),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Book = z.infer<typeof BookSchema>;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Next Step (ask user after completion)
|
|
68
|
+
|
|
69
|
+
- Create REST API routes or server functions with TanStack Start/Router to query these tables.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: forms
|
|
3
|
+
description: Best practices for building forms including validation and field types.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
|
|
8
|
+
- Always use `useLayoutForm` with pre-defined components.
|
|
9
|
+
- Define `width` for all form fields based on the expected content length.
|
|
10
|
+
- Derive Zod schemas from Drizzle table schemas using `createSelectSchema` with `t()` for validation messages.
|
|
11
|
+
|
|
12
|
+
## File Placement
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/routes/features/-components — Form components scoped to a feature
|
|
16
|
+
src/lib/schemas/ — Zod schemas
|
|
17
|
+
wcz-layout/hooks — shared hooks from npm package
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// Imports
|
|
24
|
+
import { useLayoutForm } from "wcz-layout/hooks";
|
|
25
|
+
import { LibrarySchema } from "~/lib/schemas/library";
|
|
26
|
+
|
|
27
|
+
// Form Component
|
|
28
|
+
interface FormProps {
|
|
29
|
+
defaultValues: Library;
|
|
30
|
+
onSubmit: (value: Library) => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Form: FC<FormProps> = ({ defaultValues, onSubmit }) => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
|
|
36
|
+
const form = useLayoutForm({
|
|
37
|
+
defaultValues,
|
|
38
|
+
validators: { onChange: LibrarySchema },
|
|
39
|
+
onSubmit: async ({ value, formApi }) => {
|
|
40
|
+
await onSubmit(value);
|
|
41
|
+
formApi.reset();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<form
|
|
47
|
+
onSubmit={(event) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
form.handleSubmit();
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{/* some styling */}
|
|
54
|
+
<form.AppField name="name">
|
|
55
|
+
{(field) => <field.TextField label={t("Library.Name")} required sx={{ width: 420 }} />}
|
|
56
|
+
</form.AppField>
|
|
57
|
+
{/* some styling */}
|
|
58
|
+
<form.AppForm>
|
|
59
|
+
<form.SubmitButton variant="contained">{t("Submit")}</form.SubmitButton>
|
|
60
|
+
</form.AppForm>
|
|
61
|
+
</form>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Autocomplete
|
|
66
|
+
<field.Autocomplete
|
|
67
|
+
options={options}
|
|
68
|
+
sx={{ width: 250 }}
|
|
69
|
+
autoHighlight
|
|
70
|
+
autoSelect
|
|
71
|
+
autoComplete
|
|
72
|
+
loading={isLoading}
|
|
73
|
+
textFieldProps={{
|
|
74
|
+
label: t("Customer"),
|
|
75
|
+
required: true,
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
// Checkbox
|
|
80
|
+
<field.Checkbox label={t("IsThisTrue")} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Field Types
|
|
84
|
+
|
|
85
|
+
`Autocomplete` · `Checkbox` · `DatePicker` · `DateRangePicker` · `DateTimePicker` · `DateTimeRangePicker` · `NumberField` · `RadioGroup` · `Slider` · `SubmitButton` · `Switch` · `TextField` · `TimePicker` · `TimeRangePicker`
|