create-brainerce-store 1.68.0 → 1.72.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/README.md +31 -10
- package/dist/index.js +197 -105
- package/messages/en.json +63 -3
- package/messages/he.json +63 -3
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +14 -7
- package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1017
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +1 -1
- package/templates/nextjs/base/src/app/register/page.tsx +67 -64
- package/templates/nextjs/base/src/components/account/order-history.tsx +25 -39
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +30 -11
- package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
- package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
- package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +71 -2
- package/templates/nextjs/base/src/core/lib/auth.ts +155 -154
- package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
- package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +42 -6
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
- package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
- package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +21 -3
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +44 -7
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +15 -1
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +38 -4
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +54 -11
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
- package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
- package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +127 -96
|
@@ -32,10 +32,29 @@ function OrderConfirmationContent() {
|
|
|
32
32
|
try {
|
|
33
33
|
const client = getClient();
|
|
34
34
|
|
|
35
|
-
// Clear cart state after successful payment
|
|
36
|
-
|
|
35
|
+
// Clear cart state after successful payment. handlePaymentSuccess is
|
|
36
|
+
// SYNCHRONOUS: it returns the outcome rather than a promise, so there
|
|
37
|
+
// is nothing to await. Read the result, then refresh the cart AFTER
|
|
38
|
+
// it, or the header badge can be repopulated from the pre-clear cart.
|
|
39
|
+
//
|
|
40
|
+
// mode 'full' - whole cart cleared
|
|
41
|
+
// mode 'partial' - only the purchased items were removed (partial
|
|
42
|
+
// checkout); whatever is left is still for sale
|
|
43
|
+
// mode 'none' - nothing to clear. Usually the SDK's idempotency
|
|
44
|
+
// guard: React Strict Mode runs this effect twice
|
|
45
|
+
// in development, and a refresh of this URL hits it
|
|
46
|
+
// too. On a good order this is expected, so it must
|
|
47
|
+
// never become a warning to the shopper.
|
|
48
|
+
const clearResult = client.handlePaymentSuccess(checkoutId!);
|
|
37
49
|
await refreshCart();
|
|
38
50
|
|
|
51
|
+
if (!clearResult.cleared && clearResult.mode !== 'none') {
|
|
52
|
+
// A clear that neither succeeded nor was skipped leaves purchased
|
|
53
|
+
// items in the badge. Nothing the shopper can act on, but a silent
|
|
54
|
+
// gap here looks exactly like a working cart.
|
|
55
|
+
console.warn('Cart not cleared after payment:', clearResult);
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
// For redirect-based payment providers, the customer returns with
|
|
40
59
|
// provider params in the URL — CardCom: lowprofilecode; PayPal:
|
|
41
60
|
// token + PayerID. Send these to the backend for server-side
|
|
@@ -192,7 +192,7 @@ export default async function ProductDetailPage({ params }: Props) {
|
|
|
192
192
|
<div className="mx-auto max-w-7xl px-4 pt-6 sm:px-6 lg:px-8">
|
|
193
193
|
<Breadcrumbs items={breadcrumbItems} />
|
|
194
194
|
</div>
|
|
195
|
-
<ProductClientSection product={product} />
|
|
195
|
+
<ProductClientSection product={product} stockAlertsEnabled={storeInfo?.stockAlertsEnabled} />
|
|
196
196
|
{/* Visible Q&A + the FAQPage JSON-LD above read the same product.faq —
|
|
197
197
|
the extractable format AI answer engines cite. */}
|
|
198
198
|
<ProductFaqSection product={product} locale={locale} />
|
|
@@ -1,64 +1,67 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter, Link } from '@/core/lib/navigation';
|
|
5
|
-
import { useAuth } from '@/core/providers/store-provider';
|
|
6
|
-
import { proxyRegister } from '@/core/lib/auth';
|
|
7
|
-
import { RegisterForm } from '@/components/auth/register-form';
|
|
8
|
-
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
9
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
-
|
|
11
|
-
export default function RegisterPage() {
|
|
12
|
-
const router = useRouter();
|
|
13
|
-
const auth = useAuth();
|
|
14
|
-
const t = useTranslations('auth');
|
|
15
|
-
const [error, setError] = useState<string | null>(null);
|
|
16
|
-
|
|
17
|
-
async function handleRegister(data: {
|
|
18
|
-
firstName: string;
|
|
19
|
-
lastName: string;
|
|
20
|
-
email: string;
|
|
21
|
-
password: string;
|
|
22
|
-
acceptsMarketing: boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter, Link } from '@/core/lib/navigation';
|
|
5
|
+
import { useAuth } from '@/core/providers/store-provider';
|
|
6
|
+
import { proxyRegister } from '@/core/lib/auth';
|
|
7
|
+
import { RegisterForm } from '@/components/auth/register-form';
|
|
8
|
+
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
9
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
|
|
11
|
+
export default function RegisterPage() {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const auth = useAuth();
|
|
14
|
+
const t = useTranslations('auth');
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
async function handleRegister(data: {
|
|
18
|
+
firstName: string;
|
|
19
|
+
lastName: string;
|
|
20
|
+
email: string;
|
|
21
|
+
password: string;
|
|
22
|
+
acceptsMarketing: boolean;
|
|
23
|
+
/** Month and day only, never a year. Present together or not at all. */
|
|
24
|
+
birthMonth?: number;
|
|
25
|
+
birthDay?: number;
|
|
26
|
+
}) {
|
|
27
|
+
try {
|
|
28
|
+
setError(null);
|
|
29
|
+
const result = await proxyRegister(data);
|
|
30
|
+
|
|
31
|
+
if (result.requiresVerification) {
|
|
32
|
+
// Cookie already set by proxy; verify-email uses it for auth
|
|
33
|
+
router.push('/verify-email');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cookie was set by the proxy; refresh auth state
|
|
38
|
+
await auth.login();
|
|
39
|
+
router.push('/');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
|
|
42
|
+
setError(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
48
|
+
<div className="w-full max-w-md space-y-6">
|
|
49
|
+
<div className="text-center">
|
|
50
|
+
<h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
|
|
51
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<RegisterForm onSubmit={handleRegister} error={error} />
|
|
55
|
+
|
|
56
|
+
<OAuthButtons />
|
|
57
|
+
|
|
58
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
59
|
+
{t('alreadyHaveAccount')}{' '}
|
|
60
|
+
<Link href="/login" className="text-primary font-medium hover:underline">
|
|
61
|
+
{t('signIn')}
|
|
62
|
+
</Link>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -9,35 +9,29 @@ import { useCurrency } from '@/core/lib/use-currency';
|
|
|
9
9
|
import { useTranslations } from '@/core/lib/translations';
|
|
10
10
|
import { cn } from '@/core/lib/utils';
|
|
11
11
|
import { OrderCustomizations } from './order-customizations';
|
|
12
|
-
import { OrderStatusTimeline } from './order-status-timeline';
|
|
12
|
+
import { OrderStatusTimeline, ORDER_STATUS_LABEL_KEYS } from './order-status-timeline';
|
|
13
13
|
import { OrderShippingBlock } from './order-shipping-block';
|
|
14
14
|
import { OrderPaymentBlock } from './order-payment-block';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
className: 'bg-red-100 text-red-800 dark:bg-red-950/30 dark:text-red-400',
|
|
36
|
-
},
|
|
37
|
-
refunded: {
|
|
38
|
-
labelKey: 'statusRefunded',
|
|
39
|
-
className: 'bg-orange-100 text-orange-800 dark:bg-orange-950/30 dark:text-orange-400',
|
|
40
|
-
},
|
|
16
|
+
/**
|
|
17
|
+
* Badge colour per order status. The API sends the status UPPERCASE and
|
|
18
|
+
* verbatim, so these keys are uppercase too. Do NOT lowercase `order.status`
|
|
19
|
+
* before the lookup: every row would miss and render as "Pending".
|
|
20
|
+
* Labels come from ORDER_STATUS_LABEL_KEYS so the two stay in step.
|
|
21
|
+
*/
|
|
22
|
+
const STATUS_STYLES: Record<OrderStatus, string> = {
|
|
23
|
+
DRAFT: 'bg-muted text-muted-foreground',
|
|
24
|
+
PENDING: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-950/30 dark:text-yellow-400',
|
|
25
|
+
PROCESSING: 'bg-blue-100 text-blue-800 dark:bg-blue-950/30 dark:text-blue-400',
|
|
26
|
+
ON_HOLD: 'bg-amber-100 text-amber-800 dark:bg-amber-950/30 dark:text-amber-400',
|
|
27
|
+
PAID: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/30 dark:text-emerald-400',
|
|
28
|
+
SHIPPED: 'bg-purple-100 text-purple-800 dark:bg-purple-950/30 dark:text-purple-400',
|
|
29
|
+
DELIVERED: 'bg-green-100 text-green-800 dark:bg-green-950/30 dark:text-green-400',
|
|
30
|
+
COMPLETED: 'bg-green-100 text-green-800 dark:bg-green-950/30 dark:text-green-400',
|
|
31
|
+
FULFILLED: 'bg-teal-100 text-teal-800 dark:bg-teal-950/30 dark:text-teal-400',
|
|
32
|
+
CANCELLED: 'bg-red-100 text-red-800 dark:bg-red-950/30 dark:text-red-400',
|
|
33
|
+
REFUNDED: 'bg-orange-100 text-orange-800 dark:bg-orange-950/30 dark:text-orange-400',
|
|
34
|
+
PARTIALLY_REFUNDED: 'bg-orange-100 text-orange-800 dark:bg-orange-950/30 dark:text-orange-400',
|
|
41
35
|
};
|
|
42
36
|
|
|
43
37
|
interface OrderHistoryProps {
|
|
@@ -82,8 +76,8 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
82
76
|
const t = useTranslations('account');
|
|
83
77
|
const tc = useTranslations('common');
|
|
84
78
|
const [expanded, setExpanded] = useState(false);
|
|
85
|
-
const
|
|
86
|
-
|
|
79
|
+
const statusLabelKey = ORDER_STATUS_LABEL_KEYS[order.status] || 'statusPending';
|
|
80
|
+
const statusClassName = STATUS_STYLES[order.status] || STATUS_STYLES.PENDING;
|
|
87
81
|
const currency = useCurrency(order.currency);
|
|
88
82
|
const totalAmount = order.totalAmount || order.total || '0';
|
|
89
83
|
|
|
@@ -103,18 +97,10 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
103
97
|
<span
|
|
104
98
|
className={cn(
|
|
105
99
|
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
|
106
|
-
|
|
100
|
+
statusClassName
|
|
107
101
|
)}
|
|
108
102
|
>
|
|
109
|
-
{t(
|
|
110
|
-
statusConfig.labelKey as
|
|
111
|
-
| 'statusPending'
|
|
112
|
-
| 'statusProcessing'
|
|
113
|
-
| 'statusShipped'
|
|
114
|
-
| 'statusDelivered'
|
|
115
|
-
| 'statusCancelled'
|
|
116
|
-
| 'statusRefunded'
|
|
117
|
-
)}
|
|
103
|
+
{t(statusLabelKey)}
|
|
118
104
|
</span>
|
|
119
105
|
</div>
|
|
120
106
|
<div className="text-muted-foreground mt-1 flex items-center gap-4 text-xs">
|
|
@@ -125,7 +111,7 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
125
111
|
month: 'short',
|
|
126
112
|
day: 'numeric',
|
|
127
113
|
})
|
|
128
|
-
: '
|
|
114
|
+
: ''}
|
|
129
115
|
</span>
|
|
130
116
|
<span>
|
|
131
117
|
{order.items.length} {order.items.length === 1 ? tc('item') : tc('items')}
|
|
@@ -4,21 +4,40 @@ import type { Order, OrderStatus } from 'brainerce';
|
|
|
4
4
|
import { useTranslations } from '@/core/lib/translations';
|
|
5
5
|
import { cn } from '@/core/lib/utils';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Message key for every order status the API can return.
|
|
9
|
+
*
|
|
10
|
+
* The API sends these values UPPERCASE and verbatim (`PENDING`, `ON_HOLD`,
|
|
11
|
+
* ...). Never lowercase them before the lookup: a lowercase key silently
|
|
12
|
+
* misses and every row falls through to the default label.
|
|
13
|
+
*/
|
|
14
|
+
export type OrderStatusLabelKey =
|
|
15
|
+
| 'statusDraft'
|
|
9
16
|
| 'statusPending'
|
|
10
17
|
| 'statusProcessing'
|
|
18
|
+
| 'statusOnHold'
|
|
19
|
+
| 'statusPaid'
|
|
11
20
|
| 'statusShipped'
|
|
12
21
|
| 'statusDelivered'
|
|
22
|
+
| 'statusCompleted'
|
|
23
|
+
| 'statusFulfilled'
|
|
13
24
|
| 'statusCancelled'
|
|
14
25
|
| 'statusRefunded'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
| 'statusPartiallyRefunded';
|
|
27
|
+
|
|
28
|
+
export const ORDER_STATUS_LABEL_KEYS: Record<OrderStatus, OrderStatusLabelKey> = {
|
|
29
|
+
DRAFT: 'statusDraft',
|
|
30
|
+
PENDING: 'statusPending',
|
|
31
|
+
PROCESSING: 'statusProcessing',
|
|
32
|
+
ON_HOLD: 'statusOnHold',
|
|
33
|
+
PAID: 'statusPaid',
|
|
34
|
+
SHIPPED: 'statusShipped',
|
|
35
|
+
DELIVERED: 'statusDelivered',
|
|
36
|
+
COMPLETED: 'statusCompleted',
|
|
37
|
+
FULFILLED: 'statusFulfilled',
|
|
38
|
+
CANCELLED: 'statusCancelled',
|
|
39
|
+
REFUNDED: 'statusRefunded',
|
|
40
|
+
PARTIALLY_REFUNDED: 'statusPartiallyRefunded',
|
|
22
41
|
};
|
|
23
42
|
|
|
24
43
|
interface OrderStatusTimelineProps {
|
|
@@ -35,7 +54,7 @@ export function OrderStatusTimeline({ history, className }: OrderStatusTimelineP
|
|
|
35
54
|
<p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
|
|
36
55
|
<ol className="space-y-1.5">
|
|
37
56
|
{history.map((entry, idx) => {
|
|
38
|
-
const key =
|
|
57
|
+
const key = ORDER_STATUS_LABEL_KEYS[entry.status] || 'statusPending';
|
|
39
58
|
const when = new Date(entry.at);
|
|
40
59
|
const whenStr = isNaN(when.getTime())
|
|
41
60
|
? entry.at
|
|
@@ -56,7 +75,7 @@ export function OrderStatusTimeline({ history, className }: OrderStatusTimelineP
|
|
|
56
75
|
/>
|
|
57
76
|
<span className="text-foreground font-medium">{t(key)}</span>
|
|
58
77
|
<span className="text-muted-foreground">· {whenStr}</span>
|
|
59
|
-
{entry.note && <span className="text-muted-foreground truncate"
|
|
78
|
+
{entry.note && <span className="text-muted-foreground truncate">({entry.note})</span>}
|
|
60
79
|
</li>
|
|
61
80
|
);
|
|
62
81
|
})}
|