@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.
Files changed (176) hide show
  1. package/package.json +226 -0
  2. package/src/api/auth.ts +108 -0
  3. package/src/api/carts.ts +506 -0
  4. package/src/api/categories.ts +184 -0
  5. package/src/api/checkout.ts +440 -0
  6. package/src/api/collections.ts +130 -0
  7. package/src/api/consent.ts +75 -0
  8. package/src/api/content.ts +125 -0
  9. package/src/api/customers.ts +307 -0
  10. package/src/api/gift-cards.ts +112 -0
  11. package/src/api/http.ts +122 -0
  12. package/src/api/index.ts +29 -0
  13. package/src/api/integrations.ts +130 -0
  14. package/src/api/menus.ts +77 -0
  15. package/src/api/metaobjects.ts +136 -0
  16. package/src/api/orders.ts +290 -0
  17. package/src/api/products.ts +303 -0
  18. package/src/api/redirects.ts +37 -0
  19. package/src/api/regions.ts +141 -0
  20. package/src/api/reviews.ts +259 -0
  21. package/src/api/search.ts +133 -0
  22. package/src/api/types.ts +91 -0
  23. package/src/cart-drawer/cart-drawer.tsx +86 -0
  24. package/src/cart-drawer/context.tsx +569 -0
  25. package/src/cart-drawer/continue-shopping.tsx +28 -0
  26. package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
  27. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
  28. package/src/cart-drawer/empty.tsx +52 -0
  29. package/src/cart-drawer/free-gift.tsx +71 -0
  30. package/src/cart-drawer/gift-wrap.tsx +83 -0
  31. package/src/cart-drawer/header.tsx +52 -0
  32. package/src/cart-drawer/index.ts +69 -0
  33. package/src/cart-drawer/item/index.tsx +164 -0
  34. package/src/cart-drawer/item/quantity.tsx +100 -0
  35. package/src/cart-drawer/item/upsell.tsx +110 -0
  36. package/src/cart-drawer/item/variant.tsx +46 -0
  37. package/src/cart-drawer/labels-bg.ts +72 -0
  38. package/src/cart-drawer/labels.ts +119 -0
  39. package/src/cart-drawer/notes.tsx +131 -0
  40. package/src/cart-drawer/payment-badges.tsx +96 -0
  41. package/src/cart-drawer/promo-banner.tsx +43 -0
  42. package/src/cart-drawer/rewards-points.tsx +78 -0
  43. package/src/cart-drawer/sticky-footer.tsx +73 -0
  44. package/src/cart-drawer/summary-breakdown.tsx +196 -0
  45. package/src/cart-drawer/template.tsx +225 -0
  46. package/src/cart-drawer/tiered-progress.tsx +168 -0
  47. package/src/checkout/address-error-copy.ts +119 -0
  48. package/src/checkout/address-form.tsx +224 -0
  49. package/src/checkout/address-select.tsx +79 -0
  50. package/src/checkout/boxnow-locker-selector.tsx +410 -0
  51. package/src/checkout/checkout-client.tsx +222 -0
  52. package/src/checkout/company-details.tsx +94 -0
  53. package/src/checkout/compare-addresses.ts +40 -0
  54. package/src/checkout/context.tsx +76 -0
  55. package/src/checkout/discount-section.tsx +218 -0
  56. package/src/checkout/econt-office-selector.tsx +332 -0
  57. package/src/checkout/error-message.tsx +25 -0
  58. package/src/checkout/geocode.ts +154 -0
  59. package/src/checkout/gift-card-section.tsx +224 -0
  60. package/src/checkout/index.ts +74 -0
  61. package/src/checkout/labels-bg.ts +128 -0
  62. package/src/checkout/labels.ts +263 -0
  63. package/src/checkout/line-item-card.tsx +152 -0
  64. package/src/checkout/order-summary.tsx +524 -0
  65. package/src/checkout/payment-button.tsx +373 -0
  66. package/src/checkout/payment-error-copy.ts +269 -0
  67. package/src/checkout/payment-method-list.tsx +365 -0
  68. package/src/checkout/payment-wrapper.tsx +102 -0
  69. package/src/checkout/promotion-error-copy.ts +124 -0
  70. package/src/checkout/shipping-method-list.tsx +335 -0
  71. package/src/checkout/stripe-wrapper.tsx +165 -0
  72. package/src/checkout/use-checkout-orchestration.ts +1504 -0
  73. package/src/common/cart-button-client.tsx +39 -0
  74. package/src/common/cart-button.tsx +28 -0
  75. package/src/common/country-select.tsx +65 -0
  76. package/src/common/delete-button.tsx +66 -0
  77. package/src/common/index.ts +17 -0
  78. package/src/common/language-select.tsx +78 -0
  79. package/src/common/localized-link.tsx +45 -0
  80. package/src/common/skeleton.tsx +29 -0
  81. package/src/index.ts +12 -0
  82. package/src/lib/cart-helpers.ts +113 -0
  83. package/src/lib/dual-price.tsx +73 -0
  84. package/src/lib/get-percentage-diff.ts +5 -0
  85. package/src/lib/get-product-price.ts +133 -0
  86. package/src/lib/hooks/use-intersection.ts +30 -0
  87. package/src/lib/hooks/use-toggle-state.ts +25 -0
  88. package/src/lib/money.ts +73 -0
  89. package/src/lib/payment-constants.ts +66 -0
  90. package/src/lib/product.ts +22 -0
  91. package/src/lib/sort-products.ts +63 -0
  92. package/src/lib/store-api-error.ts +36 -0
  93. package/src/lib/utils.ts +16 -0
  94. package/src/order/context.tsx +32 -0
  95. package/src/order/index.ts +63 -0
  96. package/src/order/labels-bg.ts +39 -0
  97. package/src/order/labels.ts +79 -0
  98. package/src/order/order-address-card.tsx +47 -0
  99. package/src/order/order-completed-template.tsx +165 -0
  100. package/src/order/order-confirmation-header.tsx +65 -0
  101. package/src/order/order-delivery-card.tsx +258 -0
  102. package/src/order/order-help-section.tsx +47 -0
  103. package/src/order/order-item.tsx +201 -0
  104. package/src/order/order-items-list.tsx +52 -0
  105. package/src/order/order-payment-card.tsx +95 -0
  106. package/src/order/order-timeline.tsx +141 -0
  107. package/src/order/order-totals.tsx +245 -0
  108. package/src/primitives/field.tsx +125 -0
  109. package/src/primitives/select-field.tsx +77 -0
  110. package/src/primitives/ui/accordion.tsx +61 -0
  111. package/src/primitives/ui/button.tsx +68 -0
  112. package/src/primitives/ui/collapsible.tsx +16 -0
  113. package/src/primitives/ui/dialog.tsx +112 -0
  114. package/src/primitives/ui/input.tsx +30 -0
  115. package/src/primitives/ui/label.tsx +31 -0
  116. package/src/primitives/ui/popover.tsx +38 -0
  117. package/src/primitives/ui/select.tsx +163 -0
  118. package/src/primitives/ui/sheet.tsx +131 -0
  119. package/src/primitives/ui/tabs.tsx +62 -0
  120. package/src/products/context.tsx +34 -0
  121. package/src/products/image-gallery.tsx +43 -0
  122. package/src/products/index.ts +44 -0
  123. package/src/products/labels-bg.ts +35 -0
  124. package/src/products/labels.ts +57 -0
  125. package/src/products/mobile-actions.tsx +180 -0
  126. package/src/products/option-select.tsx +67 -0
  127. package/src/products/preview-price.tsx +36 -0
  128. package/src/products/product-actions-wrapper.tsx +58 -0
  129. package/src/products/product-actions.tsx +217 -0
  130. package/src/products/product-info.tsx +43 -0
  131. package/src/products/product-preview.tsx +49 -0
  132. package/src/products/product-price.tsx +69 -0
  133. package/src/products/product-tabs.tsx +169 -0
  134. package/src/products/product-template.tsx +114 -0
  135. package/src/products/purchase-options.tsx +130 -0
  136. package/src/products/related-products.tsx +86 -0
  137. package/src/products/thumbnail.tsx +71 -0
  138. package/src/products/variant-matching.ts +71 -0
  139. package/src/reviews-ui/helpers.ts +174 -0
  140. package/src/reviews-ui/index.ts +74 -0
  141. package/src/reviews-ui/labels-bg.ts +91 -0
  142. package/src/reviews-ui/labels.ts +199 -0
  143. package/src/reviews-ui/photo-upload.tsx +345 -0
  144. package/src/reviews-ui/review-list.tsx +249 -0
  145. package/src/reviews-ui/review-widget.tsx +224 -0
  146. package/src/reviews-ui/review-wizard.tsx +560 -0
  147. package/src/reviews-ui/star-badge.tsx +104 -0
  148. package/src/reviews-ui/wizard-state.ts +81 -0
  149. package/src/store/category-template.tsx +129 -0
  150. package/src/store/collection-template.tsx +139 -0
  151. package/src/store/index.ts +41 -0
  152. package/src/store/labels-bg.ts +22 -0
  153. package/src/store/labels.ts +52 -0
  154. package/src/store/paginated-products.tsx +116 -0
  155. package/src/store/pagination.tsx +103 -0
  156. package/src/store/search-params.ts +256 -0
  157. package/src/store/search-template.tsx +249 -0
  158. package/src/store/skeleton-product-grid.tsx +26 -0
  159. package/src/store/sort-select.tsx +81 -0
  160. package/src/store/store-template.tsx +65 -0
  161. package/src/tracking/attribution.ts +418 -0
  162. package/src/tracking/consent-banner.tsx +355 -0
  163. package/src/tracking/consent-init.tsx +44 -0
  164. package/src/tracking/consent.ts +243 -0
  165. package/src/tracking/fbq.ts +168 -0
  166. package/src/tracking/ga4.tsx +49 -0
  167. package/src/tracking/get-tracking-attribution.ts +224 -0
  168. package/src/tracking/get-tracking-config.ts +50 -0
  169. package/src/tracking/gtag.ts +200 -0
  170. package/src/tracking/index.ts +133 -0
  171. package/src/tracking/meta-pixel.tsx +166 -0
  172. package/src/tracking/rybbit-events.ts +242 -0
  173. package/src/tracking/rybbit.tsx +40 -0
  174. package/src/tracking/types.ts +185 -0
  175. package/src/tracking/use-engagement-time.ts +58 -0
  176. 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
+ }