create-brainerce-store 1.26.0 → 1.27.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.
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import type { CheckoutCustomFieldDefinition } from 'brainerce';
4
+ import { useTranslations } from '@/lib/translations';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface CustomFieldsStepProps {
8
+ fields: CheckoutCustomFieldDefinition[];
9
+ values: Record<string, unknown>;
10
+ onChange: (key: string, value: unknown) => void;
11
+ onApply: () => void;
12
+ loading?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ export function CustomFieldsStep({
17
+ fields,
18
+ values,
19
+ onChange,
20
+ onApply,
21
+ loading = false,
22
+ className,
23
+ }: CustomFieldsStepProps) {
24
+ const t = useTranslations('checkout');
25
+
26
+ const isMissing = fields.some((f) => {
27
+ if (!f.required) return false;
28
+ const v = values[f.key];
29
+ return v === undefined || v === null || v === '';
30
+ });
31
+
32
+ return (
33
+ <div className={cn('space-y-4', className)}>
34
+ <p className="text-muted-foreground text-sm">{t('customFieldsSubtitle')}</p>
35
+
36
+ {fields.map((field) => {
37
+ const value = values[field.key];
38
+ const labelEl = (
39
+ <label
40
+ htmlFor={`cf-${field.key}`}
41
+ className="text-foreground mb-1 block text-sm font-medium"
42
+ >
43
+ {field.name}
44
+ {field.required && <span className="text-destructive ms-1">*</span>}
45
+ </label>
46
+ );
47
+ const helpEl = field.description ? (
48
+ <p className="text-muted-foreground mt-1 text-xs">{field.description}</p>
49
+ ) : null;
50
+
51
+ switch (field.type) {
52
+ case 'TEXT':
53
+ return (
54
+ <div key={field.key}>
55
+ {labelEl}
56
+ <input
57
+ id={`cf-${field.key}`}
58
+ type="text"
59
+ value={(value as string) ?? ''}
60
+ onChange={(e) => onChange(field.key, e.target.value)}
61
+ required={field.required}
62
+ minLength={field.minLength ?? undefined}
63
+ maxLength={field.maxLength ?? undefined}
64
+ className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
65
+ />
66
+ {helpEl}
67
+ </div>
68
+ );
69
+
70
+ case 'TEXTAREA':
71
+ return (
72
+ <div key={field.key}>
73
+ {labelEl}
74
+ <textarea
75
+ id={`cf-${field.key}`}
76
+ value={(value as string) ?? ''}
77
+ onChange={(e) => onChange(field.key, e.target.value)}
78
+ required={field.required}
79
+ minLength={field.minLength ?? undefined}
80
+ maxLength={field.maxLength ?? undefined}
81
+ rows={3}
82
+ className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
83
+ />
84
+ {helpEl}
85
+ </div>
86
+ );
87
+
88
+ case 'NUMBER':
89
+ return (
90
+ <div key={field.key}>
91
+ {labelEl}
92
+ <input
93
+ id={`cf-${field.key}`}
94
+ type="number"
95
+ value={(value as number | string) ?? ''}
96
+ onChange={(e) =>
97
+ onChange(field.key, e.target.value === '' ? '' : Number(e.target.value))
98
+ }
99
+ required={field.required}
100
+ min={field.minValue ?? undefined}
101
+ max={field.maxValue ?? undefined}
102
+ className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
103
+ />
104
+ {helpEl}
105
+ </div>
106
+ );
107
+
108
+ case 'BOOLEAN':
109
+ return (
110
+ <div key={field.key} className="flex items-start gap-2">
111
+ <input
112
+ id={`cf-${field.key}`}
113
+ type="checkbox"
114
+ checked={value === true}
115
+ onChange={(e) => onChange(field.key, e.target.checked)}
116
+ className="mt-1"
117
+ />
118
+ <div className="flex-1">
119
+ <label
120
+ htmlFor={`cf-${field.key}`}
121
+ className="text-foreground text-sm font-medium"
122
+ >
123
+ {field.name}
124
+ {field.required && <span className="text-destructive ms-1">*</span>}
125
+ </label>
126
+ {helpEl}
127
+ </div>
128
+ </div>
129
+ );
130
+
131
+ case 'SELECT':
132
+ return (
133
+ <div key={field.key}>
134
+ {labelEl}
135
+ <select
136
+ id={`cf-${field.key}`}
137
+ value={(value as string) ?? ''}
138
+ onChange={(e) => onChange(field.key, e.target.value)}
139
+ required={field.required}
140
+ className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
141
+ >
142
+ <option value="">{t('customFieldsSelectPlaceholder')}</option>
143
+ {field.options?.map((opt) => (
144
+ <option key={opt.value} value={opt.value}>
145
+ {opt.label}
146
+ </option>
147
+ ))}
148
+ </select>
149
+ {helpEl}
150
+ </div>
151
+ );
152
+
153
+ case 'DATE':
154
+ return (
155
+ <div key={field.key}>
156
+ {labelEl}
157
+ <input
158
+ id={`cf-${field.key}`}
159
+ type="date"
160
+ value={(value as string) ?? ''}
161
+ onChange={(e) => onChange(field.key, e.target.value)}
162
+ required={field.required}
163
+ className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
164
+ />
165
+ {helpEl}
166
+ </div>
167
+ );
168
+
169
+ default:
170
+ return null;
171
+ }
172
+ })}
173
+
174
+ <button
175
+ type="button"
176
+ onClick={onApply}
177
+ disabled={loading || isMissing}
178
+ className="bg-primary text-primary-foreground w-full rounded px-4 py-3 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
179
+ >
180
+ {loading ? t('customFieldsApplying') : t('customFieldsApply')}
181
+ </button>
182
+ </div>
183
+ );
184
+ }