create-brainerce-store 1.4.1 → 1.5.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.
Files changed (36) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +266 -258
  3. package/messages/he.json +266 -258
  4. package/package.json +45 -45
  5. package/templates/nextjs/base/src/app/account/page.tsx +108 -108
  6. package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -90
  7. package/templates/nextjs/base/src/app/cart/page.tsx +110 -110
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +614 -614
  9. package/templates/nextjs/base/src/app/login/page.tsx +58 -58
  10. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +193 -193
  11. package/templates/nextjs/base/src/app/page.tsx +98 -98
  12. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +435 -435
  13. package/templates/nextjs/base/src/app/products/page.tsx +246 -246
  14. package/templates/nextjs/base/src/app/register/page.tsx +68 -68
  15. package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -293
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +198 -198
  17. package/templates/nextjs/base/src/components/account/profile-section.tsx +210 -75
  18. package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -94
  19. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  20. package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -184
  21. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  22. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -70
  23. package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -134
  24. package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -103
  25. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +305 -305
  26. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +64 -64
  27. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +350 -344
  28. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +199 -199
  29. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -110
  30. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +65 -65
  31. package/templates/nextjs/base/src/components/layout/footer.tsx +38 -38
  32. package/templates/nextjs/base/src/components/layout/header.tsx +332 -332
  33. package/templates/nextjs/base/src/components/products/product-card.tsx +96 -96
  34. package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -35
  35. package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -32
  36. package/templates/nextjs/base/src/lib/translations.ts +11 -11
@@ -1,75 +1,210 @@
1
- 'use client';
2
-
3
- import type { CustomerProfile } from 'brainerce';
4
- import { useTranslations } from '@/lib/translations';
5
- import { cn } from '@/lib/utils';
6
-
7
- interface ProfileSectionProps {
8
- profile: CustomerProfile;
9
- className?: string;
10
- }
11
-
12
- export function ProfileSection({ profile, className }: ProfileSectionProps) {
13
- const t = useTranslations('account');
14
- const fullName = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
15
- const initials =
16
- [profile.firstName?.[0], profile.lastName?.[0]].filter(Boolean).join('').toUpperCase() ||
17
- profile.email[0].toUpperCase();
18
-
19
- return (
20
- <div className={cn('border-border rounded-lg border p-6', className)}>
21
- <div className="flex items-start gap-4">
22
- {/* Avatar */}
23
- <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">
24
- {initials}
25
- </div>
26
-
27
- <div className="min-w-0 flex-1">
28
- {fullName && (
29
- <h2 className="text-foreground truncate text-lg font-semibold">{fullName}</h2>
30
- )}
31
- <p className="text-muted-foreground truncate text-sm">{profile.email}</p>
32
-
33
- <div className="mt-2 flex items-center gap-2">
34
- {profile.emailVerified ? (
35
- <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">
36
- <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37
- <path
38
- strokeLinecap="round"
39
- strokeLinejoin="round"
40
- strokeWidth={2}
41
- d="M5 13l4 4L19 7"
42
- />
43
- </svg>
44
- {t('verified')}
45
- </span>
46
- ) : (
47
- <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">
48
- <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
- <path
50
- strokeLinecap="round"
51
- strokeLinejoin="round"
52
- strokeWidth={2}
53
- d="M12 9v2m0 4h.01"
54
- />
55
- </svg>
56
- {t('unverified')}
57
- </span>
58
- )}
59
- </div>
60
-
61
- {profile.phone && <p className="text-muted-foreground mt-2 text-sm">{profile.phone}</p>}
62
-
63
- <p className="text-muted-foreground mt-3 text-xs">
64
- {t('memberSince')}{' '}
65
- {new Date(profile.createdAt).toLocaleDateString(undefined, {
66
- year: 'numeric',
67
- month: 'long',
68
- day: 'numeric',
69
- })}
70
- </p>
71
- </div>
72
- </div>
73
- </div>
74
- );
75
- }
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 w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:border-primary"
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 w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:border-primary"
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 w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:border-primary"
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 className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
148
+ <path strokeLinecap="round" strokeLinejoin="round" 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" />
149
+ </svg>
150
+ </button>
151
+ </div>
152
+ <p className="text-muted-foreground truncate text-sm">{profile.email}</p>
153
+
154
+ <div className="mt-2 flex items-center gap-2">
155
+ {profile.emailVerified ? (
156
+ <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">
157
+ <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
158
+ <path
159
+ strokeLinecap="round"
160
+ strokeLinejoin="round"
161
+ strokeWidth={2}
162
+ d="M5 13l4 4L19 7"
163
+ />
164
+ </svg>
165
+ {t('verified')}
166
+ </span>
167
+ ) : (
168
+ <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">
169
+ <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
170
+ <path
171
+ strokeLinecap="round"
172
+ strokeLinejoin="round"
173
+ strokeWidth={2}
174
+ d="M12 9v2m0 4h.01"
175
+ />
176
+ </svg>
177
+ {t('unverified')}
178
+ </span>
179
+ )}
180
+ </div>
181
+
182
+ {profile.phone && <p className="text-muted-foreground mt-2 text-sm">{profile.phone}</p>}
183
+
184
+ <p className="text-muted-foreground mt-3 text-xs">
185
+ {t('memberSince')}{' '}
186
+ {new Date(profile.createdAt).toLocaleDateString(undefined, {
187
+ year: 'numeric',
188
+ month: 'long',
189
+ day: 'numeric',
190
+ })}
191
+ </p>
192
+ </>
193
+ )}
194
+
195
+ {/* Success/Error message */}
196
+ {message && (
197
+ <p
198
+ className={cn(
199
+ 'mt-2 text-sm',
200
+ message.type === 'success' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
201
+ )}
202
+ >
203
+ {message.text}
204
+ </p>
205
+ )}
206
+ </div>
207
+ </div>
208
+ </div>
209
+ );
210
+ }
@@ -1,94 +1,94 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useTranslations } from '@/lib/translations';
5
- import { cn } from '@/lib/utils';
6
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
-
8
- interface LoginFormProps {
9
- onSubmit: (email: string, password: string) => Promise<void>;
10
- error?: string | null;
11
- className?: string;
12
- }
13
-
14
- export function LoginForm({ onSubmit, error, className }: LoginFormProps) {
15
- const t = useTranslations('auth');
16
- const [email, setEmail] = useState('');
17
- const [password, setPassword] = useState('');
18
- const [loading, setLoading] = useState(false);
19
-
20
- async function handleSubmit(e: React.FormEvent) {
21
- e.preventDefault();
22
- if (loading) return;
23
-
24
- try {
25
- setLoading(true);
26
- await onSubmit(email, password);
27
- } finally {
28
- setLoading(false);
29
- }
30
- }
31
-
32
- return (
33
- <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
34
- {error && (
35
- <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
36
- {error}
37
- </div>
38
- )}
39
-
40
- <div>
41
- <label htmlFor="login-email" className="text-foreground mb-1.5 block text-sm font-medium">
42
- {t('email')}
43
- </label>
44
- <input
45
- id="login-email"
46
- type="email"
47
- required
48
- value={email}
49
- onChange={(e) => setEmail(e.target.value)}
50
- placeholder={t('emailPlaceholder')}
51
- autoComplete="email"
52
- 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"
53
- />
54
- </div>
55
-
56
- <div>
57
- <label
58
- htmlFor="login-password"
59
- className="text-foreground mb-1.5 block text-sm font-medium"
60
- >
61
- {t('password')}
62
- </label>
63
- <input
64
- id="login-password"
65
- type="password"
66
- required
67
- value={password}
68
- onChange={(e) => setPassword(e.target.value)}
69
- placeholder={t('passwordPlaceholder')}
70
- autoComplete="current-password"
71
- 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"
72
- />
73
- </div>
74
-
75
- <button
76
- type="submit"
77
- disabled={loading}
78
- className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
79
- >
80
- {loading ? (
81
- <>
82
- <LoadingSpinner
83
- size="sm"
84
- className="border-primary-foreground/30 border-t-primary-foreground"
85
- />
86
- {t('signingIn')}
87
- </>
88
- ) : (
89
- t('signIn')
90
- )}
91
- </button>
92
- </form>
93
- );
94
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useTranslations } from '@/lib/translations';
5
+ import { cn } from '@/lib/utils';
6
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
+
8
+ interface LoginFormProps {
9
+ onSubmit: (email: string, password: string) => Promise<void>;
10
+ error?: string | null;
11
+ className?: string;
12
+ }
13
+
14
+ export function LoginForm({ onSubmit, error, className }: LoginFormProps) {
15
+ const t = useTranslations('auth');
16
+ const [email, setEmail] = useState('');
17
+ const [password, setPassword] = useState('');
18
+ const [loading, setLoading] = useState(false);
19
+
20
+ async function handleSubmit(e: React.FormEvent) {
21
+ e.preventDefault();
22
+ if (loading) return;
23
+
24
+ try {
25
+ setLoading(true);
26
+ await onSubmit(email, password);
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ }
31
+
32
+ return (
33
+ <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
34
+ {error && (
35
+ <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
36
+ {error}
37
+ </div>
38
+ )}
39
+
40
+ <div>
41
+ <label htmlFor="login-email" className="text-foreground mb-1.5 block text-sm font-medium">
42
+ {t('email')}
43
+ </label>
44
+ <input
45
+ id="login-email"
46
+ type="email"
47
+ required
48
+ value={email}
49
+ onChange={(e) => setEmail(e.target.value)}
50
+ placeholder={t('emailPlaceholder')}
51
+ autoComplete="email"
52
+ 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"
53
+ />
54
+ </div>
55
+
56
+ <div>
57
+ <label
58
+ htmlFor="login-password"
59
+ className="text-foreground mb-1.5 block text-sm font-medium"
60
+ >
61
+ {t('password')}
62
+ </label>
63
+ <input
64
+ id="login-password"
65
+ type="password"
66
+ required
67
+ value={password}
68
+ onChange={(e) => setPassword(e.target.value)}
69
+ placeholder={t('passwordPlaceholder')}
70
+ autoComplete="current-password"
71
+ 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"
72
+ />
73
+ </div>
74
+
75
+ <button
76
+ type="submit"
77
+ disabled={loading}
78
+ className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
79
+ >
80
+ {loading ? (
81
+ <>
82
+ <LoadingSpinner
83
+ size="sm"
84
+ className="border-primary-foreground/30 border-t-primary-foreground"
85
+ />
86
+ {t('signingIn')}
87
+ </>
88
+ ) : (
89
+ t('signIn')
90
+ )}
91
+ </button>
92
+ </form>
93
+ );
94
+ }