@cartbase/storefront 0.17.0 → 0.18.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/package.json +258 -256
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/products.ts +43 -1
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +259 -259
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +277 -164
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/index.ts +71 -68
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/quantity.tsx +8 -14
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -0
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/checkout/use-checkout-orchestration.ts +1592 -1592
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/bg.ts +467 -467
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +466 -466
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/index.ts +68 -59
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-actions-wrapper.tsx +63 -62
- package/src/products/product-actions.tsx +151 -150
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/product-template.tsx +163 -162
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -0
- package/src/products/use-product-actions.ts +199 -164
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +73 -73
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
- package/theme/theme.css +150 -150
- package/theme/tokens.css +106 -106
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { StoreImage as Image } from "../../lib/media-image"
|
|
4
|
-
import Link from "next/link"
|
|
5
|
-
import { type ReactNode } from "react"
|
|
6
|
-
|
|
7
|
-
import type { CartLineItem } from "../../api/carts"
|
|
8
|
-
import { Price } from "../../lib/price"
|
|
9
|
-
import { cn } from "../../lib/utils"
|
|
10
|
-
import { useCartDrawer } from "../context"
|
|
11
|
-
import { CartItemQuantity } from "./quantity"
|
|
12
|
-
import { CartItemVariant } from "./variant"
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* CartItem — a single cart line row: thumbnail, title, variant, quantity
|
|
16
|
-
* stepper, price (with optional strikethrough), remove button.
|
|
17
|
-
*
|
|
18
|
-
* Ported from `@1click/ui/src/cart-drawer/item/index.tsx` (v2.3.1). Cartbase
|
|
19
|
-
* data seam: `item` is the SDK's decorated `CartLineItem`
|
|
20
|
-
* (`@cartbase/storefront/api/carts`) — per-line `total`/`original_total` are
|
|
21
|
-
* server-computed EUR major units. Remove goes through the provider's
|
|
22
|
-
* SDK-wired `removeItem` (DELETE line-items/:id, returns the full decorated
|
|
23
|
-
* cart) instead of the Medusa server action. The variant row renders the
|
|
24
|
-
* line's flat `variant_title` — Cartbase cart lines carry the variant title
|
|
25
|
-
* string, not an embedded variant object.
|
|
26
|
-
*
|
|
27
|
-
* `children` is rendered below the main row — intended for per-item upsell
|
|
28
|
-
* slots (see `CartItemUpsell` in `./upsell.tsx`).
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
type CartItemProps = {
|
|
32
|
-
item: CartLineItem
|
|
33
|
-
currencyCode: string
|
|
34
|
-
children?: ReactNode
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function CartItem({ item, currencyCode, children }: CartItemProps) {
|
|
38
|
-
const { labels, hrefs, removeItem } = useCartDrawer()
|
|
39
|
-
|
|
40
|
-
// Optimistic remove: the item is filtered out of the optimistic cart
|
|
41
|
-
// immediately on click, so the row vanishes at React update speed.
|
|
42
|
-
// The deleteLineItem SDK call runs in the background. On failure
|
|
43
|
-
// useOptimistic auto-reverts (confirmed snapshot unchanged) and the
|
|
44
|
-
// row reappears.
|
|
45
|
-
const handleRemove = () => {
|
|
46
|
-
void removeItem(item.id)
|
|
47
|
-
}
|
|
48
|
-
const removing = false // optimistic remove unmounts the row immediately
|
|
49
|
-
|
|
50
|
-
const total = item.total ?? 0
|
|
51
|
-
const originalTotal = item.original_total ?? 0
|
|
52
|
-
const hasDiscount = total < originalTotal
|
|
53
|
-
const productHref = `${hrefs.productPrefix}/${item.product_handle}`
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div
|
|
57
|
-
className={cn(
|
|
58
|
-
"px-6 py-5 transition-opacity",
|
|
59
|
-
removing && "opacity-30 pointer-events-none"
|
|
60
|
-
)}
|
|
61
|
-
>
|
|
62
|
-
<div className="flex gap-4">
|
|
63
|
-
<Link href={productHref} className="flex-shrink-0">
|
|
64
|
-
<div
|
|
65
|
-
className="w-[64px] h-[84px] sm:w-[72px] sm:h-[72px] rounded-xl overflow-hidden bg-muted relative"
|
|
66
|
-
style={{ boxShadow: "0 1px 3px rgba(0,0,0,0.06)" }}
|
|
67
|
-
>
|
|
68
|
-
{item.thumbnail ? (
|
|
69
|
-
<Image
|
|
70
|
-
src={item.thumbnail}
|
|
71
|
-
alt={item.product_title || ""}
|
|
72
|
-
fill
|
|
73
|
-
className="object-cover"
|
|
74
|
-
sizes="72px"
|
|
75
|
-
/>
|
|
76
|
-
) : (
|
|
77
|
-
<div className="w-full h-full flex items-center justify-center">
|
|
78
|
-
<svg
|
|
79
|
-
width="20"
|
|
80
|
-
height="20"
|
|
81
|
-
viewBox="0 0 24 24"
|
|
82
|
-
fill="none"
|
|
83
|
-
stroke="currentColor"
|
|
84
|
-
className="text-muted-foreground"
|
|
85
|
-
strokeWidth="1.2"
|
|
86
|
-
>
|
|
87
|
-
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
88
|
-
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
89
|
-
<path
|
|
90
|
-
d="M21 15l-5-5L5 21"
|
|
91
|
-
strokeLinecap="round"
|
|
92
|
-
strokeLinejoin="round"
|
|
93
|
-
/>
|
|
94
|
-
</svg>
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
</Link>
|
|
99
|
-
|
|
100
|
-
<div className="flex-1 min-w-0 flex flex-col">
|
|
101
|
-
<div className="flex items-baseline justify-between gap-3">
|
|
102
|
-
<div className="min-w-0">
|
|
103
|
-
<Link
|
|
104
|
-
href={productHref}
|
|
105
|
-
className="text-sm font-semibold text-foreground hover:text-muted-foreground transition-colors line-clamp-2 leading-snug block"
|
|
106
|
-
>
|
|
107
|
-
{item.product_title}
|
|
108
|
-
</Link>
|
|
109
|
-
<CartItemVariant variantTitle={item.variant_title} />
|
|
110
|
-
</div>
|
|
111
|
-
<div className="text-right flex-shrink-0">
|
|
112
|
-
{hasDiscount && (
|
|
113
|
-
<span className="text-xs text-muted-foreground line-through block leading-none mb-0.5">
|
|
114
|
-
<Price
|
|
115
|
-
amount={originalTotal}
|
|
116
|
-
currencyCode={currencyCode}
|
|
117
|
-
className="text-xs text-muted-foreground"
|
|
118
|
-
/>
|
|
119
|
-
</span>
|
|
120
|
-
)}
|
|
121
|
-
<Price
|
|
122
|
-
amount={total}
|
|
123
|
-
currencyCode={currencyCode}
|
|
124
|
-
className={cn(
|
|
125
|
-
"text-sm font-bold",
|
|
126
|
-
hasDiscount ? "text-success" : "text-foreground"
|
|
127
|
-
)}
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div className="flex items-center justify-between mt-3">
|
|
133
|
-
<CartItemQuantity lineId={item.id} quantity={item.quantity} />
|
|
134
|
-
<button
|
|
135
|
-
type="button"
|
|
136
|
-
onClick={handleRemove}
|
|
137
|
-
className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all"
|
|
138
|
-
aria-label={labels.remove}
|
|
139
|
-
>
|
|
140
|
-
<svg
|
|
141
|
-
width="16"
|
|
142
|
-
height="16"
|
|
143
|
-
viewBox="0 0 16 16"
|
|
144
|
-
fill="none"
|
|
145
|
-
stroke="currentColor"
|
|
146
|
-
strokeWidth="1.3"
|
|
147
|
-
strokeLinecap="round"
|
|
148
|
-
strokeLinejoin="round"
|
|
149
|
-
>
|
|
150
|
-
<path d="M2.5 4.5h11M5.5 4.5V3a1 1 0 011-1h3a1 1 0 011 1v1.5M12.5 4.5v9a1 1 0 01-1 1h-7a1 1 0 01-1-1v-9" />
|
|
151
|
-
<path d="M6.5 7v4M9.5 7v4" />
|
|
152
|
-
</svg>
|
|
153
|
-
</button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{children}
|
|
159
|
-
<div className="mt-5 border-b border-border" />
|
|
160
|
-
</div>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { StoreImage as Image } from "../../lib/media-image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { type ReactNode } from "react"
|
|
6
|
+
|
|
7
|
+
import type { CartLineItem } from "../../api/carts"
|
|
8
|
+
import { Price } from "../../lib/price"
|
|
9
|
+
import { cn } from "../../lib/utils"
|
|
10
|
+
import { useCartDrawer } from "../context"
|
|
11
|
+
import { CartItemQuantity } from "./quantity"
|
|
12
|
+
import { CartItemVariant } from "./variant"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CartItem — a single cart line row: thumbnail, title, variant, quantity
|
|
16
|
+
* stepper, price (with optional strikethrough), remove button.
|
|
17
|
+
*
|
|
18
|
+
* Ported from `@1click/ui/src/cart-drawer/item/index.tsx` (v2.3.1). Cartbase
|
|
19
|
+
* data seam: `item` is the SDK's decorated `CartLineItem`
|
|
20
|
+
* (`@cartbase/storefront/api/carts`) — per-line `total`/`original_total` are
|
|
21
|
+
* server-computed EUR major units. Remove goes through the provider's
|
|
22
|
+
* SDK-wired `removeItem` (DELETE line-items/:id, returns the full decorated
|
|
23
|
+
* cart) instead of the Medusa server action. The variant row renders the
|
|
24
|
+
* line's flat `variant_title` — Cartbase cart lines carry the variant title
|
|
25
|
+
* string, not an embedded variant object.
|
|
26
|
+
*
|
|
27
|
+
* `children` is rendered below the main row — intended for per-item upsell
|
|
28
|
+
* slots (see `CartItemUpsell` in `./upsell.tsx`).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type CartItemProps = {
|
|
32
|
+
item: CartLineItem
|
|
33
|
+
currencyCode: string
|
|
34
|
+
children?: ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function CartItem({ item, currencyCode, children }: CartItemProps) {
|
|
38
|
+
const { labels, hrefs, removeItem } = useCartDrawer()
|
|
39
|
+
|
|
40
|
+
// Optimistic remove: the item is filtered out of the optimistic cart
|
|
41
|
+
// immediately on click, so the row vanishes at React update speed.
|
|
42
|
+
// The deleteLineItem SDK call runs in the background. On failure
|
|
43
|
+
// useOptimistic auto-reverts (confirmed snapshot unchanged) and the
|
|
44
|
+
// row reappears.
|
|
45
|
+
const handleRemove = () => {
|
|
46
|
+
void removeItem(item.id)
|
|
47
|
+
}
|
|
48
|
+
const removing = false // optimistic remove unmounts the row immediately
|
|
49
|
+
|
|
50
|
+
const total = item.total ?? 0
|
|
51
|
+
const originalTotal = item.original_total ?? 0
|
|
52
|
+
const hasDiscount = total < originalTotal
|
|
53
|
+
const productHref = `${hrefs.productPrefix}/${item.product_handle}`
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className={cn(
|
|
58
|
+
"px-6 py-5 transition-opacity",
|
|
59
|
+
removing && "opacity-30 pointer-events-none"
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex gap-4">
|
|
63
|
+
<Link href={productHref} className="flex-shrink-0">
|
|
64
|
+
<div
|
|
65
|
+
className="w-[64px] h-[84px] sm:w-[72px] sm:h-[72px] rounded-xl overflow-hidden bg-muted relative"
|
|
66
|
+
style={{ boxShadow: "0 1px 3px rgba(0,0,0,0.06)" }}
|
|
67
|
+
>
|
|
68
|
+
{item.thumbnail ? (
|
|
69
|
+
<Image
|
|
70
|
+
src={item.thumbnail}
|
|
71
|
+
alt={item.product_title || ""}
|
|
72
|
+
fill
|
|
73
|
+
className="object-cover"
|
|
74
|
+
sizes="72px"
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
78
|
+
<svg
|
|
79
|
+
width="20"
|
|
80
|
+
height="20"
|
|
81
|
+
viewBox="0 0 24 24"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
className="text-muted-foreground"
|
|
85
|
+
strokeWidth="1.2"
|
|
86
|
+
>
|
|
87
|
+
<rect x="3" y="3" width="18" height="18" rx="3" />
|
|
88
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
89
|
+
<path
|
|
90
|
+
d="M21 15l-5-5L5 21"
|
|
91
|
+
strokeLinecap="round"
|
|
92
|
+
strokeLinejoin="round"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</Link>
|
|
99
|
+
|
|
100
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
101
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
102
|
+
<div className="min-w-0">
|
|
103
|
+
<Link
|
|
104
|
+
href={productHref}
|
|
105
|
+
className="text-sm font-semibold text-foreground hover:text-muted-foreground transition-colors line-clamp-2 leading-snug block"
|
|
106
|
+
>
|
|
107
|
+
{item.product_title}
|
|
108
|
+
</Link>
|
|
109
|
+
<CartItemVariant variantTitle={item.variant_title} />
|
|
110
|
+
</div>
|
|
111
|
+
<div className="text-right flex-shrink-0">
|
|
112
|
+
{hasDiscount && (
|
|
113
|
+
<span className="text-xs text-muted-foreground line-through block leading-none mb-0.5">
|
|
114
|
+
<Price
|
|
115
|
+
amount={originalTotal}
|
|
116
|
+
currencyCode={currencyCode}
|
|
117
|
+
className="text-xs text-muted-foreground"
|
|
118
|
+
/>
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
<Price
|
|
122
|
+
amount={total}
|
|
123
|
+
currencyCode={currencyCode}
|
|
124
|
+
className={cn(
|
|
125
|
+
"text-sm font-bold",
|
|
126
|
+
hasDiscount ? "text-success" : "text-foreground"
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="flex items-center justify-between mt-3">
|
|
133
|
+
<CartItemQuantity lineId={item.id} quantity={item.quantity} />
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={handleRemove}
|
|
137
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all"
|
|
138
|
+
aria-label={labels.remove}
|
|
139
|
+
>
|
|
140
|
+
<svg
|
|
141
|
+
width="16"
|
|
142
|
+
height="16"
|
|
143
|
+
viewBox="0 0 16 16"
|
|
144
|
+
fill="none"
|
|
145
|
+
stroke="currentColor"
|
|
146
|
+
strokeWidth="1.3"
|
|
147
|
+
strokeLinecap="round"
|
|
148
|
+
strokeLinejoin="round"
|
|
149
|
+
>
|
|
150
|
+
<path d="M2.5 4.5h11M5.5 4.5V3a1 1 0 011-1h3a1 1 0 011 1v1.5M12.5 4.5v9a1 1 0 01-1 1h-7a1 1 0 01-1-1v-9" />
|
|
151
|
+
<path d="M6.5 7v4M9.5 7v4" />
|
|
152
|
+
</svg>
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{children}
|
|
159
|
+
<div className="mt-5 border-b border-border" />
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
|
|
5
3
|
import { cn } from "../../lib/utils"
|
|
6
4
|
import { useCartDrawer } from "../context"
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* CartItemQuantity — +/- quantity stepper for a single cart line item.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* Instant: every click shows at once and the stepper never locks; the
|
|
10
|
+
* provider combines fast clicks on one line and sends the number the
|
|
11
|
+
* shopper stopped at (`mutation-queue.ts`). On failure the optimistic state
|
|
12
|
+
* reverts automatically (confirmed snapshot unchanged). Until 2026-09-16
|
|
13
|
+
* the stepper locked for the whole round trip, so fast clicks were lost.
|
|
16
14
|
*
|
|
17
15
|
* Ported from `@1click/ui/src/cart-drawer/item/quantity.tsx` (v2.3.1).
|
|
18
16
|
* Cartbase data seam: `updateQuantity` wraps
|
|
@@ -36,21 +34,17 @@ export function CartItemQuantity({
|
|
|
36
34
|
className,
|
|
37
35
|
}: CartItemQuantityProps) {
|
|
38
36
|
const { updateQuantity } = useCartDrawer()
|
|
39
|
-
const [updating, setUpdating] = useState(false)
|
|
40
37
|
|
|
41
38
|
const change = (newQty: number) => {
|
|
42
|
-
if (newQty < 1 || newQty > maxQuantity
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// through the provider's onOptimisticError.
|
|
46
|
-
void updateQuantity(lineId, newQty).finally(() => setUpdating(false))
|
|
39
|
+
if (newQty < 1 || newQty > maxQuantity) return
|
|
40
|
+
// Failures are funneled through the provider's onOptimisticError.
|
|
41
|
+
void updateQuantity(lineId, newQty)
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
return (
|
|
50
45
|
<div
|
|
51
46
|
className={cn(
|
|
52
47
|
"inline-flex items-center border border-border rounded-lg h-9",
|
|
53
|
-
updating && "opacity-50 pointer-events-none",
|
|
54
48
|
className
|
|
55
49
|
)}
|
|
56
50
|
>
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { StoreImage as Image } from "../../lib/media-image"
|
|
4
|
-
import Link from "next/link"
|
|
5
|
-
|
|
6
|
-
import { convertToLocale } from "../../lib/money"
|
|
7
|
-
import { useCartDrawer } from "../context"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* CartItemUpsell — slot rendered INSIDE a cart item for per-product
|
|
11
|
-
* complementary recommendations. Accepts a list of products and an
|
|
12
|
-
* onAdd handler.
|
|
13
|
-
*
|
|
14
|
-
* Ported from `@1click/ui/src/cart-drawer/item/upsell.tsx` (v2.3.1).
|
|
15
|
-
* Cartbase data seam: source stores fed this from Medusa product fetches —
|
|
16
|
-
* feed it from `@cartbase/storefront/api/search` `listRelatedProducts`
|
|
17
|
-
* (per-anchor complements: manual admin picks + deterministic fallback)
|
|
18
|
-
* mapped via `toCrossSellProduct` (`../cross-sell-sidebar`). `variantId`
|
|
19
|
-
* was added to `UpsellProduct` and to the `onAdd` signature so the handler
|
|
20
|
-
* can call the provider's `addItem(variantId)` directly — Cartbase carts add
|
|
21
|
-
* by variant id, not product id. Prices are EUR major units.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export type UpsellProduct = {
|
|
25
|
-
id: string
|
|
26
|
-
title: string
|
|
27
|
-
handle: string
|
|
28
|
-
thumbnail: string | null
|
|
29
|
-
price: number
|
|
30
|
-
currencyCode: string
|
|
31
|
-
/** Variant to add (Cartbase add-to-cart is by variant id). */
|
|
32
|
-
variantId?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type CartItemUpsellProps = {
|
|
36
|
-
label?: string
|
|
37
|
-
products: UpsellProduct[]
|
|
38
|
-
onAdd?: (productId: string, variantId?: string) => void
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function CartItemUpsell({
|
|
42
|
-
label,
|
|
43
|
-
products,
|
|
44
|
-
onAdd,
|
|
45
|
-
}: CartItemUpsellProps) {
|
|
46
|
-
const { labels, hrefs } = useCartDrawer()
|
|
47
|
-
if (!products.length) return null
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div className="mt-3 pt-3 border-t border-border">
|
|
51
|
-
<p className="text-xs font-medium text-muted-foreground mb-2">
|
|
52
|
-
{label ?? labels.pairsWellWith}
|
|
53
|
-
</p>
|
|
54
|
-
<div className="space-y-2">
|
|
55
|
-
{products.map((product) => (
|
|
56
|
-
<div
|
|
57
|
-
key={product.id}
|
|
58
|
-
className="flex items-center gap-3 p-2 rounded-lg bg-muted hover:bg-muted/70 transition-colors"
|
|
59
|
-
>
|
|
60
|
-
<div className="w-10 h-10 rounded-lg overflow-hidden bg-card flex-shrink-0 relative">
|
|
61
|
-
{product.thumbnail ? (
|
|
62
|
-
<Image
|
|
63
|
-
src={product.thumbnail}
|
|
64
|
-
alt={product.title}
|
|
65
|
-
fill
|
|
66
|
-
className="object-cover"
|
|
67
|
-
sizes="40px"
|
|
68
|
-
/>
|
|
69
|
-
) : (
|
|
70
|
-
<div className="w-full h-full bg-muted" />
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
<div className="flex-1 min-w-0">
|
|
74
|
-
<Link
|
|
75
|
-
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
76
|
-
className="text-xs font-medium text-foreground truncate block"
|
|
77
|
-
>
|
|
78
|
-
{product.title}
|
|
79
|
-
</Link>
|
|
80
|
-
<span className="text-xs text-muted-foreground">
|
|
81
|
-
{convertToLocale({
|
|
82
|
-
amount: product.price,
|
|
83
|
-
currency_code: product.currencyCode,
|
|
84
|
-
})}
|
|
85
|
-
</span>
|
|
86
|
-
</div>
|
|
87
|
-
<button
|
|
88
|
-
type="button"
|
|
89
|
-
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
90
|
-
className="w-7 h-7 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-base hover:text-foreground transition-colors flex-shrink-0"
|
|
91
|
-
aria-label={`${labels.addToCart} ${product.title}`}
|
|
92
|
-
>
|
|
93
|
-
<svg
|
|
94
|
-
width="12"
|
|
95
|
-
height="12"
|
|
96
|
-
viewBox="0 0 12 12"
|
|
97
|
-
fill="none"
|
|
98
|
-
stroke="currentColor"
|
|
99
|
-
strokeWidth="1.5"
|
|
100
|
-
strokeLinecap="round"
|
|
101
|
-
>
|
|
102
|
-
<path d="M6 2.5v7M2.5 6h7" />
|
|
103
|
-
</svg>
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
))}
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { StoreImage as Image } from "../../lib/media-image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
|
|
6
|
+
import { convertToLocale } from "../../lib/money"
|
|
7
|
+
import { useCartDrawer } from "../context"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CartItemUpsell — slot rendered INSIDE a cart item for per-product
|
|
11
|
+
* complementary recommendations. Accepts a list of products and an
|
|
12
|
+
* onAdd handler.
|
|
13
|
+
*
|
|
14
|
+
* Ported from `@1click/ui/src/cart-drawer/item/upsell.tsx` (v2.3.1).
|
|
15
|
+
* Cartbase data seam: source stores fed this from Medusa product fetches —
|
|
16
|
+
* feed it from `@cartbase/storefront/api/search` `listRelatedProducts`
|
|
17
|
+
* (per-anchor complements: manual admin picks + deterministic fallback)
|
|
18
|
+
* mapped via `toCrossSellProduct` (`../cross-sell-sidebar`). `variantId`
|
|
19
|
+
* was added to `UpsellProduct` and to the `onAdd` signature so the handler
|
|
20
|
+
* can call the provider's `addItem(variantId)` directly — Cartbase carts add
|
|
21
|
+
* by variant id, not product id. Prices are EUR major units.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type UpsellProduct = {
|
|
25
|
+
id: string
|
|
26
|
+
title: string
|
|
27
|
+
handle: string
|
|
28
|
+
thumbnail: string | null
|
|
29
|
+
price: number
|
|
30
|
+
currencyCode: string
|
|
31
|
+
/** Variant to add (Cartbase add-to-cart is by variant id). */
|
|
32
|
+
variantId?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type CartItemUpsellProps = {
|
|
36
|
+
label?: string
|
|
37
|
+
products: UpsellProduct[]
|
|
38
|
+
onAdd?: (productId: string, variantId?: string) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function CartItemUpsell({
|
|
42
|
+
label,
|
|
43
|
+
products,
|
|
44
|
+
onAdd,
|
|
45
|
+
}: CartItemUpsellProps) {
|
|
46
|
+
const { labels, hrefs } = useCartDrawer()
|
|
47
|
+
if (!products.length) return null
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="mt-3 pt-3 border-t border-border">
|
|
51
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">
|
|
52
|
+
{label ?? labels.pairsWellWith}
|
|
53
|
+
</p>
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
{products.map((product) => (
|
|
56
|
+
<div
|
|
57
|
+
key={product.id}
|
|
58
|
+
className="flex items-center gap-3 p-2 rounded-lg bg-muted hover:bg-muted/70 transition-colors"
|
|
59
|
+
>
|
|
60
|
+
<div className="w-10 h-10 rounded-lg overflow-hidden bg-card flex-shrink-0 relative">
|
|
61
|
+
{product.thumbnail ? (
|
|
62
|
+
<Image
|
|
63
|
+
src={product.thumbnail}
|
|
64
|
+
alt={product.title}
|
|
65
|
+
fill
|
|
66
|
+
className="object-cover"
|
|
67
|
+
sizes="40px"
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="w-full h-full bg-muted" />
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex-1 min-w-0">
|
|
74
|
+
<Link
|
|
75
|
+
href={`${hrefs.productPrefix}/${product.handle}`}
|
|
76
|
+
className="text-xs font-medium text-foreground truncate block"
|
|
77
|
+
>
|
|
78
|
+
{product.title}
|
|
79
|
+
</Link>
|
|
80
|
+
<span className="text-xs text-muted-foreground">
|
|
81
|
+
{convertToLocale({
|
|
82
|
+
amount: product.price,
|
|
83
|
+
currency_code: product.currencyCode,
|
|
84
|
+
})}
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => onAdd?.(product.id, product.variantId)}
|
|
90
|
+
className="w-7 h-7 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-base hover:text-foreground transition-colors flex-shrink-0"
|
|
91
|
+
aria-label={`${labels.addToCart} ${product.title}`}
|
|
92
|
+
>
|
|
93
|
+
<svg
|
|
94
|
+
width="12"
|
|
95
|
+
height="12"
|
|
96
|
+
viewBox="0 0 12 12"
|
|
97
|
+
fill="none"
|
|
98
|
+
stroke="currentColor"
|
|
99
|
+
strokeWidth="1.5"
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
>
|
|
102
|
+
<path d="M6 2.5v7M2.5 6h7" />
|
|
103
|
+
</svg>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|