create-brainerce-store 1.5.1 → 1.5.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 +266 -266
- package/messages/he.json +266 -266
- package/package.json +45 -45
- package/templates/nextjs/base/src/app/account/page.tsx +112 -112
- package/templates/nextjs/base/src/app/checkout/page.tsx +23 -1
- package/templates/nextjs/base/src/components/account/profile-section.tsx +224 -224
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import type { CustomerProfile } from 'brainerce';
|
|
5
|
-
import { getClient } from '@/lib/brainerce';
|
|
6
|
-
import { useTranslations } from '@/lib/translations';
|
|
7
|
-
import { cn } from '@/lib/utils';
|
|
8
|
-
|
|
9
|
-
interface ProfileSectionProps {
|
|
10
|
-
profile: CustomerProfile;
|
|
11
|
-
onProfileUpdate?: (updated: CustomerProfile) => void;
|
|
12
|
-
className?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ProfileSection({ profile, onProfileUpdate, className }: ProfileSectionProps) {
|
|
16
|
-
const t = useTranslations('account');
|
|
17
|
-
const [editing, setEditing] = useState(false);
|
|
18
|
-
const [saving, setSaving] = useState(false);
|
|
19
|
-
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
20
|
-
const [form, setForm] = useState({
|
|
21
|
-
firstName: profile.firstName || '',
|
|
22
|
-
lastName: profile.lastName || '',
|
|
23
|
-
phone: profile.phone || '',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const fullName = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
|
|
27
|
-
const initials =
|
|
28
|
-
[profile.firstName?.[0], profile.lastName?.[0]].filter(Boolean).join('').toUpperCase() ||
|
|
29
|
-
profile.email[0].toUpperCase();
|
|
30
|
-
|
|
31
|
-
function startEditing() {
|
|
32
|
-
setForm({
|
|
33
|
-
firstName: profile.firstName || '',
|
|
34
|
-
lastName: profile.lastName || '',
|
|
35
|
-
phone: profile.phone || '',
|
|
36
|
-
});
|
|
37
|
-
setMessage(null);
|
|
38
|
-
setEditing(true);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function cancelEditing() {
|
|
42
|
-
setEditing(false);
|
|
43
|
-
setMessage(null);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function handleSave(e: React.FormEvent) {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
setSaving(true);
|
|
49
|
-
setMessage(null);
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const client = getClient();
|
|
53
|
-
const updated = await client.updateMyProfile({
|
|
54
|
-
firstName: form.firstName || undefined,
|
|
55
|
-
lastName: form.lastName || undefined,
|
|
56
|
-
phone: form.phone || undefined,
|
|
57
|
-
});
|
|
58
|
-
onProfileUpdate?.(updated);
|
|
59
|
-
setEditing(false);
|
|
60
|
-
setMessage({ type: 'success', text: t('profileUpdated') });
|
|
61
|
-
setTimeout(() => setMessage(null), 3000);
|
|
62
|
-
} catch {
|
|
63
|
-
setMessage({ type: 'error', text: t('profileUpdateFailed') });
|
|
64
|
-
} finally {
|
|
65
|
-
setSaving(false);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className={cn('border-border rounded-lg border p-6', className)}>
|
|
71
|
-
<div className="flex items-start gap-4">
|
|
72
|
-
{/* Avatar */}
|
|
73
|
-
<div className="bg-primary/10 text-primary flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full text-lg font-semibold">
|
|
74
|
-
{initials}
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div className="min-w-0 flex-1">
|
|
78
|
-
{editing ? (
|
|
79
|
-
<form onSubmit={handleSave} className="space-y-3">
|
|
80
|
-
<div className="grid grid-cols-2 gap-3">
|
|
81
|
-
<div>
|
|
82
|
-
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
83
|
-
{t('firstName')}
|
|
84
|
-
</label>
|
|
85
|
-
<input
|
|
86
|
-
type="text"
|
|
87
|
-
value={form.firstName}
|
|
88
|
-
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
|
89
|
-
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
90
|
-
autoFocus
|
|
91
|
-
/>
|
|
92
|
-
</div>
|
|
93
|
-
<div>
|
|
94
|
-
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
95
|
-
{t('lastName')}
|
|
96
|
-
</label>
|
|
97
|
-
<input
|
|
98
|
-
type="text"
|
|
99
|
-
value={form.lastName}
|
|
100
|
-
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
|
|
101
|
-
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
102
|
-
/>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<div>
|
|
106
|
-
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
107
|
-
{t('phone')}
|
|
108
|
-
</label>
|
|
109
|
-
<input
|
|
110
|
-
type="tel"
|
|
111
|
-
value={form.phone}
|
|
112
|
-
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
|
113
|
-
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
<p className="text-muted-foreground truncate text-sm">{profile.email}</p>
|
|
117
|
-
<div className="flex items-center gap-2">
|
|
118
|
-
<button
|
|
119
|
-
type="submit"
|
|
120
|
-
disabled={saving}
|
|
121
|
-
className="bg-primary text-primary-foreground rounded-md px-4 py-1.5 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
122
|
-
>
|
|
123
|
-
{saving ? '...' : t('save')}
|
|
124
|
-
</button>
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
onClick={cancelEditing}
|
|
128
|
-
disabled={saving}
|
|
129
|
-
className="text-muted-foreground hover:text-foreground rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
|
|
130
|
-
>
|
|
131
|
-
{t('cancel')}
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
</form>
|
|
135
|
-
) : (
|
|
136
|
-
<>
|
|
137
|
-
<div className="flex items-center gap-2">
|
|
138
|
-
{fullName && (
|
|
139
|
-
<h2 className="text-foreground truncate text-lg font-semibold">{fullName}</h2>
|
|
140
|
-
)}
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={startEditing}
|
|
144
|
-
className="text-muted-foreground hover:text-foreground flex-shrink-0 rounded p-1 transition-colors"
|
|
145
|
-
title={t('editProfile')}
|
|
146
|
-
>
|
|
147
|
-
<svg
|
|
148
|
-
className="h-4 w-4"
|
|
149
|
-
fill="none"
|
|
150
|
-
viewBox="0 0 24 24"
|
|
151
|
-
stroke="currentColor"
|
|
152
|
-
strokeWidth={2}
|
|
153
|
-
>
|
|
154
|
-
<path
|
|
155
|
-
strokeLinecap="round"
|
|
156
|
-
strokeLinejoin="round"
|
|
157
|
-
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
158
|
-
/>
|
|
159
|
-
</svg>
|
|
160
|
-
</button>
|
|
161
|
-
</div>
|
|
162
|
-
<p className="text-muted-foreground truncate text-sm">{profile.email}</p>
|
|
163
|
-
|
|
164
|
-
<div className="mt-2 flex items-center gap-2">
|
|
165
|
-
{profile.emailVerified ? (
|
|
166
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-950/30 dark:text-green-400">
|
|
167
|
-
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
168
|
-
<path
|
|
169
|
-
strokeLinecap="round"
|
|
170
|
-
strokeLinejoin="round"
|
|
171
|
-
strokeWidth={2}
|
|
172
|
-
d="M5 13l4 4L19 7"
|
|
173
|
-
/>
|
|
174
|
-
</svg>
|
|
175
|
-
{t('verified')}
|
|
176
|
-
</span>
|
|
177
|
-
) : (
|
|
178
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-950/30 dark:text-orange-400">
|
|
179
|
-
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
180
|
-
<path
|
|
181
|
-
strokeLinecap="round"
|
|
182
|
-
strokeLinejoin="round"
|
|
183
|
-
strokeWidth={2}
|
|
184
|
-
d="M12 9v2m0 4h.01"
|
|
185
|
-
/>
|
|
186
|
-
</svg>
|
|
187
|
-
{t('unverified')}
|
|
188
|
-
</span>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
{profile.phone && (
|
|
193
|
-
<p className="text-muted-foreground mt-2 text-sm">{profile.phone}</p>
|
|
194
|
-
)}
|
|
195
|
-
|
|
196
|
-
<p className="text-muted-foreground mt-3 text-xs">
|
|
197
|
-
{t('memberSince')}{' '}
|
|
198
|
-
{new Date(profile.createdAt).toLocaleDateString(undefined, {
|
|
199
|
-
year: 'numeric',
|
|
200
|
-
month: 'long',
|
|
201
|
-
day: 'numeric',
|
|
202
|
-
})}
|
|
203
|
-
</p>
|
|
204
|
-
</>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
{/* Success/Error message */}
|
|
208
|
-
{message && (
|
|
209
|
-
<p
|
|
210
|
-
className={cn(
|
|
211
|
-
'mt-2 text-sm',
|
|
212
|
-
message.type === 'success'
|
|
213
|
-
? 'text-green-600 dark:text-green-400'
|
|
214
|
-
: 'text-red-600 dark:text-red-400'
|
|
215
|
-
)}
|
|
216
|
-
>
|
|
217
|
-
{message.text}
|
|
218
|
-
</p>
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { CustomerProfile } from 'brainerce';
|
|
5
|
+
import { getClient } from '@/lib/brainerce';
|
|
6
|
+
import { useTranslations } from '@/lib/translations';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface ProfileSectionProps {
|
|
10
|
+
profile: CustomerProfile;
|
|
11
|
+
onProfileUpdate?: (updated: CustomerProfile) => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProfileSection({ profile, onProfileUpdate, className }: ProfileSectionProps) {
|
|
16
|
+
const t = useTranslations('account');
|
|
17
|
+
const [editing, setEditing] = useState(false);
|
|
18
|
+
const [saving, setSaving] = useState(false);
|
|
19
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
20
|
+
const [form, setForm] = useState({
|
|
21
|
+
firstName: profile.firstName || '',
|
|
22
|
+
lastName: profile.lastName || '',
|
|
23
|
+
phone: profile.phone || '',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const fullName = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
|
|
27
|
+
const initials =
|
|
28
|
+
[profile.firstName?.[0], profile.lastName?.[0]].filter(Boolean).join('').toUpperCase() ||
|
|
29
|
+
profile.email[0].toUpperCase();
|
|
30
|
+
|
|
31
|
+
function startEditing() {
|
|
32
|
+
setForm({
|
|
33
|
+
firstName: profile.firstName || '',
|
|
34
|
+
lastName: profile.lastName || '',
|
|
35
|
+
phone: profile.phone || '',
|
|
36
|
+
});
|
|
37
|
+
setMessage(null);
|
|
38
|
+
setEditing(true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cancelEditing() {
|
|
42
|
+
setEditing(false);
|
|
43
|
+
setMessage(null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function handleSave(e: React.FormEvent) {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
setSaving(true);
|
|
49
|
+
setMessage(null);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const client = getClient();
|
|
53
|
+
const updated = await client.updateMyProfile({
|
|
54
|
+
firstName: form.firstName || undefined,
|
|
55
|
+
lastName: form.lastName || undefined,
|
|
56
|
+
phone: form.phone || undefined,
|
|
57
|
+
});
|
|
58
|
+
onProfileUpdate?.(updated);
|
|
59
|
+
setEditing(false);
|
|
60
|
+
setMessage({ type: 'success', text: t('profileUpdated') });
|
|
61
|
+
setTimeout(() => setMessage(null), 3000);
|
|
62
|
+
} catch {
|
|
63
|
+
setMessage({ type: 'error', text: t('profileUpdateFailed') });
|
|
64
|
+
} finally {
|
|
65
|
+
setSaving(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={cn('border-border rounded-lg border p-6', className)}>
|
|
71
|
+
<div className="flex items-start gap-4">
|
|
72
|
+
{/* Avatar */}
|
|
73
|
+
<div className="bg-primary/10 text-primary flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full text-lg font-semibold">
|
|
74
|
+
{initials}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="min-w-0 flex-1">
|
|
78
|
+
{editing ? (
|
|
79
|
+
<form onSubmit={handleSave} className="space-y-3">
|
|
80
|
+
<div className="grid grid-cols-2 gap-3">
|
|
81
|
+
<div>
|
|
82
|
+
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
83
|
+
{t('firstName')}
|
|
84
|
+
</label>
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
value={form.firstName}
|
|
88
|
+
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
|
89
|
+
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
90
|
+
autoFocus
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
95
|
+
{t('lastName')}
|
|
96
|
+
</label>
|
|
97
|
+
<input
|
|
98
|
+
type="text"
|
|
99
|
+
value={form.lastName}
|
|
100
|
+
onChange={(e) => setForm((f) => ({ ...f, lastName: e.target.value }))}
|
|
101
|
+
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<label className="text-muted-foreground mb-1 block text-xs font-medium">
|
|
107
|
+
{t('phone')}
|
|
108
|
+
</label>
|
|
109
|
+
<input
|
|
110
|
+
type="tel"
|
|
111
|
+
value={form.phone}
|
|
112
|
+
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
|
113
|
+
className="border-border bg-background text-foreground focus:border-primary w-full rounded-md border px-3 py-1.5 text-sm outline-none"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<p className="text-muted-foreground truncate text-sm">{profile.email}</p>
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<button
|
|
119
|
+
type="submit"
|
|
120
|
+
disabled={saving}
|
|
121
|
+
className="bg-primary text-primary-foreground rounded-md px-4 py-1.5 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
122
|
+
>
|
|
123
|
+
{saving ? '...' : t('save')}
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={cancelEditing}
|
|
128
|
+
disabled={saving}
|
|
129
|
+
className="text-muted-foreground hover:text-foreground rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
|
|
130
|
+
>
|
|
131
|
+
{t('cancel')}
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</form>
|
|
135
|
+
) : (
|
|
136
|
+
<>
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
{fullName && (
|
|
139
|
+
<h2 className="text-foreground truncate text-lg font-semibold">{fullName}</h2>
|
|
140
|
+
)}
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={startEditing}
|
|
144
|
+
className="text-muted-foreground hover:text-foreground flex-shrink-0 rounded p-1 transition-colors"
|
|
145
|
+
title={t('editProfile')}
|
|
146
|
+
>
|
|
147
|
+
<svg
|
|
148
|
+
className="h-4 w-4"
|
|
149
|
+
fill="none"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
strokeWidth={2}
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
strokeLinecap="round"
|
|
156
|
+
strokeLinejoin="round"
|
|
157
|
+
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
<p className="text-muted-foreground truncate text-sm">{profile.email}</p>
|
|
163
|
+
|
|
164
|
+
<div className="mt-2 flex items-center gap-2">
|
|
165
|
+
{profile.emailVerified ? (
|
|
166
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-950/30 dark:text-green-400">
|
|
167
|
+
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
168
|
+
<path
|
|
169
|
+
strokeLinecap="round"
|
|
170
|
+
strokeLinejoin="round"
|
|
171
|
+
strokeWidth={2}
|
|
172
|
+
d="M5 13l4 4L19 7"
|
|
173
|
+
/>
|
|
174
|
+
</svg>
|
|
175
|
+
{t('verified')}
|
|
176
|
+
</span>
|
|
177
|
+
) : (
|
|
178
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-950/30 dark:text-orange-400">
|
|
179
|
+
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
180
|
+
<path
|
|
181
|
+
strokeLinecap="round"
|
|
182
|
+
strokeLinejoin="round"
|
|
183
|
+
strokeWidth={2}
|
|
184
|
+
d="M12 9v2m0 4h.01"
|
|
185
|
+
/>
|
|
186
|
+
</svg>
|
|
187
|
+
{t('unverified')}
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{profile.phone && (
|
|
193
|
+
<p className="text-muted-foreground mt-2 text-sm">{profile.phone}</p>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
<p className="text-muted-foreground mt-3 text-xs">
|
|
197
|
+
{t('memberSince')}{' '}
|
|
198
|
+
{new Date(profile.createdAt).toLocaleDateString(undefined, {
|
|
199
|
+
year: 'numeric',
|
|
200
|
+
month: 'long',
|
|
201
|
+
day: 'numeric',
|
|
202
|
+
})}
|
|
203
|
+
</p>
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* Success/Error message */}
|
|
208
|
+
{message && (
|
|
209
|
+
<p
|
|
210
|
+
className={cn(
|
|
211
|
+
'mt-2 text-sm',
|
|
212
|
+
message.type === 'success'
|
|
213
|
+
? 'text-green-600 dark:text-green-400'
|
|
214
|
+
: 'text-red-600 dark:text-red-400'
|
|
215
|
+
)}
|
|
216
|
+
>
|
|
217
|
+
{message.text}
|
|
218
|
+
</p>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|