create-brainerce-store 1.41.1 → 1.43.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 +16 -11
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +200 -0
- package/templates/nextjs/base/next.config.ts +22 -0
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +10 -4
- package/templates/nextjs/base/src/app/checkout/page.tsx +982 -981
- package/templates/nextjs/base/src/app/layout.tsx.ejs +14 -6
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +118 -80
- package/templates/nextjs/base/src/components/account/order-history.tsx +2 -1
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +2 -3
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +2 -3
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +3 -3
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +2 -3
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +4 -1
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +2 -3
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +2 -3
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +2 -3
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +2 -3
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +5 -4
- package/templates/nextjs/base/src/components/products/product-card.tsx +3 -3
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +2 -3
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +129 -121
- package/templates/nextjs/base/src/components/shared/price-display.tsx +2 -3
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +42 -0
- package/templates/nextjs/base/src/lib/resolve-currency.ts +25 -0
- package/templates/nextjs/base/src/lib/store-info.ts +48 -0
- package/templates/nextjs/base/src/lib/use-currency.ts +24 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +37 -14
|
@@ -5,7 +5,7 @@ import { StoreProvider } from '@/providers/store-provider';
|
|
|
5
5
|
import { AnnouncementBar } from '@/components/content/announcement-bar';
|
|
6
6
|
import { SiteHeader } from '@/components/content/site-header';
|
|
7
7
|
import { SiteFooter } from '@/components/content/site-footer';
|
|
8
|
-
import { getServerClient } from '@/lib/brainerce';
|
|
8
|
+
import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
|
|
9
9
|
import { getDirection, supportedLocales } from '@/i18n';
|
|
10
10
|
import { getNonce } from '@/lib/nonce';
|
|
11
11
|
import '../globals.css';
|
|
@@ -61,10 +61,14 @@ export default async function RootLayout({
|
|
|
61
61
|
// seeded a particular content type yet. New stores ship with default rows
|
|
62
62
|
// seeded by the backend (StoresService.seedDefaultContent).
|
|
63
63
|
const client = getServerClient(locale);
|
|
64
|
-
const [announcements, siteHeader, siteFooter] = await Promise.all([
|
|
64
|
+
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
65
65
|
client.content.announcement.list(locale).catch(() => []),
|
|
66
66
|
client.content.header.get('main', locale).catch(() => null),
|
|
67
67
|
client.content.footer.get('main', locale).catch(() => null),
|
|
68
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
69
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
70
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
71
|
+
fetchStoreInfo(locale),
|
|
68
72
|
]);
|
|
69
73
|
|
|
70
74
|
return (
|
|
@@ -83,7 +87,7 @@ export default async function RootLayout({
|
|
|
83
87
|
/>
|
|
84
88
|
</head>
|
|
85
89
|
<body className={font.className}>
|
|
86
|
-
<StoreProvider locale={locale}>
|
|
90
|
+
<StoreProvider locale={locale} initialStoreInfo={storeInfo}>
|
|
87
91
|
<div className="min-h-screen flex flex-col">
|
|
88
92
|
<AnnouncementBar announcements={announcements} />
|
|
89
93
|
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
@@ -102,7 +106,7 @@ import { StoreProvider } from '@/providers/store-provider';
|
|
|
102
106
|
import { AnnouncementBar } from '@/components/content/announcement-bar';
|
|
103
107
|
import { SiteHeader } from '@/components/content/site-header';
|
|
104
108
|
import { SiteFooter } from '@/components/content/site-footer';
|
|
105
|
-
import { getServerClient } from '@/lib/brainerce';
|
|
109
|
+
import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
|
|
106
110
|
import { getNonce } from '@/lib/nonce';
|
|
107
111
|
import './globals.css';
|
|
108
112
|
|
|
@@ -150,10 +154,14 @@ export default async function RootLayout({
|
|
|
150
154
|
// seeded a particular content type yet. New stores ship with default rows
|
|
151
155
|
// seeded by the backend (StoresService.seedDefaultContent).
|
|
152
156
|
const client = getServerClient();
|
|
153
|
-
const [announcements, siteHeader, siteFooter] = await Promise.all([
|
|
157
|
+
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
154
158
|
client.content.announcement.list().catch(() => []),
|
|
155
159
|
client.content.header.get('main').catch(() => null),
|
|
156
160
|
client.content.footer.get('main').catch(() => null),
|
|
161
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
162
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
163
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
164
|
+
fetchStoreInfo(),
|
|
157
165
|
]);
|
|
158
166
|
|
|
159
167
|
return (
|
|
@@ -172,7 +180,7 @@ export default async function RootLayout({
|
|
|
172
180
|
/>
|
|
173
181
|
</head>
|
|
174
182
|
<body className={font.className}>
|
|
175
|
-
<StoreProvider>
|
|
183
|
+
<StoreProvider initialStoreInfo={storeInfo}>
|
|
176
184
|
<div className="min-h-screen flex flex-col">
|
|
177
185
|
<AnnouncementBar announcements={announcements} />
|
|
178
186
|
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
@@ -1,80 +1,118 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
import { notFound } from 'next/navigation';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// emit
|
|
25
|
-
const
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import { getProductPriceInfo } from 'brainerce';
|
|
4
|
+
import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
|
|
5
|
+
import { resolveCurrency } from '@/lib/resolve-currency';
|
|
6
|
+
import { buildMetaDescription } from '@/lib/seo';
|
|
7
|
+
import { decodeSlug } from '@/lib/utils';
|
|
8
|
+
import { ProductJsonLd } from '@/components/seo/product-json-ld';
|
|
9
|
+
import { ReviewsSection } from '@/components/reviews/reviews-section';
|
|
10
|
+
import { ProductClientSection } from './product-client-section';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
params: Promise<{ slug: string; locale?: string }>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
17
|
+
const { slug: rawSlug, locale } = await params;
|
|
18
|
+
const slug = decodeSlug(rawSlug);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const client = getServerClient(locale);
|
|
22
|
+
// Fetch product + store info in parallel; storeInfo gives us the live
|
|
23
|
+
// currency for OG price tags (with env-var + USD fallbacks so we never
|
|
24
|
+
// silently emit a wrong currency when the API is briefly unreachable).
|
|
25
|
+
const [product, storeInfo] = await Promise.all([
|
|
26
|
+
client.getProductBySlug(slug),
|
|
27
|
+
fetchStoreInfo(locale),
|
|
28
|
+
]);
|
|
29
|
+
const imageUrl = product.images?.[0]?.url;
|
|
30
|
+
// Prefer merchant-authored SEO copy; fall back to a stripped+truncated
|
|
31
|
+
// version of the visible description, then product name. We must NEVER
|
|
32
|
+
// emit raw HTML or a mid-word cut into <meta name="description">.
|
|
33
|
+
const seoTitle = (product as { seoTitle?: string | null }).seoTitle || product.name;
|
|
34
|
+
const seoDescription =
|
|
35
|
+
(product as { seoDescription?: string | null }).seoDescription ||
|
|
36
|
+
buildMetaDescription(product.description) ||
|
|
37
|
+
product.name;
|
|
38
|
+
|
|
39
|
+
// OG product meta tags drive WhatsApp / Facebook / X link previews and
|
|
40
|
+
// Google Merchant Center product enrichment. Emitting the literal "0"
|
|
41
|
+
// here (the previous bug) causes price-zero link cards. Use the real
|
|
42
|
+
// effective price from the SDK helper.
|
|
43
|
+
const priceInfo = getProductPriceInfo(product);
|
|
44
|
+
const currency = resolveCurrency(storeInfo);
|
|
45
|
+
const priceAmount = priceInfo.price > 0 ? priceInfo.price.toFixed(2) : null;
|
|
46
|
+
const inStock = product.inventory?.canPurchase !== false;
|
|
47
|
+
const brandName = (product as { brand?: { name?: string } | null }).brand?.name;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
title: seoTitle,
|
|
51
|
+
description: seoDescription,
|
|
52
|
+
alternates: {
|
|
53
|
+
canonical: `/products/${slug}`,
|
|
54
|
+
},
|
|
55
|
+
openGraph: {
|
|
56
|
+
title: seoTitle,
|
|
57
|
+
description: seoDescription,
|
|
58
|
+
images: imageUrl ? [{ url: imageUrl, alt: product.name }] : [],
|
|
59
|
+
type: 'website',
|
|
60
|
+
},
|
|
61
|
+
twitter: {
|
|
62
|
+
card: 'summary_large_image',
|
|
63
|
+
title: seoTitle,
|
|
64
|
+
description: seoDescription,
|
|
65
|
+
images: imageUrl ? [imageUrl] : [],
|
|
66
|
+
},
|
|
67
|
+
// Emit the OG product extension (Facebook / WhatsApp / X link previews,
|
|
68
|
+
// Google Merchant Center). Skips the price pair entirely when the SDK
|
|
69
|
+
// can't determine a positive amount, rather than shipping "0".
|
|
70
|
+
other: {
|
|
71
|
+
...(priceAmount
|
|
72
|
+
? {
|
|
73
|
+
'og:price:amount': priceAmount,
|
|
74
|
+
'og:price:currency': currency,
|
|
75
|
+
'product:price:amount': priceAmount,
|
|
76
|
+
'product:price:currency': currency,
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
'product:availability': inStock ? 'in stock' : 'out of stock',
|
|
80
|
+
'product:condition': 'new',
|
|
81
|
+
...(brandName ? { 'product:brand': brandName } : {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
return {
|
|
86
|
+
title: 'Product not found',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default async function ProductDetailPage({ params }: Props) {
|
|
92
|
+
const { slug: rawSlug, locale } = await params;
|
|
93
|
+
const slug = decodeSlug(rawSlug);
|
|
94
|
+
|
|
95
|
+
let product;
|
|
96
|
+
try {
|
|
97
|
+
const client = getServerClient(locale);
|
|
98
|
+
product = await client.getProductBySlug(slug);
|
|
99
|
+
} catch {
|
|
100
|
+
notFound();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
104
|
+
const productUrl = `${baseUrl}/products/${slug}`;
|
|
105
|
+
// Reuse the cached storeInfo from generateMetadata's call — React cache()
|
|
106
|
+
// collapses both into one backend request per render. resolveCurrency owns
|
|
107
|
+
// the env-var + USD fallback chain so this stays a single source of truth.
|
|
108
|
+
const storeInfo = await fetchStoreInfo(locale);
|
|
109
|
+
const currency = resolveCurrency(storeInfo);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
<ProductJsonLd product={product} url={productUrl} currency={currency} />
|
|
114
|
+
<ProductClientSection product={product} />
|
|
115
|
+
<ReviewsSection productId={product.id} />
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -5,6 +5,7 @@ import Image from 'next/image';
|
|
|
5
5
|
import type { Order, OrderStatus, OrderDownloadLink } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
8
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
8
9
|
import { useTranslations } from '@/lib/translations';
|
|
9
10
|
import { cn } from '@/lib/utils';
|
|
10
11
|
import { OrderCustomizations } from './order-customizations';
|
|
@@ -83,7 +84,7 @@ function OrderCard({ order }: { order: Order }) {
|
|
|
83
84
|
const [expanded, setExpanded] = useState(false);
|
|
84
85
|
const statusConfig =
|
|
85
86
|
STATUS_CONFIG[order.status?.toLowerCase() as OrderStatus] || STATUS_CONFIG.pending;
|
|
86
|
-
const currency = order.currency
|
|
87
|
+
const currency = useCurrency(order.currency);
|
|
87
88
|
const totalAmount = order.totalAmount || order.total || '0';
|
|
88
89
|
|
|
89
90
|
return (
|
|
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
|
-
import {
|
|
7
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
8
8
|
import { useTranslations } from '@/lib/translations';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
10
|
|
|
@@ -16,9 +16,8 @@ interface CartBundleOfferCardProps {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
19
|
-
const { storeInfo } = useStoreInfo();
|
|
20
19
|
const t = useTranslations('cart');
|
|
21
|
-
const currency =
|
|
20
|
+
const currency = useCurrency();
|
|
22
21
|
const [adding, setAdding] = useState(false);
|
|
23
22
|
|
|
24
23
|
const offered = offer.offeredProducts;
|
|
@@ -6,7 +6,7 @@ import type { CartItem as CartItemType } from 'brainerce';
|
|
|
6
6
|
import { getCartItemImage, formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
8
8
|
import { useTranslations } from '@/lib/translations';
|
|
9
|
-
import {
|
|
9
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
10
10
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
11
11
|
import { cn } from '@/lib/utils';
|
|
12
12
|
|
|
@@ -19,8 +19,7 @@ interface CartItemProps {
|
|
|
19
19
|
export function CartItem({ item, onUpdate, className }: CartItemProps) {
|
|
20
20
|
const t = useTranslations('common');
|
|
21
21
|
const td = useTranslations('productDetail');
|
|
22
|
-
const
|
|
23
|
-
const currency = storeInfo?.currency || 'USD';
|
|
22
|
+
const currency = useCurrency();
|
|
24
23
|
const [updating, setUpdating] = useState(false);
|
|
25
24
|
const [removing, setRemoving] = useState(false);
|
|
26
25
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { formatPrice } from 'brainerce';
|
|
4
4
|
import { useTranslations } from '@/lib/translations';
|
|
5
|
-
import {
|
|
5
|
+
import { useCart } from '@/providers/store-provider';
|
|
6
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
|
|
8
9
|
interface CartSummaryProps {
|
|
@@ -12,9 +13,8 @@ interface CartSummaryProps {
|
|
|
12
13
|
export function CartSummary({ className }: CartSummaryProps) {
|
|
13
14
|
const t = useTranslations('cart');
|
|
14
15
|
const tc = useTranslations('common');
|
|
15
|
-
const { storeInfo } = useStoreInfo();
|
|
16
16
|
const { totals, cart } = useCart();
|
|
17
|
-
const currency =
|
|
17
|
+
const currency = useCurrency();
|
|
18
18
|
|
|
19
19
|
const rules = cart?.appliedDiscounts;
|
|
20
20
|
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
@@ -5,7 +5,7 @@ import Image from 'next/image';
|
|
|
5
5
|
import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { getClient } from '@/lib/brainerce';
|
|
8
|
-
import {
|
|
8
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
9
9
|
import { useTranslations } from '@/lib/translations';
|
|
10
10
|
import { cn } from '@/lib/utils';
|
|
11
11
|
|
|
@@ -22,9 +22,8 @@ export function CartUpgradeBanner({
|
|
|
22
22
|
onUpgrade,
|
|
23
23
|
className,
|
|
24
24
|
}: CartUpgradeBannerProps) {
|
|
25
|
-
const { storeInfo } = useStoreInfo();
|
|
26
25
|
const t = useTranslations('cart');
|
|
27
|
-
const currency =
|
|
26
|
+
const currency = useCurrency();
|
|
28
27
|
const [upgrading, setUpgrading] = useState(false);
|
|
29
28
|
const [dismissed, setDismissed] = useState(false);
|
|
30
29
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { formatPrice } from 'brainerce';
|
|
4
4
|
import { useStoreInfo, useCart } from '@/providers/store-provider';
|
|
5
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
5
6
|
import { useTranslations } from '@/lib/translations';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
|
|
@@ -10,9 +11,12 @@ interface FreeShippingBarProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function FreeShippingBar({ className }: FreeShippingBarProps) {
|
|
14
|
+
// Hooks must be called unconditionally and in the same order on every
|
|
15
|
+
// render — keep all of them above any early `return null` branch.
|
|
13
16
|
const t = useTranslations('cart');
|
|
14
17
|
const { storeInfo } = useStoreInfo();
|
|
15
18
|
const { totals } = useCart();
|
|
19
|
+
const currency = useCurrency();
|
|
16
20
|
|
|
17
21
|
const upsell = storeInfo?.upsell;
|
|
18
22
|
const threshold = upsell?.freeShippingThreshold;
|
|
@@ -25,7 +29,6 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
|
|
|
25
29
|
const remaining = Math.max(0, threshold - subtotal);
|
|
26
30
|
const progress = Math.min(100, (subtotal / threshold) * 100);
|
|
27
31
|
const qualified = remaining <= 0;
|
|
28
|
-
const currency = storeInfo?.currency || 'USD';
|
|
29
32
|
|
|
30
33
|
// Don't show if already qualified
|
|
31
34
|
if (qualified) {
|
|
@@ -4,7 +4,7 @@ import { useState, useMemo } from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import type { OrderBump, RecommendationVariant } from 'brainerce';
|
|
6
6
|
import { formatPrice, getVariantOptions } from 'brainerce';
|
|
7
|
-
import {
|
|
7
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
8
8
|
import { useTranslations } from '@/lib/translations';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
10
|
|
|
@@ -17,9 +17,8 @@ interface OrderBumpCardProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: OrderBumpCardProps) {
|
|
20
|
-
const { storeInfo } = useStoreInfo();
|
|
21
20
|
const t = useTranslations('checkout');
|
|
22
|
-
const currency =
|
|
21
|
+
const currency = useCurrency();
|
|
23
22
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
24
23
|
|
|
25
24
|
const product = bump.bumpProduct;
|
|
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
import type { PickupLocation } from 'brainerce';
|
|
5
5
|
import { formatPrice } from 'brainerce';
|
|
6
6
|
import { useTranslations } from '@/lib/translations';
|
|
7
|
-
import {
|
|
7
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
|
|
10
10
|
interface PickupStepProps {
|
|
@@ -28,8 +28,7 @@ export function PickupStep({
|
|
|
28
28
|
const t = useTranslations('checkout');
|
|
29
29
|
const tf = useTranslations('checkoutForm');
|
|
30
30
|
const tc = useTranslations('common');
|
|
31
|
-
const
|
|
32
|
-
const currency = storeInfo?.currency || 'USD';
|
|
31
|
+
const currency = useCurrency();
|
|
33
32
|
|
|
34
33
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
35
34
|
const [email, setEmail] = useState(initialEmail);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { ShippingRate } from 'brainerce';
|
|
4
4
|
import { formatPrice } from 'brainerce';
|
|
5
5
|
import { useTranslations } from '@/lib/translations';
|
|
6
|
-
import {
|
|
6
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
7
7
|
import { cn } from '@/lib/utils';
|
|
8
8
|
|
|
9
9
|
interface ShippingStepProps {
|
|
@@ -23,8 +23,7 @@ export function ShippingStep({
|
|
|
23
23
|
}: ShippingStepProps) {
|
|
24
24
|
const t = useTranslations('checkout');
|
|
25
25
|
const tc = useTranslations('common');
|
|
26
|
-
const
|
|
27
|
-
const currency = storeInfo?.currency || 'USD';
|
|
26
|
+
const currency = useCurrency();
|
|
28
27
|
|
|
29
28
|
if (rates.length === 0) {
|
|
30
29
|
return (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { TaxBreakdown } from 'brainerce';
|
|
4
4
|
import { formatPrice } from 'brainerce';
|
|
5
5
|
import { useTranslations } from '@/lib/translations';
|
|
6
|
-
import {
|
|
6
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
7
7
|
import { cn } from '@/lib/utils';
|
|
8
8
|
|
|
9
9
|
interface TaxDisplayProps {
|
|
@@ -19,8 +19,7 @@ interface TaxDisplayProps {
|
|
|
19
19
|
export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: TaxDisplayProps) {
|
|
20
20
|
const t = useTranslations('checkout');
|
|
21
21
|
const tc = useTranslations('common');
|
|
22
|
-
const
|
|
23
|
-
const currency = storeInfo?.currency || 'USD';
|
|
22
|
+
const currency = useCurrency();
|
|
24
23
|
|
|
25
24
|
// Before address is set
|
|
26
25
|
if (!addressSet) {
|
|
@@ -4,8 +4,8 @@ import { useState } from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import type { Product, ProductRecommendation } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
|
-
import { useCart } from '@/providers/store-provider';
|
|
8
|
-
import {
|
|
7
|
+
import { useCart, useStoreInfo } from '@/providers/store-provider';
|
|
8
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
9
9
|
import { useTranslations } from '@/lib/translations';
|
|
10
10
|
import { cn } from '@/lib/utils';
|
|
11
11
|
|
|
@@ -88,9 +88,12 @@ export function FrequentlyBoughtTogether({
|
|
|
88
88
|
currentProduct,
|
|
89
89
|
className,
|
|
90
90
|
}: FrequentlyBoughtTogetherProps) {
|
|
91
|
+
// Hooks must be called unconditionally and in the same order on every
|
|
92
|
+
// render — keep all of them above any early `return null` branch.
|
|
91
93
|
const { storeInfo } = useStoreInfo();
|
|
92
94
|
const { refreshCart } = useCart();
|
|
93
95
|
const t = useTranslations('productDetail');
|
|
96
|
+
const currency = useCurrency();
|
|
94
97
|
|
|
95
98
|
// Only show up to 3 cross-sells
|
|
96
99
|
const crossSells = items.slice(0, 3);
|
|
@@ -101,8 +104,6 @@ export function FrequentlyBoughtTogether({
|
|
|
101
104
|
if (!storeInfo?.upsell?.frequentlyBoughtTogetherEnabled) return null;
|
|
102
105
|
if (crossSells.length === 0) return null;
|
|
103
106
|
|
|
104
|
-
const currency = storeInfo.currency || 'USD';
|
|
105
|
-
|
|
106
107
|
const currentPrice = getEffectivePrice(currentProduct);
|
|
107
108
|
const currentImage = currentProduct.images?.[0];
|
|
108
109
|
const currentImageUrl = currentImage
|
|
@@ -9,7 +9,8 @@ import { useTranslations } from '@/lib/translations';
|
|
|
9
9
|
import { PriceDisplay } from '@/components/shared/price-display';
|
|
10
10
|
import { StockBadge } from '@/components/products/stock-badge';
|
|
11
11
|
import { DiscountBadge } from '@/components/products/discount-badge';
|
|
12
|
-
import { useCart
|
|
12
|
+
import { useCart } from '@/providers/store-provider';
|
|
13
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
13
14
|
import { cn } from '@/lib/utils';
|
|
14
15
|
|
|
15
16
|
interface ProductCardProps {
|
|
@@ -18,8 +19,7 @@ interface ProductCardProps {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function VariantPriceRange({ product }: { product: Product }) {
|
|
21
|
-
const
|
|
22
|
-
const currency = storeInfo?.currency || 'USD';
|
|
22
|
+
const currency = useCurrency();
|
|
23
23
|
|
|
24
24
|
let min: number;
|
|
25
25
|
let max: number;
|
|
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
|
|
4
4
|
import type { Product, ProductVariant } from 'brainerce';
|
|
5
5
|
import { getVariantOptions, getProductSwatches, formatPrice } from 'brainerce';
|
|
6
6
|
import type { InventoryInfo } from 'brainerce';
|
|
7
|
-
import {
|
|
7
|
+
import { useCurrency } from '@/lib/use-currency';
|
|
8
8
|
import { useTranslations } from '@/lib/translations';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
10
|
|
|
@@ -33,9 +33,8 @@ export function VariantSelector({
|
|
|
33
33
|
onVariantChange,
|
|
34
34
|
className,
|
|
35
35
|
}: VariantSelectorProps) {
|
|
36
|
-
const { storeInfo } = useStoreInfo();
|
|
37
36
|
const t = useTranslations('productDetail');
|
|
38
|
-
const currency =
|
|
37
|
+
const currency = useCurrency();
|
|
39
38
|
const variants = useMemo(() => product.variants || [], [product.variants]);
|
|
40
39
|
|
|
41
40
|
// Get swatch metadata from product attribute options
|