create-brainerce-store 1.33.1 → 1.33.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/dist/index.js +1 -1
- package/messages/en.json +2 -1
- package/messages/he.json +2 -1
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/contact/page.tsx +96 -29
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.33.
|
|
34
|
+
version: "1.33.2",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
|
@@ -429,6 +429,7 @@
|
|
|
429
429
|
"sendAnother": "Send another message",
|
|
430
430
|
"thanksTitle": "Thank you!",
|
|
431
431
|
"thanksBody": "Your message has been received. We'll reply by email as soon as possible.",
|
|
432
|
-
"genericError": "Something went wrong. Please try again."
|
|
432
|
+
"genericError": "Something went wrong. Please try again.",
|
|
433
|
+
"fieldRequired": "{label} is required"
|
|
433
434
|
}
|
|
434
435
|
}
|
package/messages/he.json
CHANGED
|
@@ -429,6 +429,7 @@
|
|
|
429
429
|
"sendAnother": "שליחת הודעה נוספת",
|
|
430
430
|
"thanksTitle": "תודה!",
|
|
431
431
|
"thanksBody": "ההודעה התקבלה. נחזור אליך באימייל בהקדם האפשרי.",
|
|
432
|
-
"genericError": "משהו השתבש. אנא נסה שוב."
|
|
432
|
+
"genericError": "משהו השתבש. אנא נסה שוב.",
|
|
433
|
+
"fieldRequired": "השדה {label} הוא חובה"
|
|
433
434
|
}
|
|
434
435
|
}
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* MULTI_SELECT/CHECKBOX/URL/DATE)
|
|
13
13
|
* • toggle required, set placeholder/helpText/enumValues/validation
|
|
14
14
|
* • provide per-locale translations for every label/placeholder/helpText/successMessage
|
|
15
|
+
* • set field width (FULL/HALF/THIRD) for multi-column layouts
|
|
15
16
|
*
|
|
16
17
|
* Everything below is generic rendering logic — it adapts automatically to any
|
|
17
18
|
* form shape returned by the API, so **you should not hardcode field keys or
|
|
@@ -51,17 +52,36 @@ function isEmpty(v: FieldValue): boolean {
|
|
|
51
52
|
return v === false;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** Map field.width → Tailwind col-span utilities (6-column grid). */
|
|
56
|
+
function widthColSpan(width?: string): string {
|
|
57
|
+
switch (width) {
|
|
58
|
+
case 'HALF':
|
|
59
|
+
return 'col-span-6 sm:col-span-3';
|
|
60
|
+
case 'THIRD':
|
|
61
|
+
return 'col-span-6 sm:col-span-2';
|
|
62
|
+
default:
|
|
63
|
+
return 'col-span-6';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
const inputClass =
|
|
55
68
|
'border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2';
|
|
56
69
|
|
|
70
|
+
const inputErrorClass =
|
|
71
|
+
'border-red-400 bg-background text-foreground placeholder:text-muted-foreground focus:ring-red-200 focus:border-red-500 h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2';
|
|
72
|
+
|
|
57
73
|
const textareaClass =
|
|
58
74
|
'border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2';
|
|
59
75
|
|
|
76
|
+
const textareaErrorClass =
|
|
77
|
+
'border-red-400 bg-background text-foreground placeholder:text-muted-foreground focus:ring-red-200 focus:border-red-500 w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2';
|
|
78
|
+
|
|
60
79
|
export default function ContactPage() {
|
|
61
80
|
const t = useTranslations('contact');
|
|
62
81
|
const [schema, setSchema] = useState<ContactFormPublic | null>(null);
|
|
63
82
|
const [schemaError, setSchemaError] = useState<string | null>(null);
|
|
64
83
|
const [values, setValues] = useState<Record<string, FieldValue>>({});
|
|
84
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
65
85
|
const [honeypot, setHoneypot] = useState('');
|
|
66
86
|
const [loading, setLoading] = useState(false);
|
|
67
87
|
const [sent, setSent] = useState(false);
|
|
@@ -95,6 +115,14 @@ export default function ContactPage() {
|
|
|
95
115
|
|
|
96
116
|
function updateValue(key: string, value: FieldValue) {
|
|
97
117
|
setValues((prev) => ({ ...prev, [key]: value }));
|
|
118
|
+
// Clear field error on input change
|
|
119
|
+
if (errors[key]) {
|
|
120
|
+
setErrors((prev) => {
|
|
121
|
+
const next = { ...prev };
|
|
122
|
+
delete next[key];
|
|
123
|
+
return next;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
async function handleSubmit(e: React.FormEvent) {
|
|
@@ -106,6 +134,19 @@ export default function ContactPage() {
|
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
// ── Client-side required-field validation ──
|
|
138
|
+
const newErrors: Record<string, string> = {};
|
|
139
|
+
for (const field of schema.fields) {
|
|
140
|
+
if (field.isRequired && isEmpty(values[field.key] ?? defaultValueFor(field))) {
|
|
141
|
+
newErrors[field.key] = t('fieldRequired', { label: field.label });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(newErrors).length > 0) {
|
|
145
|
+
setErrors(newErrors);
|
|
146
|
+
return; // block submission — do NOT send to server
|
|
147
|
+
}
|
|
148
|
+
setErrors({});
|
|
149
|
+
|
|
109
150
|
try {
|
|
110
151
|
setLoading(true);
|
|
111
152
|
setSubmitError(null);
|
|
@@ -183,7 +224,7 @@ export default function ContactPage() {
|
|
|
183
224
|
</button>
|
|
184
225
|
</div>
|
|
185
226
|
) : (
|
|
186
|
-
<form onSubmit={handleSubmit} className="space-y-
|
|
227
|
+
<form onSubmit={handleSubmit} className="space-y-2" noValidate>
|
|
187
228
|
<div
|
|
188
229
|
aria-hidden="true"
|
|
189
230
|
className="pointer-events-none absolute -start-[10000px] h-0 w-0 overflow-hidden"
|
|
@@ -199,15 +240,19 @@ export default function ContactPage() {
|
|
|
199
240
|
/>
|
|
200
241
|
</div>
|
|
201
242
|
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
243
|
+
{/* 6-column grid — FULL=span 6, HALF=span 3, THIRD=span 2; stacks on mobile */}
|
|
244
|
+
<div className="grid grid-cols-6 gap-4">
|
|
245
|
+
{schema.fields.map((field) => (
|
|
246
|
+
<DynamicField
|
|
247
|
+
key={field.key}
|
|
248
|
+
field={field}
|
|
249
|
+
value={values[field.key] ?? defaultValueFor(field)}
|
|
250
|
+
error={errors[field.key]}
|
|
251
|
+
onChange={(v) => updateValue(field.key, v)}
|
|
252
|
+
t={t}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
211
256
|
|
|
212
257
|
<button
|
|
213
258
|
type="submit"
|
|
@@ -235,24 +280,34 @@ export default function ContactPage() {
|
|
|
235
280
|
function DynamicField({
|
|
236
281
|
field,
|
|
237
282
|
value,
|
|
283
|
+
error,
|
|
238
284
|
onChange,
|
|
239
285
|
t,
|
|
240
286
|
}: {
|
|
241
287
|
field: ContactFormPublicField;
|
|
242
288
|
value: FieldValue;
|
|
289
|
+
error?: string;
|
|
243
290
|
onChange: (v: FieldValue) => void;
|
|
244
291
|
t: (key: string, values?: Record<string, string>) => string;
|
|
245
292
|
}) {
|
|
246
293
|
const id = `contact-${field.key}`;
|
|
294
|
+
const hasError = !!error;
|
|
247
295
|
const label = (
|
|
248
296
|
<label htmlFor={id} className="text-foreground mb-1.5 block text-sm font-medium">
|
|
249
297
|
{field.label}
|
|
250
|
-
{
|
|
298
|
+
{field.isRequired ? (
|
|
299
|
+
<span className="text-red-500 ms-0.5" aria-hidden>*</span>
|
|
300
|
+
) : (
|
|
301
|
+
<span className="text-muted-foreground ms-1">({t('optional')})</span>
|
|
302
|
+
)}
|
|
251
303
|
</label>
|
|
252
304
|
);
|
|
253
305
|
const help = field.helpText ? (
|
|
254
306
|
<p className="text-muted-foreground mt-1 text-xs">{field.helpText}</p>
|
|
255
307
|
) : null;
|
|
308
|
+
const errorEl = error ? (
|
|
309
|
+
<p className="text-red-500 mt-1 text-xs">{error}</p>
|
|
310
|
+
) : null;
|
|
256
311
|
|
|
257
312
|
const maxLength = field.validation?.maxLength;
|
|
258
313
|
const minLength = field.validation?.minLength;
|
|
@@ -260,11 +315,13 @@ function DynamicField({
|
|
|
260
315
|
const max = field.validation?.max;
|
|
261
316
|
const pattern = field.validation?.pattern;
|
|
262
317
|
const strVal = typeof value === 'string' ? value : '';
|
|
318
|
+
const iClass = hasError ? inputErrorClass : inputClass;
|
|
319
|
+
const tClass = hasError ? textareaErrorClass : textareaClass;
|
|
263
320
|
|
|
264
321
|
switch (field.type) {
|
|
265
322
|
case 'TEXTAREA':
|
|
266
323
|
return (
|
|
267
|
-
<div>
|
|
324
|
+
<div className={widthColSpan(field.width)}>
|
|
268
325
|
{label}
|
|
269
326
|
<textarea
|
|
270
327
|
id={id}
|
|
@@ -275,22 +332,23 @@ function DynamicField({
|
|
|
275
332
|
placeholder={field.placeholder}
|
|
276
333
|
value={strVal}
|
|
277
334
|
onChange={(e) => onChange(e.target.value)}
|
|
278
|
-
className={
|
|
335
|
+
className={tClass}
|
|
279
336
|
/>
|
|
280
337
|
{help}
|
|
338
|
+
{errorEl}
|
|
281
339
|
</div>
|
|
282
340
|
);
|
|
283
341
|
|
|
284
342
|
case 'SELECT':
|
|
285
343
|
return (
|
|
286
|
-
<div>
|
|
344
|
+
<div className={widthColSpan(field.width)}>
|
|
287
345
|
{label}
|
|
288
346
|
<select
|
|
289
347
|
id={id}
|
|
290
348
|
required={field.isRequired}
|
|
291
349
|
value={strVal}
|
|
292
350
|
onChange={(e) => onChange(e.target.value)}
|
|
293
|
-
className={
|
|
351
|
+
className={iClass}
|
|
294
352
|
>
|
|
295
353
|
<option value="">—</option>
|
|
296
354
|
{field.enumValues?.map((opt) => (
|
|
@@ -300,13 +358,14 @@ function DynamicField({
|
|
|
300
358
|
))}
|
|
301
359
|
</select>
|
|
302
360
|
{help}
|
|
361
|
+
{errorEl}
|
|
303
362
|
</div>
|
|
304
363
|
);
|
|
305
364
|
|
|
306
365
|
case 'MULTI_SELECT': {
|
|
307
366
|
const arr = Array.isArray(value) ? value : [];
|
|
308
367
|
return (
|
|
309
|
-
<div>
|
|
368
|
+
<div className={widthColSpan(field.width)}>
|
|
310
369
|
{label}
|
|
311
370
|
<div className="space-y-2">
|
|
312
371
|
{field.enumValues?.map((opt) => {
|
|
@@ -327,13 +386,14 @@ function DynamicField({
|
|
|
327
386
|
})}
|
|
328
387
|
</div>
|
|
329
388
|
{help}
|
|
389
|
+
{errorEl}
|
|
330
390
|
</div>
|
|
331
391
|
);
|
|
332
392
|
}
|
|
333
393
|
|
|
334
394
|
case 'CHECKBOX':
|
|
335
395
|
return (
|
|
336
|
-
<div>
|
|
396
|
+
<div className={widthColSpan(field.width)}>
|
|
337
397
|
<label htmlFor={id} className="flex items-start gap-2 text-sm">
|
|
338
398
|
<input
|
|
339
399
|
id={id}
|
|
@@ -346,12 +406,13 @@ function DynamicField({
|
|
|
346
406
|
<span className="text-foreground">{field.label}</span>
|
|
347
407
|
</label>
|
|
348
408
|
{help}
|
|
409
|
+
{errorEl}
|
|
349
410
|
</div>
|
|
350
411
|
);
|
|
351
412
|
|
|
352
413
|
case 'NUMBER':
|
|
353
414
|
return (
|
|
354
|
-
<div>
|
|
415
|
+
<div className={widthColSpan(field.width)}>
|
|
355
416
|
{label}
|
|
356
417
|
<input
|
|
357
418
|
id={id}
|
|
@@ -362,15 +423,16 @@ function DynamicField({
|
|
|
362
423
|
placeholder={field.placeholder}
|
|
363
424
|
value={strVal}
|
|
364
425
|
onChange={(e) => onChange(e.target.value)}
|
|
365
|
-
className={
|
|
426
|
+
className={iClass}
|
|
366
427
|
/>
|
|
367
428
|
{help}
|
|
429
|
+
{errorEl}
|
|
368
430
|
</div>
|
|
369
431
|
);
|
|
370
432
|
|
|
371
433
|
case 'EMAIL':
|
|
372
434
|
return (
|
|
373
|
-
<div>
|
|
435
|
+
<div className={widthColSpan(field.width)}>
|
|
374
436
|
{label}
|
|
375
437
|
<input
|
|
376
438
|
id={id}
|
|
@@ -380,15 +442,16 @@ function DynamicField({
|
|
|
380
442
|
placeholder={field.placeholder}
|
|
381
443
|
value={strVal}
|
|
382
444
|
onChange={(e) => onChange(e.target.value)}
|
|
383
|
-
className={
|
|
445
|
+
className={iClass}
|
|
384
446
|
/>
|
|
385
447
|
{help}
|
|
448
|
+
{errorEl}
|
|
386
449
|
</div>
|
|
387
450
|
);
|
|
388
451
|
|
|
389
452
|
case 'PHONE':
|
|
390
453
|
return (
|
|
391
|
-
<div>
|
|
454
|
+
<div className={widthColSpan(field.width)}>
|
|
392
455
|
{label}
|
|
393
456
|
<input
|
|
394
457
|
id={id}
|
|
@@ -398,15 +461,16 @@ function DynamicField({
|
|
|
398
461
|
placeholder={field.placeholder}
|
|
399
462
|
value={strVal}
|
|
400
463
|
onChange={(e) => onChange(e.target.value)}
|
|
401
|
-
className={
|
|
464
|
+
className={iClass}
|
|
402
465
|
/>
|
|
403
466
|
{help}
|
|
467
|
+
{errorEl}
|
|
404
468
|
</div>
|
|
405
469
|
);
|
|
406
470
|
|
|
407
471
|
case 'URL':
|
|
408
472
|
return (
|
|
409
|
-
<div>
|
|
473
|
+
<div className={widthColSpan(field.width)}>
|
|
410
474
|
{label}
|
|
411
475
|
<input
|
|
412
476
|
id={id}
|
|
@@ -415,15 +479,16 @@ function DynamicField({
|
|
|
415
479
|
placeholder={field.placeholder}
|
|
416
480
|
value={strVal}
|
|
417
481
|
onChange={(e) => onChange(e.target.value)}
|
|
418
|
-
className={
|
|
482
|
+
className={iClass}
|
|
419
483
|
/>
|
|
420
484
|
{help}
|
|
485
|
+
{errorEl}
|
|
421
486
|
</div>
|
|
422
487
|
);
|
|
423
488
|
|
|
424
489
|
case 'DATE':
|
|
425
490
|
return (
|
|
426
|
-
<div>
|
|
491
|
+
<div className={widthColSpan(field.width)}>
|
|
427
492
|
{label}
|
|
428
493
|
<input
|
|
429
494
|
id={id}
|
|
@@ -431,16 +496,17 @@ function DynamicField({
|
|
|
431
496
|
required={field.isRequired}
|
|
432
497
|
value={strVal}
|
|
433
498
|
onChange={(e) => onChange(e.target.value)}
|
|
434
|
-
className={
|
|
499
|
+
className={iClass}
|
|
435
500
|
/>
|
|
436
501
|
{help}
|
|
502
|
+
{errorEl}
|
|
437
503
|
</div>
|
|
438
504
|
);
|
|
439
505
|
|
|
440
506
|
case 'TEXT':
|
|
441
507
|
default:
|
|
442
508
|
return (
|
|
443
|
-
<div>
|
|
509
|
+
<div className={widthColSpan(field.width)}>
|
|
444
510
|
{label}
|
|
445
511
|
<input
|
|
446
512
|
id={id}
|
|
@@ -452,9 +518,10 @@ function DynamicField({
|
|
|
452
518
|
placeholder={field.placeholder}
|
|
453
519
|
value={strVal}
|
|
454
520
|
onChange={(e) => onChange(e.target.value)}
|
|
455
|
-
className={
|
|
521
|
+
className={iClass}
|
|
456
522
|
/>
|
|
457
523
|
{help}
|
|
524
|
+
{errorEl}
|
|
458
525
|
</div>
|
|
459
526
|
);
|
|
460
527
|
}
|