create-brainerce-store 1.62.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 +5 -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 -612
- 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,123 +1,123 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import type { CartItem as CartItemType } from 'brainerce';
|
|
6
|
-
import { getCartItemImage, formatPrice } from 'brainerce';
|
|
7
|
-
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
10
|
-
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
11
|
-
import { cn } from '@/core/lib/utils';
|
|
12
|
-
|
|
13
|
-
interface CartItemProps {
|
|
14
|
-
item: CartItemType;
|
|
15
|
-
onUpdate: () => void;
|
|
16
|
-
className?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* DESIGN ME — single cart line: image, name, variant, unit price, quantity
|
|
21
|
-
* controls, remove, line total; the `item` prop comes from useCartPage().cart.
|
|
22
|
-
*
|
|
23
|
-
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
24
|
-
*/
|
|
25
|
-
export function CartItem({ item, onUpdate, className }: CartItemProps) {
|
|
26
|
-
const t = useTranslations('common');
|
|
27
|
-
const td = useTranslations('productDetail');
|
|
28
|
-
const currency = useCurrency();
|
|
29
|
-
const [updating, setUpdating] = useState(false);
|
|
30
|
-
const [removing, setRemoving] = useState(false);
|
|
31
|
-
|
|
32
|
-
const productName = item.product.name;
|
|
33
|
-
const imageUrl = getCartItemImage(item);
|
|
34
|
-
const variantName = item.variant?.name;
|
|
35
|
-
const unitPrice = parseFloat(item.unitPrice);
|
|
36
|
-
const lineTotal = unitPrice * item.quantity;
|
|
37
|
-
|
|
38
|
-
async function handleQuantityChange(newQuantity: number) {
|
|
39
|
-
if (newQuantity < 1 || updating) return;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
setUpdating(true);
|
|
43
|
-
const client = getClient();
|
|
44
|
-
await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId || undefined);
|
|
45
|
-
onUpdate();
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error('Failed to update quantity:', err);
|
|
48
|
-
} finally {
|
|
49
|
-
setUpdating(false);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function handleRemove() {
|
|
54
|
-
if (removing) return;
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
setRemoving(true);
|
|
58
|
-
const client = getClient();
|
|
59
|
-
await client.smartRemoveFromCart(item.productId, item.variantId || undefined);
|
|
60
|
-
onUpdate();
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error('Failed to remove item:', err);
|
|
63
|
-
} finally {
|
|
64
|
-
setRemoving(false);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div className={cn('flex items-start gap-4', className)}>
|
|
70
|
-
{/* Image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
71
|
-
<div className="relative h-20 w-20">
|
|
72
|
-
{imageUrl ? (
|
|
73
|
-
<Image src={imageUrl} alt={productName} fill sizes="80px" />
|
|
74
|
-
) : (
|
|
75
|
-
<span className="sr-only">{productName}</span>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{/* Details */}
|
|
80
|
-
<div>
|
|
81
|
-
<h3>{productName}</h3>
|
|
82
|
-
|
|
83
|
-
{/* Variant name */}
|
|
84
|
-
{variantName && <p>{variantName}</p>}
|
|
85
|
-
|
|
86
|
-
{/* Unit price */}
|
|
87
|
-
<p>{formatPrice(unitPrice, { currency }) as string}</p>
|
|
88
|
-
|
|
89
|
-
{/* Quantity controls */}
|
|
90
|
-
<div className="flex items-center gap-3">
|
|
91
|
-
<div className="flex items-center gap-2">
|
|
92
|
-
<button
|
|
93
|
-
type="button"
|
|
94
|
-
onClick={() => handleQuantityChange(item.quantity - 1)}
|
|
95
|
-
disabled={updating || item.quantity <= 1}
|
|
96
|
-
aria-label={td('decreaseQuantity')}
|
|
97
|
-
>
|
|
98
|
-
-
|
|
99
|
-
</button>
|
|
100
|
-
<span aria-live="polite">
|
|
101
|
-
{updating ? <LoadingSpinner size="sm" /> : item.quantity}
|
|
102
|
-
</span>
|
|
103
|
-
<button
|
|
104
|
-
type="button"
|
|
105
|
-
onClick={() => handleQuantityChange(item.quantity + 1)}
|
|
106
|
-
disabled={updating}
|
|
107
|
-
aria-label={td('increaseQuantity')}
|
|
108
|
-
>
|
|
109
|
-
+
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<button type="button" onClick={handleRemove} disabled={removing}>
|
|
114
|
-
{removing ? t('removing') : t('remove')}
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
{/* Line total */}
|
|
120
|
-
<span>{formatPrice(lineTotal, { currency }) as string}</span>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { CartItem as CartItemType } from 'brainerce';
|
|
6
|
+
import { getCartItemImage, formatPrice } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { useCurrency } from '@/core/lib/use-currency';
|
|
10
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
11
|
+
import { cn } from '@/core/lib/utils';
|
|
12
|
+
|
|
13
|
+
interface CartItemProps {
|
|
14
|
+
item: CartItemType;
|
|
15
|
+
onUpdate: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DESIGN ME — single cart line: image, name, variant, unit price, quantity
|
|
21
|
+
* controls, remove, line total; the `item` prop comes from useCartPage().cart.
|
|
22
|
+
*
|
|
23
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
24
|
+
*/
|
|
25
|
+
export function CartItem({ item, onUpdate, className }: CartItemProps) {
|
|
26
|
+
const t = useTranslations('common');
|
|
27
|
+
const td = useTranslations('productDetail');
|
|
28
|
+
const currency = useCurrency();
|
|
29
|
+
const [updating, setUpdating] = useState(false);
|
|
30
|
+
const [removing, setRemoving] = useState(false);
|
|
31
|
+
|
|
32
|
+
const productName = item.product.name;
|
|
33
|
+
const imageUrl = getCartItemImage(item);
|
|
34
|
+
const variantName = item.variant?.name;
|
|
35
|
+
const unitPrice = parseFloat(item.unitPrice);
|
|
36
|
+
const lineTotal = unitPrice * item.quantity;
|
|
37
|
+
|
|
38
|
+
async function handleQuantityChange(newQuantity: number) {
|
|
39
|
+
if (newQuantity < 1 || updating) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
setUpdating(true);
|
|
43
|
+
const client = getClient();
|
|
44
|
+
await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId || undefined);
|
|
45
|
+
onUpdate();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to update quantity:', err);
|
|
48
|
+
} finally {
|
|
49
|
+
setUpdating(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function handleRemove() {
|
|
54
|
+
if (removing) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
setRemoving(true);
|
|
58
|
+
const client = getClient();
|
|
59
|
+
await client.smartRemoveFromCart(item.productId, item.variantId || undefined);
|
|
60
|
+
onUpdate();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('Failed to remove item:', err);
|
|
63
|
+
} finally {
|
|
64
|
+
setRemoving(false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn('flex items-start gap-4', className)}>
|
|
70
|
+
{/* Image — `relative` + fixed box are layout-critical for next/image fill */}
|
|
71
|
+
<div className="relative h-20 w-20">
|
|
72
|
+
{imageUrl ? (
|
|
73
|
+
<Image src={imageUrl} alt={productName} fill sizes="80px" />
|
|
74
|
+
) : (
|
|
75
|
+
<span className="sr-only">{productName}</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Details */}
|
|
80
|
+
<div>
|
|
81
|
+
<h3>{productName}</h3>
|
|
82
|
+
|
|
83
|
+
{/* Variant name */}
|
|
84
|
+
{variantName && <p>{variantName}</p>}
|
|
85
|
+
|
|
86
|
+
{/* Unit price */}
|
|
87
|
+
<p>{formatPrice(unitPrice, { currency }) as string}</p>
|
|
88
|
+
|
|
89
|
+
{/* Quantity controls */}
|
|
90
|
+
<div className="flex items-center gap-3">
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => handleQuantityChange(item.quantity - 1)}
|
|
95
|
+
disabled={updating || item.quantity <= 1}
|
|
96
|
+
aria-label={td('decreaseQuantity')}
|
|
97
|
+
>
|
|
98
|
+
-
|
|
99
|
+
</button>
|
|
100
|
+
<span aria-live="polite">
|
|
101
|
+
{updating ? <LoadingSpinner size="sm" /> : item.quantity}
|
|
102
|
+
</span>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={() => handleQuantityChange(item.quantity + 1)}
|
|
106
|
+
disabled={updating}
|
|
107
|
+
aria-label={td('increaseQuantity')}
|
|
108
|
+
>
|
|
109
|
+
+
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<button type="button" onClick={handleRemove} disabled={removing}>
|
|
114
|
+
{removing ? t('removing') : t('remove')}
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Line total */}
|
|
120
|
+
<span>{formatPrice(lineTotal, { currency }) as string}</span>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { CartNudge } from 'brainerce';
|
|
4
|
-
import { cn } from '@/core/lib/utils';
|
|
5
|
-
|
|
6
|
-
interface CartNudgesProps {
|
|
7
|
-
nudges: CartNudge[];
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* DESIGN ME — discount-rule nudges on the cart page ("add X more for Y");
|
|
13
|
-
* texts come from useCartPage().cart.nudges.
|
|
14
|
-
*
|
|
15
|
-
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
16
|
-
*/
|
|
17
|
-
export function CartNudges({ nudges, className }: CartNudgesProps) {
|
|
18
|
-
if (nudges.length === 0) return null;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<ul className={cn(className)} aria-label="Cart suggestions">
|
|
22
|
-
{nudges.map((nudge) => (
|
|
23
|
-
<li key={nudge.ruleId}>{nudge.text}</li>
|
|
24
|
-
))}
|
|
25
|
-
</ul>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CartNudge } from 'brainerce';
|
|
4
|
+
import { cn } from '@/core/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface CartNudgesProps {
|
|
7
|
+
nudges: CartNudge[];
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DESIGN ME — discount-rule nudges on the cart page ("add X more for Y");
|
|
13
|
+
* texts come from useCartPage().cart.nudges.
|
|
14
|
+
*
|
|
15
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
16
|
+
*/
|
|
17
|
+
export function CartNudges({ nudges, className }: CartNudgesProps) {
|
|
18
|
+
if (nudges.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<ul className={cn(className)} aria-label="Cart suggestions">
|
|
22
|
+
{nudges.map((nudge) => (
|
|
23
|
+
<li key={nudge.ruleId}>{nudge.text}</li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,99 +1,99 @@
|
|
|
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
|
-
* DESIGN ME — order summary (subtotal, rule/coupon discounts, shipping, tax
|
|
15
|
-
* note, total); all figures come from useCart().totals and useCart().cart.
|
|
16
|
-
* Keep every discount branch — merchants rely on the per-rule breakdown.
|
|
17
|
-
*
|
|
18
|
-
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
19
|
-
*/
|
|
20
|
-
export function CartSummary({ className }: CartSummaryProps) {
|
|
21
|
-
const t = useTranslations('cart');
|
|
22
|
-
const tc = useTranslations('common');
|
|
23
|
-
const { totals, cart } = useCart();
|
|
24
|
-
const currency = useCurrency();
|
|
25
|
-
|
|
26
|
-
const rules = cart?.appliedDiscounts;
|
|
27
|
-
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
28
|
-
const couponAmt = totals.discount - ruleAmt;
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<section className={cn(className)}>
|
|
32
|
-
<h3>{t('orderSummary')}</h3>
|
|
33
|
-
|
|
34
|
-
<dl>
|
|
35
|
-
{/* Subtotal */}
|
|
36
|
-
<div className="flex items-center justify-between gap-4">
|
|
37
|
-
<dt>{tc('subtotal')}</dt>
|
|
38
|
-
<dd>{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
{/* Rule discounts - show each rule by name */}
|
|
42
|
-
{rules && rules.length > 0
|
|
43
|
-
? rules.map((rule) => (
|
|
44
|
-
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
45
|
-
<dt>{rule.ruleName}</dt>
|
|
46
|
-
<dd>-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}</dd>
|
|
47
|
-
</div>
|
|
48
|
-
))
|
|
49
|
-
: ruleAmt > 0 && (
|
|
50
|
-
<div className="flex items-center justify-between gap-4">
|
|
51
|
-
<dt>{tc('generalDiscount')}</dt>
|
|
52
|
-
<dd>-{formatPrice(ruleAmt, { currency }) as string}</dd>
|
|
53
|
-
</div>
|
|
54
|
-
)}
|
|
55
|
-
|
|
56
|
-
{/* Coupon discount */}
|
|
57
|
-
{cart?.couponCode && couponAmt > 0 && (
|
|
58
|
-
<div className="flex items-center justify-between gap-4">
|
|
59
|
-
<dt>
|
|
60
|
-
{tc('couponDiscount')} ({cart.couponCode})
|
|
61
|
-
</dt>
|
|
62
|
-
<dd>-{formatPrice(couponAmt, { currency }) as string}</dd>
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
|
|
66
|
-
{/* Fallback: generic discount when no breakdown available */}
|
|
67
|
-
{totals.discount > 0 &&
|
|
68
|
-
ruleAmt <= 0 &&
|
|
69
|
-
!cart?.couponCode &&
|
|
70
|
-
(!rules || rules.length === 0) && (
|
|
71
|
-
<div className="flex items-center justify-between gap-4">
|
|
72
|
-
<dt>{tc('discount')}</dt>
|
|
73
|
-
<dd>-{formatPrice(totals.discount, { currency }) as string}</dd>
|
|
74
|
-
</div>
|
|
75
|
-
)}
|
|
76
|
-
|
|
77
|
-
{/* Shipping */}
|
|
78
|
-
{totals.shipping > 0 && (
|
|
79
|
-
<div className="flex items-center justify-between gap-4">
|
|
80
|
-
<dt>{tc('shipping')}</dt>
|
|
81
|
-
<dd>{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
|
|
85
|
-
{/* Tax */}
|
|
86
|
-
<div className="flex items-center justify-between gap-4">
|
|
87
|
-
<dt>{tc('tax')}</dt>
|
|
88
|
-
<dd>{t('taxAtCheckout')}</dd>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Total */}
|
|
92
|
-
<div className="flex items-center justify-between gap-4">
|
|
93
|
-
<dt>{tc('total')}</dt>
|
|
94
|
-
<dd>{formatPrice(totals.total, { currency }) as string}</dd>
|
|
95
|
-
</div>
|
|
96
|
-
</dl>
|
|
97
|
-
</section>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
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
|
+
* DESIGN ME — order summary (subtotal, rule/coupon discounts, shipping, tax
|
|
15
|
+
* note, total); all figures come from useCart().totals and useCart().cart.
|
|
16
|
+
* Keep every discount branch — merchants rely on the per-rule breakdown.
|
|
17
|
+
*
|
|
18
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
19
|
+
*/
|
|
20
|
+
export function CartSummary({ className }: CartSummaryProps) {
|
|
21
|
+
const t = useTranslations('cart');
|
|
22
|
+
const tc = useTranslations('common');
|
|
23
|
+
const { totals, cart } = useCart();
|
|
24
|
+
const currency = useCurrency();
|
|
25
|
+
|
|
26
|
+
const rules = cart?.appliedDiscounts;
|
|
27
|
+
const ruleAmt = cart?.ruleDiscountAmount ? parseFloat(cart.ruleDiscountAmount) : 0;
|
|
28
|
+
const couponAmt = totals.discount - ruleAmt;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<section className={cn(className)}>
|
|
32
|
+
<h3>{t('orderSummary')}</h3>
|
|
33
|
+
|
|
34
|
+
<dl>
|
|
35
|
+
{/* Subtotal */}
|
|
36
|
+
<div className="flex items-center justify-between gap-4">
|
|
37
|
+
<dt>{tc('subtotal')}</dt>
|
|
38
|
+
<dd>{formatPrice(totals.subtotal, { currency }) as string}</dd>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Rule discounts - show each rule by name */}
|
|
42
|
+
{rules && rules.length > 0
|
|
43
|
+
? rules.map((rule) => (
|
|
44
|
+
<div key={rule.ruleId} className="flex items-center justify-between gap-4">
|
|
45
|
+
<dt>{rule.ruleName}</dt>
|
|
46
|
+
<dd>-{formatPrice(parseFloat(rule.discountAmount), { currency }) as string}</dd>
|
|
47
|
+
</div>
|
|
48
|
+
))
|
|
49
|
+
: ruleAmt > 0 && (
|
|
50
|
+
<div className="flex items-center justify-between gap-4">
|
|
51
|
+
<dt>{tc('generalDiscount')}</dt>
|
|
52
|
+
<dd>-{formatPrice(ruleAmt, { currency }) as string}</dd>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Coupon discount */}
|
|
57
|
+
{cart?.couponCode && couponAmt > 0 && (
|
|
58
|
+
<div className="flex items-center justify-between gap-4">
|
|
59
|
+
<dt>
|
|
60
|
+
{tc('couponDiscount')} ({cart.couponCode})
|
|
61
|
+
</dt>
|
|
62
|
+
<dd>-{formatPrice(couponAmt, { currency }) as string}</dd>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{/* Fallback: generic discount when no breakdown available */}
|
|
67
|
+
{totals.discount > 0 &&
|
|
68
|
+
ruleAmt <= 0 &&
|
|
69
|
+
!cart?.couponCode &&
|
|
70
|
+
(!rules || rules.length === 0) && (
|
|
71
|
+
<div className="flex items-center justify-between gap-4">
|
|
72
|
+
<dt>{tc('discount')}</dt>
|
|
73
|
+
<dd>-{formatPrice(totals.discount, { currency }) as string}</dd>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{/* Shipping */}
|
|
78
|
+
{totals.shipping > 0 && (
|
|
79
|
+
<div className="flex items-center justify-between gap-4">
|
|
80
|
+
<dt>{tc('shipping')}</dt>
|
|
81
|
+
<dd>{formatPrice(totals.shipping, { currency }) as string}</dd>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Tax */}
|
|
86
|
+
<div className="flex items-center justify-between gap-4">
|
|
87
|
+
<dt>{tc('tax')}</dt>
|
|
88
|
+
<dd>{t('taxAtCheckout')}</dd>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Total */}
|
|
92
|
+
<div className="flex items-center justify-between gap-4">
|
|
93
|
+
<dt>{tc('total')}</dt>
|
|
94
|
+
<dd>{formatPrice(totals.total, { currency }) as string}</dd>
|
|
95
|
+
</div>
|
|
96
|
+
</dl>
|
|
97
|
+
</section>
|
|
98
|
+
);
|
|
99
|
+
}
|