create-brainerce-store 1.73.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 +35 -2
- package/messages/en.json +27 -1
- package/messages/he.json +27 -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/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 +145 -40
- 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 +58 -7
- 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 +97 -6
- 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 +10 -0
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +45 -2
- package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
- 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/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-item.tsx +10 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +56 -11
- 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/stock-badge.tsx +17 -2
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
- package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
- 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/stock-badge.tsx +17 -2
|
@@ -1,107 +1,106 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { formatPrice } from 'brainerce';
|
|
4
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
-
import { useCart } from '@/core/providers/store-provider';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
5
|
+
import { useCart } from '@/core/providers/store-provider';
|
|
6
|
+
import { TaxEstimateLine } from '@/ui/cart/tax-estimate-line';
|
|
7
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
8
|
+
import { cn } from '@/core/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface CartSummaryProps {
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Order summary (subtotal, rule/coupon discounts, shipping, tax note, total);
|
|
16
|
+
* all figures come from useCart().totals and useCart().cart. Every discount
|
|
17
|
+
* branch is kept — merchants rely on the per-rule breakdown.
|
|
18
|
+
*/
|
|
19
|
+
export function CartSummary({ className }: CartSummaryProps) {
|
|
20
|
+
const t = useTranslations('cart');
|
|
21
|
+
const tc = useTranslations('common');
|
|
22
|
+
const { totals, cart } = useCart();
|
|
23
|
+
const currency = useCurrency();
|
|
24
|
+
|
|
25
|
+
const rules = cart?.appliedDiscounts;
|
|
26
|
+
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
27
|
+
const couponAmt = totals.discount - ruleAmt;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section className={cn(className)}>
|
|
31
|
+
<h3 className="mb-4 font-sans text-base font-semibold">{t('orderSummary')}</h3>
|
|
32
|
+
|
|
33
|
+
<dl className="space-y-2.5 text-sm">
|
|
34
|
+
{/* Subtotal */}
|
|
35
|
+
<div className="flex items-center justify-between gap-4">
|
|
36
|
+
<dt className="text-muted-foreground">{tc('subtotal')}</dt>
|
|
37
|
+
<dd className="font-medium">{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Rule discounts - show each rule by name */}
|
|
41
|
+
{rules && rules.length > 0
|
|
42
|
+
? rules.map((rule) => (
|
|
43
|
+
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
44
|
+
<dt className="text-primary">{rule.ruleName}</dt>
|
|
45
|
+
<dd className="font-medium text-primary">
|
|
46
|
+
-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}
|
|
47
|
+
</dd>
|
|
48
|
+
</div>
|
|
49
|
+
))
|
|
50
|
+
: ruleAmt > 0 && (
|
|
51
|
+
<div className="flex items-center justify-between gap-4">
|
|
52
|
+
<dt className="text-primary">{tc('generalDiscount')}</dt>
|
|
53
|
+
<dd className="font-medium text-primary">
|
|
54
|
+
-{formatPrice(ruleAmt, { currency }) as string}
|
|
55
|
+
</dd>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{/* Coupon discount */}
|
|
60
|
+
{cart?.couponCode && couponAmt > 0 && (
|
|
61
|
+
<div className="flex items-center justify-between gap-4">
|
|
62
|
+
<dt className="text-primary">
|
|
63
|
+
{tc('couponDiscount')} ({cart.couponCode})
|
|
64
|
+
</dt>
|
|
65
|
+
<dd className="font-medium text-primary">
|
|
66
|
+
-{formatPrice(couponAmt, { currency }) as string}
|
|
67
|
+
</dd>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Fallback: generic discount when no breakdown available */}
|
|
72
|
+
{totals.discount > 0 &&
|
|
73
|
+
ruleAmt <= 0 &&
|
|
74
|
+
!cart?.couponCode &&
|
|
75
|
+
(!rules || rules.length === 0) && (
|
|
76
|
+
<div className="flex items-center justify-between gap-4">
|
|
77
|
+
<dt className="text-primary">{tc('discount')}</dt>
|
|
78
|
+
<dd className="font-medium text-primary">
|
|
79
|
+
-{formatPrice(totals.discount, { currency }) as string}
|
|
80
|
+
</dd>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* Shipping */}
|
|
85
|
+
{totals.shipping > 0 && (
|
|
86
|
+
<div className="flex items-center justify-between gap-4">
|
|
87
|
+
<dt className="text-muted-foreground">{tc('shipping')}</dt>
|
|
88
|
+
<dd className="font-medium">{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Tax: a real estimate when the store has a rate for the buyer's
|
|
93
|
+
country, and the original "calculated at checkout" row otherwise. */}
|
|
94
|
+
<TaxEstimateLine />
|
|
95
|
+
|
|
96
|
+
{/* Total */}
|
|
97
|
+
<div className="flex items-center justify-between gap-4 border-t pt-3">
|
|
98
|
+
<dt className="text-base font-semibold text-foreground">{tc('total')}</dt>
|
|
99
|
+
<dd className="text-lg font-bold text-foreground">
|
|
100
|
+
{formatPrice(totals.total, { currency }) as string}
|
|
101
|
+
</dd>
|
|
102
|
+
</div>
|
|
103
|
+
</dl>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -29,6 +29,12 @@ export function CartView() {
|
|
|
29
29
|
bundles,
|
|
30
30
|
reservationExpired,
|
|
31
31
|
unavailableItems,
|
|
32
|
+
selectedItemIds,
|
|
33
|
+
selectedItems,
|
|
34
|
+
toggleItemSelected,
|
|
35
|
+
allSelected,
|
|
36
|
+
setAllSelected,
|
|
37
|
+
checkoutHref,
|
|
32
38
|
canProceedToCheckout,
|
|
33
39
|
onReservationExpired,
|
|
34
40
|
} = useCartPage();
|
|
@@ -83,19 +89,44 @@ export function CartView() {
|
|
|
83
89
|
<CartNudges nudges={cart.nudges} className="mb-4" />
|
|
84
90
|
)}
|
|
85
91
|
|
|
92
|
+
{/* Select-all. Partial checkout: only the ticked lines are sent to
|
|
93
|
+
createCheckout, and whatever stays unticked is still in the cart
|
|
94
|
+
(and still for sale) after payment. */}
|
|
95
|
+
<div className="mb-3 flex items-center gap-3 px-1">
|
|
96
|
+
<input
|
|
97
|
+
id="cart-select-all"
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={allSelected}
|
|
100
|
+
onChange={(event) => setAllSelected(event.target.checked)}
|
|
101
|
+
className="accent-primary h-4 w-4 cursor-pointer"
|
|
102
|
+
/>
|
|
103
|
+
<label htmlFor="cart-select-all" className="text-foreground cursor-pointer text-sm">
|
|
104
|
+
{tc('all')}
|
|
105
|
+
</label>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
86
108
|
{/* Cart items */}
|
|
87
109
|
<ul className="card divide-y overflow-hidden">
|
|
88
110
|
{cart.items.map((item) => (
|
|
89
|
-
<li key={item.id} className="p-4 sm:p-5">
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
<li key={item.id} className="flex items-start gap-3 p-4 sm:p-5">
|
|
112
|
+
<input
|
|
113
|
+
type="checkbox"
|
|
114
|
+
checked={selectedItemIds.includes(item.id)}
|
|
115
|
+
onChange={() => toggleItemSelected(item.id)}
|
|
116
|
+
aria-label={item.product.name}
|
|
117
|
+
className="accent-primary mt-8 h-4 w-4 shrink-0 cursor-pointer"
|
|
118
|
+
/>
|
|
119
|
+
<div className="min-w-0 flex-1">
|
|
120
|
+
<CartItem item={item} onUpdate={refreshCart} />
|
|
121
|
+
{upgrades?.upgrades?.[item.productId] && (
|
|
122
|
+
<CartUpgradeBanner
|
|
123
|
+
suggestion={upgrades.upgrades[item.productId]}
|
|
124
|
+
cartItem={item}
|
|
125
|
+
onUpgrade={refreshCart}
|
|
126
|
+
className="mt-4"
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
99
130
|
</li>
|
|
100
131
|
))}
|
|
101
132
|
</ul>
|
|
@@ -128,13 +159,27 @@ export function CartView() {
|
|
|
128
159
|
<aside className="lg:sticky lg:top-24">
|
|
129
160
|
<div className="card space-y-5 p-5 sm:p-6">
|
|
130
161
|
<FreeShippingBar />
|
|
162
|
+
{/* Totals are for the WHOLE cart. A partial selection is priced
|
|
163
|
+
authoritatively on the checkout page, which is the only place
|
|
164
|
+
the server has scoped the discounts and tax to the subset. */}
|
|
131
165
|
<CartSummary />
|
|
132
166
|
|
|
167
|
+
{/* Shown once the shopper unticks anything. At zero selected this
|
|
168
|
+
is what explains the disabled button below — a count, so it
|
|
169
|
+
needs no copy of its own. */}
|
|
170
|
+
{!allSelected && (
|
|
171
|
+
<p className="text-muted-foreground text-center text-xs">
|
|
172
|
+
{selectedItems.length} {selectedItems.length === 1 ? tc('item') : tc('items')}
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
|
|
133
176
|
{/* Proceed to checkout. When the gate is closed this must be a
|
|
134
177
|
real disabled control, never a styled-down link: an anchor
|
|
135
178
|
ignores `disabled` and would still navigate. */}
|
|
136
179
|
{canProceedToCheckout ? (
|
|
137
|
-
|
|
180
|
+
/* `checkoutHref` carries `?items=` only for a strict subset; a
|
|
181
|
+
full cart links to a bare /checkout exactly as before. */
|
|
182
|
+
<Link href={checkoutHref} className="btn-primary btn-lg w-full">
|
|
138
183
|
{t('proceedToCheckout')}
|
|
139
184
|
<IconArrowEnd size={20} className="rtl-flip" />
|
|
140
185
|
</Link>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Non-binding tax preview for the cart.
|
|
5
|
+
*
|
|
6
|
+
* Shoppers in VAT markets expect a tax figure before they reach the address
|
|
7
|
+
* form, and "calculated at checkout" reads as an unpleasant surprise waiting
|
|
8
|
+
* to happen. `estimateTax` gives them a number early without pretending it is
|
|
9
|
+
* final.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ NEVER PRESENT THIS AS THE REAL TAX. The authoritative calculation runs at
|
|
12
|
+
* checkout against the full shipping address. A preview has no province, so a
|
|
13
|
+
* Canadian estimate shows federal GST alone and the provincial PST/QST joins
|
|
14
|
+
* it at checkout. That is what `note` is for, and why the "calculated at
|
|
15
|
+
* checkout" caveat stays on the row even when a figure is showing.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ NO ESTIMATE IS RENDERED when `appliesTax` is false, which is what an
|
|
18
|
+
* empty or freshly created store returns: tax is off, no country reached us,
|
|
19
|
+
* or no active rate covers it. In that case (and when the call fails, and
|
|
20
|
+
* while it is in flight, and on an empty cart) this falls back to EXACTLY the
|
|
21
|
+
* row the cart shipped with before the estimate existed: "Tax / calculated at
|
|
22
|
+
* checkout". There is no spinner, no error box, and no empty amount.
|
|
23
|
+
*
|
|
24
|
+
* `getStoreTaxClasses()` would let you add a separate "9% VAT" transparency
|
|
25
|
+
* badge, but `rateName` + `rate` already come back on this response and say
|
|
26
|
+
* the same thing, so a second request would buy nothing.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useEffect, useState } from 'react';
|
|
30
|
+
import type { TaxEstimateResponse } from 'brainerce';
|
|
31
|
+
import { formatPrice } from 'brainerce';
|
|
32
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
33
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
34
|
+
import { useCart, useRegion } from '@/core/providers/store-provider';
|
|
35
|
+
|
|
36
|
+
export function useTaxEstimate(): TaxEstimateResponse | null {
|
|
37
|
+
const { totals } = useCart();
|
|
38
|
+
// The same country the region lookup already extracted from the edge. Asking
|
|
39
|
+
// for it twice would be two sources that can disagree.
|
|
40
|
+
const { buyerCountry } = useRegion();
|
|
41
|
+
const [estimate, setEstimate] = useState<TaxEstimateResponse | null>(null);
|
|
42
|
+
const subtotal = totals.subtotal;
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// An empty cart has nothing to tax. Skipping the call also keeps a
|
|
46
|
+
// brand-new store from firing a request on every page load forever.
|
|
47
|
+
if (!(subtotal > 0)) {
|
|
48
|
+
setEstimate(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
getClient()
|
|
53
|
+
.estimateTax({ ...(buyerCountry ? { country: buyerCountry } : {}), subtotal })
|
|
54
|
+
.then((result) => {
|
|
55
|
+
if (!cancelled) setEstimate(result);
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
// Swallowed on purpose. A tax preview is a courtesy; it must never
|
|
59
|
+
// take the cart down with it. The caller falls back to the plain
|
|
60
|
+
// "calculated at checkout" row.
|
|
61
|
+
if (!cancelled) setEstimate(null);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
}, [subtotal, buyerCountry]);
|
|
67
|
+
|
|
68
|
+
return estimate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function TaxEstimateLine() {
|
|
72
|
+
const t = useTranslations('cart');
|
|
73
|
+
const tc = useTranslations('common');
|
|
74
|
+
const estimate = useTaxEstimate();
|
|
75
|
+
|
|
76
|
+
// No estimate: the row the cart has always shown.
|
|
77
|
+
if (!estimate || !estimate.appliesTax) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex items-center justify-between gap-4">
|
|
80
|
+
<dt className="text-muted-foreground">{tc('tax')}</dt>
|
|
81
|
+
<dd className="text-muted-foreground">{t('taxAtCheckout')}</dd>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `rateName` is one rate or several joined ("GST + QST"); `rate` is the
|
|
87
|
+
// summed effective percent and is null when no rule matched.
|
|
88
|
+
const label =
|
|
89
|
+
estimate.rateName && estimate.rate != null
|
|
90
|
+
? `${estimate.rateName} ${estimate.rate}%`
|
|
91
|
+
: (estimate.rateName ?? tc('tax'));
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-1">
|
|
95
|
+
<div className="flex items-center justify-between gap-4">
|
|
96
|
+
<dt className="text-muted-foreground">{label}</dt>
|
|
97
|
+
<dd className="text-foreground font-medium">
|
|
98
|
+
{formatPrice(estimate.estimatedTax, { currency: estimate.currency }) as string}
|
|
99
|
+
</dd>
|
|
100
|
+
</div>
|
|
101
|
+
{/* The caveat travels WITH the number, not in a footnote nobody reads. */}
|
|
102
|
+
<p className="text-muted-foreground text-xs">{t('taxAtCheckout')}</p>
|
|
103
|
+
{/* Server-written and already localized — never replace it with copy of
|
|
104
|
+
your own, it is what explains a partial estimate. */}
|
|
105
|
+
{estimate.note ? <p className="text-muted-foreground text-xs">{estimate.note}</p> : null}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -10,7 +10,16 @@ const BENEFITS = [
|
|
|
10
10
|
{ icon: IconReturn, titleKey: 'benefit4Title', descKey: 'benefit4Desc' },
|
|
11
11
|
] as const;
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Trust/benefits row under the hero.
|
|
15
|
+
*
|
|
16
|
+
* The copy here deliberately states no policy the store has not configured.
|
|
17
|
+
* Do not restore claims like "free nationwide delivery", "30-day returns" or
|
|
18
|
+
* "full warranty": a new store has no such policy, and shipping a storefront
|
|
19
|
+
* that asserts one publishes a false promise under the merchant's name. Replace
|
|
20
|
+
* these strings with the merchant's real terms once they exist, or point at the
|
|
21
|
+
* policy pages.
|
|
22
|
+
*/
|
|
14
23
|
export function BenefitsBand() {
|
|
15
24
|
const t = useTranslations('home');
|
|
16
25
|
|