create-brainerce-store 1.42.0 → 1.43.1
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 +1 -1
- package/messages/en.json +1 -0
- package/messages/he.json +1 -0
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +68 -69
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +91 -87
- package/templates/nextjs/base/src/app/checkout/page.tsx +120 -97
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +5 -4
- 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 +37 -28
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +5 -4
- package/templates/nextjs/base/src/components/products/product-card.tsx +226 -226
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +2 -3
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +7 -7
- package/templates/nextjs/base/src/components/shared/price-display.tsx +2 -6
- package/templates/nextjs/base/src/lib/resolve-currency.ts +20 -0
- package/templates/nextjs/base/src/lib/use-currency.ts +19 -0
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
|
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
3
|
import { getProductPriceInfo } from 'brainerce';
|
|
4
4
|
import { getServerClient, fetchStoreInfo } from '@/lib/brainerce';
|
|
5
|
+
import { resolveCurrency } from '@/lib/resolve-currency';
|
|
5
6
|
import { buildMetaDescription } from '@/lib/seo';
|
|
6
7
|
import { decodeSlug } from '@/lib/utils';
|
|
7
8
|
import { ProductJsonLd } from '@/components/seo/product-json-ld';
|
|
@@ -40,7 +41,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
40
41
|
// here (the previous bug) causes price-zero link cards. Use the real
|
|
41
42
|
// effective price from the SDK helper.
|
|
42
43
|
const priceInfo = getProductPriceInfo(product);
|
|
43
|
-
const currency = storeInfo
|
|
44
|
+
const currency = resolveCurrency(storeInfo);
|
|
44
45
|
const priceAmount = priceInfo.price > 0 ? priceInfo.price.toFixed(2) : null;
|
|
45
46
|
const inStock = product.inventory?.canPurchase !== false;
|
|
46
47
|
const brandName = (product as { brand?: { name?: string } | null }).brand?.name;
|
|
@@ -102,10 +103,10 @@ export default async function ProductDetailPage({ params }: Props) {
|
|
|
102
103
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || '';
|
|
103
104
|
const productUrl = `${baseUrl}/products/${slug}`;
|
|
104
105
|
// Reuse the cached storeInfo from generateMetadata's call — React cache()
|
|
105
|
-
// collapses both into one backend request per render.
|
|
106
|
-
// var
|
|
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.
|
|
107
108
|
const storeInfo = await fetchStoreInfo(locale);
|
|
108
|
-
const currency = storeInfo
|
|
109
|
+
const currency = resolveCurrency(storeInfo);
|
|
109
110
|
|
|
110
111
|
return (
|
|
111
112
|
<>
|
|
@@ -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 || process.env.NEXT_PUBLIC_STORE_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 || process.env.NEXT_PUBLIC_STORE_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 || process.env.NEXT_PUBLIC_STORE_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 || process.env.NEXT_PUBLIC_STORE_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 || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
|
|
22
|
+
const currency = useCurrency();
|
|
24
23
|
|
|
25
24
|
// Before address is set
|
|
26
25
|
if (!addressSet) {
|
|
@@ -32,34 +31,44 @@ export function TaxDisplay({ addressSet, taxAmount, taxBreakdown, className }: T
|
|
|
32
31
|
);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
|
|
34
|
+
// Inclusive-pricing stores back the tax out of the displayed price, so
|
|
35
|
+
// `checkout.taxAmount` is stored as 0 and the real VAT lives on the
|
|
36
|
+
// aggregated breakdown. Surface whichever is present.
|
|
37
|
+
const explicitTax = taxAmount ? parseFloat(taxAmount) : 0;
|
|
38
|
+
const tax =
|
|
39
|
+
explicitTax > 0
|
|
40
|
+
? explicitTax
|
|
41
|
+
: typeof taxBreakdown?.totalTax === 'number'
|
|
42
|
+
? taxBreakdown.totalTax
|
|
43
|
+
: 0;
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// When there's a per-rate breakdown, prefer per-row display ("VAT 18% ₪3.05").
|
|
46
|
+
// Otherwise show a single aggregated tax line.
|
|
47
|
+
const hasBreakdown = !!taxBreakdown?.breakdown && taxBreakdown.breakdown.length > 0;
|
|
48
|
+
|
|
49
|
+
if (hasBreakdown) {
|
|
50
|
+
return (
|
|
51
|
+
<div className={cn('space-y-1', className)}>
|
|
52
|
+
{taxBreakdown!.breakdown.map((item, index) => (
|
|
53
|
+
<div key={index} className="flex items-center justify-between text-sm">
|
|
54
|
+
<span className="text-muted-foreground">
|
|
55
|
+
{item.name} ({(item.rate * 100).toFixed(1)}%)
|
|
56
|
+
</span>
|
|
57
|
+
<span className="text-foreground font-medium">
|
|
58
|
+
{formatPrice(item.amount, { currency }) as string}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
45
62
|
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
className="text-muted-foreground flex items-center justify-between text-xs"
|
|
54
|
-
>
|
|
55
|
-
<span>
|
|
56
|
-
{item.name} ({(item.rate * 100).toFixed(1)}%)
|
|
57
|
-
</span>
|
|
58
|
-
<span>{formatPrice(item.amount, { currency }) as string}</span>
|
|
59
|
-
</div>
|
|
60
|
-
))}
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn('flex items-center justify-between text-sm', className)}>
|
|
68
|
+
<span className="text-muted-foreground">{tc('tax')}</span>
|
|
69
|
+
<span className="text-foreground font-medium">
|
|
70
|
+
{tax > 0 ? (formatPrice(tax, { currency }) as string) : t('noTax')}
|
|
71
|
+
</span>
|
|
63
72
|
</div>
|
|
64
73
|
);
|
|
65
74
|
}
|
|
@@ -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 || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
|
|
105
|
-
|
|
106
107
|
const currentPrice = getEffectivePrice(currentProduct);
|
|
107
108
|
const currentImage = currentProduct.images?.[0];
|
|
108
109
|
const currentImageUrl = currentImage
|