@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,69 @@
|
|
|
1
|
+
// @cartbase/storefront/cart-drawer — barrel export
|
|
2
|
+
//
|
|
3
|
+
// Re-exports every cart drawer primitive + the optional default assembly.
|
|
4
|
+
// Stores can import from this subpath:
|
|
5
|
+
//
|
|
6
|
+
// import {
|
|
7
|
+
// CartDrawerProvider,
|
|
8
|
+
// CartDrawer,
|
|
9
|
+
// CartDrawerHeader,
|
|
10
|
+
// CartItem,
|
|
11
|
+
// CartStickyFooter,
|
|
12
|
+
// CartDrawerTemplate,
|
|
13
|
+
// } from "@cartbase/storefront/cart-drawer"
|
|
14
|
+
//
|
|
15
|
+
// Prefer the per-file subpaths in app code (tree-shaking + RSC boundary
|
|
16
|
+
// clarity) — every file here is also exported individually in package.json.
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
CartDrawerProvider,
|
|
20
|
+
useCartDrawer,
|
|
21
|
+
reduceCart,
|
|
22
|
+
makeOptimisticLine,
|
|
23
|
+
type OptimisticCartAction,
|
|
24
|
+
type OptimisticAddDisplay,
|
|
25
|
+
type OptimisticCartError,
|
|
26
|
+
} from "./context"
|
|
27
|
+
export {
|
|
28
|
+
defaultCartDrawerLabels,
|
|
29
|
+
type CartDrawerLabels,
|
|
30
|
+
} from "./labels"
|
|
31
|
+
export { bulgarianCartDrawerLabels } from "./labels-bg"
|
|
32
|
+
|
|
33
|
+
export { CartDrawer } from "./cart-drawer"
|
|
34
|
+
export { CartDrawerHeader } from "./header"
|
|
35
|
+
export { CartPromoBanner } from "./promo-banner"
|
|
36
|
+
export {
|
|
37
|
+
CartTieredProgress,
|
|
38
|
+
computeTierProgress,
|
|
39
|
+
type CartTier,
|
|
40
|
+
type TierProgress,
|
|
41
|
+
} from "./tiered-progress"
|
|
42
|
+
export { CartItem } from "./item"
|
|
43
|
+
export { CartItemQuantity } from "./item/quantity"
|
|
44
|
+
export { CartItemVariant } from "./item/variant"
|
|
45
|
+
export { CartItemUpsell, type UpsellProduct } from "./item/upsell"
|
|
46
|
+
export { CartFreeGift } from "./free-gift"
|
|
47
|
+
export { CartGiftWrap } from "./gift-wrap"
|
|
48
|
+
export { CartNotes } from "./notes"
|
|
49
|
+
export { CartRewardsPoints } from "./rewards-points"
|
|
50
|
+
export {
|
|
51
|
+
CartCrossSellSidebar,
|
|
52
|
+
toCrossSellProduct,
|
|
53
|
+
type CrossSellSidebarProduct,
|
|
54
|
+
} from "./cross-sell-sidebar"
|
|
55
|
+
export {
|
|
56
|
+
CartCrossSellCarousel,
|
|
57
|
+
type CrossSellCarouselProduct,
|
|
58
|
+
} from "./cross-sell-carousel"
|
|
59
|
+
export {
|
|
60
|
+
CartSummaryBreakdown,
|
|
61
|
+
selectSummaryRows,
|
|
62
|
+
type SummaryCartTotals,
|
|
63
|
+
type SummaryRow,
|
|
64
|
+
} from "./summary-breakdown"
|
|
65
|
+
export { CartStickyFooter } from "./sticky-footer"
|
|
66
|
+
export { CartPaymentBadges } from "./payment-badges"
|
|
67
|
+
export { CartContinueShopping } from "./continue-shopping"
|
|
68
|
+
export { CartEmpty } from "./empty"
|
|
69
|
+
export { CartDrawerTemplate, type CartDrawerConfig } from "./template"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { type ReactNode } from "react"
|
|
6
|
+
|
|
7
|
+
import type { CartLineItem } from "../../api/carts"
|
|
8
|
+
import { DualPrice } from "../../lib/dual-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 dual-currency + optional strikethrough), remove button.
|
|
17
|
+
*
|
|
18
|
+
* Ported from `@1click/ui/src/cart-drawer/item/index.tsx` (v2.3.1). Barter
|
|
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` — barter 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
|
+
<DualPrice
|
|
115
|
+
amount={originalTotal}
|
|
116
|
+
currencyCode={currencyCode}
|
|
117
|
+
className="text-xs text-muted-foreground"
|
|
118
|
+
bgnClassName="hidden"
|
|
119
|
+
/>
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
<DualPrice
|
|
123
|
+
amount={total}
|
|
124
|
+
currencyCode={currencyCode}
|
|
125
|
+
className={cn(
|
|
126
|
+
"text-sm font-bold",
|
|
127
|
+
hasDiscount ? "text-success" : "text-foreground"
|
|
128
|
+
)}
|
|
129
|
+
bgnClassName="text-muted-foreground text-[10px] ml-1"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="flex items-center justify-between mt-3">
|
|
135
|
+
<CartItemQuantity lineId={item.id} quantity={item.quantity} />
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={handleRemove}
|
|
139
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all"
|
|
140
|
+
aria-label={labels.remove}
|
|
141
|
+
>
|
|
142
|
+
<svg
|
|
143
|
+
width="16"
|
|
144
|
+
height="16"
|
|
145
|
+
viewBox="0 0 16 16"
|
|
146
|
+
fill="none"
|
|
147
|
+
stroke="currentColor"
|
|
148
|
+
strokeWidth="1.3"
|
|
149
|
+
strokeLinecap="round"
|
|
150
|
+
strokeLinejoin="round"
|
|
151
|
+
>
|
|
152
|
+
<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" />
|
|
153
|
+
<path d="M6.5 7v4M9.5 7v4" />
|
|
154
|
+
</svg>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{children}
|
|
161
|
+
<div className="mt-5 border-b border-border" />
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import { useCartDrawer } from "../context"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CartItemQuantity — +/- quantity stepper for a single cart line item.
|
|
10
|
+
*
|
|
11
|
+
* Optimistic: the provider's `updateQuantity` dispatches an
|
|
12
|
+
* "update_quantity" action against the useOptimistic state BEFORE awaiting
|
|
13
|
+
* the SDK call. The stepper number changes instantly; the server work
|
|
14
|
+
* happens in the background. On failure the optimistic state reverts
|
|
15
|
+
* automatically (confirmed snapshot unchanged).
|
|
16
|
+
*
|
|
17
|
+
* Ported from `@1click/ui/src/cart-drawer/item/quantity.tsx` (v2.3.1).
|
|
18
|
+
* Barter data seam: `updateQuantity` wraps
|
|
19
|
+
* `POST /api/store/carts/:id/line-items/:lineId` with body `{quantity}` —
|
|
20
|
+
* the barter contract accepts ONLY `quantity` (no metadata on update) and
|
|
21
|
+
* quantity 0 deletes the line (unreachable here: the stepper floor is 1;
|
|
22
|
+
* removal is the dedicated remove button).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type CartItemQuantityProps = {
|
|
26
|
+
lineId: string
|
|
27
|
+
quantity: number
|
|
28
|
+
maxQuantity?: number
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CartItemQuantity({
|
|
33
|
+
lineId,
|
|
34
|
+
quantity,
|
|
35
|
+
maxQuantity = 10,
|
|
36
|
+
className,
|
|
37
|
+
}: CartItemQuantityProps) {
|
|
38
|
+
const { updateQuantity } = useCartDrawer()
|
|
39
|
+
const [updating, setUpdating] = useState(false)
|
|
40
|
+
|
|
41
|
+
const change = (newQty: number) => {
|
|
42
|
+
if (newQty < 1 || newQty > maxQuantity || updating) return
|
|
43
|
+
setUpdating(true)
|
|
44
|
+
// Resolves true/false when the server settles; failures are funneled
|
|
45
|
+
// through the provider's onOptimisticError.
|
|
46
|
+
void updateQuantity(lineId, newQty).finally(() => setUpdating(false))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
"inline-flex items-center border border-border rounded-lg h-9",
|
|
53
|
+
updating && "opacity-50 pointer-events-none",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => change(quantity - 1)}
|
|
60
|
+
disabled={quantity <= 1}
|
|
61
|
+
className="w-9 h-full flex items-center justify-center text-muted-foreground hover:text-foreground disabled:text-muted-foreground disabled:cursor-not-allowed transition-colors"
|
|
62
|
+
aria-label="Decrease quantity"
|
|
63
|
+
>
|
|
64
|
+
<svg
|
|
65
|
+
width="12"
|
|
66
|
+
height="12"
|
|
67
|
+
viewBox="0 0 12 12"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
strokeWidth="1.5"
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
>
|
|
73
|
+
<path d="M2.5 6h7" />
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
76
|
+
<span className="w-8 text-center text-sm font-medium text-foreground tabular-nums select-none">
|
|
77
|
+
{quantity}
|
|
78
|
+
</span>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={() => change(quantity + 1)}
|
|
82
|
+
disabled={quantity >= maxQuantity}
|
|
83
|
+
className="w-9 h-full flex items-center justify-center text-muted-foreground hover:text-foreground disabled:text-muted-foreground disabled:cursor-not-allowed transition-colors"
|
|
84
|
+
aria-label="Increase quantity"
|
|
85
|
+
>
|
|
86
|
+
<svg
|
|
87
|
+
width="12"
|
|
88
|
+
height="12"
|
|
89
|
+
viewBox="0 0 12 12"
|
|
90
|
+
fill="none"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
strokeWidth="1.5"
|
|
93
|
+
strokeLinecap="round"
|
|
94
|
+
>
|
|
95
|
+
<path d="M6 2.5v7M2.5 6h7" />
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/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
|
+
* Barter 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 — barter 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 (barter 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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CartItemVariant — renders the variant description (e.g. "Size M / Red")
|
|
5
|
+
* under the product title in a cart line.
|
|
6
|
+
*
|
|
7
|
+
* Ported from `@1click/ui/src/cart-drawer/item/variant.tsx` (v2.3.1).
|
|
8
|
+
* Barter data seam: the source rendered `variant.options[].value` from an
|
|
9
|
+
* embedded Medusa variant object; barter cart lines carry the flat
|
|
10
|
+
* `variant_title` string (SDK `CartLineItem.variant_title`) instead, so
|
|
11
|
+
* that is the primary prop. Callers that fetched the full variant (PDP
|
|
12
|
+
* context) can still pass structural `options` and get the original
|
|
13
|
+
* separator rendering.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type CartItemVariantProps = {
|
|
17
|
+
/** Flat variant title from the cart line (`item.variant_title`). */
|
|
18
|
+
variantTitle?: string | null
|
|
19
|
+
/** Optional option values when the caller has the full variant. */
|
|
20
|
+
options?: Array<{ value: string }> | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CartItemVariant({ variantTitle, options }: CartItemVariantProps) {
|
|
24
|
+
if (options?.length) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
27
|
+
{options.map((opt, i) => (
|
|
28
|
+
<span key={i} className="text-xs text-muted-foreground">
|
|
29
|
+
{opt.value}
|
|
30
|
+
{i < options.length - 1 && (
|
|
31
|
+
<span className="ml-2 text-muted-foreground">/</span>
|
|
32
|
+
)}
|
|
33
|
+
</span>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!variantTitle) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
43
|
+
<span className="text-xs text-muted-foreground">{variantTitle}</span>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CartDrawerLabels } from "./labels"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bulgarian translations for the cart drawer.
|
|
5
|
+
*
|
|
6
|
+
* Ships alongside the English defaults so Bulgarian stores get the
|
|
7
|
+
* translated copy with a single import instead of hand-writing every
|
|
8
|
+
* string. Strings match the originals from mindpages-storefront, plus the
|
|
9
|
+
* barter summary-breakdown row additions.
|
|
10
|
+
*
|
|
11
|
+
* Usage in a store's layout:
|
|
12
|
+
*
|
|
13
|
+
* import {
|
|
14
|
+
* CartDrawerProvider,
|
|
15
|
+
* bulgarianCartDrawerLabels,
|
|
16
|
+
* } from "@cartbase/storefront/cart-drawer"
|
|
17
|
+
*
|
|
18
|
+
* <CartDrawerProvider cart={cart} labels={bulgarianCartDrawerLabels}>
|
|
19
|
+
* ...
|
|
20
|
+
* </CartDrawerProvider>
|
|
21
|
+
*
|
|
22
|
+
* For multi-language stores, pick the matching labels object at render
|
|
23
|
+
* time based on the active locale (the store owns its locale cookie; see
|
|
24
|
+
* docs/storefront/regions.md for the locale catalog).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const bulgarianCartDrawerLabels: CartDrawerLabels = {
|
|
28
|
+
yourCart: "Вашата кошница",
|
|
29
|
+
item: "артикул",
|
|
30
|
+
items: "артикула",
|
|
31
|
+
closeCart: "Затвори кошницата",
|
|
32
|
+
|
|
33
|
+
emptyCartTitle: "Кошницата е празна",
|
|
34
|
+
emptyCartMessage: "Все още нямате добавени продукти.",
|
|
35
|
+
browseProducts: "Разгледай продуктите",
|
|
36
|
+
|
|
37
|
+
awayFromFreeShipping: "до безплатна доставка",
|
|
38
|
+
allRewardsUnlocked: "Всички награди са отключени",
|
|
39
|
+
|
|
40
|
+
addGiftWrap: "Добави подаръчна опаковка",
|
|
41
|
+
giftNote: "Картичка",
|
|
42
|
+
orderNote: "Бележка",
|
|
43
|
+
giftNotePlaceholder: "Добави лично съобщение...",
|
|
44
|
+
orderNotePlaceholder: "Специални инструкции за поръчката...",
|
|
45
|
+
|
|
46
|
+
estimatedRewardPoints: "Очаквани точки за награда",
|
|
47
|
+
|
|
48
|
+
pairsWellWith: "Подходящи допълнения",
|
|
49
|
+
youllLoveThis: "Ще ви хареса",
|
|
50
|
+
youMightAlsoLike: "Може да ви хареса",
|
|
51
|
+
addToCart: "Добави",
|
|
52
|
+
|
|
53
|
+
subtotal: "Междинна сума",
|
|
54
|
+
taxAndShipping: "С включен ДДС. Доставката се изчислява при плащане.",
|
|
55
|
+
shipping: "Доставка",
|
|
56
|
+
free: "БЕЗПЛАТНА",
|
|
57
|
+
calculatedAtCheckout: "Изчислява се при плащане",
|
|
58
|
+
total: "Общо",
|
|
59
|
+
secureCheckout: "Към поръчка",
|
|
60
|
+
shippingAndTaxCalculatedAtCheckout:
|
|
61
|
+
"Доставката и данъкът се изчисляват при плащане",
|
|
62
|
+
|
|
63
|
+
discount: "Отстъпка",
|
|
64
|
+
tax: "ДДС",
|
|
65
|
+
codFee: "Такса наложен платеж",
|
|
66
|
+
giftCard: "Подаръчна карта",
|
|
67
|
+
remainingToPay: "Остава за плащане",
|
|
68
|
+
|
|
69
|
+
continueShopping: "Продължи пазаруването",
|
|
70
|
+
|
|
71
|
+
remove: "Премахни",
|
|
72
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart drawer labels — default English copy.
|
|
3
|
+
*
|
|
4
|
+
* Every user-facing string in the cart drawer comes from a `CartDrawerLabels`
|
|
5
|
+
* object. Stores override strings by passing a partial labels object to
|
|
6
|
+
* `CartDrawerProvider`. English is the baseline; stores wanting Bulgarian,
|
|
7
|
+
* German, etc. pass their own translations.
|
|
8
|
+
*
|
|
9
|
+
* Ported from `@1click/ui/src/cart-drawer/labels.ts` (v2.3.1). Barter
|
|
10
|
+
* additions: the summary-breakdown row labels (`discount`, `tax`, `codFee`,
|
|
11
|
+
* `giftCard`, `remainingToPay`) — the decorated cart carries
|
|
12
|
+
* discount/tax/COD-fee/gift-card tender fields that the breakdown renders,
|
|
13
|
+
* so the copy for those rows lives here too.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type CartDrawerLabels = {
|
|
17
|
+
// Header
|
|
18
|
+
yourCart: string
|
|
19
|
+
item: string
|
|
20
|
+
items: string
|
|
21
|
+
closeCart: string
|
|
22
|
+
|
|
23
|
+
// Empty state
|
|
24
|
+
emptyCartTitle: string
|
|
25
|
+
emptyCartMessage: string
|
|
26
|
+
browseProducts: string
|
|
27
|
+
|
|
28
|
+
// Tiered progress
|
|
29
|
+
awayFromFreeShipping: string
|
|
30
|
+
allRewardsUnlocked: string
|
|
31
|
+
|
|
32
|
+
// Gift wrap / notes
|
|
33
|
+
addGiftWrap: string
|
|
34
|
+
giftNote: string
|
|
35
|
+
orderNote: string
|
|
36
|
+
giftNotePlaceholder: string
|
|
37
|
+
orderNotePlaceholder: string
|
|
38
|
+
|
|
39
|
+
// Rewards
|
|
40
|
+
estimatedRewardPoints: string
|
|
41
|
+
|
|
42
|
+
// Cross-sell
|
|
43
|
+
pairsWellWith: string
|
|
44
|
+
youllLoveThis: string
|
|
45
|
+
youMightAlsoLike: string
|
|
46
|
+
addToCart: string
|
|
47
|
+
|
|
48
|
+
// Summary / footer
|
|
49
|
+
subtotal: string
|
|
50
|
+
taxAndShipping: string
|
|
51
|
+
shipping: string
|
|
52
|
+
free: string
|
|
53
|
+
calculatedAtCheckout: string
|
|
54
|
+
total: string
|
|
55
|
+
secureCheckout: string
|
|
56
|
+
shippingAndTaxCalculatedAtCheckout: string
|
|
57
|
+
|
|
58
|
+
// Summary breakdown rows (barter decorated-cart fields)
|
|
59
|
+
discount: string
|
|
60
|
+
tax: string
|
|
61
|
+
/** Fallback only — the server's `cod_fee_label` wins when present. */
|
|
62
|
+
codFee: string
|
|
63
|
+
/** Prefix for an applied gift-card row; rendered as `<giftCard> ••1234`. */
|
|
64
|
+
giftCard: string
|
|
65
|
+
/** Label for `gift_card_remainder` — what the remainder provider charges. */
|
|
66
|
+
remainingToPay: string
|
|
67
|
+
|
|
68
|
+
// Continue shopping
|
|
69
|
+
continueShopping: string
|
|
70
|
+
|
|
71
|
+
// A11y / buttons
|
|
72
|
+
remove: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const defaultCartDrawerLabels: CartDrawerLabels = {
|
|
76
|
+
yourCart: "Your cart",
|
|
77
|
+
item: "item",
|
|
78
|
+
items: "items",
|
|
79
|
+
closeCart: "Close cart",
|
|
80
|
+
|
|
81
|
+
emptyCartTitle: "Your cart is empty",
|
|
82
|
+
emptyCartMessage: "You haven't added any products yet.",
|
|
83
|
+
browseProducts: "Browse products",
|
|
84
|
+
|
|
85
|
+
awayFromFreeShipping: "away from free shipping",
|
|
86
|
+
allRewardsUnlocked: "All rewards unlocked",
|
|
87
|
+
|
|
88
|
+
addGiftWrap: "Add gift wrapping",
|
|
89
|
+
giftNote: "Gift note",
|
|
90
|
+
orderNote: "Order note",
|
|
91
|
+
giftNotePlaceholder: "Add a personal message...",
|
|
92
|
+
orderNotePlaceholder: "Special instructions for this order...",
|
|
93
|
+
|
|
94
|
+
estimatedRewardPoints: "Estimated reward points",
|
|
95
|
+
|
|
96
|
+
pairsWellWith: "Pairs well with",
|
|
97
|
+
youllLoveThis: "You'll love this",
|
|
98
|
+
youMightAlsoLike: "You might also like",
|
|
99
|
+
addToCart: "Add",
|
|
100
|
+
|
|
101
|
+
subtotal: "Subtotal",
|
|
102
|
+
taxAndShipping: "VAT included. Shipping calculated at checkout.",
|
|
103
|
+
shipping: "Shipping",
|
|
104
|
+
free: "FREE",
|
|
105
|
+
calculatedAtCheckout: "Calculated at checkout",
|
|
106
|
+
total: "Total",
|
|
107
|
+
secureCheckout: "Checkout",
|
|
108
|
+
shippingAndTaxCalculatedAtCheckout: "Shipping and tax calculated at checkout",
|
|
109
|
+
|
|
110
|
+
discount: "Discount",
|
|
111
|
+
tax: "VAT",
|
|
112
|
+
codFee: "Cash on delivery fee",
|
|
113
|
+
giftCard: "Gift card",
|
|
114
|
+
remainingToPay: "Remaining to pay",
|
|
115
|
+
|
|
116
|
+
continueShopping: "Continue shopping",
|
|
117
|
+
|
|
118
|
+
remove: "Remove",
|
|
119
|
+
}
|