create-brainerce-store 1.61.0 → 1.63.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/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/index.js +10 -2
- package/messages/en.json +502 -502
- package/messages/he.json +502 -502
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +60 -60
- package/templates/nextjs/base/.env.local.ejs +24 -24
- package/templates/nextjs/base/.eslintrc.json +57 -57
- package/templates/nextjs/base/.mcp.json +8 -8
- package/templates/nextjs/base/AI-GUIDE.md +174 -174
- package/templates/nextjs/base/TRANSLATIONS.md +200 -200
- package/templates/nextjs/base/package.json.ejs +52 -52
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +262 -262
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
- package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +106 -106
- package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
- package/templates/nextjs/base/src/app/globals.css +36 -36
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +10 -0
- package/templates/nextjs/base/src/app/llms.txt/route.ts +44 -44
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
- package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
- package/templates/nextjs/base/src/app/page.tsx +64 -64
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +175 -175
- package/templates/nextjs/base/src/app/products/page.tsx +19 -19
- package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -564
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
- package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +179 -179
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
- package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
- package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
- package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
- package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
- package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
- package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
- package/templates/nextjs/base/src/core/lib/store-info.ts +9 -0
- package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
- package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
- package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
- package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
- package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
- package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
- package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -20
- package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
- package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
- package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
- package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
- package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
- package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
- package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
- package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
- package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
- package/templates/nextjs/base/tailwind.config.ts +69 -69
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +10 -0
- package/templates/nextjs/designs/atelier/globals.css +402 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
- package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
- package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
- package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
- package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -27
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -19
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
- package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
- package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
- package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
- package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
- package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { CartNudge } from 'brainerce';
|
|
4
|
-
import { cn } from '@/core/lib/utils';
|
|
5
|
-
import { IconSparkle } from '@/ui/shared/icons';
|
|
6
|
-
|
|
7
|
-
interface CartNudgesProps {
|
|
8
|
-
nudges: CartNudge[];
|
|
9
|
-
className?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Discount-rule nudges on the cart page ("add X more for Y");
|
|
14
|
-
* texts come from useCartPage().cart.nudges.
|
|
15
|
-
*/
|
|
16
|
-
export function CartNudges({ nudges, className }: CartNudgesProps) {
|
|
17
|
-
if (nudges.length === 0) return null;
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<ul className={cn('space-y-2', className)} aria-label="Cart suggestions">
|
|
21
|
-
{nudges.map((nudge) => (
|
|
22
|
-
<li
|
|
23
|
-
key={nudge.ruleId}
|
|
24
|
-
className="flex items-center gap-2.5 rounded-lg bg-secondary px-3.5 py-2.5 text-sm font-medium text-foreground"
|
|
25
|
-
>
|
|
26
|
-
<span aria-hidden="true" className="shrink-0 text-primary">
|
|
27
|
-
<IconSparkle size={18} />
|
|
28
|
-
</span>
|
|
29
|
-
{nudge.text}
|
|
30
|
-
</li>
|
|
31
|
-
))}
|
|
32
|
-
</ul>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CartNudge } from 'brainerce';
|
|
4
|
+
import { cn } from '@/core/lib/utils';
|
|
5
|
+
import { IconSparkle } from '@/ui/shared/icons';
|
|
6
|
+
|
|
7
|
+
interface CartNudgesProps {
|
|
8
|
+
nudges: CartNudge[];
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Discount-rule nudges on the cart page ("add X more for Y");
|
|
14
|
+
* texts come from useCartPage().cart.nudges.
|
|
15
|
+
*/
|
|
16
|
+
export function CartNudges({ nudges, className }: CartNudgesProps) {
|
|
17
|
+
if (nudges.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ul className={cn('space-y-2', className)} aria-label="Cart suggestions">
|
|
21
|
+
{nudges.map((nudge) => (
|
|
22
|
+
<li
|
|
23
|
+
key={nudge.ruleId}
|
|
24
|
+
className="flex items-center gap-2.5 rounded-lg bg-secondary px-3.5 py-2.5 text-sm font-medium text-foreground"
|
|
25
|
+
>
|
|
26
|
+
<span aria-hidden="true" className="shrink-0 text-primary">
|
|
27
|
+
<IconSparkle size={18} />
|
|
28
|
+
</span>
|
|
29
|
+
{nudge.text}
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -1,107 +1,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 { useCurrency } from '@/core/lib/use-currency';
|
|
7
|
-
import { cn } from '@/core/lib/utils';
|
|
8
|
-
|
|
9
|
-
interface CartSummaryProps {
|
|
10
|
-
className?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Order summary (subtotal, rule/coupon discounts, shipping, tax note, total);
|
|
15
|
-
* all figures come from useCart().totals and useCart().cart. Every discount
|
|
16
|
-
* branch is kept — merchants rely on the per-rule breakdown.
|
|
17
|
-
*/
|
|
18
|
-
export function CartSummary({ className }: CartSummaryProps) {
|
|
19
|
-
const t = useTranslations('cart');
|
|
20
|
-
const tc = useTranslations('common');
|
|
21
|
-
const { totals, cart } = useCart();
|
|
22
|
-
const currency = useCurrency();
|
|
23
|
-
|
|
24
|
-
const rules = cart?.appliedDiscounts;
|
|
25
|
-
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
26
|
-
const couponAmt = totals.discount - ruleAmt;
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<section className={cn(className)}>
|
|
30
|
-
<h3 className="mb-4 font-sans text-base font-semibold">{t('orderSummary')}</h3>
|
|
31
|
-
|
|
32
|
-
<dl className="space-y-2.5 text-sm">
|
|
33
|
-
{/* Subtotal */}
|
|
34
|
-
<div className="flex items-center justify-between gap-4">
|
|
35
|
-
<dt className="text-muted-foreground">{tc('subtotal')}</dt>
|
|
36
|
-
<dd className="font-medium">{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
{/* Rule discounts - show each rule by name */}
|
|
40
|
-
{rules && rules.length > 0
|
|
41
|
-
? rules.map((rule) => (
|
|
42
|
-
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
43
|
-
<dt className="text-primary">{rule.ruleName}</dt>
|
|
44
|
-
<dd className="font-medium text-primary">
|
|
45
|
-
-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}
|
|
46
|
-
</dd>
|
|
47
|
-
</div>
|
|
48
|
-
))
|
|
49
|
-
: ruleAmt > 0 && (
|
|
50
|
-
<div className="flex items-center justify-between gap-4">
|
|
51
|
-
<dt className="text-primary">{tc('generalDiscount')}</dt>
|
|
52
|
-
<dd className="font-medium text-primary">
|
|
53
|
-
-{formatPrice(ruleAmt, { currency }) as string}
|
|
54
|
-
</dd>
|
|
55
|
-
</div>
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
{/* Coupon discount */}
|
|
59
|
-
{cart?.couponCode && couponAmt > 0 && (
|
|
60
|
-
<div className="flex items-center justify-between gap-4">
|
|
61
|
-
<dt className="text-primary">
|
|
62
|
-
{tc('couponDiscount')} ({cart.couponCode})
|
|
63
|
-
</dt>
|
|
64
|
-
<dd className="font-medium text-primary">
|
|
65
|
-
-{formatPrice(couponAmt, { currency }) as string}
|
|
66
|
-
</dd>
|
|
67
|
-
</div>
|
|
68
|
-
)}
|
|
69
|
-
|
|
70
|
-
{/* Fallback: generic discount when no breakdown available */}
|
|
71
|
-
{totals.discount > 0 &&
|
|
72
|
-
ruleAmt <= 0 &&
|
|
73
|
-
!cart?.couponCode &&
|
|
74
|
-
(!rules || rules.length === 0) && (
|
|
75
|
-
<div className="flex items-center justify-between gap-4">
|
|
76
|
-
<dt className="text-primary">{tc('discount')}</dt>
|
|
77
|
-
<dd className="font-medium text-primary">
|
|
78
|
-
-{formatPrice(totals.discount, { currency }) as string}
|
|
79
|
-
</dd>
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{/* Shipping */}
|
|
84
|
-
{totals.shipping > 0 && (
|
|
85
|
-
<div className="flex items-center justify-between gap-4">
|
|
86
|
-
<dt className="text-muted-foreground">{tc('shipping')}</dt>
|
|
87
|
-
<dd className="font-medium">{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
|
|
91
|
-
{/* Tax */}
|
|
92
|
-
<div className="flex items-center justify-between gap-4">
|
|
93
|
-
<dt className="text-muted-foreground">{tc('tax')}</dt>
|
|
94
|
-
<dd className="text-muted-foreground">{t('taxAtCheckout')}</dd>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
{/* Total */}
|
|
98
|
-
<div className="flex items-center justify-between gap-4 border-t pt-3">
|
|
99
|
-
<dt className="text-base font-semibold text-foreground">{tc('total')}</dt>
|
|
100
|
-
<dd className="text-lg font-bold text-foreground">
|
|
101
|
-
{formatPrice(totals.total, { currency }) as string}
|
|
102
|
-
</dd>
|
|
103
|
-
</div>
|
|
104
|
-
</dl>
|
|
105
|
-
</section>
|
|
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 { useCurrency } from '@/core/lib/use-currency';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface CartSummaryProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Order summary (subtotal, rule/coupon discounts, shipping, tax note, total);
|
|
15
|
+
* all figures come from useCart().totals and useCart().cart. Every discount
|
|
16
|
+
* branch is kept — merchants rely on the per-rule breakdown.
|
|
17
|
+
*/
|
|
18
|
+
export function CartSummary({ className }: CartSummaryProps) {
|
|
19
|
+
const t = useTranslations('cart');
|
|
20
|
+
const tc = useTranslations('common');
|
|
21
|
+
const { totals, cart } = useCart();
|
|
22
|
+
const currency = useCurrency();
|
|
23
|
+
|
|
24
|
+
const rules = cart?.appliedDiscounts;
|
|
25
|
+
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
26
|
+
const couponAmt = totals.discount - ruleAmt;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section className={cn(className)}>
|
|
30
|
+
<h3 className="mb-4 font-sans text-base font-semibold">{t('orderSummary')}</h3>
|
|
31
|
+
|
|
32
|
+
<dl className="space-y-2.5 text-sm">
|
|
33
|
+
{/* Subtotal */}
|
|
34
|
+
<div className="flex items-center justify-between gap-4">
|
|
35
|
+
<dt className="text-muted-foreground">{tc('subtotal')}</dt>
|
|
36
|
+
<dd className="font-medium">{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Rule discounts - show each rule by name */}
|
|
40
|
+
{rules && rules.length > 0
|
|
41
|
+
? rules.map((rule) => (
|
|
42
|
+
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
43
|
+
<dt className="text-primary">{rule.ruleName}</dt>
|
|
44
|
+
<dd className="font-medium text-primary">
|
|
45
|
+
-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}
|
|
46
|
+
</dd>
|
|
47
|
+
</div>
|
|
48
|
+
))
|
|
49
|
+
: ruleAmt > 0 && (
|
|
50
|
+
<div className="flex items-center justify-between gap-4">
|
|
51
|
+
<dt className="text-primary">{tc('generalDiscount')}</dt>
|
|
52
|
+
<dd className="font-medium text-primary">
|
|
53
|
+
-{formatPrice(ruleAmt, { currency }) as string}
|
|
54
|
+
</dd>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{/* Coupon discount */}
|
|
59
|
+
{cart?.couponCode && couponAmt > 0 && (
|
|
60
|
+
<div className="flex items-center justify-between gap-4">
|
|
61
|
+
<dt className="text-primary">
|
|
62
|
+
{tc('couponDiscount')} ({cart.couponCode})
|
|
63
|
+
</dt>
|
|
64
|
+
<dd className="font-medium text-primary">
|
|
65
|
+
-{formatPrice(couponAmt, { currency }) as string}
|
|
66
|
+
</dd>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{/* Fallback: generic discount when no breakdown available */}
|
|
71
|
+
{totals.discount > 0 &&
|
|
72
|
+
ruleAmt <= 0 &&
|
|
73
|
+
!cart?.couponCode &&
|
|
74
|
+
(!rules || rules.length === 0) && (
|
|
75
|
+
<div className="flex items-center justify-between gap-4">
|
|
76
|
+
<dt className="text-primary">{tc('discount')}</dt>
|
|
77
|
+
<dd className="font-medium text-primary">
|
|
78
|
+
-{formatPrice(totals.discount, { currency }) as string}
|
|
79
|
+
</dd>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* Shipping */}
|
|
84
|
+
{totals.shipping > 0 && (
|
|
85
|
+
<div className="flex items-center justify-between gap-4">
|
|
86
|
+
<dt className="text-muted-foreground">{tc('shipping')}</dt>
|
|
87
|
+
<dd className="font-medium">{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{/* Tax */}
|
|
92
|
+
<div className="flex items-center justify-between gap-4">
|
|
93
|
+
<dt className="text-muted-foreground">{tc('tax')}</dt>
|
|
94
|
+
<dd className="text-muted-foreground">{t('taxAtCheckout')}</dd>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Total */}
|
|
98
|
+
<div className="flex items-center justify-between gap-4 border-t pt-3">
|
|
99
|
+
<dt className="text-base font-semibold text-foreground">{tc('total')}</dt>
|
|
100
|
+
<dd className="text-lg font-bold text-foreground">
|
|
101
|
+
{formatPrice(totals.total, { currency }) as string}
|
|
102
|
+
</dd>
|
|
103
|
+
</div>
|
|
104
|
+
</dl>
|
|
105
|
+
</section>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
|
|
6
|
-
import { formatPrice } from 'brainerce';
|
|
7
|
-
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
-
import { cn } from '@/core/lib/utils';
|
|
11
|
-
import { IconX } from '@/ui/shared/icons';
|
|
12
|
-
|
|
13
|
-
interface CartUpgradeBannerProps {
|
|
14
|
-
suggestion: CartUpgradeSuggestion;
|
|
15
|
-
cartItem: CartItemType;
|
|
16
|
-
onUpgrade: () => void;
|
|
17
|
-
className?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Per-line upgrade suggestion under a cart item ("upgrade to X for +Y");
|
|
22
|
-
* the suggestion comes from useCartPage().upgrades keyed by productId.
|
|
23
|
-
* Keeps the session-scoped dismiss and the swap logic.
|
|
24
|
-
*/
|
|
25
|
-
export function CartUpgradeBanner({
|
|
26
|
-
suggestion,
|
|
27
|
-
cartItem,
|
|
28
|
-
onUpgrade,
|
|
29
|
-
className,
|
|
30
|
-
}: CartUpgradeBannerProps) {
|
|
31
|
-
const t = useTranslations('cart');
|
|
32
|
-
const currency = useCurrency();
|
|
33
|
-
const [upgrading, setUpgrading] = useState(false);
|
|
34
|
-
const [dismissed, setDismissed] = useState(false);
|
|
35
|
-
|
|
36
|
-
const storageKey = `dismissed_upgrade_${suggestion.sourceProductId}`;
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
try {
|
|
40
|
-
if (sessionStorage.getItem(storageKey)) {
|
|
41
|
-
setDismissed(true);
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
/* ignore */
|
|
45
|
-
}
|
|
46
|
-
}, [storageKey]);
|
|
47
|
-
|
|
48
|
-
if (dismissed) return null;
|
|
49
|
-
|
|
50
|
-
const target = suggestion.targetProduct;
|
|
51
|
-
const firstImage = target.images?.[0];
|
|
52
|
-
const imageUrl = firstImage
|
|
53
|
-
? typeof firstImage === 'string'
|
|
54
|
-
? firstImage
|
|
55
|
-
: firstImage.url
|
|
56
|
-
: null;
|
|
57
|
-
const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
|
|
58
|
-
|
|
59
|
-
function handleDismiss() {
|
|
60
|
-
try {
|
|
61
|
-
sessionStorage.setItem(storageKey, '1');
|
|
62
|
-
} catch {
|
|
63
|
-
/* ignore */
|
|
64
|
-
}
|
|
65
|
-
setDismissed(true);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function handleUpgrade() {
|
|
69
|
-
if (upgrading) return;
|
|
70
|
-
try {
|
|
71
|
-
setUpgrading(true);
|
|
72
|
-
const client = getClient();
|
|
73
|
-
await client.smartRemoveFromCart(cartItem.productId, cartItem.variantId || undefined);
|
|
74
|
-
await client.smartAddToCart({ productId: target.id, quantity: cartItem.quantity });
|
|
75
|
-
onUpgrade();
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error('Failed to upgrade cart item:', err);
|
|
78
|
-
} finally {
|
|
79
|
-
setUpgrading(false);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div
|
|
85
|
-
className={cn(
|
|
86
|
-
'flex items-center gap-3 rounded-lg border border-primary/25 bg-secondary/60 p-3',
|
|
87
|
-
className
|
|
88
|
-
)}
|
|
89
|
-
>
|
|
90
|
-
{/* Product image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
91
|
-
<span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-background">
|
|
92
|
-
{imageUrl ? (
|
|
93
|
-
<Image src={imageUrl} alt={target.name} fill sizes="48px" className="object-cover" />
|
|
94
|
-
) : null}
|
|
95
|
-
</span>
|
|
96
|
-
|
|
97
|
-
{/* Text */}
|
|
98
|
-
<p className="min-w-0 flex-1 text-sm text-foreground">
|
|
99
|
-
{t('upgradeFor', { name: target.name, amount: formattedDelta })}
|
|
100
|
-
</p>
|
|
101
|
-
|
|
102
|
-
{/* Upgrade button */}
|
|
103
|
-
<button
|
|
104
|
-
type="button"
|
|
105
|
-
onClick={handleUpgrade}
|
|
106
|
-
disabled={upgrading}
|
|
107
|
-
className="btn-primary btn-sm shrink-0"
|
|
108
|
-
>
|
|
109
|
-
{upgrading ? t('upgrading') : t('upgrade')}
|
|
110
|
-
</button>
|
|
111
|
-
|
|
112
|
-
{/* Dismiss button */}
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
onClick={handleDismiss}
|
|
116
|
-
aria-label={t('dismissUpgrade')}
|
|
117
|
-
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
118
|
-
>
|
|
119
|
-
<IconX size={16} />
|
|
120
|
-
</button>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
|
|
6
|
+
import { formatPrice } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
import { cn } from '@/core/lib/utils';
|
|
11
|
+
import { IconX } from '@/ui/shared/icons';
|
|
12
|
+
|
|
13
|
+
interface CartUpgradeBannerProps {
|
|
14
|
+
suggestion: CartUpgradeSuggestion;
|
|
15
|
+
cartItem: CartItemType;
|
|
16
|
+
onUpgrade: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Per-line upgrade suggestion under a cart item ("upgrade to X for +Y");
|
|
22
|
+
* the suggestion comes from useCartPage().upgrades keyed by productId.
|
|
23
|
+
* Keeps the session-scoped dismiss and the swap logic.
|
|
24
|
+
*/
|
|
25
|
+
export function CartUpgradeBanner({
|
|
26
|
+
suggestion,
|
|
27
|
+
cartItem,
|
|
28
|
+
onUpgrade,
|
|
29
|
+
className,
|
|
30
|
+
}: CartUpgradeBannerProps) {
|
|
31
|
+
const t = useTranslations('cart');
|
|
32
|
+
const currency = useCurrency();
|
|
33
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
34
|
+
const [dismissed, setDismissed] = useState(false);
|
|
35
|
+
|
|
36
|
+
const storageKey = `dismissed_upgrade_${suggestion.sourceProductId}`;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
try {
|
|
40
|
+
if (sessionStorage.getItem(storageKey)) {
|
|
41
|
+
setDismissed(true);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
/* ignore */
|
|
45
|
+
}
|
|
46
|
+
}, [storageKey]);
|
|
47
|
+
|
|
48
|
+
if (dismissed) return null;
|
|
49
|
+
|
|
50
|
+
const target = suggestion.targetProduct;
|
|
51
|
+
const firstImage = target.images?.[0];
|
|
52
|
+
const imageUrl = firstImage
|
|
53
|
+
? typeof firstImage === 'string'
|
|
54
|
+
? firstImage
|
|
55
|
+
: firstImage.url
|
|
56
|
+
: null;
|
|
57
|
+
const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
|
|
58
|
+
|
|
59
|
+
function handleDismiss() {
|
|
60
|
+
try {
|
|
61
|
+
sessionStorage.setItem(storageKey, '1');
|
|
62
|
+
} catch {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
setDismissed(true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleUpgrade() {
|
|
69
|
+
if (upgrading) return;
|
|
70
|
+
try {
|
|
71
|
+
setUpgrading(true);
|
|
72
|
+
const client = getClient();
|
|
73
|
+
await client.smartRemoveFromCart(cartItem.productId, cartItem.variantId || undefined);
|
|
74
|
+
await client.smartAddToCart({ productId: target.id, quantity: cartItem.quantity });
|
|
75
|
+
onUpgrade();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Failed to upgrade cart item:', err);
|
|
78
|
+
} finally {
|
|
79
|
+
setUpgrading(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
'flex items-center gap-3 rounded-lg border border-primary/25 bg-secondary/60 p-3',
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{/* Product image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
91
|
+
<span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-background">
|
|
92
|
+
{imageUrl ? (
|
|
93
|
+
<Image src={imageUrl} alt={target.name} fill sizes="48px" className="object-cover" />
|
|
94
|
+
) : null}
|
|
95
|
+
</span>
|
|
96
|
+
|
|
97
|
+
{/* Text */}
|
|
98
|
+
<p className="min-w-0 flex-1 text-sm text-foreground">
|
|
99
|
+
{t('upgradeFor', { name: target.name, amount: formattedDelta })}
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
{/* Upgrade button */}
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={handleUpgrade}
|
|
106
|
+
disabled={upgrading}
|
|
107
|
+
className="btn-primary btn-sm shrink-0"
|
|
108
|
+
>
|
|
109
|
+
{upgrading ? t('upgrading') : t('upgrade')}
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{/* Dismiss button */}
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={handleDismiss}
|
|
116
|
+
aria-label={t('dismissUpgrade')}
|
|
117
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
118
|
+
>
|
|
119
|
+
<IconX size={16} />
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|