torch-glare 2.4.1 → 2.4.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/apps/lib/components/Button.tsx +1 -1
- package/apps/lib/components/Card.tsx +47 -20
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/DatePicker.tsx +2 -0
- package/apps/lib/components/Drawer.tsx +66 -24
- package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
- package/apps/lib/components/FormBuilder/context.ts +71 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
- package/apps/lib/components/FormBuilder/header.tsx +105 -0
- package/apps/lib/components/FormBuilder/index.ts +37 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
- package/apps/lib/components/FormBuilder/submit.tsx +41 -0
- package/apps/lib/components/FormBuilder/types.ts +264 -0
- package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
- package/apps/lib/components/FormRenderer/index.ts +9 -0
- package/apps/lib/components/FormRenderer/types.ts +79 -0
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/ImageAttachment.tsx +36 -61
- package/apps/lib/components/Label.tsx +49 -42
- package/apps/lib/components/RadioCard.tsx +2 -0
- package/apps/lib/components/SearchableSelect.tsx +9 -3
- package/apps/lib/components/SectionBlock.tsx +16 -8
- package/apps/lib/components/Select.tsx +41 -120
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/components/Textarea.tsx +1 -1
- package/apps/lib/registry.json +51 -58
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/color.ts +175 -0
- package/docs/components/card.md +4 -2
- package/docs/components/chart-block-tool.md +5 -4
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/drawer.md +153 -102
- package/docs/components/form-builder.md +270 -0
- package/docs/components/form-renderer.md +228 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/image-attachment.md +10 -4
- package/docs/components/table-dnd-wrapper.md +5 -3
- package/docs/components/text-editor.md +18 -0
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +408 -0
- package/docs/how-to/guides.md +153 -179
- package/docs/tutorials/building-first-form.md +150 -159
- package/package.json +1 -1
- /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
- /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
- /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
|
@@ -16,9 +16,19 @@ related:
|
|
|
16
16
|
|
|
17
17
|
Learn how to build complete, production-ready forms with TORCH Glare. This tutorial covers everything from basic inputs to validation and error handling.
|
|
18
18
|
|
|
19
|
+
> **Build real forms with `FormBuilder`.** In an actual app, do **not** hand-wire fields with
|
|
20
|
+
> `useState` + `LabelField`/`InputField` rows as this tutorial does — that boilerplate is exactly
|
|
21
|
+
> what [`FormBuilder`](../components/form-builder.md) removes. With `FormBuilder` each field is one
|
|
22
|
+
> JSX child (`<FormBuilder.Text name="…" label="…" required />`) and validation comes from a
|
|
23
|
+
> react-hook-form resolver. Add [`FormRenderer`](../components/form-renderer.md) for page/drawer
|
|
24
|
+
> chrome + an `actions` slot for the Save, and [`FormSummary`](../components/form-summary.md) for live totals.
|
|
25
|
+
> See the **[Forms with FormBuilder](../how-to/forms-with-form-builder.md)** guide. The manual
|
|
26
|
+
> approach below is kept only to show what `FormBuilder` does under the hood.
|
|
27
|
+
|
|
19
28
|
## What You'll Build
|
|
20
29
|
|
|
21
30
|
A user registration form with:
|
|
31
|
+
|
|
22
32
|
- Multiple input types (text, email, password)
|
|
23
33
|
- Form validation
|
|
24
34
|
- Error handling and display
|
|
@@ -40,29 +50,27 @@ Let's start with a basic form structure:
|
|
|
40
50
|
|
|
41
51
|
```tsx
|
|
42
52
|
// components/RegistrationForm.tsx
|
|
43
|
-
|
|
53
|
+
"use client";
|
|
44
54
|
|
|
45
|
-
import { useState } from
|
|
55
|
+
import { useState } from "react";
|
|
46
56
|
import { Button } from "@/components/Button";
|
|
47
57
|
|
|
48
58
|
export default function RegistrationForm() {
|
|
49
59
|
const [formData, setFormData] = useState({
|
|
50
|
-
fullName:
|
|
51
|
-
email:
|
|
52
|
-
password:
|
|
53
|
-
confirmPassword:
|
|
60
|
+
fullName: "",
|
|
61
|
+
email: "",
|
|
62
|
+
password: "",
|
|
63
|
+
confirmPassword: "",
|
|
54
64
|
});
|
|
55
65
|
|
|
56
66
|
const handleSubmit = (e: React.FormEvent) => {
|
|
57
67
|
e.preventDefault();
|
|
58
|
-
console.log(
|
|
68
|
+
console.log("Form submitted:", formData);
|
|
59
69
|
};
|
|
60
70
|
|
|
61
71
|
return (
|
|
62
72
|
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
|
|
63
|
-
<h2 className="typography-display-medium-bold mb-6">
|
|
64
|
-
Create Account
|
|
65
|
-
</h2>
|
|
73
|
+
<h2 className="typography-display-medium-bold mb-6">Create Account</h2>
|
|
66
74
|
|
|
67
75
|
{/* We'll add fields here */}
|
|
68
76
|
|
|
@@ -83,37 +91,35 @@ export default function RegistrationForm() {
|
|
|
83
91
|
`LabelField` combines a label and input field with built-in styling:
|
|
84
92
|
|
|
85
93
|
```tsx
|
|
86
|
-
import { useState } from
|
|
94
|
+
import { useState } from "react";
|
|
87
95
|
import { Button } from "@/components/Button";
|
|
88
96
|
import { LabelField } from "@/components/LabelField";
|
|
89
97
|
|
|
90
98
|
export default function RegistrationForm() {
|
|
91
99
|
const [formData, setFormData] = useState({
|
|
92
|
-
fullName:
|
|
93
|
-
email:
|
|
94
|
-
password:
|
|
95
|
-
confirmPassword:
|
|
100
|
+
fullName: "",
|
|
101
|
+
email: "",
|
|
102
|
+
password: "",
|
|
103
|
+
confirmPassword: "",
|
|
96
104
|
});
|
|
97
105
|
|
|
98
106
|
const handleChange = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
-
setFormData(prev => ({
|
|
107
|
+
setFormData((prev) => ({
|
|
100
108
|
...prev,
|
|
101
|
-
[field]: e.target.value
|
|
109
|
+
[field]: e.target.value,
|
|
102
110
|
}));
|
|
103
111
|
};
|
|
104
112
|
|
|
105
113
|
return (
|
|
106
114
|
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
|
|
107
|
-
<h2 className="typography-display-medium-bold mb-6">
|
|
108
|
-
Create Account
|
|
109
|
-
</h2>
|
|
115
|
+
<h2 className="typography-display-medium-bold mb-6">Create Account</h2>
|
|
110
116
|
|
|
111
117
|
<LabelField
|
|
112
118
|
theme="light"
|
|
113
119
|
label="Full Name"
|
|
114
120
|
requiredLabel="*"
|
|
115
121
|
value={formData.fullName}
|
|
116
|
-
onChange={handleChange(
|
|
122
|
+
onChange={handleChange("fullName")}
|
|
117
123
|
placeholder="John Doe"
|
|
118
124
|
/>
|
|
119
125
|
|
|
@@ -123,7 +129,7 @@ export default function RegistrationForm() {
|
|
|
123
129
|
requiredLabel="*"
|
|
124
130
|
type="email"
|
|
125
131
|
value={formData.email}
|
|
126
|
-
onChange={handleChange(
|
|
132
|
+
onChange={handleChange("email")}
|
|
127
133
|
placeholder="you@example.com"
|
|
128
134
|
/>
|
|
129
135
|
|
|
@@ -133,7 +139,7 @@ export default function RegistrationForm() {
|
|
|
133
139
|
requiredLabel="*"
|
|
134
140
|
type="password"
|
|
135
141
|
value={formData.password}
|
|
136
|
-
onChange={handleChange(
|
|
142
|
+
onChange={handleChange("password")}
|
|
137
143
|
placeholder="••••••••"
|
|
138
144
|
/>
|
|
139
145
|
|
|
@@ -143,7 +149,7 @@ export default function RegistrationForm() {
|
|
|
143
149
|
requiredLabel="*"
|
|
144
150
|
type="password"
|
|
145
151
|
value={formData.confirmPassword}
|
|
146
|
-
onChange={handleChange(
|
|
152
|
+
onChange={handleChange("confirmPassword")}
|
|
147
153
|
placeholder="••••••••"
|
|
148
154
|
/>
|
|
149
155
|
|
|
@@ -162,9 +168,9 @@ export default function RegistrationForm() {
|
|
|
162
168
|
Let's add comprehensive validation:
|
|
163
169
|
|
|
164
170
|
```tsx
|
|
165
|
-
|
|
171
|
+
"use client";
|
|
166
172
|
|
|
167
|
-
import { useState, FormEvent } from
|
|
173
|
+
import { useState, FormEvent } from "react";
|
|
168
174
|
import { Button } from "@/components/Button";
|
|
169
175
|
import { FieldHint } from "@/components/FieldHint";
|
|
170
176
|
import { LabelField } from "@/components/LabelField";
|
|
@@ -178,10 +184,10 @@ interface FormErrors {
|
|
|
178
184
|
|
|
179
185
|
export default function RegistrationForm() {
|
|
180
186
|
const [formData, setFormData] = useState({
|
|
181
|
-
fullName:
|
|
182
|
-
email:
|
|
183
|
-
password:
|
|
184
|
-
confirmPassword:
|
|
187
|
+
fullName: "",
|
|
188
|
+
email: "",
|
|
189
|
+
password: "",
|
|
190
|
+
confirmPassword: "",
|
|
185
191
|
});
|
|
186
192
|
|
|
187
193
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
@@ -189,49 +195,48 @@ export default function RegistrationForm() {
|
|
|
189
195
|
|
|
190
196
|
// Validation functions
|
|
191
197
|
const validateFullName = (name: string): string | undefined => {
|
|
192
|
-
if (!name.trim()) return
|
|
193
|
-
if (name.trim().length < 2) return
|
|
198
|
+
if (!name.trim()) return "Full name is required";
|
|
199
|
+
if (name.trim().length < 2) return "Name must be at least 2 characters";
|
|
194
200
|
return undefined;
|
|
195
201
|
};
|
|
196
202
|
|
|
197
203
|
const validateEmail = (email: string): string | undefined => {
|
|
198
|
-
if (!email) return
|
|
204
|
+
if (!email) return "Email is required";
|
|
199
205
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
200
|
-
if (!emailRegex.test(email)) return
|
|
206
|
+
if (!emailRegex.test(email)) return "Please enter a valid email";
|
|
201
207
|
return undefined;
|
|
202
208
|
};
|
|
203
209
|
|
|
204
210
|
const validatePassword = (password: string): string | undefined => {
|
|
205
|
-
if (!password) return
|
|
206
|
-
if (password.length < 8) return
|
|
207
|
-
if (!/[A-Z]/.test(password)) return
|
|
208
|
-
if (!/[a-z]/.test(password)) return
|
|
209
|
-
if (!/[0-9]/.test(password)) return
|
|
211
|
+
if (!password) return "Password is required";
|
|
212
|
+
if (password.length < 8) return "Password must be at least 8 characters";
|
|
213
|
+
if (!/[A-Z]/.test(password)) return "Password must contain an uppercase letter";
|
|
214
|
+
if (!/[a-z]/.test(password)) return "Password must contain a lowercase letter";
|
|
215
|
+
if (!/[0-9]/.test(password)) return "Password must contain a number";
|
|
210
216
|
return undefined;
|
|
211
217
|
};
|
|
212
218
|
|
|
213
219
|
const validateConfirmPassword = (confirmPassword: string): string | undefined => {
|
|
214
|
-
if (!confirmPassword) return
|
|
215
|
-
if (confirmPassword !== formData.password) return
|
|
220
|
+
if (!confirmPassword) return "Please confirm your password";
|
|
221
|
+
if (confirmPassword !== formData.password) return "Passwords do not match";
|
|
216
222
|
return undefined;
|
|
217
223
|
};
|
|
218
224
|
|
|
219
225
|
// Handle field changes
|
|
220
|
-
const handleChange =
|
|
221
|
-
e: React.ChangeEvent<HTMLInputElement>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
};
|
|
226
|
+
const handleChange =
|
|
227
|
+
(field: keyof typeof formData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
228
|
+
const value = e.target.value;
|
|
229
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
230
|
+
|
|
231
|
+
// Validate on change if field has been touched
|
|
232
|
+
if (touched[field]) {
|
|
233
|
+
validateField(field, value);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
231
236
|
|
|
232
237
|
// Handle blur events
|
|
233
238
|
const handleBlur = (field: keyof typeof formData) => () => {
|
|
234
|
-
setTouched(prev => ({ ...prev, [field]: true }));
|
|
239
|
+
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
235
240
|
validateField(field, formData[field]);
|
|
236
241
|
};
|
|
237
242
|
|
|
@@ -240,21 +245,21 @@ export default function RegistrationForm() {
|
|
|
240
245
|
let error: string | undefined;
|
|
241
246
|
|
|
242
247
|
switch (field) {
|
|
243
|
-
case
|
|
248
|
+
case "fullName":
|
|
244
249
|
error = validateFullName(value);
|
|
245
250
|
break;
|
|
246
|
-
case
|
|
251
|
+
case "email":
|
|
247
252
|
error = validateEmail(value);
|
|
248
253
|
break;
|
|
249
|
-
case
|
|
254
|
+
case "password":
|
|
250
255
|
error = validatePassword(value);
|
|
251
256
|
break;
|
|
252
|
-
case
|
|
257
|
+
case "confirmPassword":
|
|
253
258
|
error = validateConfirmPassword(value);
|
|
254
259
|
break;
|
|
255
260
|
}
|
|
256
261
|
|
|
257
|
-
setErrors(prev => ({ ...prev, [field]: error }));
|
|
262
|
+
setErrors((prev) => ({ ...prev, [field]: error }));
|
|
258
263
|
};
|
|
259
264
|
|
|
260
265
|
// Validate all fields
|
|
@@ -274,7 +279,7 @@ export default function RegistrationForm() {
|
|
|
274
279
|
confirmPassword: true,
|
|
275
280
|
});
|
|
276
281
|
|
|
277
|
-
return !Object.values(newErrors).some(error => error !== undefined);
|
|
282
|
+
return !Object.values(newErrors).some((error) => error !== undefined);
|
|
278
283
|
};
|
|
279
284
|
|
|
280
285
|
// Handle form submission
|
|
@@ -282,16 +287,14 @@ export default function RegistrationForm() {
|
|
|
282
287
|
e.preventDefault();
|
|
283
288
|
|
|
284
289
|
if (validateForm()) {
|
|
285
|
-
console.log(
|
|
290
|
+
console.log("Form is valid:", formData);
|
|
286
291
|
// Submit form data
|
|
287
292
|
}
|
|
288
293
|
};
|
|
289
294
|
|
|
290
295
|
return (
|
|
291
296
|
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
|
|
292
|
-
<h2 className="typography-display-medium-bold mb-6">
|
|
293
|
-
Create Account
|
|
294
|
-
</h2>
|
|
297
|
+
<h2 className="typography-display-medium-bold mb-6">Create Account</h2>
|
|
295
298
|
|
|
296
299
|
<div>
|
|
297
300
|
<LabelField
|
|
@@ -299,8 +302,8 @@ export default function RegistrationForm() {
|
|
|
299
302
|
label="Full Name"
|
|
300
303
|
requiredLabel="*"
|
|
301
304
|
value={formData.fullName}
|
|
302
|
-
onChange={handleChange(
|
|
303
|
-
onBlur={handleBlur(
|
|
305
|
+
onChange={handleChange("fullName")}
|
|
306
|
+
onBlur={handleBlur("fullName")}
|
|
304
307
|
placeholder="John Doe"
|
|
305
308
|
errorMessage={touched.fullName ? errors.fullName : undefined}
|
|
306
309
|
/>
|
|
@@ -313,8 +316,8 @@ export default function RegistrationForm() {
|
|
|
313
316
|
requiredLabel="*"
|
|
314
317
|
type="email"
|
|
315
318
|
value={formData.email}
|
|
316
|
-
onChange={handleChange(
|
|
317
|
-
onBlur={handleBlur(
|
|
319
|
+
onChange={handleChange("email")}
|
|
320
|
+
onBlur={handleBlur("email")}
|
|
318
321
|
placeholder="you@example.com"
|
|
319
322
|
errorMessage={touched.email ? errors.email : undefined}
|
|
320
323
|
/>
|
|
@@ -327,8 +330,8 @@ export default function RegistrationForm() {
|
|
|
327
330
|
requiredLabel="*"
|
|
328
331
|
type="password"
|
|
329
332
|
value={formData.password}
|
|
330
|
-
onChange={handleChange(
|
|
331
|
-
onBlur={handleBlur(
|
|
333
|
+
onChange={handleChange("password")}
|
|
334
|
+
onBlur={handleBlur("password")}
|
|
332
335
|
placeholder="••••••••"
|
|
333
336
|
errorMessage={touched.password ? errors.password : undefined}
|
|
334
337
|
/>
|
|
@@ -341,8 +344,8 @@ export default function RegistrationForm() {
|
|
|
341
344
|
requiredLabel="*"
|
|
342
345
|
type="password"
|
|
343
346
|
value={formData.confirmPassword}
|
|
344
|
-
onChange={handleChange(
|
|
345
|
-
onBlur={handleBlur(
|
|
347
|
+
onChange={handleChange("confirmPassword")}
|
|
348
|
+
onBlur={handleBlur("confirmPassword")}
|
|
346
349
|
placeholder="••••••••"
|
|
347
350
|
errorMessage={touched.confirmPassword ? errors.confirmPassword : undefined}
|
|
348
351
|
/>
|
|
@@ -376,8 +379,8 @@ import { PasswordLevel } from "@/components/PasswordLevel";
|
|
|
376
379
|
requiredLabel="*"
|
|
377
380
|
type="password"
|
|
378
381
|
value={formData.password}
|
|
379
|
-
onChange={handleChange(
|
|
380
|
-
onBlur={handleBlur(
|
|
382
|
+
onChange={handleChange("password")}
|
|
383
|
+
onBlur={handleBlur("password")}
|
|
381
384
|
placeholder="••••••••"
|
|
382
385
|
errorMessage={touched.password ? errors.password : undefined}
|
|
383
386
|
/>
|
|
@@ -388,7 +391,7 @@ import { PasswordLevel } from "@/components/PasswordLevel";
|
|
|
388
391
|
<PasswordLevel theme="light" value={formData.password} />
|
|
389
392
|
</div>
|
|
390
393
|
)}
|
|
391
|
-
</div
|
|
394
|
+
</div>;
|
|
392
395
|
```
|
|
393
396
|
|
|
394
397
|
---
|
|
@@ -409,8 +412,8 @@ import { LabelField } from "@/components/LabelField";
|
|
|
409
412
|
requiredLabel="*"
|
|
410
413
|
type="email"
|
|
411
414
|
value={formData.email}
|
|
412
|
-
onChange={handleChange(
|
|
413
|
-
onBlur={handleBlur(
|
|
415
|
+
onChange={handleChange("email")}
|
|
416
|
+
onBlur={handleBlur("email")}
|
|
414
417
|
placeholder="you@example.com"
|
|
415
418
|
errorMessage={touched.email ? errors.email : undefined}
|
|
416
419
|
/>
|
|
@@ -423,7 +426,7 @@ import { LabelField } from "@/components/LabelField";
|
|
|
423
426
|
className="mt-2"
|
|
424
427
|
/>
|
|
425
428
|
)}
|
|
426
|
-
</div
|
|
429
|
+
</div>;
|
|
427
430
|
```
|
|
428
431
|
|
|
429
432
|
---
|
|
@@ -444,12 +447,12 @@ const handleSubmit = async (e: FormEvent) => {
|
|
|
444
447
|
|
|
445
448
|
try {
|
|
446
449
|
// Simulate API call
|
|
447
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
448
451
|
|
|
449
|
-
console.log(
|
|
452
|
+
console.log("Registration successful:", formData);
|
|
450
453
|
// Handle success (e.g., redirect, show success message)
|
|
451
454
|
} catch (error) {
|
|
452
|
-
console.error(
|
|
455
|
+
console.error("Registration failed:", error);
|
|
453
456
|
// Handle error
|
|
454
457
|
} finally {
|
|
455
458
|
setIsSubmitting(false);
|
|
@@ -457,15 +460,9 @@ const handleSubmit = async (e: FormEvent) => {
|
|
|
457
460
|
};
|
|
458
461
|
|
|
459
462
|
// Update button
|
|
460
|
-
<Button
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
type="submit"
|
|
464
|
-
className="w-full"
|
|
465
|
-
disabled={isSubmitting}
|
|
466
|
-
>
|
|
467
|
-
{isSubmitting ? 'Registering...' : 'Register'}
|
|
468
|
-
</Button>
|
|
463
|
+
<Button theme="light" variant="PrimeStyle" type="submit" className="w-full" disabled={isSubmitting}>
|
|
464
|
+
{isSubmitting ? "Registering..." : "Register"}
|
|
465
|
+
</Button>;
|
|
469
466
|
```
|
|
470
467
|
|
|
471
468
|
---
|
|
@@ -483,7 +480,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|
|
483
480
|
e.preventDefault();
|
|
484
481
|
|
|
485
482
|
if (!validateForm()) {
|
|
486
|
-
toast.error(
|
|
483
|
+
toast.error("Please fix the errors in the form");
|
|
487
484
|
return;
|
|
488
485
|
}
|
|
489
486
|
|
|
@@ -491,21 +488,21 @@ const handleSubmit = async (e: FormEvent) => {
|
|
|
491
488
|
|
|
492
489
|
try {
|
|
493
490
|
// Simulate API call
|
|
494
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
491
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
495
492
|
|
|
496
|
-
toast.success(
|
|
493
|
+
toast.success("Account created successfully!");
|
|
497
494
|
|
|
498
495
|
// Reset form
|
|
499
496
|
setFormData({
|
|
500
|
-
fullName:
|
|
501
|
-
email:
|
|
502
|
-
password:
|
|
503
|
-
confirmPassword:
|
|
497
|
+
fullName: "",
|
|
498
|
+
email: "",
|
|
499
|
+
password: "",
|
|
500
|
+
confirmPassword: "",
|
|
504
501
|
});
|
|
505
502
|
setErrors({});
|
|
506
503
|
setTouched({});
|
|
507
504
|
} catch (error) {
|
|
508
|
-
toast.error(
|
|
505
|
+
toast.error("Registration failed. Please try again.");
|
|
509
506
|
} finally {
|
|
510
507
|
setIsSubmitting(false);
|
|
511
508
|
}
|
|
@@ -519,9 +516,9 @@ const handleSubmit = async (e: FormEvent) => {
|
|
|
519
516
|
Here's the complete, production-ready form:
|
|
520
517
|
|
|
521
518
|
```tsx
|
|
522
|
-
|
|
519
|
+
"use client";
|
|
523
520
|
|
|
524
|
-
import { useState, FormEvent } from
|
|
521
|
+
import { useState, FormEvent } from "react";
|
|
525
522
|
import { Button } from "@/components/Button";
|
|
526
523
|
import { FieldHint } from "@/components/FieldHint";
|
|
527
524
|
import { LabelField } from "@/components/LabelField";
|
|
@@ -537,10 +534,10 @@ interface FormErrors {
|
|
|
537
534
|
|
|
538
535
|
export default function RegistrationForm() {
|
|
539
536
|
const [formData, setFormData] = useState({
|
|
540
|
-
fullName:
|
|
541
|
-
email:
|
|
542
|
-
password:
|
|
543
|
-
confirmPassword:
|
|
537
|
+
fullName: "",
|
|
538
|
+
email: "",
|
|
539
|
+
password: "",
|
|
540
|
+
confirmPassword: "",
|
|
544
541
|
});
|
|
545
542
|
|
|
546
543
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
@@ -549,46 +546,45 @@ export default function RegistrationForm() {
|
|
|
549
546
|
|
|
550
547
|
// Validation functions
|
|
551
548
|
const validateFullName = (name: string): string | undefined => {
|
|
552
|
-
if (!name.trim()) return
|
|
553
|
-
if (name.trim().length < 2) return
|
|
549
|
+
if (!name.trim()) return "Full name is required";
|
|
550
|
+
if (name.trim().length < 2) return "Name must be at least 2 characters";
|
|
554
551
|
return undefined;
|
|
555
552
|
};
|
|
556
553
|
|
|
557
554
|
const validateEmail = (email: string): string | undefined => {
|
|
558
|
-
if (!email) return
|
|
555
|
+
if (!email) return "Email is required";
|
|
559
556
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
560
|
-
if (!emailRegex.test(email)) return
|
|
557
|
+
if (!emailRegex.test(email)) return "Please enter a valid email";
|
|
561
558
|
return undefined;
|
|
562
559
|
};
|
|
563
560
|
|
|
564
561
|
const validatePassword = (password: string): string | undefined => {
|
|
565
|
-
if (!password) return
|
|
566
|
-
if (password.length < 8) return
|
|
567
|
-
if (!/[A-Z]/.test(password)) return
|
|
568
|
-
if (!/[a-z]/.test(password)) return
|
|
569
|
-
if (!/[0-9]/.test(password)) return
|
|
562
|
+
if (!password) return "Password is required";
|
|
563
|
+
if (password.length < 8) return "Password must be at least 8 characters";
|
|
564
|
+
if (!/[A-Z]/.test(password)) return "Password must contain an uppercase letter";
|
|
565
|
+
if (!/[a-z]/.test(password)) return "Password must contain a lowercase letter";
|
|
566
|
+
if (!/[0-9]/.test(password)) return "Password must contain a number";
|
|
570
567
|
return undefined;
|
|
571
568
|
};
|
|
572
569
|
|
|
573
570
|
const validateConfirmPassword = (confirmPassword: string): string | undefined => {
|
|
574
|
-
if (!confirmPassword) return
|
|
575
|
-
if (confirmPassword !== formData.password) return
|
|
571
|
+
if (!confirmPassword) return "Please confirm your password";
|
|
572
|
+
if (confirmPassword !== formData.password) return "Passwords do not match";
|
|
576
573
|
return undefined;
|
|
577
574
|
};
|
|
578
575
|
|
|
579
|
-
const handleChange =
|
|
580
|
-
e: React.ChangeEvent<HTMLInputElement>
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
setFormData(prev => ({ ...prev, [field]: value }));
|
|
576
|
+
const handleChange =
|
|
577
|
+
(field: keyof typeof formData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
578
|
+
const value = e.target.value;
|
|
579
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
584
580
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
581
|
+
if (touched[field]) {
|
|
582
|
+
validateField(field, value);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
589
585
|
|
|
590
586
|
const handleBlur = (field: keyof typeof formData) => () => {
|
|
591
|
-
setTouched(prev => ({ ...prev, [field]: true }));
|
|
587
|
+
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
592
588
|
validateField(field, formData[field]);
|
|
593
589
|
};
|
|
594
590
|
|
|
@@ -596,21 +592,21 @@ export default function RegistrationForm() {
|
|
|
596
592
|
let error: string | undefined;
|
|
597
593
|
|
|
598
594
|
switch (field) {
|
|
599
|
-
case
|
|
595
|
+
case "fullName":
|
|
600
596
|
error = validateFullName(value);
|
|
601
597
|
break;
|
|
602
|
-
case
|
|
598
|
+
case "email":
|
|
603
599
|
error = validateEmail(value);
|
|
604
600
|
break;
|
|
605
|
-
case
|
|
601
|
+
case "password":
|
|
606
602
|
error = validatePassword(value);
|
|
607
603
|
break;
|
|
608
|
-
case
|
|
604
|
+
case "confirmPassword":
|
|
609
605
|
error = validateConfirmPassword(value);
|
|
610
606
|
break;
|
|
611
607
|
}
|
|
612
608
|
|
|
613
|
-
setErrors(prev => ({ ...prev, [field]: error }));
|
|
609
|
+
setErrors((prev) => ({ ...prev, [field]: error }));
|
|
614
610
|
};
|
|
615
611
|
|
|
616
612
|
const validateForm = (): boolean => {
|
|
@@ -629,14 +625,14 @@ export default function RegistrationForm() {
|
|
|
629
625
|
confirmPassword: true,
|
|
630
626
|
});
|
|
631
627
|
|
|
632
|
-
return !Object.values(newErrors).some(error => error !== undefined);
|
|
628
|
+
return !Object.values(newErrors).some((error) => error !== undefined);
|
|
633
629
|
};
|
|
634
630
|
|
|
635
631
|
const handleSubmit = async (e: FormEvent) => {
|
|
636
632
|
e.preventDefault();
|
|
637
633
|
|
|
638
634
|
if (!validateForm()) {
|
|
639
|
-
toast.error(
|
|
635
|
+
toast.error("Please fix the errors in the form");
|
|
640
636
|
return;
|
|
641
637
|
}
|
|
642
638
|
|
|
@@ -644,21 +640,21 @@ export default function RegistrationForm() {
|
|
|
644
640
|
|
|
645
641
|
try {
|
|
646
642
|
// Simulate API call
|
|
647
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
643
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
648
644
|
|
|
649
|
-
toast.success(
|
|
645
|
+
toast.success("Account created successfully!");
|
|
650
646
|
|
|
651
647
|
// Reset form
|
|
652
648
|
setFormData({
|
|
653
|
-
fullName:
|
|
654
|
-
email:
|
|
655
|
-
password:
|
|
656
|
-
confirmPassword:
|
|
649
|
+
fullName: "",
|
|
650
|
+
email: "",
|
|
651
|
+
password: "",
|
|
652
|
+
confirmPassword: "",
|
|
657
653
|
});
|
|
658
654
|
setErrors({});
|
|
659
655
|
setTouched({});
|
|
660
656
|
} catch (error) {
|
|
661
|
-
toast.error(
|
|
657
|
+
toast.error("Registration failed. Please try again.");
|
|
662
658
|
} finally {
|
|
663
659
|
setIsSubmitting(false);
|
|
664
660
|
}
|
|
@@ -676,9 +672,7 @@ export default function RegistrationForm() {
|
|
|
676
672
|
"
|
|
677
673
|
>
|
|
678
674
|
<div className="mb-6">
|
|
679
|
-
<h2 className="typography-display-medium-bold mb-2">
|
|
680
|
-
Create Account
|
|
681
|
-
</h2>
|
|
675
|
+
<h2 className="typography-display-medium-bold mb-2">Create Account</h2>
|
|
682
676
|
<p className="typography-body-medium-regular text-content-presentation-global-secondary">
|
|
683
677
|
Join us and get started today
|
|
684
678
|
</p>
|
|
@@ -690,8 +684,8 @@ export default function RegistrationForm() {
|
|
|
690
684
|
label="Full Name"
|
|
691
685
|
requiredLabel="*"
|
|
692
686
|
value={formData.fullName}
|
|
693
|
-
onChange={handleChange(
|
|
694
|
-
onBlur={handleBlur(
|
|
687
|
+
onChange={handleChange("fullName")}
|
|
688
|
+
onBlur={handleBlur("fullName")}
|
|
695
689
|
placeholder="John Doe"
|
|
696
690
|
errorMessage={touched.fullName ? errors.fullName : undefined}
|
|
697
691
|
/>
|
|
@@ -704,8 +698,8 @@ export default function RegistrationForm() {
|
|
|
704
698
|
requiredLabel="*"
|
|
705
699
|
type="email"
|
|
706
700
|
value={formData.email}
|
|
707
|
-
onChange={handleChange(
|
|
708
|
-
onBlur={handleBlur(
|
|
701
|
+
onChange={handleChange("email")}
|
|
702
|
+
onBlur={handleBlur("email")}
|
|
709
703
|
placeholder="you@example.com"
|
|
710
704
|
errorMessage={touched.email ? errors.email : undefined}
|
|
711
705
|
/>
|
|
@@ -726,8 +720,8 @@ export default function RegistrationForm() {
|
|
|
726
720
|
requiredLabel="*"
|
|
727
721
|
type="password"
|
|
728
722
|
value={formData.password}
|
|
729
|
-
onChange={handleChange(
|
|
730
|
-
onBlur={handleBlur(
|
|
723
|
+
onChange={handleChange("password")}
|
|
724
|
+
onBlur={handleBlur("password")}
|
|
731
725
|
placeholder="••••••••"
|
|
732
726
|
errorMessage={touched.password ? errors.password : undefined}
|
|
733
727
|
/>
|
|
@@ -745,8 +739,8 @@ export default function RegistrationForm() {
|
|
|
745
739
|
requiredLabel="*"
|
|
746
740
|
type="password"
|
|
747
741
|
value={formData.confirmPassword}
|
|
748
|
-
onChange={handleChange(
|
|
749
|
-
onBlur={handleBlur(
|
|
742
|
+
onChange={handleChange("confirmPassword")}
|
|
743
|
+
onBlur={handleBlur("confirmPassword")}
|
|
750
744
|
placeholder="••••••••"
|
|
751
745
|
errorMessage={touched.confirmPassword ? errors.confirmPassword : undefined}
|
|
752
746
|
/>
|
|
@@ -759,11 +753,11 @@ export default function RegistrationForm() {
|
|
|
759
753
|
className="w-full"
|
|
760
754
|
disabled={isSubmitting}
|
|
761
755
|
>
|
|
762
|
-
{isSubmitting ?
|
|
756
|
+
{isSubmitting ? "Registering..." : "Register"}
|
|
763
757
|
</Button>
|
|
764
758
|
|
|
765
759
|
<p className="typography-body-small-regular text-content-presentation-global-secondary text-center">
|
|
766
|
-
Already have an account?{
|
|
760
|
+
Already have an account?{" "}
|
|
767
761
|
<a href="/login" className="text-content-presentation-action-light-primary">
|
|
768
762
|
Sign in
|
|
769
763
|
</a>
|
|
@@ -806,11 +800,8 @@ errorMessage={touched.email ? errors.email : undefined}
|
|
|
806
800
|
### 3. Disable Submit During Submission
|
|
807
801
|
|
|
808
802
|
```tsx
|
|
809
|
-
<Button
|
|
810
|
-
|
|
811
|
-
disabled={isSubmitting || Object.keys(errors).length > 0}
|
|
812
|
-
>
|
|
813
|
-
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
803
|
+
<Button type="submit" disabled={isSubmitting || Object.keys(errors).length > 0}>
|
|
804
|
+
{isSubmitting ? "Submitting..." : "Submit"}
|
|
814
805
|
</Button>
|
|
815
806
|
```
|
|
816
807
|
|
|
@@ -818,10 +809,10 @@ errorMessage={touched.email ? errors.email : undefined}
|
|
|
818
809
|
|
|
819
810
|
```tsx
|
|
820
811
|
// ✓ Good - Specific and actionable
|
|
821
|
-
|
|
812
|
+
"Password must contain at least one uppercase letter";
|
|
822
813
|
|
|
823
814
|
// ✗ Bad - Vague
|
|
824
|
-
|
|
815
|
+
"Invalid password";
|
|
825
816
|
```
|
|
826
817
|
|
|
827
818
|
### 5. Reset Form After Success
|
|
@@ -831,7 +822,7 @@ const handleSuccess = () => {
|
|
|
831
822
|
setFormData(initialState);
|
|
832
823
|
setErrors({});
|
|
833
824
|
setTouched({});
|
|
834
|
-
toast.success(
|
|
825
|
+
toast.success("Success!");
|
|
835
826
|
};
|
|
836
827
|
```
|
|
837
828
|
|