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.
Files changed (38) hide show
  1. package/dist/index.js +62 -5
  2. package/messages/en.json +258 -0
  3. package/messages/he.json +258 -0
  4. package/package.json +3 -2
  5. package/templates/nextjs/base/src/app/account/page.tsx +108 -105
  6. package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -88
  7. package/templates/nextjs/base/src/app/cart/page.tsx +110 -109
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +46 -43
  9. package/templates/nextjs/base/src/app/layout.tsx.ejs +8 -5
  10. package/templates/nextjs/base/src/app/login/page.tsx +58 -56
  11. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +18 -23
  12. package/templates/nextjs/base/src/app/page.tsx +98 -95
  13. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +16 -12
  14. package/templates/nextjs/base/src/app/products/page.tsx +246 -243
  15. package/templates/nextjs/base/src/app/register/page.tsx +68 -66
  16. package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -291
  17. package/templates/nextjs/base/src/components/account/order-history.tsx +198 -184
  18. package/templates/nextjs/base/src/components/account/profile-section.tsx +75 -73
  19. package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -92
  20. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -134
  21. package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -177
  22. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -150
  23. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -67
  24. package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -131
  25. package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -100
  26. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +28 -25
  27. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +6 -4
  28. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +133 -103
  29. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +15 -11
  30. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -111
  31. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +7 -4
  32. package/templates/nextjs/base/src/components/layout/footer.tsx +38 -35
  33. package/templates/nextjs/base/src/components/layout/header.tsx +332 -329
  34. package/templates/nextjs/base/src/components/products/product-card.tsx +3 -1
  35. package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -33
  36. package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -30
  37. package/templates/nextjs/base/src/i18n.ts.ejs +5 -0
  38. 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('Please select a pickup location');
45
+ setError(t('pickupLocationRequired'));
42
46
  return;
43
47
  }
44
48
  if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
45
- setError('Please enter a valid email');
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">Select pickup location</p>
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 ? 'Free' : (formatPrice(price, { currency }) as string)}
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">Your details</p>
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
- Email <span className="text-destructive">*</span>
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
- First Name
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
- Last Name
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
- Phone
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="+1234567890 (optional)"
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 ? 'Saving...' : 'Continue to Payment'}
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 { useStoreInfo } from '@/providers/store-provider';
6
- import { cn } from '@/lib/utils';
7
-
8
- interface ShippingStepProps {
9
- rates: ShippingRate[];
10
- selectedRateId: string | null;
11
- onSelect: (rateId: string) => void;
12
- loading?: boolean;
13
- className?: string;
14
- }
15
-
16
- export function ShippingStep({
17
- rates,
18
- selectedRateId,
19
- onSelect,
20
- loading = false,
21
- className,
22
- }: ShippingStepProps) {
23
- const { storeInfo } = useStoreInfo();
24
- const currency = storeInfo?.currency || 'USD';
25
-
26
- if (rates.length === 0) {
27
- return (
28
- <div className={cn('py-8 text-center', className)}>
29
- <svg
30
- className="text-muted-foreground mx-auto mb-3 h-10 w-10"
31
- fill="none"
32
- viewBox="0 0 24 24"
33
- stroke="currentColor"
34
- >
35
- <path
36
- strokeLinecap="round"
37
- strokeLinejoin="round"
38
- strokeWidth={1.5}
39
- d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
40
- />
41
- </svg>
42
- <p className="text-muted-foreground text-sm">
43
- No shipping options available for this address.
44
- </p>
45
- <p className="text-muted-foreground mt-1 text-xs">
46
- Please try a different address or contact support.
47
- </p>
48
- </div>
49
- );
50
- }
51
-
52
- return (
53
- <div className={cn('space-y-3', className)}>
54
- {rates.map((rate) => {
55
- const price = parseFloat(rate.price);
56
- const isFree = price === 0;
57
- const isSelected = selectedRateId === rate.id;
58
-
59
- return (
60
- <button
61
- key={rate.id}
62
- type="button"
63
- onClick={() => onSelect(rate.id)}
64
- disabled={loading}
65
- className={cn(
66
- 'flex w-full items-center gap-4 rounded border px-4 py-3 text-start transition-colors',
67
- isSelected
68
- ? 'border-primary bg-primary/5'
69
- : 'border-border hover:border-muted-foreground',
70
- loading && 'cursor-not-allowed opacity-60'
71
- )}
72
- >
73
- {/* Radio indicator */}
74
- <div
75
- className={cn(
76
- 'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border-2',
77
- isSelected ? 'border-primary' : 'border-muted-foreground/40'
78
- )}
79
- >
80
- {isSelected && <div className="bg-primary h-2 w-2 rounded-full" />}
81
- </div>
82
-
83
- {/* Rate info */}
84
- <div className="min-w-0 flex-1">
85
- <p className="text-foreground text-sm font-medium">{rate.name}</p>
86
- {rate.description && (
87
- <p className="text-muted-foreground mt-0.5 text-xs">{rate.description}</p>
88
- )}
89
- {rate.estimatedDays != null && (
90
- <p className="text-muted-foreground mt-0.5 text-xs">
91
- Estimated delivery: {rate.estimatedDays}{' '}
92
- {rate.estimatedDays === 1 ? 'day' : 'days'}
93
- </p>
94
- )}
95
- </div>
96
-
97
- {/* Price */}
98
- <span
99
- className={cn(
100
- 'flex-shrink-0 text-sm font-medium',
101
- isFree ? 'text-primary' : 'text-foreground'
102
- )}
103
- >
104
- {isFree ? 'Free' : (formatPrice(price, { currency }) as string)}
105
- </span>
106
- </button>
107
- );
108
- })}
109
- </div>
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">Tax</span>
27
- <span className="text-muted-foreground text-xs">Calculated after address entry</span>
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">Tax</span>
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) : 'No tax'}
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 { useStoreInfo } from '@/providers/store-provider';
5
-
6
- export function Footer() {
7
- const { storeInfo } = useStoreInfo();
8
- const year = new Date().getFullYear();
9
-
10
- return (
11
- <footer className="border-border bg-background border-t">
12
- <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
13
- <div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
14
- <p className="text-muted-foreground text-sm">
15
- {year} {storeInfo?.name || 'Store'}. All rights reserved.
16
- </p>
17
- <nav className="flex items-center gap-4">
18
- <Link
19
- href="/products"
20
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
21
- >
22
- Products
23
- </Link>
24
- <Link
25
- href="/account"
26
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
27
- >
28
- Account
29
- </Link>
30
- </nav>
31
- </div>
32
- </div>
33
- </footer>
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
+ }