create-brainerce-store 1.0.0 → 1.1.0

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.
@@ -80,7 +80,7 @@ export function OAuthButtons({ className }: OAuthButtonsProps) {
80
80
  try {
81
81
  setRedirecting(provider);
82
82
  const client = getClient();
83
- const redirectUrl = window.location.origin + '/auth/callback?provider=' + provider;
83
+ const redirectUrl = window.location.origin + '/auth/callback';
84
84
  const result = await client.getOAuthAuthorizeUrl(provider, { redirectUrl });
85
85
  window.location.href = result.authorizationUrl;
86
86
  } catch (err) {
@@ -1,273 +1,302 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import type { SetShippingAddressDto } from 'brainerce';
5
- import { cn } from '@/lib/utils';
6
-
7
- interface CheckoutFormProps {
8
- onSubmit: (address: SetShippingAddressDto) => void;
9
- loading?: boolean;
10
- initialValues?: Partial<SetShippingAddressDto>;
11
- className?: string;
12
- }
13
-
14
- export function CheckoutForm({
15
- onSubmit,
16
- loading = false,
17
- initialValues,
18
- className,
19
- }: CheckoutFormProps) {
20
- const [formData, setFormData] = useState<SetShippingAddressDto>({
21
- email: initialValues?.email || '',
22
- firstName: initialValues?.firstName || '',
23
- lastName: initialValues?.lastName || '',
24
- line1: initialValues?.line1 || '',
25
- line2: initialValues?.line2 || '',
26
- city: initialValues?.city || '',
27
- region: initialValues?.region || '',
28
- postalCode: initialValues?.postalCode || '',
29
- country: initialValues?.country || '',
30
- phone: initialValues?.phone || '',
31
- });
32
- const [errors, setErrors] = useState<Record<string, string>>({});
33
-
34
- function validate(): boolean {
35
- const newErrors: Record<string, string> = {};
36
-
37
- if (!formData.email.trim()) {
38
- newErrors.email = 'Email is required';
39
- } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
40
- newErrors.email = 'Please enter a valid email';
41
- }
42
-
43
- if (!formData.firstName.trim()) {
44
- newErrors.firstName = 'First name is required';
45
- }
46
- if (!formData.lastName.trim()) {
47
- newErrors.lastName = 'Last name is required';
48
- }
49
- if (!formData.line1.trim()) {
50
- newErrors.line1 = 'Address is required';
51
- }
52
- if (!formData.city.trim()) {
53
- newErrors.city = 'City is required';
54
- }
55
- if (!formData.postalCode.trim()) {
56
- newErrors.postalCode = 'Postal code is required';
57
- }
58
- if (!formData.country.trim()) {
59
- newErrors.country = 'Country is required';
60
- }
61
-
62
- setErrors(newErrors);
63
- return Object.keys(newErrors).length === 0;
64
- }
65
-
66
- function handleSubmit(e: React.FormEvent) {
67
- e.preventDefault();
68
- if (validate()) {
69
- onSubmit(formData);
70
- }
71
- }
72
-
73
- function updateField(field: keyof SetShippingAddressDto, value: string) {
74
- setFormData((prev) => ({ ...prev, [field]: value }));
75
- if (errors[field]) {
76
- setErrors((prev) => {
77
- const next = { ...prev };
78
- delete next[field];
79
- return next;
80
- });
81
- }
82
- }
83
-
84
- return (
85
- <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
86
- {/* Email */}
87
- <div>
88
- <label htmlFor="email" className="text-foreground mb-1 block text-sm font-medium">
89
- Email <span className="text-destructive">*</span>
90
- </label>
91
- <input
92
- id="email"
93
- type="email"
94
- value={formData.email}
95
- onChange={(e) => updateField('email', e.target.value)}
96
- className={cn(
97
- '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',
98
- errors.email ? 'border-destructive' : 'border-border'
99
- )}
100
- placeholder="your@email.com"
101
- />
102
- {errors.email && <p className="text-destructive mt-1 text-xs">{errors.email}</p>}
103
- </div>
104
-
105
- {/* Name row */}
106
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
107
- <div>
108
- <label htmlFor="firstName" className="text-foreground mb-1 block text-sm font-medium">
109
- First Name <span className="text-destructive">*</span>
110
- </label>
111
- <input
112
- id="firstName"
113
- type="text"
114
- value={formData.firstName}
115
- onChange={(e) => updateField('firstName', e.target.value)}
116
- className={cn(
117
- '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',
118
- errors.firstName ? 'border-destructive' : 'border-border'
119
- )}
120
- />
121
- {errors.firstName && <p className="text-destructive mt-1 text-xs">{errors.firstName}</p>}
122
- </div>
123
-
124
- <div>
125
- <label htmlFor="lastName" className="text-foreground mb-1 block text-sm font-medium">
126
- Last Name <span className="text-destructive">*</span>
127
- </label>
128
- <input
129
- id="lastName"
130
- type="text"
131
- value={formData.lastName}
132
- onChange={(e) => updateField('lastName', e.target.value)}
133
- className={cn(
134
- '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',
135
- errors.lastName ? 'border-destructive' : 'border-border'
136
- )}
137
- />
138
- {errors.lastName && <p className="text-destructive mt-1 text-xs">{errors.lastName}</p>}
139
- </div>
140
- </div>
141
-
142
- {/* Address line 1 */}
143
- <div>
144
- <label htmlFor="line1" className="text-foreground mb-1 block text-sm font-medium">
145
- Address <span className="text-destructive">*</span>
146
- </label>
147
- <input
148
- id="line1"
149
- type="text"
150
- value={formData.line1}
151
- onChange={(e) => updateField('line1', e.target.value)}
152
- className={cn(
153
- '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',
154
- errors.line1 ? 'border-destructive' : 'border-border'
155
- )}
156
- placeholder="Street address"
157
- />
158
- {errors.line1 && <p className="text-destructive mt-1 text-xs">{errors.line1}</p>}
159
- </div>
160
-
161
- {/* Address line 2 */}
162
- <div>
163
- <label htmlFor="line2" className="text-foreground mb-1 block text-sm font-medium">
164
- Apartment, suite, etc.
165
- </label>
166
- <input
167
- id="line2"
168
- type="text"
169
- value={formData.line2 || ''}
170
- onChange={(e) => updateField('line2', e.target.value)}
171
- className="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"
172
- placeholder="Apt, suite, unit, etc. (optional)"
173
- />
174
- </div>
175
-
176
- {/* City + Region row */}
177
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
178
- <div>
179
- <label htmlFor="city" className="text-foreground mb-1 block text-sm font-medium">
180
- City <span className="text-destructive">*</span>
181
- </label>
182
- <input
183
- id="city"
184
- type="text"
185
- value={formData.city}
186
- onChange={(e) => updateField('city', e.target.value)}
187
- className={cn(
188
- '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',
189
- errors.city ? 'border-destructive' : 'border-border'
190
- )}
191
- />
192
- {errors.city && <p className="text-destructive mt-1 text-xs">{errors.city}</p>}
193
- </div>
194
-
195
- <div>
196
- <label htmlFor="region" className="text-foreground mb-1 block text-sm font-medium">
197
- State / Region
198
- </label>
199
- <input
200
- id="region"
201
- type="text"
202
- value={formData.region || ''}
203
- onChange={(e) => updateField('region', e.target.value)}
204
- className="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"
205
- />
206
- </div>
207
- </div>
208
-
209
- {/* Postal code + Country row */}
210
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
211
- <div>
212
- <label htmlFor="postalCode" className="text-foreground mb-1 block text-sm font-medium">
213
- Postal Code <span className="text-destructive">*</span>
214
- </label>
215
- <input
216
- id="postalCode"
217
- type="text"
218
- value={formData.postalCode}
219
- onChange={(e) => updateField('postalCode', e.target.value)}
220
- className={cn(
221
- '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',
222
- errors.postalCode ? 'border-destructive' : 'border-border'
223
- )}
224
- />
225
- {errors.postalCode && (
226
- <p className="text-destructive mt-1 text-xs">{errors.postalCode}</p>
227
- )}
228
- </div>
229
-
230
- <div>
231
- <label htmlFor="country" className="text-foreground mb-1 block text-sm font-medium">
232
- Country <span className="text-destructive">*</span>
233
- </label>
234
- <input
235
- id="country"
236
- type="text"
237
- value={formData.country}
238
- onChange={(e) => updateField('country', e.target.value)}
239
- className={cn(
240
- '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',
241
- errors.country ? 'border-destructive' : 'border-border'
242
- )}
243
- placeholder="e.g. US, IL, GB"
244
- />
245
- {errors.country && <p className="text-destructive mt-1 text-xs">{errors.country}</p>}
246
- </div>
247
- </div>
248
-
249
- {/* Phone */}
250
- <div>
251
- <label htmlFor="phone" className="text-foreground mb-1 block text-sm font-medium">
252
- Phone
253
- </label>
254
- <input
255
- id="phone"
256
- type="tel"
257
- value={formData.phone || ''}
258
- onChange={(e) => updateField('phone', e.target.value)}
259
- className="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"
260
- placeholder="+1234567890 (optional)"
261
- />
262
- </div>
263
-
264
- <button
265
- type="submit"
266
- disabled={loading}
267
- className="bg-primary text-primary-foreground w-full rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
268
- >
269
- {loading ? 'Saving...' : 'Continue to Shipping'}
270
- </button>
271
- </form>
272
- );
273
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import type { SetShippingAddressDto, ShippingDestinations } from 'brainerce';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface CheckoutFormProps {
8
+ onSubmit: (address: SetShippingAddressDto) => void;
9
+ loading?: boolean;
10
+ initialValues?: Partial<SetShippingAddressDto>;
11
+ destinations?: ShippingDestinations | null;
12
+ className?: string;
13
+ }
14
+
15
+ export function CheckoutForm({
16
+ onSubmit,
17
+ loading = false,
18
+ initialValues,
19
+ destinations,
20
+ className,
21
+ }: CheckoutFormProps) {
22
+ const [formData, setFormData] = useState<SetShippingAddressDto>({
23
+ email: initialValues?.email || '',
24
+ firstName: initialValues?.firstName || '',
25
+ lastName: initialValues?.lastName || '',
26
+ line1: initialValues?.line1 || '',
27
+ line2: initialValues?.line2 || '',
28
+ city: initialValues?.city || '',
29
+ region: initialValues?.region || '',
30
+ postalCode: initialValues?.postalCode || '',
31
+ country: initialValues?.country || '',
32
+ phone: initialValues?.phone || '',
33
+ });
34
+ const [errors, setErrors] = useState<Record<string, string>>({});
35
+
36
+ const hasCountryOptions = destinations && destinations.countries.length > 0;
37
+ const countryRegions = destinations?.regions[formData.country];
38
+ const hasRegionOptions = countryRegions && countryRegions.length > 0;
39
+
40
+ function validate(): boolean {
41
+ const newErrors: Record<string, string> = {};
42
+
43
+ if (!formData.email.trim()) {
44
+ newErrors.email = 'Email is required';
45
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
46
+ newErrors.email = 'Please enter a valid email';
47
+ }
48
+
49
+ if (!formData.firstName.trim()) {
50
+ newErrors.firstName = 'First name is required';
51
+ }
52
+ if (!formData.lastName.trim()) {
53
+ newErrors.lastName = 'Last name is required';
54
+ }
55
+ if (!formData.line1.trim()) {
56
+ newErrors.line1 = 'Address is required';
57
+ }
58
+ if (!formData.city.trim()) {
59
+ newErrors.city = 'City is required';
60
+ }
61
+ if (!formData.postalCode.trim()) {
62
+ newErrors.postalCode = 'Postal code is required';
63
+ }
64
+ if (!formData.country.trim()) {
65
+ newErrors.country = 'Country is required';
66
+ }
67
+
68
+ setErrors(newErrors);
69
+ return Object.keys(newErrors).length === 0;
70
+ }
71
+
72
+ function handleSubmit(e: React.FormEvent) {
73
+ e.preventDefault();
74
+ if (validate()) {
75
+ onSubmit(formData);
76
+ }
77
+ }
78
+
79
+ function updateField(field: keyof SetShippingAddressDto, value: string) {
80
+ setFormData((prev) => {
81
+ const next = { ...prev, [field]: value };
82
+ // Reset region when country changes
83
+ if (field === 'country' && value !== prev.country) {
84
+ next.region = '';
85
+ }
86
+ return next;
87
+ });
88
+ if (errors[field]) {
89
+ setErrors((prev) => {
90
+ const next = { ...prev };
91
+ delete next[field];
92
+ return next;
93
+ });
94
+ }
95
+ }
96
+
97
+ const inputClass =
98
+ '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';
99
+ const selectClass =
100
+ 'bg-background text-foreground focus:ring-primary/20 focus:border-primary h-10 w-full appearance-none rounded border px-3 text-sm focus:outline-none focus:ring-2';
101
+
102
+ return (
103
+ <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
104
+ {/* Email */}
105
+ <div>
106
+ <label htmlFor="email" className="text-foreground mb-1 block text-sm font-medium">
107
+ Email <span className="text-destructive">*</span>
108
+ </label>
109
+ <input
110
+ id="email"
111
+ type="email"
112
+ value={formData.email}
113
+ onChange={(e) => updateField('email', e.target.value)}
114
+ className={cn(inputClass, errors.email ? 'border-destructive' : 'border-border')}
115
+ placeholder="your@email.com"
116
+ />
117
+ {errors.email && <p className="text-destructive mt-1 text-xs">{errors.email}</p>}
118
+ </div>
119
+
120
+ {/* Name row */}
121
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
122
+ <div>
123
+ <label htmlFor="firstName" className="text-foreground mb-1 block text-sm font-medium">
124
+ First Name <span className="text-destructive">*</span>
125
+ </label>
126
+ <input
127
+ id="firstName"
128
+ type="text"
129
+ value={formData.firstName}
130
+ onChange={(e) => updateField('firstName', e.target.value)}
131
+ className={cn(inputClass, errors.firstName ? 'border-destructive' : 'border-border')}
132
+ />
133
+ {errors.firstName && <p className="text-destructive mt-1 text-xs">{errors.firstName}</p>}
134
+ </div>
135
+
136
+ <div>
137
+ <label htmlFor="lastName" className="text-foreground mb-1 block text-sm font-medium">
138
+ Last Name <span className="text-destructive">*</span>
139
+ </label>
140
+ <input
141
+ id="lastName"
142
+ type="text"
143
+ value={formData.lastName}
144
+ onChange={(e) => updateField('lastName', e.target.value)}
145
+ className={cn(inputClass, errors.lastName ? 'border-destructive' : 'border-border')}
146
+ />
147
+ {errors.lastName && <p className="text-destructive mt-1 text-xs">{errors.lastName}</p>}
148
+ </div>
149
+ </div>
150
+
151
+ {/* Country + Region row */}
152
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
153
+ <div>
154
+ <label htmlFor="country" className="text-foreground mb-1 block text-sm font-medium">
155
+ Country <span className="text-destructive">*</span>
156
+ </label>
157
+ {hasCountryOptions ? (
158
+ <select
159
+ id="country"
160
+ value={formData.country}
161
+ onChange={(e) => updateField('country', e.target.value)}
162
+ className={cn(selectClass, errors.country ? 'border-destructive' : 'border-border')}
163
+ >
164
+ <option value="">Select country</option>
165
+ {destinations.countries.map((c) => (
166
+ <option key={c.code} value={c.code}>
167
+ {c.name}
168
+ </option>
169
+ ))}
170
+ </select>
171
+ ) : (
172
+ <input
173
+ id="country"
174
+ type="text"
175
+ value={formData.country}
176
+ onChange={(e) => updateField('country', e.target.value)}
177
+ className={cn(inputClass, errors.country ? 'border-destructive' : 'border-border')}
178
+ placeholder="e.g. US, IL, GB"
179
+ />
180
+ )}
181
+ {errors.country && <p className="text-destructive mt-1 text-xs">{errors.country}</p>}
182
+ </div>
183
+
184
+ <div>
185
+ <label htmlFor="region" className="text-foreground mb-1 block text-sm font-medium">
186
+ State / Region
187
+ </label>
188
+ {hasRegionOptions ? (
189
+ <select
190
+ id="region"
191
+ value={formData.region || ''}
192
+ onChange={(e) => updateField('region', e.target.value)}
193
+ className={cn(selectClass, 'border-border')}
194
+ >
195
+ <option value="">Select region</option>
196
+ {countryRegions.map((r) => (
197
+ <option key={r.code} value={r.code}>
198
+ {r.name}
199
+ </option>
200
+ ))}
201
+ </select>
202
+ ) : (
203
+ <input
204
+ id="region"
205
+ type="text"
206
+ value={formData.region || ''}
207
+ onChange={(e) => updateField('region', e.target.value)}
208
+ className={cn(inputClass, 'border-border')}
209
+ />
210
+ )}
211
+ </div>
212
+ </div>
213
+
214
+ {/* Address line 1 */}
215
+ <div>
216
+ <label htmlFor="line1" className="text-foreground mb-1 block text-sm font-medium">
217
+ Address <span className="text-destructive">*</span>
218
+ </label>
219
+ <input
220
+ id="line1"
221
+ type="text"
222
+ value={formData.line1}
223
+ onChange={(e) => updateField('line1', e.target.value)}
224
+ className={cn(inputClass, errors.line1 ? 'border-destructive' : 'border-border')}
225
+ placeholder="Street address"
226
+ />
227
+ {errors.line1 && <p className="text-destructive mt-1 text-xs">{errors.line1}</p>}
228
+ </div>
229
+
230
+ {/* Address line 2 */}
231
+ <div>
232
+ <label htmlFor="line2" className="text-foreground mb-1 block text-sm font-medium">
233
+ Apartment, suite, etc.
234
+ </label>
235
+ <input
236
+ id="line2"
237
+ type="text"
238
+ value={formData.line2 || ''}
239
+ onChange={(e) => updateField('line2', e.target.value)}
240
+ className={cn(inputClass, 'border-border')}
241
+ placeholder="Apt, suite, unit, etc. (optional)"
242
+ />
243
+ </div>
244
+
245
+ {/* City + Postal code row */}
246
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
247
+ <div>
248
+ <label htmlFor="city" className="text-foreground mb-1 block text-sm font-medium">
249
+ City <span className="text-destructive">*</span>
250
+ </label>
251
+ <input
252
+ id="city"
253
+ type="text"
254
+ value={formData.city}
255
+ onChange={(e) => updateField('city', e.target.value)}
256
+ className={cn(inputClass, errors.city ? 'border-destructive' : 'border-border')}
257
+ />
258
+ {errors.city && <p className="text-destructive mt-1 text-xs">{errors.city}</p>}
259
+ </div>
260
+
261
+ <div>
262
+ <label htmlFor="postalCode" className="text-foreground mb-1 block text-sm font-medium">
263
+ Postal Code <span className="text-destructive">*</span>
264
+ </label>
265
+ <input
266
+ id="postalCode"
267
+ type="text"
268
+ value={formData.postalCode}
269
+ onChange={(e) => updateField('postalCode', e.target.value)}
270
+ className={cn(inputClass, errors.postalCode ? 'border-destructive' : 'border-border')}
271
+ />
272
+ {errors.postalCode && (
273
+ <p className="text-destructive mt-1 text-xs">{errors.postalCode}</p>
274
+ )}
275
+ </div>
276
+ </div>
277
+
278
+ {/* Phone */}
279
+ <div>
280
+ <label htmlFor="phone" className="text-foreground mb-1 block text-sm font-medium">
281
+ Phone
282
+ </label>
283
+ <input
284
+ id="phone"
285
+ type="tel"
286
+ value={formData.phone || ''}
287
+ onChange={(e) => updateField('phone', e.target.value)}
288
+ className={cn(inputClass, 'border-border')}
289
+ placeholder="+1234567890 (optional)"
290
+ />
291
+ </div>
292
+
293
+ <button
294
+ type="submit"
295
+ disabled={loading}
296
+ className="bg-primary text-primary-foreground w-full rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
297
+ >
298
+ {loading ? 'Saving...' : 'Continue to Shipping'}
299
+ </button>
300
+ </form>
301
+ );
302
+ }