create-brainerce-store 1.27.5 → 1.27.6
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 +3 -12
- package/messages/en.json +382 -378
- package/messages/he.json +382 -378
- package/package.json +46 -46
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/src/app/checkout/page.tsx +973 -972
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +271 -271
- package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -59
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +27 -12
- package/templates/nextjs/base/src/app/products/page.tsx +475 -475
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +258 -184
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +592 -592
- package/templates/nextjs/base/src/lib/navigation.tsx.ejs +60 -60
- package/templates/nextjs/themes/luxury/globals.css +399 -399
- package/templates/nextjs/themes/luxury/theme.json +23 -23
- package/templates/nextjs/themes/playful/globals.css +400 -400
- package/templates/nextjs/themes/playful/theme.json +23 -23
|
@@ -1,184 +1,258 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{field.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { CheckoutCustomFieldDefinition } from 'brainerce';
|
|
5
|
+
import { useTranslations } from '@/lib/translations';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
interface CustomFieldsStepProps {
|
|
9
|
+
fields: CheckoutCustomFieldDefinition[];
|
|
10
|
+
values: Record<string, unknown>;
|
|
11
|
+
onChange: (key: string, value: unknown) => void;
|
|
12
|
+
onApply: () => void;
|
|
13
|
+
onUploadFile?: (file: File) => Promise<{ url: string; key: string }>;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
19
|
+
const ACCEPTED_IMAGE_TYPES = 'image/jpeg,image/png,image/webp,image/gif';
|
|
20
|
+
|
|
21
|
+
export function CustomFieldsStep({
|
|
22
|
+
fields,
|
|
23
|
+
values,
|
|
24
|
+
onChange,
|
|
25
|
+
onApply,
|
|
26
|
+
onUploadFile,
|
|
27
|
+
loading = false,
|
|
28
|
+
className,
|
|
29
|
+
}: CustomFieldsStepProps) {
|
|
30
|
+
const t = useTranslations('checkout');
|
|
31
|
+
const [uploadingKeys, setUploadingKeys] = useState<Set<string>>(new Set());
|
|
32
|
+
|
|
33
|
+
const isMissing = fields.some((f) => {
|
|
34
|
+
if (!f.required) return false;
|
|
35
|
+
const v = values[f.key];
|
|
36
|
+
return v === undefined || v === null || v === '';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn('space-y-4', className)}>
|
|
41
|
+
<p className="text-muted-foreground text-sm">{t('customFieldsSubtitle')}</p>
|
|
42
|
+
|
|
43
|
+
{fields.map((field) => {
|
|
44
|
+
const value = values[field.key];
|
|
45
|
+
const labelEl = (
|
|
46
|
+
<label
|
|
47
|
+
htmlFor={`cf-${field.key}`}
|
|
48
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
49
|
+
>
|
|
50
|
+
{field.name}
|
|
51
|
+
{field.required && <span className="text-destructive ms-1">*</span>}
|
|
52
|
+
</label>
|
|
53
|
+
);
|
|
54
|
+
const helpEl = field.description ? (
|
|
55
|
+
<p className="text-muted-foreground mt-1 text-xs">{field.description}</p>
|
|
56
|
+
) : null;
|
|
57
|
+
|
|
58
|
+
switch (field.type) {
|
|
59
|
+
case 'TEXT':
|
|
60
|
+
return (
|
|
61
|
+
<div key={field.key}>
|
|
62
|
+
{labelEl}
|
|
63
|
+
<input
|
|
64
|
+
id={`cf-${field.key}`}
|
|
65
|
+
type="text"
|
|
66
|
+
value={(value as string) ?? ''}
|
|
67
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
68
|
+
required={field.required}
|
|
69
|
+
minLength={field.minLength ?? undefined}
|
|
70
|
+
maxLength={field.maxLength ?? undefined}
|
|
71
|
+
className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
|
|
72
|
+
/>
|
|
73
|
+
{helpEl}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
case 'TEXTAREA':
|
|
78
|
+
return (
|
|
79
|
+
<div key={field.key}>
|
|
80
|
+
{labelEl}
|
|
81
|
+
<textarea
|
|
82
|
+
id={`cf-${field.key}`}
|
|
83
|
+
value={(value as string) ?? ''}
|
|
84
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
85
|
+
required={field.required}
|
|
86
|
+
minLength={field.minLength ?? undefined}
|
|
87
|
+
maxLength={field.maxLength ?? undefined}
|
|
88
|
+
rows={3}
|
|
89
|
+
className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
|
|
90
|
+
/>
|
|
91
|
+
{helpEl}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
case 'NUMBER':
|
|
96
|
+
return (
|
|
97
|
+
<div key={field.key}>
|
|
98
|
+
{labelEl}
|
|
99
|
+
<input
|
|
100
|
+
id={`cf-${field.key}`}
|
|
101
|
+
type="number"
|
|
102
|
+
value={(value as number | string) ?? ''}
|
|
103
|
+
onChange={(e) =>
|
|
104
|
+
onChange(field.key, e.target.value === '' ? '' : Number(e.target.value))
|
|
105
|
+
}
|
|
106
|
+
required={field.required}
|
|
107
|
+
min={field.minValue ?? undefined}
|
|
108
|
+
max={field.maxValue ?? undefined}
|
|
109
|
+
className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
|
|
110
|
+
/>
|
|
111
|
+
{helpEl}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
case 'BOOLEAN':
|
|
116
|
+
return (
|
|
117
|
+
<div key={field.key} className="flex items-start gap-2">
|
|
118
|
+
<input
|
|
119
|
+
id={`cf-${field.key}`}
|
|
120
|
+
type="checkbox"
|
|
121
|
+
checked={value === true}
|
|
122
|
+
onChange={(e) => onChange(field.key, e.target.checked)}
|
|
123
|
+
className="mt-1"
|
|
124
|
+
/>
|
|
125
|
+
<div className="flex-1">
|
|
126
|
+
<label
|
|
127
|
+
htmlFor={`cf-${field.key}`}
|
|
128
|
+
className="text-foreground text-sm font-medium"
|
|
129
|
+
>
|
|
130
|
+
{field.name}
|
|
131
|
+
{field.required && <span className="text-destructive ms-1">*</span>}
|
|
132
|
+
</label>
|
|
133
|
+
{helpEl}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
case 'SELECT':
|
|
139
|
+
return (
|
|
140
|
+
<div key={field.key}>
|
|
141
|
+
{labelEl}
|
|
142
|
+
<select
|
|
143
|
+
id={`cf-${field.key}`}
|
|
144
|
+
value={(value as string) ?? ''}
|
|
145
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
146
|
+
required={field.required}
|
|
147
|
+
className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
|
|
148
|
+
>
|
|
149
|
+
<option value="">{t('customFieldsSelectPlaceholder')}</option>
|
|
150
|
+
{field.options?.map((opt) => (
|
|
151
|
+
<option key={opt.value} value={opt.value}>
|
|
152
|
+
{opt.label}
|
|
153
|
+
</option>
|
|
154
|
+
))}
|
|
155
|
+
</select>
|
|
156
|
+
{helpEl}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
case 'DATE':
|
|
161
|
+
return (
|
|
162
|
+
<div key={field.key}>
|
|
163
|
+
{labelEl}
|
|
164
|
+
<input
|
|
165
|
+
id={`cf-${field.key}`}
|
|
166
|
+
type="date"
|
|
167
|
+
value={(value as string) ?? ''}
|
|
168
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
169
|
+
required={field.required}
|
|
170
|
+
className="border-border bg-background text-foreground w-full rounded border px-3 py-2 text-sm"
|
|
171
|
+
/>
|
|
172
|
+
{helpEl}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
case 'IMAGE': {
|
|
177
|
+
const isUploading = uploadingKeys.has(field.key);
|
|
178
|
+
return (
|
|
179
|
+
<div key={field.key}>
|
|
180
|
+
{labelEl}
|
|
181
|
+
{value ? (
|
|
182
|
+
<div className="relative inline-block">
|
|
183
|
+
<img
|
|
184
|
+
src={value as string}
|
|
185
|
+
alt={field.name}
|
|
186
|
+
className="border-border max-h-32 rounded border object-contain"
|
|
187
|
+
/>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => onChange(field.key, '')}
|
|
191
|
+
className="bg-background/80 text-foreground absolute end-1 top-1 rounded-full p-1 text-xs leading-none"
|
|
192
|
+
aria-label={t('customFieldsImageRemove')}
|
|
193
|
+
>
|
|
194
|
+
✕
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<label
|
|
199
|
+
htmlFor={`cf-${field.key}`}
|
|
200
|
+
className={cn(
|
|
201
|
+
'border-border flex cursor-pointer flex-col items-center gap-2 rounded border-2 border-dashed p-6 text-center transition-colors',
|
|
202
|
+
isUploading ? 'opacity-50' : 'hover:border-primary/40',
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
<span className="text-muted-foreground text-sm">
|
|
206
|
+
{isUploading ? t('customFieldsImageUploading') : t('customFieldsImageUpload')}
|
|
207
|
+
</span>
|
|
208
|
+
<input
|
|
209
|
+
id={`cf-${field.key}`}
|
|
210
|
+
type="file"
|
|
211
|
+
accept={ACCEPTED_IMAGE_TYPES}
|
|
212
|
+
disabled={isUploading || !onUploadFile}
|
|
213
|
+
className="hidden"
|
|
214
|
+
onChange={async (e) => {
|
|
215
|
+
const file = e.target.files?.[0];
|
|
216
|
+
if (!file || !onUploadFile) return;
|
|
217
|
+
if (file.size > MAX_IMAGE_SIZE) {
|
|
218
|
+
alert(t('customFieldsImageTooLarge'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
setUploadingKeys((prev) => new Set(prev).add(field.key));
|
|
222
|
+
try {
|
|
223
|
+
const result = await onUploadFile(file);
|
|
224
|
+
onChange(field.key, result.url);
|
|
225
|
+
} catch {
|
|
226
|
+
// Upload failed — user can retry
|
|
227
|
+
} finally {
|
|
228
|
+
setUploadingKeys((prev) => {
|
|
229
|
+
const next = new Set(prev);
|
|
230
|
+
next.delete(field.key);
|
|
231
|
+
return next;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</label>
|
|
237
|
+
)}
|
|
238
|
+
{helpEl}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
default:
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
})}
|
|
247
|
+
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
onClick={onApply}
|
|
251
|
+
disabled={loading || isMissing}
|
|
252
|
+
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"
|
|
253
|
+
>
|
|
254
|
+
{loading ? t('customFieldsApplying') : t('customFieldsApply')}
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|