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 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.1",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.33.1",
3
+ "version": "1.33.2",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -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-4" noValidate>
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
- {schema.fields.map((field) => (
203
- <DynamicField
204
- key={field.key}
205
- field={field}
206
- value={values[field.key] ?? defaultValueFor(field)}
207
- onChange={(v) => updateValue(field.key, v)}
208
- t={t}
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
- {!field.isRequired && <span className="text-muted-foreground ms-1">({t('optional')})</span>}
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={textareaClass}
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={inputClass}
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={inputClass}
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={inputClass}
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={inputClass}
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={inputClass}
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={inputClass}
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={inputClass}
521
+ className={iClass}
456
522
  />
457
523
  {help}
524
+ {errorEl}
458
525
  </div>
459
526
  );
460
527
  }