create-brainerce-store 1.72.0 → 1.74.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 +37 -4
- package/messages/en.json +30 -1
- package/messages/he.json +30 -1
- package/package.json +1 -1
- package/templates/nextjs/base/AGENTS.md.ejs +63 -16
- package/templates/nextjs/base/AI-GUIDE.md +17 -4
- package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
- package/templates/nextjs/base/TRANSLATIONS.md +207 -207
- package/templates/nextjs/base/src/app/account/page.tsx +168 -122
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
- package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
- package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
- package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
- package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
- package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
- package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
- package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
- package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
- package/templates/nextjs/base/src/core/lib/region.ts +112 -0
- package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
- package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
- package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
- package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
- package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
- package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
- package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
- package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
- package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
- package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
- package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
- package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
- package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
- package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
- package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
- package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
- package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
- package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
- package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { Order, OrderStatus } from 'brainerce';
|
|
4
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
-
import { cn } from '@/core/lib/utils';
|
|
6
|
-
|
|
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'
|
|
16
|
-
| 'statusPending'
|
|
17
|
-
| 'statusProcessing'
|
|
18
|
-
| 'statusOnHold'
|
|
19
|
-
| 'statusPaid'
|
|
20
|
-
| 'statusShipped'
|
|
21
|
-
| 'statusDelivered'
|
|
22
|
-
| 'statusCompleted'
|
|
23
|
-
| 'statusFulfilled'
|
|
24
|
-
| 'statusCancelled'
|
|
25
|
-
| 'statusRefunded'
|
|
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',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
interface OrderStatusTimelineProps {
|
|
44
|
-
history: Order['statusHistory'];
|
|
45
|
-
className?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function OrderStatusTimeline({ history, className }: OrderStatusTimelineProps) {
|
|
49
|
-
const t = useTranslations('account');
|
|
50
|
-
if (!history || history.length === 0) return null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className={cn('border-border border-t pt-2', className)}>
|
|
54
|
-
<p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
|
|
55
|
-
<ol className="space-y-1.5">
|
|
56
|
-
{history.map((entry, idx) => {
|
|
57
|
-
const key = ORDER_STATUS_LABEL_KEYS[entry.status] || 'statusPending';
|
|
58
|
-
const when = new Date(entry.at);
|
|
59
|
-
const whenStr = isNaN(when.getTime())
|
|
60
|
-
? entry.at
|
|
61
|
-
: when.toLocaleString(undefined, {
|
|
62
|
-
year: 'numeric',
|
|
63
|
-
month: 'short',
|
|
64
|
-
day: 'numeric',
|
|
65
|
-
hour: '2-digit',
|
|
66
|
-
minute: '2-digit',
|
|
67
|
-
});
|
|
68
|
-
return (
|
|
69
|
-
<li key={`${entry.status}-${idx}`} className="flex items-center gap-2 text-xs">
|
|
70
|
-
<span
|
|
71
|
-
className={cn(
|
|
72
|
-
'bg-primary inline-block h-2 w-2 flex-shrink-0 rounded-full',
|
|
73
|
-
idx === history.length - 1 ? 'opacity-100' : 'opacity-60'
|
|
74
|
-
)}
|
|
75
|
-
/>
|
|
76
|
-
<span className="text-foreground font-medium">{t(key)}</span>
|
|
77
|
-
<span className="text-muted-foreground">· {whenStr}</span>
|
|
78
|
-
{entry.note && <span className="text-muted-foreground truncate">({entry.note})</span>}
|
|
79
|
-
</li>
|
|
80
|
-
);
|
|
81
|
-
})}
|
|
82
|
-
</ol>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Order, OrderStatus } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
+
import { cn } from '@/core/lib/utils';
|
|
6
|
+
|
|
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'
|
|
16
|
+
| 'statusPending'
|
|
17
|
+
| 'statusProcessing'
|
|
18
|
+
| 'statusOnHold'
|
|
19
|
+
| 'statusPaid'
|
|
20
|
+
| 'statusShipped'
|
|
21
|
+
| 'statusDelivered'
|
|
22
|
+
| 'statusCompleted'
|
|
23
|
+
| 'statusFulfilled'
|
|
24
|
+
| 'statusCancelled'
|
|
25
|
+
| 'statusRefunded'
|
|
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',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface OrderStatusTimelineProps {
|
|
44
|
+
history: Order['statusHistory'];
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function OrderStatusTimeline({ history, className }: OrderStatusTimelineProps) {
|
|
49
|
+
const t = useTranslations('account');
|
|
50
|
+
if (!history || history.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn('border-border border-t pt-2', className)}>
|
|
54
|
+
<p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
|
|
55
|
+
<ol className="space-y-1.5">
|
|
56
|
+
{history.map((entry, idx) => {
|
|
57
|
+
const key = ORDER_STATUS_LABEL_KEYS[entry.status] || 'statusPending';
|
|
58
|
+
const when = new Date(entry.at);
|
|
59
|
+
const whenStr = isNaN(when.getTime())
|
|
60
|
+
? entry.at
|
|
61
|
+
: when.toLocaleString(undefined, {
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
month: 'short',
|
|
64
|
+
day: 'numeric',
|
|
65
|
+
hour: '2-digit',
|
|
66
|
+
minute: '2-digit',
|
|
67
|
+
});
|
|
68
|
+
return (
|
|
69
|
+
<li key={`${entry.status}-${idx}`} className="flex items-center gap-2 text-xs">
|
|
70
|
+
<span
|
|
71
|
+
className={cn(
|
|
72
|
+
'bg-primary inline-block h-2 w-2 flex-shrink-0 rounded-full',
|
|
73
|
+
idx === history.length - 1 ? 'opacity-100' : 'opacity-60'
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
<span className="text-foreground font-medium">{t(key)}</span>
|
|
77
|
+
<span className="text-muted-foreground">· {whenStr}</span>
|
|
78
|
+
{entry.note && <span className="text-muted-foreground truncate">({entry.note})</span>}
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</ol>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The customer's vaulted payment methods.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ AUTO-HIDES. `getMySavedPaymentMethods()` returns `[]` for everyone who has
|
|
7
|
+
* never vaulted a card, which on a new store is everyone, so the whole section
|
|
8
|
+
* renders null. It also renders null when the call fails or the shopper is
|
|
9
|
+
* logged out. There is no empty state and no error box: a shopper who has no
|
|
10
|
+
* saved cards does not need to be told about a feature they have not used.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ THIS IS NOT ONE-CLICK CHECKOUT, and there is deliberately no "pay with
|
|
13
|
+
* this card" button. The ONLY storefront call that charges a vaulted card is
|
|
14
|
+
* `subscribeToMembership({ planId, savedPaymentTokenId })`. Nothing on the
|
|
15
|
+
* normal checkout accepts a saved token, so a pay-with-saved-card button would
|
|
16
|
+
* be a dead control. This is a read-only list.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ NO CARD DATA IS HERE AND NONE IS AVAILABLE. The response carries display
|
|
19
|
+
* fields only: `paymentMethod`, `brand`, `last4`, `expMonth`, `expYear`,
|
|
20
|
+
* `isDefault`. Every one of those can be null except the first and last.
|
|
21
|
+
*
|
|
22
|
+
* Cards get here by checking out with `createPaymentIntent(checkoutId,
|
|
23
|
+
* { saveCard: true })`, which is honored only when the checkout has a known
|
|
24
|
+
* `customerId` (so never for a guest) and is silently ignored by providers
|
|
25
|
+
* without tokenization (Grow today). Treat vaulting as best-effort and never
|
|
26
|
+
* promise the shopper their card was definitely saved.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useEffect, useState } from 'react';
|
|
30
|
+
import type { StorefrontSavedPaymentMethod } from 'brainerce';
|
|
31
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
32
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
33
|
+
import { useAuth } from '@/core/providers/store-provider';
|
|
34
|
+
import { cn } from '@/core/lib/utils';
|
|
35
|
+
|
|
36
|
+
/** "12/2027" from expMonth/expYear, or null when the provider gave neither. */
|
|
37
|
+
function formatExpiry(month: number | null, year: number | null): string | null {
|
|
38
|
+
if (month == null || year == null) return null;
|
|
39
|
+
return `${String(month).padStart(2, '0')}/${year}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SavedPaymentMethods({ className }: { className?: string }) {
|
|
43
|
+
const t = useTranslations('account');
|
|
44
|
+
const { isLoggedIn } = useAuth();
|
|
45
|
+
const [methods, setMethods] = useState<StorefrontSavedPaymentMethod[]>([]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isLoggedIn) {
|
|
49
|
+
setMethods([]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
getClient()
|
|
54
|
+
.getMySavedPaymentMethods()
|
|
55
|
+
.then((result) => {
|
|
56
|
+
if (!cancelled) setMethods(Array.isArray(result) ? result : []);
|
|
57
|
+
})
|
|
58
|
+
.catch(() => {
|
|
59
|
+
// Swallowed on purpose. The account page must render its profile,
|
|
60
|
+
// addresses and orders whether or not this optional list resolves.
|
|
61
|
+
if (!cancelled) setMethods([]);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
}, [isLoggedIn]);
|
|
67
|
+
|
|
68
|
+
if (methods.length === 0) return null;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={cn('', className)}>
|
|
72
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{t('paymentMethod')}</h2>
|
|
73
|
+
<ul className="border-border divide-border divide-y rounded-lg border">
|
|
74
|
+
{methods.map((method) => {
|
|
75
|
+
const expiry = formatExpiry(method.expMonth, method.expYear);
|
|
76
|
+
return (
|
|
77
|
+
<li key={method.id} className="flex items-center justify-between gap-4 px-4 py-3">
|
|
78
|
+
<span className="text-foreground text-sm font-medium">
|
|
79
|
+
{/* Brand + last4 when the provider gave them; otherwise the
|
|
80
|
+
method itself ("paypal", "bank_account"). All store data,
|
|
81
|
+
so no translation key is involved. */}
|
|
82
|
+
{method.brand && method.last4
|
|
83
|
+
? `${method.brand} •••• ${method.last4}`
|
|
84
|
+
: method.paymentMethod}
|
|
85
|
+
</span>
|
|
86
|
+
<span className="flex items-center gap-3">
|
|
87
|
+
{expiry ? (
|
|
88
|
+
<span className="text-muted-foreground text-sm" dir="ltr">
|
|
89
|
+
{expiry}
|
|
90
|
+
</span>
|
|
91
|
+
) : null}
|
|
92
|
+
{method.isDefault ? (
|
|
93
|
+
<span className="bg-secondary text-secondary-foreground rounded px-2 py-0.5 text-xs font-medium">
|
|
94
|
+
{t('defaultAddress')}
|
|
95
|
+
</span>
|
|
96
|
+
) : null}
|
|
97
|
+
</span>
|
|
98
|
+
</li>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</ul>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|