@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,62 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fresh-price fetch wrapper for ProductActions — ported from
|
|
3
|
-
* `@1click/ui/src/products/product-actions-wrapper.tsx` (v2.3.1). Data
|
|
4
|
-
* seam: the Medusa `listProducts({id})` + region-priced fetch becomes
|
|
5
|
-
* `api/products` `retrieveProduct(idOrHandle, pricingContext)` — the PDP
|
|
6
|
-
* shell renders with the (possibly cached) product while this wrapper
|
|
7
|
-
* re-fetches `calculated_price` with the live pricing context (and the
|
|
8
|
-
* customer's Bearer JWT via the client, for group-aware B2B prices).
|
|
9
|
-
*
|
|
10
|
-
* Async server component — render inside `<Suspense>` with a disabled
|
|
11
|
-
* `<ProductActions>` fallback (see ProductTemplate).
|
|
12
|
-
*/
|
|
13
|
-
import type { StorefrontClient } from "../api/http"
|
|
14
|
-
import type { PricingContextQuery } from "../api/types"
|
|
15
|
-
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
16
|
-
import { retrieveProduct } from "../api/products"
|
|
17
|
-
import { StoreApiError } from "../api/types"
|
|
18
|
-
import { ProductActions, type AddToCartInput } from "./product-actions"
|
|
19
|
-
|
|
20
|
-
type ProductActionsWrapperProps = {
|
|
21
|
-
client: StorefrontClient
|
|
22
|
-
/** Product id (`prod_…`) or handle. */
|
|
23
|
-
id: string
|
|
24
|
-
/** Pricing context so `calculated_price` is present for the price panel. */
|
|
25
|
-
pricingContext?: PricingContextQuery
|
|
26
|
-
/** The variant the address names; see the product page contract. */
|
|
27
|
-
initialVariantId?: string | null
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-price fetch wrapper for ProductActions — ported from
|
|
3
|
+
* `@1click/ui/src/products/product-actions-wrapper.tsx` (v2.3.1). Data
|
|
4
|
+
* seam: the Medusa `listProducts({id})` + region-priced fetch becomes
|
|
5
|
+
* `api/products` `retrieveProduct(idOrHandle, pricingContext)` — the PDP
|
|
6
|
+
* shell renders with the (possibly cached) product while this wrapper
|
|
7
|
+
* re-fetches `calculated_price` with the live pricing context (and the
|
|
8
|
+
* customer's Bearer JWT via the client, for group-aware B2B prices).
|
|
9
|
+
*
|
|
10
|
+
* Async server component — render inside `<Suspense>` with a disabled
|
|
11
|
+
* `<ProductActions>` fallback (see ProductTemplate).
|
|
12
|
+
*/
|
|
13
|
+
import type { StorefrontClient } from "../api/http"
|
|
14
|
+
import type { PricingContextQuery } from "../api/types"
|
|
15
|
+
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
16
|
+
import { retrieveProduct } from "../api/products"
|
|
17
|
+
import { StoreApiError } from "../api/types"
|
|
18
|
+
import { ProductActions, type AddToCartInput } from "./product-actions"
|
|
19
|
+
|
|
20
|
+
type ProductActionsWrapperProps = {
|
|
21
|
+
client: StorefrontClient
|
|
22
|
+
/** Product id (`prod_…`) or handle. */
|
|
23
|
+
id: string
|
|
24
|
+
/** Pricing context so `calculated_price` is present for the price panel. */
|
|
25
|
+
pricingContext?: PricingContextQuery
|
|
26
|
+
/** The variant the address names; see the product page contract. */
|
|
27
|
+
initialVariantId?: string | null
|
|
28
|
+
/** A server action; leave it out for the instant add through the mounted cart drawer. */
|
|
29
|
+
addToCart?: (input: AddToCartInput) => Promise<void>
|
|
30
|
+
onAddToCart?: (product: StoreProduct, variant: StoreProductVariant) => void
|
|
31
|
+
openCart?: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ProductActionsWrapper({
|
|
35
|
+
client,
|
|
36
|
+
id,
|
|
37
|
+
pricingContext,
|
|
38
|
+
initialVariantId,
|
|
39
|
+
addToCart,
|
|
40
|
+
onAddToCart,
|
|
41
|
+
openCart,
|
|
42
|
+
}: ProductActionsWrapperProps) {
|
|
43
|
+
let product: StoreProduct
|
|
44
|
+
try {
|
|
45
|
+
const res = await retrieveProduct(client, id, pricingContext)
|
|
46
|
+
product = res.product
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e instanceof StoreApiError && e.status === 404) return null
|
|
49
|
+
throw e
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<ProductActions
|
|
54
|
+
product={product}
|
|
55
|
+
initialVariantId={initialVariantId}
|
|
56
|
+
addToCart={addToCart}
|
|
57
|
+
onAddToCart={onAddToCart}
|
|
58
|
+
openCart={openCart}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { type ProductActionsWrapperProps }
|
|
@@ -1,150 +1,151 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The package's default buy panel: options, price, quantity, the button,
|
|
5
|
-
* and the sticky bar on phones. Every rule of the product page contract
|
|
6
|
-
* lives in `useProductActions`; this file is only the markup, and a store
|
|
7
|
-
* that wants its own look draws its own panel on the same hook and
|
|
8
|
-
* inherits the same behaviour (evoo's `features/product/buy-box.tsx` is
|
|
9
|
-
* the reference).
|
|
10
|
-
*
|
|
11
|
-
* Until 2026-09-15 this panel carried the behaviour itself, and carried it
|
|
12
|
-
* wrong: it wrote the variant into the address through the router as
|
|
13
|
-
* `v_id=<whole key>`, so every click was a server render and the address
|
|
14
|
-
* lagged; it chose nothing on a product with several variants until the
|
|
15
|
-
* shopper clicked; and it added one unit, always. The hook fixed all three
|
|
16
|
-
* for every consumer at once.
|
|
17
|
-
*/
|
|
18
|
-
import { useRef } from "react"
|
|
19
|
-
import { Minus, Plus } from "lucide-react"
|
|
20
|
-
|
|
21
|
-
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
22
|
-
import { useIntersection } from "../lib/hooks/use-intersection"
|
|
23
|
-
import { Button } from "../primitives/ui/button"
|
|
24
|
-
import { useProductLabels } from "./context"
|
|
25
|
-
import { OptionSelect } from "./option-select"
|
|
26
|
-
import { ProductPrice } from "./product-price"
|
|
27
|
-
import { MobileActions } from "./mobile-actions"
|
|
28
|
-
import { useProductActions, type AddToCartInput } from "./use-product-actions"
|
|
29
|
-
|
|
30
|
-
export type { AddToCartInput }
|
|
31
|
-
|
|
32
|
-
type ProductActionsProps = {
|
|
33
|
-
product: StoreProduct
|
|
34
|
-
/**
|
|
35
|
-
* The cart seam
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The package's default buy panel: options, price, quantity, the button,
|
|
5
|
+
* and the sticky bar on phones. Every rule of the product page contract
|
|
6
|
+
* lives in `useProductActions`; this file is only the markup, and a store
|
|
7
|
+
* that wants its own look draws its own panel on the same hook and
|
|
8
|
+
* inherits the same behaviour (evoo's `features/product/buy-box.tsx` is
|
|
9
|
+
* the reference).
|
|
10
|
+
*
|
|
11
|
+
* Until 2026-09-15 this panel carried the behaviour itself, and carried it
|
|
12
|
+
* wrong: it wrote the variant into the address through the router as
|
|
13
|
+
* `v_id=<whole key>`, so every click was a server render and the address
|
|
14
|
+
* lagged; it chose nothing on a product with several variants until the
|
|
15
|
+
* shopper clicked; and it added one unit, always. The hook fixed all three
|
|
16
|
+
* for every consumer at once.
|
|
17
|
+
*/
|
|
18
|
+
import { useRef } from "react"
|
|
19
|
+
import { Minus, Plus } from "lucide-react"
|
|
20
|
+
|
|
21
|
+
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
22
|
+
import { useIntersection } from "../lib/hooks/use-intersection"
|
|
23
|
+
import { Button } from "../primitives/ui/button"
|
|
24
|
+
import { useProductLabels } from "./context"
|
|
25
|
+
import { OptionSelect } from "./option-select"
|
|
26
|
+
import { ProductPrice } from "./product-price"
|
|
27
|
+
import { MobileActions } from "./mobile-actions"
|
|
28
|
+
import { useProductActions, type AddToCartInput } from "./use-product-actions"
|
|
29
|
+
|
|
30
|
+
export type { AddToCartInput }
|
|
31
|
+
|
|
32
|
+
type ProductActionsProps = {
|
|
33
|
+
product: StoreProduct
|
|
34
|
+
/**
|
|
35
|
+
* The host's own cart seam (a server action), called with the selected
|
|
36
|
+
* variant and quantity. Leave it out for the instant add through the
|
|
37
|
+
* mounted `CartDrawerProvider`: the drawer opens at the click with the
|
|
38
|
+
* product in it. May throw `StoreApiError`; `insufficient_inventory` is
|
|
39
|
+
* handled here (button flips to out of stock).
|
|
40
|
+
*/
|
|
41
|
+
addToCart?: (input: AddToCartInput) => Promise<void>
|
|
42
|
+
/**
|
|
43
|
+
* The variant the address names (`searchParams.variant`), passed from the
|
|
44
|
+
* page so the first paint is the right variant. See the product page
|
|
45
|
+
* contract in `use-product-actions.ts`.
|
|
46
|
+
*/
|
|
47
|
+
initialVariantId?: string | null
|
|
48
|
+
disabled?: boolean
|
|
49
|
+
/** Fired after a successful add (analytics: trackAddToCart trio). */
|
|
50
|
+
onAddToCart?: (product: StoreProduct, variant: StoreProductVariant) => void
|
|
51
|
+
/** Open the host's cart: at the click on the instant path, after the seam on the server-action path. */
|
|
52
|
+
openCart?: () => void
|
|
53
|
+
/** The quantity stepper; on by default. */
|
|
54
|
+
showQuantity?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ProductActions({
|
|
58
|
+
product,
|
|
59
|
+
addToCart,
|
|
60
|
+
initialVariantId,
|
|
61
|
+
disabled,
|
|
62
|
+
onAddToCart,
|
|
63
|
+
openCart,
|
|
64
|
+
showQuantity = true,
|
|
65
|
+
}: ProductActionsProps) {
|
|
66
|
+
const labels = useProductLabels()
|
|
67
|
+
const state = useProductActions({ product, addToCart, initialVariantId, onAddToCart, openCart })
|
|
68
|
+
const { variant, chosen, choose, quantity, setQuantity, inStock, isAdding, isValidVariant, add } = state
|
|
69
|
+
|
|
70
|
+
const actionsRef = useRef<HTMLDivElement>(null)
|
|
71
|
+
const inView = useIntersection(actionsRef, "0px")
|
|
72
|
+
const busy = !!disabled || isAdding
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex flex-col gap-y-2" ref={actionsRef}>
|
|
76
|
+
{(product.variants?.length ?? 0) > 1 && (
|
|
77
|
+
<div className="flex flex-col gap-y-4">
|
|
78
|
+
{(product.options || []).map((option) => (
|
|
79
|
+
<div key={option.id}>
|
|
80
|
+
<OptionSelect
|
|
81
|
+
option={option}
|
|
82
|
+
current={chosen[option.id]}
|
|
83
|
+
updateOption={choose}
|
|
84
|
+
title={option.title ?? ""}
|
|
85
|
+
data-testid="product-options"
|
|
86
|
+
disabled={busy}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
<div className="h-px bg-border" />
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<ProductPrice product={product} variant={variant} />
|
|
95
|
+
|
|
96
|
+
{showQuantity && (
|
|
97
|
+
<div className="flex flex-col gap-y-2">
|
|
98
|
+
<span className="text-sm text-foreground">{labels.quantity}</span>
|
|
99
|
+
<div className="inline-flex w-fit items-stretch rounded-lg border border-border bg-card">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => setQuantity(quantity - 1)}
|
|
103
|
+
aria-label={labels.decreaseQuantity}
|
|
104
|
+
disabled={quantity <= 1 || busy}
|
|
105
|
+
className="px-3 text-foreground transition-colors hover:bg-muted disabled:opacity-40"
|
|
106
|
+
data-testid="quantity-decrease"
|
|
107
|
+
>
|
|
108
|
+
<Minus className="size-4" aria-hidden />
|
|
109
|
+
</button>
|
|
110
|
+
<output className="min-w-10 py-2 text-center text-sm text-foreground" aria-live="polite" data-testid="quantity">
|
|
111
|
+
{quantity}
|
|
112
|
+
</output>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={() => setQuantity(quantity + 1)}
|
|
116
|
+
aria-label={labels.increaseQuantity}
|
|
117
|
+
disabled={busy}
|
|
118
|
+
className="px-3 text-foreground transition-colors hover:bg-muted disabled:opacity-40"
|
|
119
|
+
data-testid="quantity-increase"
|
|
120
|
+
>
|
|
121
|
+
<Plus className="size-4" aria-hidden />
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<Button
|
|
128
|
+
onClick={add}
|
|
129
|
+
disabled={!inStock || !variant || busy || !isValidVariant}
|
|
130
|
+
className="w-full h-10"
|
|
131
|
+
data-testid="add-product-button"
|
|
132
|
+
>
|
|
133
|
+
{!variant ? labels.selectVariant : !inStock ? labels.outOfStock : labels.addToCart}
|
|
134
|
+
</Button>
|
|
135
|
+
|
|
136
|
+
<MobileActions
|
|
137
|
+
product={product}
|
|
138
|
+
variant={variant}
|
|
139
|
+
options={chosen}
|
|
140
|
+
updateOptions={choose}
|
|
141
|
+
inStock={inStock}
|
|
142
|
+
handleAddToCart={add}
|
|
143
|
+
isAdding={isAdding}
|
|
144
|
+
show={!inView}
|
|
145
|
+
optionsDisabled={busy}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { type ProductActionsProps }
|
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PDP price display — ported from
|
|
5
|
-
* `@1click/ui/src/products/product-price.tsx` (v2.3.1). Data seam:
|
|
6
|
-
* `@medusajs/types` `HttpTypes.StoreProduct/StoreProductVariant` are
|
|
7
|
-
* replaced by the Cartbase SDK DTOs (`@cartbase/storefront/api/products`).
|
|
8
|
-
* Pricing stays SERVER truth — `lib/get-product-price` only selects and
|
|
9
|
-
* formats `variant.calculated_price` (Cartbase's flat `price_list_type`).
|
|
10
|
-
*/
|
|
11
|
-
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
12
|
-
import { cn } from "../lib/utils"
|
|
13
|
-
import { getProductPrice } from "../lib/get-product-price"
|
|
14
|
-
import { useProductLabels } from "./context"
|
|
15
|
-
|
|
16
|
-
export function ProductPrice({
|
|
17
|
-
product,
|
|
18
|
-
variant,
|
|
19
|
-
}: {
|
|
20
|
-
product: StoreProduct
|
|
21
|
-
variant?: StoreProductVariant
|
|
22
|
-
}) {
|
|
23
|
-
const labels = useProductLabels()
|
|
24
|
-
const { cheapestPrice, variantPrice } = getProductPrice({
|
|
25
|
-
product,
|
|
26
|
-
variantId: variant?.id,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const selectedPrice = variant ? variantPrice : cheapestPrice
|
|
30
|
-
|
|
31
|
-
if (!selectedPrice) {
|
|
32
|
-
return <div className="block w-32 h-9 bg-muted animate-pulse rounded" />
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div className="flex flex-col text-foreground">
|
|
37
|
-
<span
|
|
38
|
-
className={cn("text-xl font-semibold", {
|
|
39
|
-
"text-primary": selectedPrice.price_type === "sale",
|
|
40
|
-
})}
|
|
41
|
-
>
|
|
42
|
-
{!variant && `${labels.from} `}
|
|
43
|
-
<span
|
|
44
|
-
data-testid="product-price"
|
|
45
|
-
data-value={selectedPrice.calculated_price_number}
|
|
46
|
-
>
|
|
47
|
-
{selectedPrice.calculated_price}
|
|
48
|
-
</span>
|
|
49
|
-
</span>
|
|
50
|
-
{selectedPrice.price_type === "sale" && (
|
|
51
|
-
<>
|
|
52
|
-
<p>
|
|
53
|
-
<span className="text-muted-foreground">{labels.original} </span>
|
|
54
|
-
<span
|
|
55
|
-
className="line-through"
|
|
56
|
-
data-testid="original-product-price"
|
|
57
|
-
data-value={selectedPrice.original_price_number}
|
|
58
|
-
>
|
|
59
|
-
{selectedPrice.original_price}
|
|
60
|
-
</span>
|
|
61
|
-
</p>
|
|
62
|
-
<span className="text-primary">
|
|
63
|
-
-{selectedPrice.percentage_diff}%
|
|
64
|
-
</span>
|
|
65
|
-
</>
|
|
66
|
-
)}
|
|
67
|
-
</div>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PDP price display — ported from
|
|
5
|
+
* `@1click/ui/src/products/product-price.tsx` (v2.3.1). Data seam:
|
|
6
|
+
* `@medusajs/types` `HttpTypes.StoreProduct/StoreProductVariant` are
|
|
7
|
+
* replaced by the Cartbase SDK DTOs (`@cartbase/storefront/api/products`).
|
|
8
|
+
* Pricing stays SERVER truth — `lib/get-product-price` only selects and
|
|
9
|
+
* formats `variant.calculated_price` (Cartbase's flat `price_list_type`).
|
|
10
|
+
*/
|
|
11
|
+
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
12
|
+
import { cn } from "../lib/utils"
|
|
13
|
+
import { getProductPrice } from "../lib/get-product-price"
|
|
14
|
+
import { useProductLabels } from "./context"
|
|
15
|
+
|
|
16
|
+
export function ProductPrice({
|
|
17
|
+
product,
|
|
18
|
+
variant,
|
|
19
|
+
}: {
|
|
20
|
+
product: StoreProduct
|
|
21
|
+
variant?: StoreProductVariant
|
|
22
|
+
}) {
|
|
23
|
+
const labels = useProductLabels()
|
|
24
|
+
const { cheapestPrice, variantPrice } = getProductPrice({
|
|
25
|
+
product,
|
|
26
|
+
variantId: variant?.id,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const selectedPrice = variant ? variantPrice : cheapestPrice
|
|
30
|
+
|
|
31
|
+
if (!selectedPrice) {
|
|
32
|
+
return <div className="block w-32 h-9 bg-muted animate-pulse rounded" />
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col text-foreground">
|
|
37
|
+
<span
|
|
38
|
+
className={cn("text-xl font-semibold", {
|
|
39
|
+
"text-primary": selectedPrice.price_type === "sale",
|
|
40
|
+
})}
|
|
41
|
+
>
|
|
42
|
+
{!variant && `${labels.from} `}
|
|
43
|
+
<span
|
|
44
|
+
data-testid="product-price"
|
|
45
|
+
data-value={selectedPrice.calculated_price_number}
|
|
46
|
+
>
|
|
47
|
+
{selectedPrice.calculated_price}
|
|
48
|
+
</span>
|
|
49
|
+
</span>
|
|
50
|
+
{selectedPrice.price_type === "sale" && (
|
|
51
|
+
<>
|
|
52
|
+
<p>
|
|
53
|
+
<span className="text-muted-foreground">{labels.original} </span>
|
|
54
|
+
<span
|
|
55
|
+
className="line-through"
|
|
56
|
+
data-testid="original-product-price"
|
|
57
|
+
data-value={selectedPrice.original_price_number}
|
|
58
|
+
>
|
|
59
|
+
{selectedPrice.original_price}
|
|
60
|
+
</span>
|
|
61
|
+
</p>
|
|
62
|
+
<span className="text-primary">
|
|
63
|
+
-{selectedPrice.percentage_diff}%
|
|
64
|
+
</span>
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|