create-modern-react 2.1.3 → 2.3.2
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/README.md +31 -0
- package/lib/prompts.js +7 -0
- package/lib/setup.js +15 -1
- package/package.json +1 -1
- package/templates/base/.eslintrc.cjs +48 -26
- package/templates/base/package.json +4 -3
- package/templates/base/public/screenshots/healthmug.png +0 -0
- package/templates/base/public/screenshots/resumefreepro.png +0 -0
- package/templates/base/src/screens/home/index.tsx +101 -15
- package/templates/base/src/services/alertify-services.ts +0 -32
- package/templates/base/tsconfig.node.json +4 -1
- package/templates/optional/forms/index.ts +2 -0
- package/templates/optional/forms/types.ts +39 -0
- package/templates/optional/forms/use-zod-form.ts +59 -0
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
│ ✗ No toast notifications ✓ react-hot-toast │
|
|
46
46
|
│ ✗ No error boundary ✓ Built-in │
|
|
47
47
|
│ ✗ Basic ESLint ✓ 25+ rules configured │
|
|
48
|
+
│ ✗ No form validation ✓ RHF + Zod (optional) │
|
|
48
49
|
│ ✗ No state management ✓ Redux (optional) │
|
|
49
50
|
│ ✗ ~2 hours setup ✓ 15 seconds │
|
|
50
51
|
│ │
|
|
@@ -106,6 +107,7 @@ Select during project creation:
|
|
|
106
107
|
|
|
107
108
|
```
|
|
108
109
|
[ ] Redux Toolkit + Redux Persist ── State management with persistence
|
|
110
|
+
[ ] React Hook Form + Zod ────────── Type-safe form validation
|
|
109
111
|
[ ] Ant Design v5 ───────────────── Enterprise UI (replaces Shadcn/ui)
|
|
110
112
|
[ ] Husky + lint-staged ─────────── Git hooks for code quality
|
|
111
113
|
```
|
|
@@ -147,6 +149,10 @@ my-app/
|
|
|
147
149
|
│ │ └── layout/
|
|
148
150
|
│ │ ├── root-layout.tsx
|
|
149
151
|
│ │ └── error-boundary.tsx
|
|
152
|
+
│ ├── forms/ # (optional) React Hook Form + Zod
|
|
153
|
+
│ │ ├── index.ts # Barrel export
|
|
154
|
+
│ │ ├── use-zod-form.ts # Custom hook with onBlur validation
|
|
155
|
+
│ │ └── types.ts # Form TypeScript types
|
|
150
156
|
│ ├── hooks/
|
|
151
157
|
│ │ ├── use-loader.ts # Loading state management
|
|
152
158
|
│ │ ├── use-debounce.ts # Value debouncing
|
|
@@ -242,6 +248,31 @@ const debouncedQuery = useDebounce(searchQuery, 300);
|
|
|
242
248
|
const { cancelToken, cancel } = useCancelToken();
|
|
243
249
|
```
|
|
244
250
|
|
|
251
|
+
### Type-Safe Form Validation (Optional)
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { z } from 'zod';
|
|
255
|
+
import { useZodForm } from '~/forms';
|
|
256
|
+
|
|
257
|
+
const loginSchema = z.object({
|
|
258
|
+
email: z.string().email(),
|
|
259
|
+
password: z.string().min(8, 'Password must be 8+ characters'),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
function LoginForm() {
|
|
263
|
+
const form = useZodForm({ schema: loginSchema });
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
267
|
+
<input {...form.register('email')} />
|
|
268
|
+
{form.formState.errors.email?.message}
|
|
269
|
+
</form>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
*UI-agnostic • Works with Shadcn, Antd, or plain HTML • Validates onBlur*
|
|
275
|
+
|
|
245
276
|
### Path Aliases
|
|
246
277
|
|
|
247
278
|
```tsx
|
package/lib/prompts.js
CHANGED
|
@@ -94,6 +94,11 @@ async function createProject(projectName, options) {
|
|
|
94
94
|
value: 'redux',
|
|
95
95
|
checked: false
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'React Hook Form + Zod (form validation)',
|
|
99
|
+
value: 'forms',
|
|
100
|
+
checked: false
|
|
101
|
+
},
|
|
97
102
|
{
|
|
98
103
|
name: 'Ant Design v5 (replaces Shadcn/ui)',
|
|
99
104
|
value: 'antd',
|
|
@@ -153,6 +158,7 @@ async function createProject(projectName, options) {
|
|
|
153
158
|
packageManager,
|
|
154
159
|
// Optional features
|
|
155
160
|
useRedux: optionalFeatures.includes('redux'),
|
|
161
|
+
useForms: optionalFeatures.includes('forms'),
|
|
156
162
|
useAntd: optionalFeatures.includes('antd'),
|
|
157
163
|
useHusky: optionalFeatures.includes('husky'),
|
|
158
164
|
// Flags
|
|
@@ -176,6 +182,7 @@ async function createProject(projectName, options) {
|
|
|
176
182
|
console.log(chalk.cyan('├─────────────────────────────────────────────┤'));
|
|
177
183
|
console.log(chalk.cyan('│') + chalk.white(' Optional Features: ') + chalk.cyan('│'));
|
|
178
184
|
console.log(chalk.cyan('│') + ` Redux Toolkit: ${config.useRedux ? chalk.green('✓') : chalk.gray('✗')} ` + chalk.cyan('│'));
|
|
185
|
+
console.log(chalk.cyan('│') + ` RHF + Zod: ${config.useForms ? chalk.green('✓') : chalk.gray('✗')} ` + chalk.cyan('│'));
|
|
179
186
|
console.log(chalk.cyan('│') + ` Ant Design v5: ${config.useAntd ? chalk.green('✓') : chalk.gray('✗')} ` + chalk.cyan('│'));
|
|
180
187
|
console.log(chalk.cyan('│') + ` Husky hooks: ${config.useHusky ? chalk.green('✓') : chalk.gray('✗')} ` + chalk.cyan('│'));
|
|
181
188
|
console.log(chalk.cyan('└─────────────────────────────────────────────┘\n'));
|
package/lib/setup.js
CHANGED
|
@@ -38,6 +38,11 @@ async function setupProject(config) {
|
|
|
38
38
|
await updateProvidersForRedux(projectPath, config.useAntd);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (config.useForms) {
|
|
42
|
+
console.log(chalk.gray(' Adding React Hook Form + Zod...'));
|
|
43
|
+
await copyOptionalTemplate('forms', projectPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
if (config.useHusky) {
|
|
42
47
|
console.log(chalk.gray(' Adding Husky + lint-staged...'));
|
|
43
48
|
await copyOptionalTemplate('husky', projectPath);
|
|
@@ -81,6 +86,13 @@ async function updatePackageJson(config) {
|
|
|
81
86
|
dependencies['redux-persist'] = '^6.0.0';
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
// React Hook Form + Zod dependencies
|
|
90
|
+
if (config.useForms) {
|
|
91
|
+
dependencies['react-hook-form'] = '^7.54.0';
|
|
92
|
+
dependencies['zod'] = '^3.24.0';
|
|
93
|
+
dependencies['@hookform/resolvers'] = '^3.9.0';
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
// Ant Design dependencies (replaces Shadcn)
|
|
85
97
|
if (config.useAntd) {
|
|
86
98
|
dependencies['antd'] = '^5.20.0';
|
|
@@ -116,9 +128,11 @@ async function copyOptionalTemplate(templateName, projectPath) {
|
|
|
116
128
|
);
|
|
117
129
|
|
|
118
130
|
if (await fs.pathExists(optionalTemplatePath)) {
|
|
119
|
-
// For redux and
|
|
131
|
+
// For redux, antd, and forms, copy to src directory
|
|
120
132
|
if (templateName === 'redux') {
|
|
121
133
|
await fs.copy(optionalTemplatePath, path.join(projectPath, 'src/redux'));
|
|
134
|
+
} else if (templateName === 'forms') {
|
|
135
|
+
await fs.copy(optionalTemplatePath, path.join(projectPath, 'src/forms'));
|
|
122
136
|
} else if (templateName === 'antd') {
|
|
123
137
|
await fs.copy(optionalTemplatePath, path.join(projectPath, 'src/antd'));
|
|
124
138
|
// Also copy the styles file to src/styles
|
package/package.json
CHANGED
|
@@ -1,37 +1,59 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
root: true,
|
|
3
|
-
env: {
|
|
3
|
+
env: {
|
|
4
|
+
browser: true,
|
|
5
|
+
node: true,
|
|
6
|
+
es6: true,
|
|
7
|
+
},
|
|
4
8
|
extends: [
|
|
5
9
|
'eslint:recommended',
|
|
6
10
|
'plugin:@typescript-eslint/recommended',
|
|
7
|
-
'
|
|
11
|
+
'eslint-config-prettier',
|
|
8
12
|
],
|
|
9
|
-
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
13
|
+
ignorePatterns: ['dist', '.eslintrc.cjs', 'node_modules'],
|
|
10
14
|
parser: '@typescript-eslint/parser',
|
|
11
|
-
plugins: ['
|
|
15
|
+
plugins: ['@typescript-eslint', 'import'],
|
|
12
16
|
rules: {
|
|
13
|
-
'react-refresh/only-export-components': [
|
|
14
|
-
'warn',
|
|
15
|
-
{ allowConstantExport: true },
|
|
16
|
-
],
|
|
17
|
-
// Unused imports
|
|
18
|
-
'unused-imports/no-unused-imports': 'error',
|
|
19
|
-
'unused-imports/no-unused-vars': [
|
|
20
|
-
'warn',
|
|
21
|
-
{
|
|
22
|
-
vars: 'all',
|
|
23
|
-
varsIgnorePattern: '^_',
|
|
24
|
-
args: 'after-used',
|
|
25
|
-
argsIgnorePattern: '^_',
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
17
|
// TypeScript
|
|
29
|
-
'
|
|
30
|
-
'@typescript-eslint/no-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
'no-
|
|
34
|
-
'
|
|
35
|
-
|
|
18
|
+
'no-unused-vars': 'off',
|
|
19
|
+
'@typescript-eslint/no-unused-vars': ['warn'],
|
|
20
|
+
|
|
21
|
+
// Import validation
|
|
22
|
+
'import/no-unresolved': 'error',
|
|
23
|
+
'import/named': 'error',
|
|
24
|
+
|
|
25
|
+
// Core JavaScript
|
|
26
|
+
'no-undef': ['error'],
|
|
27
|
+
'no-var': ['error'],
|
|
28
|
+
'no-await-in-loop': 'error',
|
|
29
|
+
'no-constant-binary-expression': 'error',
|
|
30
|
+
'no-duplicate-imports': 'error',
|
|
31
|
+
'no-new-native-nonconstructor': 'error',
|
|
32
|
+
'no-promise-executor-return': 'error',
|
|
33
|
+
'no-self-compare': 'error',
|
|
34
|
+
'no-template-curly-in-string': 'error',
|
|
35
|
+
'no-unmodified-loop-condition': 'error',
|
|
36
|
+
'no-unreachable-loop': 'error',
|
|
37
|
+
'no-unused-private-class-members': 'error',
|
|
38
|
+
'no-use-before-define': 'error',
|
|
39
|
+
|
|
40
|
+
// React
|
|
41
|
+
'react/prop-types': 'off',
|
|
42
|
+
'react/react-in-jsx-scope': 'off',
|
|
43
|
+
|
|
44
|
+
// Disabled
|
|
45
|
+
'no-extra-boolean-cast': 'off',
|
|
46
|
+
},
|
|
47
|
+
settings: {
|
|
48
|
+
react: {
|
|
49
|
+
version: 'detect',
|
|
50
|
+
},
|
|
51
|
+
'import/resolver': {
|
|
52
|
+
typescript: {},
|
|
53
|
+
node: {
|
|
54
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
55
|
+
moduleDirectory: ['node_modules', 'src/'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
36
58
|
},
|
|
37
59
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-react-app",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite --open --port 3000",
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
|
33
33
|
"autoprefixer": "^10.4.19",
|
|
34
34
|
"eslint": "^8.57.0",
|
|
35
|
+
"eslint-config-prettier": "^9.1.0",
|
|
36
|
+
"eslint-plugin-import": "^2.29.1",
|
|
37
|
+
"eslint-import-resolver-typescript": "^3.6.1",
|
|
35
38
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
36
|
-
"eslint-plugin-react-refresh": "^0.4.7",
|
|
37
|
-
"eslint-plugin-unused-imports": "^3.2.0",
|
|
38
39
|
"postcss": "^8.4.39",
|
|
39
40
|
"prettier": "^3.3.0",
|
|
40
41
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { Moon, Sun, Github, Zap, Bot } from 'lucide-react';
|
|
2
|
+
import { Moon, Sun, Github, Zap, Bot, ExternalLink } from 'lucide-react';
|
|
3
3
|
import { Button, Card, CardContent, CardHeader, CardTitle } from '~/components/ui';
|
|
4
4
|
import { useTheme } from '~/providers';
|
|
5
5
|
|
|
@@ -16,17 +16,33 @@ export default function Home() {
|
|
|
16
16
|
<div className="w-full max-w-2xl space-y-8">
|
|
17
17
|
{/* Header */}
|
|
18
18
|
<div className="text-center">
|
|
19
|
-
<div className="mb-4 flex justify-
|
|
19
|
+
<div className="mb-4 flex items-center justify-between">
|
|
20
|
+
<div className="w-40" />
|
|
20
21
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary text-primary-foreground">
|
|
21
22
|
<Zap className="h-8 w-8" />
|
|
22
23
|
</div>
|
|
24
|
+
<Button variant="outline" asChild>
|
|
25
|
+
<a
|
|
26
|
+
href="https://github.com/abhay-rana/create-modern-react"
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
>
|
|
30
|
+
<Github className="mr-2 h-4 w-4" />
|
|
31
|
+
View CLI on GitHub
|
|
32
|
+
</a>
|
|
33
|
+
</Button>
|
|
23
34
|
</div>
|
|
24
35
|
<h1 className="text-4xl font-bold tracking-tight">
|
|
25
36
|
create-modern-react
|
|
26
37
|
</h1>
|
|
27
38
|
<p className="mt-2 text-muted-foreground">
|
|
28
|
-
|
|
39
|
+
Production-ready React + TypeScript + Tailwind in seconds
|
|
29
40
|
</p>
|
|
41
|
+
<div className="mt-4">
|
|
42
|
+
<code className="rounded-md bg-muted px-3 py-1.5 font-mono text-sm">
|
|
43
|
+
npx create-modern-react my-app
|
|
44
|
+
</code>
|
|
45
|
+
</div>
|
|
30
46
|
</div>
|
|
31
47
|
|
|
32
48
|
{/* Counter Card */}
|
|
@@ -107,18 +123,29 @@ export default function Home() {
|
|
|
107
123
|
</div>
|
|
108
124
|
</div>
|
|
109
125
|
|
|
110
|
-
{/*
|
|
111
|
-
<div className="
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
{/* Projects Showcase */}
|
|
127
|
+
<div className="space-y-4">
|
|
128
|
+
<h2 className="text-center text-2xl font-bold">
|
|
129
|
+
Production Projects created using this boilerplate
|
|
130
|
+
</h2>
|
|
131
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
132
|
+
<ProjectCard
|
|
133
|
+
category="AI/Career"
|
|
134
|
+
title="ResumeFreePro"
|
|
135
|
+
description="AI-Powered Resume Builder"
|
|
136
|
+
designStyle="Modern + Glassmorphism"
|
|
137
|
+
url="https://resumefreepro.com?utm_source=starter-template&utm_medium=landing-page&utm_campaign=create-modern-react-demo"
|
|
138
|
+
previewUrl="/screenshots/resumefreepro.png"
|
|
139
|
+
/>
|
|
140
|
+
<ProjectCard
|
|
141
|
+
category="E-Pharmacy"
|
|
142
|
+
title="HealthMug"
|
|
143
|
+
description="Online Pharmacy Platform"
|
|
144
|
+
designStyle="Clean + Professional"
|
|
145
|
+
url="https://healthmug.com?utm_source=starter-template&utm_medium=landing-page&utm_campaign=create-modern-react-demo"
|
|
146
|
+
previewUrl="/screenshots/healthmug.png"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
122
149
|
</div>
|
|
123
150
|
|
|
124
151
|
<p className="text-center text-xs text-muted-foreground">
|
|
@@ -143,3 +170,62 @@ function FeatureCard({
|
|
|
143
170
|
</div>
|
|
144
171
|
);
|
|
145
172
|
}
|
|
173
|
+
|
|
174
|
+
function ProjectCard({
|
|
175
|
+
category,
|
|
176
|
+
title,
|
|
177
|
+
description,
|
|
178
|
+
designStyle,
|
|
179
|
+
url,
|
|
180
|
+
previewUrl,
|
|
181
|
+
}: {
|
|
182
|
+
category: string;
|
|
183
|
+
title: string;
|
|
184
|
+
description: string;
|
|
185
|
+
designStyle: string;
|
|
186
|
+
url: string;
|
|
187
|
+
previewUrl: string;
|
|
188
|
+
}) {
|
|
189
|
+
return (
|
|
190
|
+
<a
|
|
191
|
+
href={url}
|
|
192
|
+
target="_blank"
|
|
193
|
+
rel="noopener noreferrer"
|
|
194
|
+
className="group block"
|
|
195
|
+
>
|
|
196
|
+
<Card className="overflow-hidden transition-all hover:-translate-y-1 hover:shadow-lg">
|
|
197
|
+
{/* Preview Area */}
|
|
198
|
+
<div className="relative h-48 overflow-hidden bg-muted">
|
|
199
|
+
<img
|
|
200
|
+
src={previewUrl}
|
|
201
|
+
alt={`${title} preview`}
|
|
202
|
+
className="h-full w-full object-cover object-top transition-transform duration-300 group-hover:scale-105"
|
|
203
|
+
/>
|
|
204
|
+
{/* Overlay on hover */}
|
|
205
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
|
206
|
+
<div className="text-center text-white">
|
|
207
|
+
<ExternalLink className="mx-auto h-8 w-8" />
|
|
208
|
+
<p className="mt-2 text-sm font-medium">View Demo</p>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<CardContent className="space-y-3 p-4">
|
|
214
|
+
{/* Category Badge */}
|
|
215
|
+
<span className="inline-block rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
|
216
|
+
{category}
|
|
217
|
+
</span>
|
|
218
|
+
|
|
219
|
+
{/* Title & Description */}
|
|
220
|
+
<div>
|
|
221
|
+
<h3 className="font-bold">{title}</h3>
|
|
222
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Design Style */}
|
|
226
|
+
<p className="text-xs text-muted-foreground">{designStyle}</p>
|
|
227
|
+
</CardContent>
|
|
228
|
+
</Card>
|
|
229
|
+
</a>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -79,38 +79,6 @@ export const Alertify = {
|
|
|
79
79
|
};
|
|
80
80
|
},
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* Show a custom toast with action button
|
|
84
|
-
*/
|
|
85
|
-
withAction(
|
|
86
|
-
message: string,
|
|
87
|
-
actionText: string,
|
|
88
|
-
onAction: () => void,
|
|
89
|
-
position: ToastPosition = 'bottom-right'
|
|
90
|
-
) {
|
|
91
|
-
if (currentToastId) {
|
|
92
|
-
toast.dismiss(currentToastId);
|
|
93
|
-
}
|
|
94
|
-
currentToastId = toast(
|
|
95
|
-
(t) => (
|
|
96
|
-
<div className="flex items-center gap-2">
|
|
97
|
-
<span>{message}</span>
|
|
98
|
-
<button
|
|
99
|
-
onClick={() => {
|
|
100
|
-
onAction();
|
|
101
|
-
toast.dismiss(t.id);
|
|
102
|
-
}}
|
|
103
|
-
className="rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
104
|
-
>
|
|
105
|
-
{actionText}
|
|
106
|
-
</button>
|
|
107
|
-
</div>
|
|
108
|
-
),
|
|
109
|
-
{ position, duration: 5000 }
|
|
110
|
-
);
|
|
111
|
-
return currentToastId;
|
|
112
|
-
},
|
|
113
|
-
|
|
114
82
|
/**
|
|
115
83
|
* Dismiss all toasts
|
|
116
84
|
*/
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"allowImportingTsExtensions": true,
|
|
11
11
|
"isolatedModules": true,
|
|
12
12
|
"moduleDetection": "force",
|
|
13
|
-
"
|
|
13
|
+
"emitDeclarationOnly": true,
|
|
14
|
+
|
|
15
|
+
/* Composite project (required for project references) */
|
|
16
|
+
"composite": true,
|
|
14
17
|
|
|
15
18
|
/* Linting */
|
|
16
19
|
"strict": true,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type UseFormReturn,
|
|
3
|
+
type FieldValues,
|
|
4
|
+
type UseFormProps,
|
|
5
|
+
} from 'react-hook-form';
|
|
6
|
+
import { type ZodType } from 'zod';
|
|
7
|
+
|
|
8
|
+
export interface UseZodFormProps<T extends FieldValues>
|
|
9
|
+
extends Omit<UseFormProps<T>, 'resolver'> {
|
|
10
|
+
schema: ZodType<T>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type UseZodFormReturn<T extends FieldValues> = UseFormReturn<T>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Common form field props for building reusable form components
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* interface TextFieldProps extends FormFieldProps {
|
|
20
|
+
* type?: 'text' | 'email' | 'password';
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* function TextField({ label, error, required, ...props }: TextFieldProps) {
|
|
24
|
+
* return (
|
|
25
|
+
* <div>
|
|
26
|
+
* {label && <label>{label}{required && '*'}</label>}
|
|
27
|
+
* <input {...props} />
|
|
28
|
+
* {error && <span className="text-red-500">{error}</span>}
|
|
29
|
+
* </div>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
export interface FormFieldProps {
|
|
34
|
+
label?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
required?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useForm,
|
|
3
|
+
type UseFormProps,
|
|
4
|
+
type FieldValues,
|
|
5
|
+
type Path,
|
|
6
|
+
type FieldErrors,
|
|
7
|
+
} from 'react-hook-form';
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
9
|
+
import { type ZodType } from 'zod';
|
|
10
|
+
|
|
11
|
+
interface UseZodFormProps<T extends FieldValues>
|
|
12
|
+
extends Omit<UseFormProps<T>, 'resolver'> {
|
|
13
|
+
schema: ZodType<T>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom hook that wraps React Hook Form with Zod validation
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const schema = z.object({
|
|
21
|
+
* email: z.string().email(),
|
|
22
|
+
* password: z.string().min(8),
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const form = useZodForm({ schema });
|
|
26
|
+
*
|
|
27
|
+
* <form onSubmit={form.handleSubmit(onSubmit)}>
|
|
28
|
+
* <input {...form.register('email')} />
|
|
29
|
+
* {form.formState.errors.email && <span>{form.formState.errors.email.message}</span>}
|
|
30
|
+
* </form>
|
|
31
|
+
*/
|
|
32
|
+
export function useZodForm<T extends FieldValues>({
|
|
33
|
+
schema,
|
|
34
|
+
mode = 'onBlur',
|
|
35
|
+
...formProps
|
|
36
|
+
}: UseZodFormProps<T>) {
|
|
37
|
+
return useForm<T>({
|
|
38
|
+
resolver: zodResolver(schema),
|
|
39
|
+
mode,
|
|
40
|
+
...formProps,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to get error message for a field
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const error = getFieldError(form.formState.errors, 'email');
|
|
49
|
+
* if (error) {
|
|
50
|
+
* console.log(error); // "Invalid email address"
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
export function getFieldError<T extends FieldValues>(
|
|
54
|
+
errors: FieldErrors<T>,
|
|
55
|
+
name: Path<T>
|
|
56
|
+
): string | undefined {
|
|
57
|
+
const error = errors[name];
|
|
58
|
+
return error?.message as string | undefined;
|
|
59
|
+
}
|