create-brainerce-store 1.3.2 → 1.4.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.
- package/dist/index.js +62 -5
- package/messages/en.json +258 -0
- package/messages/he.json +258 -0
- package/package.json +3 -2
- package/templates/nextjs/base/src/app/account/page.tsx +108 -105
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -88
- package/templates/nextjs/base/src/app/cart/page.tsx +110 -109
- package/templates/nextjs/base/src/app/checkout/page.tsx +46 -43
- package/templates/nextjs/base/src/app/layout.tsx.ejs +8 -5
- package/templates/nextjs/base/src/app/login/page.tsx +58 -56
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +18 -23
- package/templates/nextjs/base/src/app/page.tsx +98 -95
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +16 -12
- package/templates/nextjs/base/src/app/products/page.tsx +246 -243
- package/templates/nextjs/base/src/app/register/page.tsx +68 -66
- package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -291
- package/templates/nextjs/base/src/components/account/order-history.tsx +198 -184
- package/templates/nextjs/base/src/components/account/profile-section.tsx +75 -73
- package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -92
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -134
- package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -177
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -150
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -67
- package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -131
- package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -100
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +28 -25
- package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +6 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +133 -103
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +15 -11
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -111
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +7 -4
- package/templates/nextjs/base/src/components/layout/footer.tsx +38 -35
- package/templates/nextjs/base/src/components/layout/header.tsx +332 -329
- package/templates/nextjs/base/src/components/products/product-card.tsx +3 -1
- package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -33
- package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -30
- package/templates/nextjs/base/src/i18n.ts.ejs +5 -0
- package/templates/nextjs/base/src/lib/translations.ts +11 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import type { PickupLocation } from 'brainerce';
|
|
5
5
|
import { formatPrice } from 'brainerce';
|
|
6
|
+
import { useTranslations } from '@/lib/translations';
|
|
6
7
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
7
8
|
import { cn } from '@/lib/utils';
|
|
8
9
|
|
|
@@ -24,6 +25,9 @@ export function PickupStep({
|
|
|
24
25
|
initialEmail = '',
|
|
25
26
|
className,
|
|
26
27
|
}: PickupStepProps) {
|
|
28
|
+
const t = useTranslations('checkout');
|
|
29
|
+
const tf = useTranslations('checkoutForm');
|
|
30
|
+
const tc = useTranslations('common');
|
|
27
31
|
const { storeInfo } = useStoreInfo();
|
|
28
32
|
const currency = storeInfo?.currency || 'USD';
|
|
29
33
|
|
|
@@ -38,11 +42,11 @@ export function PickupStep({
|
|
|
38
42
|
e.preventDefault();
|
|
39
43
|
|
|
40
44
|
if (!selectedId) {
|
|
41
|
-
setError('
|
|
45
|
+
setError(t('pickupLocationRequired'));
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
48
|
if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
45
|
-
setError('
|
|
49
|
+
setError(tf('emailInvalid'));
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -62,7 +66,7 @@ export function PickupStep({
|
|
|
62
66
|
<form onSubmit={handleSubmit} className={cn('space-y-6', className)}>
|
|
63
67
|
{/* Pickup locations */}
|
|
64
68
|
<div className="space-y-3">
|
|
65
|
-
<p className="text-foreground text-sm font-medium">
|
|
69
|
+
<p className="text-foreground text-sm font-medium">{t('selectPickupLocation')}</p>
|
|
66
70
|
{locations.map((loc) => {
|
|
67
71
|
const price = parseFloat(loc.price);
|
|
68
72
|
const isFree = price === 0;
|
|
@@ -113,7 +117,7 @@ export function PickupStep({
|
|
|
113
117
|
isFree ? 'text-primary' : 'text-foreground'
|
|
114
118
|
)}
|
|
115
119
|
>
|
|
116
|
-
{isFree ? '
|
|
120
|
+
{isFree ? tc('free') : (formatPrice(price, { currency }) as string)}
|
|
117
121
|
</span>
|
|
118
122
|
</button>
|
|
119
123
|
);
|
|
@@ -122,11 +126,11 @@ export function PickupStep({
|
|
|
122
126
|
|
|
123
127
|
{/* Customer info */}
|
|
124
128
|
<div className="space-y-4">
|
|
125
|
-
<p className="text-foreground text-sm font-medium">
|
|
129
|
+
<p className="text-foreground text-sm font-medium">{t('yourDetails')}</p>
|
|
126
130
|
|
|
127
131
|
<div>
|
|
128
132
|
<label htmlFor="pickup-email" className="text-foreground mb-1 block text-sm">
|
|
129
|
-
|
|
133
|
+
{tf('email')} <span className="text-destructive">*</span>
|
|
130
134
|
</label>
|
|
131
135
|
<input
|
|
132
136
|
id="pickup-email"
|
|
@@ -142,7 +146,7 @@ export function PickupStep({
|
|
|
142
146
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
143
147
|
<div>
|
|
144
148
|
<label htmlFor="pickup-firstName" className="text-foreground mb-1 block text-sm">
|
|
145
|
-
|
|
149
|
+
{tf('firstName')}
|
|
146
150
|
</label>
|
|
147
151
|
<input
|
|
148
152
|
id="pickup-firstName"
|
|
@@ -154,7 +158,7 @@ export function PickupStep({
|
|
|
154
158
|
</div>
|
|
155
159
|
<div>
|
|
156
160
|
<label htmlFor="pickup-lastName" className="text-foreground mb-1 block text-sm">
|
|
157
|
-
|
|
161
|
+
{tf('lastName')}
|
|
158
162
|
</label>
|
|
159
163
|
<input
|
|
160
164
|
id="pickup-lastName"
|
|
@@ -168,7 +172,7 @@ export function PickupStep({
|
|
|
168
172
|
|
|
169
173
|
<div>
|
|
170
174
|
<label htmlFor="pickup-phone" className="text-foreground mb-1 block text-sm">
|
|
171
|
-
|
|
175
|
+
{tf('phone')}
|
|
172
176
|
</label>
|
|
173
177
|
<input
|
|
174
178
|
id="pickup-phone"
|
|
@@ -176,7 +180,7 @@ export function PickupStep({
|
|
|
176
180
|
value={phone}
|
|
177
181
|
onChange={(e) => setPhone(e.target.value)}
|
|
178
182
|
className={cn(inputClass, 'border-border')}
|
|
179
|
-
placeholder=
|
|
183
|
+
placeholder={tf('phonePlaceholder')}
|
|
180
184
|
/>
|
|
181
185
|
</div>
|
|
182
186
|
</div>
|
|
@@ -188,7 +192,7 @@ export function PickupStep({
|
|
|
188
192
|
disabled={loading || !selectedId}
|
|
189
193
|
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"
|
|
190
194
|
>
|
|
191
|
-
{loading ? '
|
|
195
|
+
{loading ? tc('saving') : t('continueToPayment')}
|
|
192
196
|
</button>
|
|
193
197
|
</form>
|
|
194
198
|
);
|
|
@@ -1,111 +1,110 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { ShippingRate } from 'brainerce';
|
|
4
|
-
import { formatPrice } from 'brainerce';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</
|
|
45
|
-
<p className="text-muted-foreground
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
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
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ShippingRate } from 'brainerce';
|
|
4
|
+
import { formatPrice } from 'brainerce';
|
|
5
|
+
import { useTranslations } from '@/lib/translations';
|
|
6
|
+
import { useStoreInfo } from '@/providers/store-provider';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface ShippingStepProps {
|
|
10
|
+
rates: ShippingRate[];
|
|
11
|
+
selectedRateId: string | null;
|
|
12
|
+
onSelect: (rateId: string) => void;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ShippingStep({
|
|
18
|
+
rates,
|
|
19
|
+
selectedRateId,
|
|
20
|
+
onSelect,
|
|
21
|
+
loading = false,
|
|
22
|
+
className,
|
|
23
|
+
}: ShippingStepProps) {
|
|
24
|
+
const t = useTranslations('checkout');
|
|
25
|
+
const tc = useTranslations('common');
|
|
26
|
+
const { storeInfo } = useStoreInfo();
|
|
27
|
+
const currency = storeInfo?.currency || 'USD';
|
|
28
|
+
|
|
29
|
+
if (rates.length === 0) {
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn('py-8 text-center', className)}>
|
|
32
|
+
<svg
|
|
33
|
+
className="text-muted-foreground mx-auto mb-3 h-10 w-10"
|
|
34
|
+
fill="none"
|
|
35
|
+
viewBox="0 0 24 24"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
>
|
|
38
|
+
<path
|
|
39
|
+
strokeLinecap="round"
|
|
40
|
+
strokeLinejoin="round"
|
|
41
|
+
strokeWidth={1.5}
|
|
42
|
+
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
<p className="text-muted-foreground text-sm">{t('noShippingOptions')}</p>
|
|
46
|
+
<p className="text-muted-foreground mt-1 text-xs">{t('noShippingOptionsHint')}</p>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn('space-y-3', className)}>
|
|
53
|
+
{rates.map((rate) => {
|
|
54
|
+
const price = parseFloat(rate.price);
|
|
55
|
+
const isFree = price === 0;
|
|
56
|
+
const isSelected = selectedRateId === rate.id;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
key={rate.id}
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => onSelect(rate.id)}
|
|
63
|
+
disabled={loading}
|
|
64
|
+
className={cn(
|
|
65
|
+
'flex w-full items-center gap-4 rounded border px-4 py-3 text-start transition-colors',
|
|
66
|
+
isSelected
|
|
67
|
+
? 'border-primary bg-primary/5'
|
|
68
|
+
: 'border-border hover:border-muted-foreground',
|
|
69
|
+
loading && 'cursor-not-allowed opacity-60'
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{/* Radio indicator */}
|
|
73
|
+
<div
|
|
74
|
+
className={cn(
|
|
75
|
+
'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border-2',
|
|
76
|
+
isSelected ? 'border-primary' : 'border-muted-foreground/40'
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
{isSelected && <div className="bg-primary h-2 w-2 rounded-full" />}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Rate info */}
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<p className="text-foreground text-sm font-medium">{rate.name}</p>
|
|
85
|
+
{rate.description && (
|
|
86
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{rate.description}</p>
|
|
87
|
+
)}
|
|
88
|
+
{rate.estimatedDays != null && (
|
|
89
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
90
|
+
{t('estimatedDelivery')} {rate.estimatedDays}{' '}
|
|
91
|
+
{rate.estimatedDays === 1 ? tc('day') : tc('days')}
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Price */}
|
|
97
|
+
<span
|
|
98
|
+
className={cn(
|
|
99
|
+
'flex-shrink-0 text-sm font-medium',
|
|
100
|
+
isFree ? 'text-primary' : 'text-foreground'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{isFree ? tc('free') : (formatPrice(price, { currency }) as string)}
|
|
104
|
+
</span>
|
|
105
|
+
</button>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { TaxBreakdown } from 'brainerce';
|
|
4
4
|
import { formatPrice } from 'brainerce';
|
|
5
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
6
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
|
|
@@ -16,6 +17,8 @@ interface TaxDisplayProps {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: TaxDisplayProps) {
|
|
20
|
+
const t = useTranslations('checkout');
|
|
21
|
+
const tc = useTranslations('common');
|
|
19
22
|
const { storeInfo } = useStoreInfo();
|
|
20
23
|
const currency = storeInfo?.currency || 'USD';
|
|
21
24
|
|
|
@@ -23,8 +26,8 @@ export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: T
|
|
|
23
26
|
if (!addressSet) {
|
|
24
27
|
return (
|
|
25
28
|
<div className={cn('flex items-center justify-between text-sm', className)}>
|
|
26
|
-
<span className="text-muted-foreground">
|
|
27
|
-
<span className="text-muted-foreground text-xs">
|
|
29
|
+
<span className="text-muted-foreground">{tc('tax')}</span>
|
|
30
|
+
<span className="text-muted-foreground text-xs">{t('calculatedAfterAddress')}</span>
|
|
28
31
|
</div>
|
|
29
32
|
);
|
|
30
33
|
}
|
|
@@ -35,9 +38,9 @@ export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: T
|
|
|
35
38
|
return (
|
|
36
39
|
<div className={cn('space-y-1', className)}>
|
|
37
40
|
<div className="flex items-center justify-between text-sm">
|
|
38
|
-
<span className="text-muted-foreground">
|
|
41
|
+
<span className="text-muted-foreground">{tc('tax')}</span>
|
|
39
42
|
<span className="text-foreground font-medium">
|
|
40
|
-
{tax > 0 ? (formatPrice(tax, { currency }) as string) : '
|
|
43
|
+
{tax > 0 ? (formatPrice(tax, { currency }) as string) : t('noTax')}
|
|
41
44
|
</span>
|
|
42
45
|
</div>
|
|
43
46
|
|
|
@@ -1,35 +1,38 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import Link from 'next/link';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
|
+
import { useStoreInfo } from '@/providers/store-provider';
|
|
6
|
+
|
|
7
|
+
export function Footer() {
|
|
8
|
+
const t = useTranslations('common');
|
|
9
|
+
const tn = useTranslations('nav');
|
|
10
|
+
const { storeInfo } = useStoreInfo();
|
|
11
|
+
const year = new Date().getFullYear();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<footer className="border-border bg-background border-t">
|
|
15
|
+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
16
|
+
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
|
17
|
+
<p className="text-muted-foreground text-sm">
|
|
18
|
+
{year} {storeInfo?.name || t('store')}. {t('allRightsReserved')}
|
|
19
|
+
</p>
|
|
20
|
+
<nav className="flex items-center gap-4">
|
|
21
|
+
<Link
|
|
22
|
+
href="/products"
|
|
23
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
24
|
+
>
|
|
25
|
+
{tn('products')}
|
|
26
|
+
</Link>
|
|
27
|
+
<Link
|
|
28
|
+
href="/account"
|
|
29
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
30
|
+
>
|
|
31
|
+
{tn('account')}
|
|
32
|
+
</Link>
|
|
33
|
+
</nav>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</footer>
|
|
37
|
+
);
|
|
38
|
+
}
|