@cartbase/storefront 0.1.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/package.json +226 -0
- package/src/api/auth.ts +108 -0
- package/src/api/carts.ts +506 -0
- package/src/api/categories.ts +184 -0
- package/src/api/checkout.ts +440 -0
- package/src/api/collections.ts +130 -0
- package/src/api/consent.ts +75 -0
- package/src/api/content.ts +125 -0
- package/src/api/customers.ts +307 -0
- package/src/api/gift-cards.ts +112 -0
- package/src/api/http.ts +122 -0
- package/src/api/index.ts +29 -0
- package/src/api/integrations.ts +130 -0
- package/src/api/menus.ts +77 -0
- package/src/api/metaobjects.ts +136 -0
- package/src/api/orders.ts +290 -0
- package/src/api/products.ts +303 -0
- package/src/api/redirects.ts +37 -0
- package/src/api/regions.ts +141 -0
- package/src/api/reviews.ts +259 -0
- package/src/api/search.ts +133 -0
- package/src/api/types.ts +91 -0
- package/src/cart-drawer/cart-drawer.tsx +86 -0
- package/src/cart-drawer/context.tsx +569 -0
- package/src/cart-drawer/continue-shopping.tsx +28 -0
- package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
- package/src/cart-drawer/empty.tsx +52 -0
- package/src/cart-drawer/free-gift.tsx +71 -0
- package/src/cart-drawer/gift-wrap.tsx +83 -0
- package/src/cart-drawer/header.tsx +52 -0
- package/src/cart-drawer/index.ts +69 -0
- package/src/cart-drawer/item/index.tsx +164 -0
- package/src/cart-drawer/item/quantity.tsx +100 -0
- package/src/cart-drawer/item/upsell.tsx +110 -0
- package/src/cart-drawer/item/variant.tsx +46 -0
- package/src/cart-drawer/labels-bg.ts +72 -0
- package/src/cart-drawer/labels.ts +119 -0
- package/src/cart-drawer/notes.tsx +131 -0
- package/src/cart-drawer/payment-badges.tsx +96 -0
- package/src/cart-drawer/promo-banner.tsx +43 -0
- package/src/cart-drawer/rewards-points.tsx +78 -0
- package/src/cart-drawer/sticky-footer.tsx +73 -0
- package/src/cart-drawer/summary-breakdown.tsx +196 -0
- package/src/cart-drawer/template.tsx +225 -0
- package/src/cart-drawer/tiered-progress.tsx +168 -0
- package/src/checkout/address-error-copy.ts +119 -0
- package/src/checkout/address-form.tsx +224 -0
- package/src/checkout/address-select.tsx +79 -0
- package/src/checkout/boxnow-locker-selector.tsx +410 -0
- package/src/checkout/checkout-client.tsx +222 -0
- package/src/checkout/company-details.tsx +94 -0
- package/src/checkout/compare-addresses.ts +40 -0
- package/src/checkout/context.tsx +76 -0
- package/src/checkout/discount-section.tsx +218 -0
- package/src/checkout/econt-office-selector.tsx +332 -0
- package/src/checkout/error-message.tsx +25 -0
- package/src/checkout/geocode.ts +154 -0
- package/src/checkout/gift-card-section.tsx +224 -0
- package/src/checkout/index.ts +74 -0
- package/src/checkout/labels-bg.ts +128 -0
- package/src/checkout/labels.ts +263 -0
- package/src/checkout/line-item-card.tsx +152 -0
- package/src/checkout/order-summary.tsx +524 -0
- package/src/checkout/payment-button.tsx +373 -0
- package/src/checkout/payment-error-copy.ts +269 -0
- package/src/checkout/payment-method-list.tsx +365 -0
- package/src/checkout/payment-wrapper.tsx +102 -0
- package/src/checkout/promotion-error-copy.ts +124 -0
- package/src/checkout/shipping-method-list.tsx +335 -0
- package/src/checkout/stripe-wrapper.tsx +165 -0
- package/src/checkout/use-checkout-orchestration.ts +1504 -0
- package/src/common/cart-button-client.tsx +39 -0
- package/src/common/cart-button.tsx +28 -0
- package/src/common/country-select.tsx +65 -0
- package/src/common/delete-button.tsx +66 -0
- package/src/common/index.ts +17 -0
- package/src/common/language-select.tsx +78 -0
- package/src/common/localized-link.tsx +45 -0
- package/src/common/skeleton.tsx +29 -0
- package/src/index.ts +12 -0
- package/src/lib/cart-helpers.ts +113 -0
- package/src/lib/dual-price.tsx +73 -0
- package/src/lib/get-percentage-diff.ts +5 -0
- package/src/lib/get-product-price.ts +133 -0
- package/src/lib/hooks/use-intersection.ts +30 -0
- package/src/lib/hooks/use-toggle-state.ts +25 -0
- package/src/lib/money.ts +73 -0
- package/src/lib/payment-constants.ts +66 -0
- package/src/lib/product.ts +22 -0
- package/src/lib/sort-products.ts +63 -0
- package/src/lib/store-api-error.ts +36 -0
- package/src/lib/utils.ts +16 -0
- package/src/order/context.tsx +32 -0
- package/src/order/index.ts +63 -0
- package/src/order/labels-bg.ts +39 -0
- package/src/order/labels.ts +79 -0
- package/src/order/order-address-card.tsx +47 -0
- package/src/order/order-completed-template.tsx +165 -0
- package/src/order/order-confirmation-header.tsx +65 -0
- package/src/order/order-delivery-card.tsx +258 -0
- package/src/order/order-help-section.tsx +47 -0
- package/src/order/order-item.tsx +201 -0
- package/src/order/order-items-list.tsx +52 -0
- package/src/order/order-payment-card.tsx +95 -0
- package/src/order/order-timeline.tsx +141 -0
- package/src/order/order-totals.tsx +245 -0
- package/src/primitives/field.tsx +125 -0
- package/src/primitives/select-field.tsx +77 -0
- package/src/primitives/ui/accordion.tsx +61 -0
- package/src/primitives/ui/button.tsx +68 -0
- package/src/primitives/ui/collapsible.tsx +16 -0
- package/src/primitives/ui/dialog.tsx +112 -0
- package/src/primitives/ui/input.tsx +30 -0
- package/src/primitives/ui/label.tsx +31 -0
- package/src/primitives/ui/popover.tsx +38 -0
- package/src/primitives/ui/select.tsx +163 -0
- package/src/primitives/ui/sheet.tsx +131 -0
- package/src/primitives/ui/tabs.tsx +62 -0
- package/src/products/context.tsx +34 -0
- package/src/products/image-gallery.tsx +43 -0
- package/src/products/index.ts +44 -0
- package/src/products/labels-bg.ts +35 -0
- package/src/products/labels.ts +57 -0
- package/src/products/mobile-actions.tsx +180 -0
- package/src/products/option-select.tsx +67 -0
- package/src/products/preview-price.tsx +36 -0
- package/src/products/product-actions-wrapper.tsx +58 -0
- package/src/products/product-actions.tsx +217 -0
- package/src/products/product-info.tsx +43 -0
- package/src/products/product-preview.tsx +49 -0
- package/src/products/product-price.tsx +69 -0
- package/src/products/product-tabs.tsx +169 -0
- package/src/products/product-template.tsx +114 -0
- package/src/products/purchase-options.tsx +130 -0
- package/src/products/related-products.tsx +86 -0
- package/src/products/thumbnail.tsx +71 -0
- package/src/products/variant-matching.ts +71 -0
- package/src/reviews-ui/helpers.ts +174 -0
- package/src/reviews-ui/index.ts +74 -0
- package/src/reviews-ui/labels-bg.ts +91 -0
- package/src/reviews-ui/labels.ts +199 -0
- package/src/reviews-ui/photo-upload.tsx +345 -0
- package/src/reviews-ui/review-list.tsx +249 -0
- package/src/reviews-ui/review-widget.tsx +224 -0
- package/src/reviews-ui/review-wizard.tsx +560 -0
- package/src/reviews-ui/star-badge.tsx +104 -0
- package/src/reviews-ui/wizard-state.ts +81 -0
- package/src/store/category-template.tsx +129 -0
- package/src/store/collection-template.tsx +139 -0
- package/src/store/index.ts +41 -0
- package/src/store/labels-bg.ts +22 -0
- package/src/store/labels.ts +52 -0
- package/src/store/paginated-products.tsx +116 -0
- package/src/store/pagination.tsx +103 -0
- package/src/store/search-params.ts +256 -0
- package/src/store/search-template.tsx +249 -0
- package/src/store/skeleton-product-grid.tsx +26 -0
- package/src/store/sort-select.tsx +81 -0
- package/src/store/store-template.tsx +65 -0
- package/src/tracking/attribution.ts +418 -0
- package/src/tracking/consent-banner.tsx +355 -0
- package/src/tracking/consent-init.tsx +44 -0
- package/src/tracking/consent.ts +243 -0
- package/src/tracking/fbq.ts +168 -0
- package/src/tracking/ga4.tsx +49 -0
- package/src/tracking/get-tracking-attribution.ts +224 -0
- package/src/tracking/get-tracking-config.ts +50 -0
- package/src/tracking/gtag.ts +200 -0
- package/src/tracking/index.ts +133 -0
- package/src/tracking/meta-pixel.tsx +166 -0
- package/src/tracking/rybbit-events.ts +242 -0
- package/src/tracking/rybbit.tsx +40 -0
- package/src/tracking/types.ts +185 -0
- package/src/tracking/use-engagement-time.ts +58 -0
- package/tailwind-preset.cjs +72 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront — products family barrel. Prefer subpath imports
|
|
3
|
+
* (`@cartbase/storefront/products/product-actions`) in apps; the barrel exists
|
|
4
|
+
* for convenience and for the extra pure module (variant-matching).
|
|
5
|
+
*/
|
|
6
|
+
export { ProductLabelsProvider, useProductLabels } from "./context"
|
|
7
|
+
export { defaultProductLabels, type ProductLabels } from "./labels"
|
|
8
|
+
export { bulgarianProductLabels } from "./labels-bg"
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
optionsAsKeymap,
|
|
12
|
+
optionsMatch,
|
|
13
|
+
findMatchingVariant,
|
|
14
|
+
type OptionChoices,
|
|
15
|
+
type VariantOptionLike,
|
|
16
|
+
} from "./variant-matching"
|
|
17
|
+
|
|
18
|
+
export { Thumbnail, type ThumbnailProps } from "./thumbnail"
|
|
19
|
+
export { PreviewPrice } from "./preview-price"
|
|
20
|
+
export { ProductPrice } from "./product-price"
|
|
21
|
+
export { OptionSelect, type OptionSelectProps } from "./option-select"
|
|
22
|
+
// subscriptions-core step 4 — one-time vs plan purchase options.
|
|
23
|
+
export {
|
|
24
|
+
PurchaseOptions,
|
|
25
|
+
previewPlanPrice,
|
|
26
|
+
cadenceLabel,
|
|
27
|
+
type PurchaseOptionsLabels,
|
|
28
|
+
} from "./purchase-options"
|
|
29
|
+
export { ImageGallery, type ImageGalleryProps } from "./image-gallery"
|
|
30
|
+
export {
|
|
31
|
+
ProductActions,
|
|
32
|
+
type ProductActionsProps,
|
|
33
|
+
type AddToCartInput,
|
|
34
|
+
} from "./product-actions"
|
|
35
|
+
export { MobileActions, type MobileActionsProps } from "./mobile-actions"
|
|
36
|
+
export { ProductTabs } from "./product-tabs"
|
|
37
|
+
export { ProductInfo, type ProductInfoProps } from "./product-info"
|
|
38
|
+
export { ProductPreview, type ProductPreviewProps } from "./product-preview"
|
|
39
|
+
export { RelatedProducts, type RelatedProductsProps } from "./related-products"
|
|
40
|
+
export {
|
|
41
|
+
ProductActionsWrapper,
|
|
42
|
+
type ProductActionsWrapperProps,
|
|
43
|
+
} from "./product-actions-wrapper"
|
|
44
|
+
export { ProductTemplate, type ProductTemplateProps } from "./product-template"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulgarian product labels — ported from
|
|
3
|
+
* `@1click/ui/src/products/labels-bg.ts` (v2.3.1) unchanged. Typed against
|
|
4
|
+
* the full `ProductLabels` shape so a missing translation is a compile
|
|
5
|
+
* error, never silent English fallback.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProductLabels } from "./labels"
|
|
8
|
+
|
|
9
|
+
export const bulgarianProductLabels: ProductLabels = {
|
|
10
|
+
selectOption: "Изберете",
|
|
11
|
+
selectVariant: "Изберете вариант",
|
|
12
|
+
outOfStock: "Изчерпан",
|
|
13
|
+
addToCart: "Добави в кошницата",
|
|
14
|
+
from: "От",
|
|
15
|
+
original: "Оригинална цена:",
|
|
16
|
+
productInformation: "Информация за продукта",
|
|
17
|
+
shippingAndReturns: "Доставка и връщане",
|
|
18
|
+
material: "Материал",
|
|
19
|
+
countryOfOrigin: "Произход",
|
|
20
|
+
type: "Тип",
|
|
21
|
+
weight: "Тегло",
|
|
22
|
+
dimensions: "Размери",
|
|
23
|
+
fastDelivery: "Бърза доставка",
|
|
24
|
+
fastDeliveryDescription:
|
|
25
|
+
"Вашата пратка ще пристигне в рамките на 3-5 работни дни до офис на куриер или до вашия адрес.",
|
|
26
|
+
simpleExchanges: "Лесна замяна",
|
|
27
|
+
simpleExchangesDescription:
|
|
28
|
+
"Размерът не е точен? Без проблем — ще заменим продукта с нов.",
|
|
29
|
+
easyReturns: "Лесно връщане",
|
|
30
|
+
easyReturnsDescription:
|
|
31
|
+
"Просто върнете продукта и ще ви възстановим парите. Без въпроси — ще направим всичко възможно връщането да бъде безпроблемно.",
|
|
32
|
+
relatedProducts: "Подобни продукти",
|
|
33
|
+
relatedProductsDescription:
|
|
34
|
+
"Може да ви заинтересуват и тези продукти.",
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-family label map — ported from `@1click/ui/src/products/labels.ts`
|
|
3
|
+
* (v2.3.1) unchanged. i18n discipline: components read labels only through
|
|
4
|
+
* `useProductLabels()` (context) or an explicit `labels` prop merge — no
|
|
5
|
+
* hardcoded copy in components.
|
|
6
|
+
*/
|
|
7
|
+
export type ProductLabels = {
|
|
8
|
+
selectOption: string
|
|
9
|
+
selectVariant: string
|
|
10
|
+
outOfStock: string
|
|
11
|
+
addToCart: string
|
|
12
|
+
from: string
|
|
13
|
+
original: string
|
|
14
|
+
productInformation: string
|
|
15
|
+
shippingAndReturns: string
|
|
16
|
+
material: string
|
|
17
|
+
countryOfOrigin: string
|
|
18
|
+
type: string
|
|
19
|
+
weight: string
|
|
20
|
+
dimensions: string
|
|
21
|
+
fastDelivery: string
|
|
22
|
+
fastDeliveryDescription: string
|
|
23
|
+
simpleExchanges: string
|
|
24
|
+
simpleExchangesDescription: string
|
|
25
|
+
easyReturns: string
|
|
26
|
+
easyReturnsDescription: string
|
|
27
|
+
relatedProducts: string
|
|
28
|
+
relatedProductsDescription: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const defaultProductLabels: ProductLabels = {
|
|
32
|
+
selectOption: "Select",
|
|
33
|
+
selectVariant: "Select variant",
|
|
34
|
+
outOfStock: "Out of stock",
|
|
35
|
+
addToCart: "Add to cart",
|
|
36
|
+
from: "From",
|
|
37
|
+
original: "Original:",
|
|
38
|
+
productInformation: "Product Information",
|
|
39
|
+
shippingAndReturns: "Shipping & Returns",
|
|
40
|
+
material: "Material",
|
|
41
|
+
countryOfOrigin: "Country of origin",
|
|
42
|
+
type: "Type",
|
|
43
|
+
weight: "Weight",
|
|
44
|
+
dimensions: "Dimensions",
|
|
45
|
+
fastDelivery: "Fast delivery",
|
|
46
|
+
fastDeliveryDescription:
|
|
47
|
+
"Your package will arrive in 3-5 business days at your pick up location or in the comfort of your home.",
|
|
48
|
+
simpleExchanges: "Simple exchanges",
|
|
49
|
+
simpleExchangesDescription:
|
|
50
|
+
"Is the fit not quite right? No worries - we'll exchange your product for a new one.",
|
|
51
|
+
easyReturns: "Easy returns",
|
|
52
|
+
easyReturnsDescription:
|
|
53
|
+
"Just return your product and we'll refund your money. No questions asked – we'll do our best to make sure your return is hassle-free.",
|
|
54
|
+
relatedProducts: "Related products",
|
|
55
|
+
relatedProductsDescription:
|
|
56
|
+
"You might also want to check out these products.",
|
|
57
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mobile sticky add-to-cart bar + options bottom sheet — ported from
|
|
5
|
+
* `@1click/ui/src/products/mobile-actions.tsx` (v2.3.1). Data seam:
|
|
6
|
+
* `HttpTypes.*` → barter SDK DTOs; everything else (layout, z-index
|
|
7
|
+
* layering above the cart drawer, morphing show/hide transition) unchanged.
|
|
8
|
+
* Pure-props component — the add flow lives in `ProductActions`.
|
|
9
|
+
*/
|
|
10
|
+
import React, { useMemo } from "react"
|
|
11
|
+
import { ChevronDown, X } from "lucide-react"
|
|
12
|
+
|
|
13
|
+
import type { StoreProduct, StoreProductVariant } from "../api/products"
|
|
14
|
+
import { cn } from "../lib/utils"
|
|
15
|
+
import { getProductPrice } from "../lib/get-product-price"
|
|
16
|
+
import { isSimpleProduct } from "../lib/product"
|
|
17
|
+
import useToggleState from "../lib/hooks/use-toggle-state"
|
|
18
|
+
import { Button } from "../primitives/ui/button"
|
|
19
|
+
import { useProductLabels } from "./context"
|
|
20
|
+
import { OptionSelect } from "./option-select"
|
|
21
|
+
|
|
22
|
+
type MobileActionsProps = {
|
|
23
|
+
product: StoreProduct
|
|
24
|
+
variant?: StoreProductVariant
|
|
25
|
+
options: Record<string, string | undefined>
|
|
26
|
+
updateOptions: (optionId: string, value: string) => void
|
|
27
|
+
inStock?: boolean
|
|
28
|
+
handleAddToCart: () => void
|
|
29
|
+
isAdding?: boolean
|
|
30
|
+
show: boolean
|
|
31
|
+
optionsDisabled: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MobileActions: React.FC<MobileActionsProps> = ({
|
|
35
|
+
product,
|
|
36
|
+
variant,
|
|
37
|
+
options,
|
|
38
|
+
updateOptions,
|
|
39
|
+
inStock,
|
|
40
|
+
handleAddToCart,
|
|
41
|
+
isAdding,
|
|
42
|
+
show,
|
|
43
|
+
optionsDisabled,
|
|
44
|
+
}) => {
|
|
45
|
+
const labels = useProductLabels()
|
|
46
|
+
const { state, open, close } = useToggleState()
|
|
47
|
+
|
|
48
|
+
const price = getProductPrice({
|
|
49
|
+
product,
|
|
50
|
+
variantId: variant?.id,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const selectedPrice = useMemo(() => {
|
|
54
|
+
if (!price) return null
|
|
55
|
+
const { variantPrice, cheapestPrice } = price
|
|
56
|
+
return variantPrice || cheapestPrice || null
|
|
57
|
+
}, [price])
|
|
58
|
+
|
|
59
|
+
const isSimple = isSimpleProduct(product)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<div
|
|
64
|
+
className={cn("lg:hidden inset-x-0 bottom-0 fixed z-50", {
|
|
65
|
+
"pointer-events-none": !show,
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className={cn(
|
|
70
|
+
"bg-card flex flex-col gap-y-3 justify-center items-center p-4 h-full w-full border-t border-border transition-all duration-300",
|
|
71
|
+
show ? "opacity-100 translate-y-0" : "opacity-0 translate-y-full"
|
|
72
|
+
)}
|
|
73
|
+
data-testid="mobile-actions"
|
|
74
|
+
>
|
|
75
|
+
<div className="flex items-center gap-x-2">
|
|
76
|
+
<span className="text-foreground" data-testid="mobile-title">
|
|
77
|
+
{product.title}
|
|
78
|
+
</span>
|
|
79
|
+
<span className="text-muted-foreground">—</span>
|
|
80
|
+
{selectedPrice ? (
|
|
81
|
+
<div className="flex items-end gap-x-2 text-foreground">
|
|
82
|
+
{selectedPrice.price_type === "sale" && (
|
|
83
|
+
<span className="line-through text-sm text-muted-foreground">
|
|
84
|
+
{selectedPrice.original_price}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
<span
|
|
88
|
+
className={cn({
|
|
89
|
+
"text-primary": selectedPrice.price_type === "sale",
|
|
90
|
+
})}
|
|
91
|
+
>
|
|
92
|
+
{selectedPrice.calculated_price}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<div />
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
<div
|
|
100
|
+
className={cn("grid w-full gap-x-4", {
|
|
101
|
+
"grid-cols-2": !isSimple,
|
|
102
|
+
"grid-cols-1": isSimple,
|
|
103
|
+
})}
|
|
104
|
+
>
|
|
105
|
+
{!isSimple && (
|
|
106
|
+
<Button
|
|
107
|
+
onClick={open}
|
|
108
|
+
variant="outline"
|
|
109
|
+
className="w-full"
|
|
110
|
+
data-testid="mobile-actions-button"
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-center justify-between w-full">
|
|
113
|
+
<span>
|
|
114
|
+
{variant
|
|
115
|
+
? Object.values(options).join(" / ")
|
|
116
|
+
: labels.selectVariant}
|
|
117
|
+
</span>
|
|
118
|
+
<ChevronDown className="w-4 h-4" />
|
|
119
|
+
</div>
|
|
120
|
+
</Button>
|
|
121
|
+
)}
|
|
122
|
+
<Button
|
|
123
|
+
onClick={handleAddToCart}
|
|
124
|
+
disabled={!inStock || !variant}
|
|
125
|
+
className="w-full"
|
|
126
|
+
data-testid="mobile-cart-button"
|
|
127
|
+
>
|
|
128
|
+
{isAdding
|
|
129
|
+
? "..."
|
|
130
|
+
: !variant
|
|
131
|
+
? labels.selectVariant
|
|
132
|
+
: !inStock
|
|
133
|
+
? labels.outOfStock
|
|
134
|
+
: labels.addToCart}
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{state && (
|
|
141
|
+
<div className="fixed inset-0 z-[75]">
|
|
142
|
+
<div
|
|
143
|
+
className="fixed inset-0 bg-foreground/75 backdrop-blur-sm"
|
|
144
|
+
onClick={close}
|
|
145
|
+
/>
|
|
146
|
+
<div className="fixed bottom-0 inset-x-0">
|
|
147
|
+
<div className="w-full flex justify-end pr-6 mb-4">
|
|
148
|
+
<button
|
|
149
|
+
onClick={close}
|
|
150
|
+
className="bg-card w-12 h-12 rounded-full text-foreground flex justify-center items-center"
|
|
151
|
+
data-testid="close-modal-button"
|
|
152
|
+
>
|
|
153
|
+
<X className="w-5 h-5" />
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="bg-card px-6 py-12 rounded-t-2xl">
|
|
157
|
+
{(product.variants?.length ?? 0) > 1 && (
|
|
158
|
+
<div className="flex flex-col gap-y-6">
|
|
159
|
+
{(product.options || []).map((option) => (
|
|
160
|
+
<div key={option.id}>
|
|
161
|
+
<OptionSelect
|
|
162
|
+
option={option}
|
|
163
|
+
current={options[option.id]}
|
|
164
|
+
updateOption={updateOptions}
|
|
165
|
+
title={option.title ?? ""}
|
|
166
|
+
disabled={optionsDisabled}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { MobileActions, type MobileActionsProps }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Option selector — ported from
|
|
5
|
+
* `@1click/ui/src/products/option-select.tsx` (v2.3.1). Data seam:
|
|
6
|
+
* `HttpTypes.StoreProductOption` → barter `StoreProductOption`
|
|
7
|
+
* (`@cartbase/storefront/api/products`; `values` is present on
|
|
8
|
+
* `product.options[]`, which is what this component receives).
|
|
9
|
+
*/
|
|
10
|
+
import React from "react"
|
|
11
|
+
import type { StoreProductOption } from "../api/products"
|
|
12
|
+
import { cn } from "../lib/utils"
|
|
13
|
+
import { useProductLabels } from "./context"
|
|
14
|
+
|
|
15
|
+
type OptionSelectProps = {
|
|
16
|
+
option: StoreProductOption
|
|
17
|
+
current: string | undefined
|
|
18
|
+
updateOption: (optionId: string, value: string) => void
|
|
19
|
+
title: string
|
|
20
|
+
disabled: boolean
|
|
21
|
+
"data-testid"?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OptionSelect: React.FC<OptionSelectProps> = ({
|
|
25
|
+
option,
|
|
26
|
+
current,
|
|
27
|
+
updateOption,
|
|
28
|
+
title,
|
|
29
|
+
"data-testid": dataTestId,
|
|
30
|
+
disabled,
|
|
31
|
+
}) => {
|
|
32
|
+
const labels = useProductLabels()
|
|
33
|
+
const filteredOptions = (option.values ?? []).map((v) => v.value)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col gap-y-3">
|
|
37
|
+
<span className="text-sm text-foreground">
|
|
38
|
+
{labels.selectOption} {title}
|
|
39
|
+
</span>
|
|
40
|
+
<div
|
|
41
|
+
className="flex flex-wrap justify-between gap-2"
|
|
42
|
+
data-testid={dataTestId}
|
|
43
|
+
>
|
|
44
|
+
{filteredOptions.map((v) => (
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => updateOption(option.id, v!)}
|
|
47
|
+
key={v}
|
|
48
|
+
className={cn(
|
|
49
|
+
"border border-border bg-muted text-sm h-10 rounded-lg p-2 flex-1",
|
|
50
|
+
{
|
|
51
|
+
"border-primary": v === current,
|
|
52
|
+
"hover:shadow-sm transition-shadow ease-in-out duration-150":
|
|
53
|
+
v !== current,
|
|
54
|
+
}
|
|
55
|
+
)}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
data-testid="option-button"
|
|
58
|
+
>
|
|
59
|
+
{v}
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { OptionSelect, type OptionSelectProps }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview (card) price — ported from
|
|
3
|
+
* `@1click/ui/src/products/preview-price.tsx` (v2.3.1) unchanged. Renders a
|
|
4
|
+
* pre-computed `VariantPrice` (from `lib/get-product-price`, which reads the
|
|
5
|
+
* server-computed `variant.calculated_price` — barter's flat
|
|
6
|
+
* `price_list_type` wire shape); this component never computes money.
|
|
7
|
+
*/
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import type { VariantPrice } from "../lib/get-product-price"
|
|
10
|
+
|
|
11
|
+
export function PreviewPrice({ price }: { price: VariantPrice }) {
|
|
12
|
+
if (!price) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{price.price_type === "sale" && (
|
|
19
|
+
<span
|
|
20
|
+
className="line-through text-muted-foreground text-sm"
|
|
21
|
+
data-testid="original-price"
|
|
22
|
+
>
|
|
23
|
+
{price.original_price}
|
|
24
|
+
</span>
|
|
25
|
+
)}
|
|
26
|
+
<span
|
|
27
|
+
className={cn("text-muted-foreground text-sm", {
|
|
28
|
+
"text-primary": price.price_type === "sale",
|
|
29
|
+
})}
|
|
30
|
+
data-testid="price"
|
|
31
|
+
>
|
|
32
|
+
{price.calculated_price}
|
|
33
|
+
</span>
|
|
34
|
+
</>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
addToCart: (input: AddToCartInput) => Promise<void>
|
|
27
|
+
onAddToCart?: (product: StoreProduct, variant: StoreProductVariant) => void
|
|
28
|
+
openCart?: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ProductActionsWrapper({
|
|
32
|
+
client,
|
|
33
|
+
id,
|
|
34
|
+
pricingContext,
|
|
35
|
+
addToCart,
|
|
36
|
+
onAddToCart,
|
|
37
|
+
openCart,
|
|
38
|
+
}: ProductActionsWrapperProps) {
|
|
39
|
+
let product: StoreProduct
|
|
40
|
+
try {
|
|
41
|
+
const res = await retrieveProduct(client, id, pricingContext)
|
|
42
|
+
product = res.product
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e instanceof StoreApiError && e.status === 404) return null
|
|
45
|
+
throw e
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ProductActions
|
|
50
|
+
product={product}
|
|
51
|
+
addToCart={addToCart}
|
|
52
|
+
onAddToCart={onAddToCart}
|
|
53
|
+
openCart={openCart}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { type ProductActionsWrapperProps }
|