@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,212 @@
1
+ "use client"
2
+
3
+ import Image from "next/image"
4
+ import Link from "next/link"
5
+ import { useRef, useState } from "react"
6
+
7
+ import { DualPrice } from "../lib/dual-price"
8
+ import { cn } from "../lib/utils"
9
+ import { useCartDrawer } from "./context"
10
+
11
+ /**
12
+ * CartCrossSellCarousel — horizontal scrollable product strip with left/right
13
+ * arrow controls. Used as mobile alternative to the sidebar or as a second
14
+ * cross-sell zone below the line items on desktop.
15
+ *
16
+ * Ported from `@1click/ui/src/cart-drawer/cross-sell-carousel.tsx` (v2.3.1).
17
+ * Barter data seam: same as the sidebar — feed from
18
+ * `@cartbase/storefront/api/products` `listProducts` or `api/search`
19
+ * `listRelatedProducts` via `toCrossSellProduct`
20
+ * (`./cross-sell-sidebar`; the card shapes are identical), wire `onAdd`
21
+ * to the provider's `addItem(variantId)`. Prices EUR major units.
22
+ */
23
+
24
+ export type CrossSellCarouselProduct = {
25
+ id: string
26
+ title: string
27
+ handle: string
28
+ thumbnail: string | null
29
+ price: number
30
+ currencyCode: string
31
+ variantId?: string
32
+ }
33
+
34
+ type CartCrossSellCarouselProps = {
35
+ label?: string
36
+ products: CrossSellCarouselProduct[]
37
+ onAdd?: (productId: string, variantId?: string) => void
38
+ }
39
+
40
+ export function CartCrossSellCarousel({
41
+ label,
42
+ products,
43
+ onAdd,
44
+ }: CartCrossSellCarouselProps) {
45
+ const { labels, hrefs } = useCartDrawer()
46
+ const scrollRef = useRef<HTMLDivElement>(null)
47
+ const [canScrollLeft, setCanScrollLeft] = useState(false)
48
+ const [canScrollRight, setCanScrollRight] = useState(true)
49
+
50
+ const checkScroll = () => {
51
+ if (!scrollRef.current) return
52
+ const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
53
+ setCanScrollLeft(scrollLeft > 4)
54
+ setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 4)
55
+ }
56
+
57
+ const scroll = (dir: "left" | "right") => {
58
+ if (!scrollRef.current) return
59
+ scrollRef.current.scrollBy({
60
+ left: dir === "left" ? -260 : 260,
61
+ behavior: "smooth",
62
+ })
63
+ setTimeout(checkScroll, 350)
64
+ }
65
+
66
+ if (!products.length) return null
67
+
68
+ return (
69
+ <div className="border-t border-border bg-muted/50 py-4">
70
+ <div className="flex items-center justify-between px-5 sm:px-6 mb-3">
71
+ <h3 className="text-sm font-semibold text-foreground">
72
+ {label ?? labels.youllLoveThis}
73
+ </h3>
74
+ <div className="hidden sm:flex items-center gap-1">
75
+ <button
76
+ type="button"
77
+ onClick={() => scroll("left")}
78
+ disabled={!canScrollLeft}
79
+ className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
80
+ aria-label="Scroll left"
81
+ >
82
+ <svg
83
+ width="14"
84
+ height="14"
85
+ viewBox="0 0 14 14"
86
+ fill="none"
87
+ stroke="currentColor"
88
+ strokeWidth="1.5"
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ >
92
+ <path d="M8.5 3L4.5 7l4 4" />
93
+ </svg>
94
+ </button>
95
+ <button
96
+ type="button"
97
+ onClick={() => scroll("right")}
98
+ disabled={!canScrollRight}
99
+ className="w-8 h-8 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-all"
100
+ aria-label="Scroll right"
101
+ >
102
+ <svg
103
+ width="14"
104
+ height="14"
105
+ viewBox="0 0 14 14"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ strokeWidth="1.5"
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ >
112
+ <path d="M5.5 3l4 4-4 4" />
113
+ </svg>
114
+ </button>
115
+ </div>
116
+ </div>
117
+
118
+ <div
119
+ ref={scrollRef}
120
+ onScroll={checkScroll}
121
+ className="flex gap-2.5 overflow-x-auto no-scrollbar"
122
+ style={{
123
+ WebkitOverflowScrolling: "touch",
124
+ paddingLeft: "1.25rem",
125
+ paddingRight: "1.25rem",
126
+ }}
127
+ >
128
+ {products.map((product) => (
129
+ <div
130
+ key={product.id}
131
+ className={cn(
132
+ "flex-shrink-0 w-[260px] bg-card rounded-xl border border-border p-2.5",
133
+ "flex gap-3 items-center snap-start"
134
+ )}
135
+ style={{ boxShadow: "0 1px 3px rgba(0,0,0,0.04)" }}
136
+ >
137
+ <Link
138
+ href={`${hrefs.productPrefix}/${product.handle}`}
139
+ className="flex-shrink-0"
140
+ >
141
+ <div className="w-14 h-14 rounded-lg overflow-hidden bg-muted relative">
142
+ {product.thumbnail ? (
143
+ <Image
144
+ src={product.thumbnail}
145
+ alt={product.title}
146
+ fill
147
+ className="object-cover"
148
+ sizes="56px"
149
+ />
150
+ ) : (
151
+ <div className="w-full h-full flex items-center justify-center">
152
+ <svg
153
+ width="16"
154
+ height="16"
155
+ viewBox="0 0 24 24"
156
+ fill="none"
157
+ stroke="currentColor"
158
+ className="text-muted-foreground"
159
+ strokeWidth="1.2"
160
+ >
161
+ <rect x="3" y="3" width="18" height="18" rx="3" />
162
+ <circle cx="8.5" cy="8.5" r="1.5" />
163
+ <path
164
+ d="M21 15l-5-5L5 21"
165
+ strokeLinecap="round"
166
+ strokeLinejoin="round"
167
+ />
168
+ </svg>
169
+ </div>
170
+ )}
171
+ </div>
172
+ </Link>
173
+
174
+ <div className="flex-1 min-w-0">
175
+ <Link
176
+ href={`${hrefs.productPrefix}/${product.handle}`}
177
+ className="text-[12px] font-semibold text-foreground line-clamp-1 leading-snug hover:text-muted-foreground transition-colors block"
178
+ >
179
+ {product.title}
180
+ </Link>
181
+ <DualPrice
182
+ amount={product.price}
183
+ currencyCode={product.currencyCode}
184
+ className="text-[13px] font-bold text-foreground block mt-0.5"
185
+ bgnClassName="text-muted-foreground text-[10px] ml-1"
186
+ />
187
+ </div>
188
+
189
+ <button
190
+ type="button"
191
+ onClick={() => onAdd?.(product.id, product.variantId)}
192
+ className="w-8 h-8 flex-shrink-0 flex items-center justify-center rounded-full border border-border text-muted-foreground hover:border-text-base hover:text-foreground active:bg-muted transition-all"
193
+ aria-label={`${labels.addToCart} ${product.title}`}
194
+ >
195
+ <svg
196
+ width="14"
197
+ height="14"
198
+ viewBox="0 0 12 12"
199
+ fill="none"
200
+ stroke="currentColor"
201
+ strokeWidth="1.5"
202
+ strokeLinecap="round"
203
+ >
204
+ <path d="M6 2.5v7M2.5 6h7" />
205
+ </svg>
206
+ </button>
207
+ </div>
208
+ ))}
209
+ </div>
210
+ </div>
211
+ )
212
+ }
@@ -0,0 +1,158 @@
1
+ "use client"
2
+
3
+ import Image from "next/image"
4
+ import Link from "next/link"
5
+
6
+ import type { StoreProduct } from "../api/products"
7
+ import { convertToLocale } from "../lib/money"
8
+ import { useCartDrawer } from "./context"
9
+
10
+ /**
11
+ * CartCrossSellSidebar — vertical product-card list for desktop cart
12
+ * drawer's left sidebar.
13
+ *
14
+ * Ported from `@1click/ui/src/cart-drawer/cross-sell-sidebar.tsx` (v2.3.1).
15
+ * Barter data seam: source stores fed this from Medusa product fetches —
16
+ * feed it from `@cartbase/storefront/api/products` `listProducts` (curated
17
+ * collection/tag) or `@cartbase/storefront/api/search` `listRelatedProducts`
18
+ * (anchor-product complements), mapped through `toCrossSellProduct` below.
19
+ * Wire `onAdd` to the provider's `addItem(variantId)`. Prices are EUR
20
+ * major units and come server-computed via `calculated_price` — never
21
+ * recompute client-side.
22
+ */
23
+
24
+ export type CrossSellSidebarProduct = {
25
+ id: string
26
+ title: string
27
+ handle: string
28
+ thumbnail: string | null
29
+ price: number
30
+ currencyCode: string
31
+ variantId?: string
32
+ }
33
+
34
+ /**
35
+ * Map a store-API product (products/search/related responses all reuse the
36
+ * canonical `StoreProduct` shape) to a cross-sell card. Picks the first
37
+ * variant carrying a server-computed `calculated_price` for the current
38
+ * pricing context; returns null when no variant is priced (pass
39
+ * `currency_code`/`region_id` on the listing call, and skip null results).
40
+ * Pure — exported for unit tests and for building `crossSell.loader`s.
41
+ */
42
+ export function toCrossSellProduct(
43
+ product: StoreProduct
44
+ ): CrossSellSidebarProduct | null {
45
+ const variant = (product.variants ?? []).find((v) => v.calculated_price)
46
+ const price = variant?.calculated_price
47
+ if (!variant || !price) return null
48
+ return {
49
+ id: product.id,
50
+ title: product.title,
51
+ handle: product.handle,
52
+ thumbnail: product.thumbnail,
53
+ price: price.calculated_amount,
54
+ currencyCode: price.currency_code,
55
+ variantId: variant.id,
56
+ }
57
+ }
58
+
59
+ type CartCrossSellSidebarProps = {
60
+ label?: string
61
+ products: CrossSellSidebarProduct[]
62
+ onAdd?: (productId: string, variantId?: string) => void
63
+ }
64
+
65
+ export function CartCrossSellSidebar({
66
+ label,
67
+ products,
68
+ onAdd,
69
+ }: CartCrossSellSidebarProps) {
70
+ const { labels, hrefs } = useCartDrawer()
71
+
72
+ if (!products.length) return null
73
+
74
+ return (
75
+ <div className="h-full flex flex-col">
76
+ <div className="px-5 pt-6 pb-4">
77
+ <h3 className="text-xs font-bold text-foreground uppercase tracking-widest">
78
+ {label ?? labels.youMightAlsoLike}
79
+ </h3>
80
+ </div>
81
+ <div className="flex-1 overflow-y-auto no-scrollbar px-4 pb-6 space-y-3">
82
+ {products.map((product) => (
83
+ <div
84
+ key={product.id}
85
+ className="bg-card rounded-[2px] p-3.5 transition-shadow hover:shadow-md"
86
+ style={{
87
+ boxShadow: "0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08)",
88
+ }}
89
+ >
90
+ <Link href={`${hrefs.productPrefix}/${product.handle}`}>
91
+ <div className="w-full aspect-square rounded-xl overflow-hidden bg-muted relative mb-3">
92
+ {product.thumbnail ? (
93
+ <Image
94
+ src={product.thumbnail}
95
+ alt={product.title}
96
+ fill
97
+ className="object-cover"
98
+ sizes="200px"
99
+ />
100
+ ) : (
101
+ <div className="w-full h-full flex items-center justify-center">
102
+ <svg
103
+ width="32"
104
+ height="32"
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ className="text-muted-foreground"
109
+ strokeWidth="1.2"
110
+ >
111
+ <rect x="3" y="3" width="18" height="18" rx="3" />
112
+ <circle cx="8.5" cy="8.5" r="1.5" />
113
+ <path
114
+ d="M21 15l-5-5L5 21"
115
+ strokeLinecap="round"
116
+ strokeLinejoin="round"
117
+ />
118
+ </svg>
119
+ </div>
120
+ )}
121
+ </div>
122
+ </Link>
123
+ <Link
124
+ href={`${hrefs.productPrefix}/${product.handle}`}
125
+ className="text-[13px] font-semibold text-foreground leading-tight line-clamp-2 hover:text-muted-foreground transition-colors block"
126
+ >
127
+ {product.title}
128
+ </Link>
129
+ <p className="text-sm font-semibold text-foreground mt-1.5">
130
+ {convertToLocale({
131
+ amount: product.price,
132
+ currency_code: product.currencyCode,
133
+ })}
134
+ </p>
135
+ <button
136
+ type="button"
137
+ onClick={() => onAdd?.(product.id, product.variantId)}
138
+ className="mt-3 w-full h-10 text-xs font-semibold text-foreground bg-card border border-border rounded-xl hover:bg-foreground hover:text-card hover:border-text-base transition-all flex items-center justify-center gap-1.5 active:scale-[0.97]"
139
+ >
140
+ {labels.addToCart}
141
+ <svg
142
+ width="12"
143
+ height="12"
144
+ viewBox="0 0 12 12"
145
+ fill="none"
146
+ stroke="currentColor"
147
+ strokeWidth="1.5"
148
+ strokeLinecap="round"
149
+ >
150
+ <path d="M2.5 6h7M7 3.5L9.5 6 7 8.5" />
151
+ </svg>
152
+ </button>
153
+ </div>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,52 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+
5
+ import { useCartDrawer } from "./context"
6
+
7
+ /**
8
+ * CartEmpty — empty-state for the cart drawer.
9
+ *
10
+ * Ported from `@1click/ui/src/cart-drawer/empty.tsx` (v2.3.1);
11
+ * originally extracted from mindpages-storefront
12
+ * src/modules/cart-drawer/cart-empty.tsx.
13
+ */
14
+
15
+ export function CartEmpty() {
16
+ const { close, labels, hrefs } = useCartDrawer()
17
+
18
+ return (
19
+ <div className="flex-1 flex flex-col items-center justify-center px-8 py-16">
20
+ <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-5">
21
+ <svg
22
+ width="28"
23
+ height="28"
24
+ viewBox="0 0 28 28"
25
+ fill="none"
26
+ stroke="currentColor"
27
+ className="text-muted-foreground"
28
+ strokeWidth="1.5"
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ >
32
+ <path d="M6 8h16l-1.5 14H7.5L6 8z" />
33
+ <path d="M10 8V6a4 4 0 018 0v2" />
34
+ </svg>
35
+ </div>
36
+ <h3 className="text-lg font-semibold text-foreground mb-1.5">
37
+ {labels.emptyCartTitle}
38
+ </h3>
39
+ <p className="text-sm text-muted-foreground text-center mb-6 max-w-[240px]">
40
+ {labels.emptyCartMessage}
41
+ </p>
42
+ <Link href={hrefs.browse} onClick={close}>
43
+ <button
44
+ type="button"
45
+ className="px-6 py-2.5 bg-foreground text-card text-sm font-medium rounded-xl hover:bg-foreground/90 transition-colors active:scale-[0.97]"
46
+ >
47
+ {labels.browseProducts}
48
+ </button>
49
+ </Link>
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import Image from "next/image"
4
+
5
+ import { convertToLocale } from "../lib/money"
6
+ import { useCartDrawer } from "./context"
7
+
8
+ /**
9
+ * CartFreeGift — a "free gift" promo card shown inside the cart drawer
10
+ * when the cart total exceeds a qualifying threshold.
11
+ *
12
+ * Ported from `@1click/ui/src/cart-drawer/free-gift.tsx` (v2.3.1);
13
+ * originally extracted from mindpages-storefront
14
+ * src/modules/cart-drawer/cart-free-gift.tsx. `originalPrice` is EUR
15
+ * major units.
16
+ */
17
+
18
+ type CartFreeGiftProps = {
19
+ title: string
20
+ description?: string
21
+ originalPrice: number
22
+ currencyCode: string
23
+ thumbnail?: string | null
24
+ }
25
+
26
+ export function CartFreeGift({
27
+ title,
28
+ description,
29
+ originalPrice,
30
+ currencyCode,
31
+ thumbnail,
32
+ }: CartFreeGiftProps) {
33
+ const { labels } = useCartDrawer()
34
+
35
+ return (
36
+ <div className="mx-5 my-3 p-4 rounded-xl border border-success/20 bg-success/5">
37
+ <div className="flex items-start gap-3">
38
+ {thumbnail && (
39
+ <div className="w-12 h-12 rounded-lg overflow-hidden bg-card flex-shrink-0 relative">
40
+ <Image
41
+ src={thumbnail}
42
+ alt={title}
43
+ fill
44
+ className="object-cover"
45
+ sizes="48px"
46
+ />
47
+ </div>
48
+ )}
49
+ <div className="flex-1 min-w-0">
50
+ <div className="flex items-center gap-2">
51
+ <span className="text-sm font-semibold text-foreground">{title}</span>
52
+ <span className="text-xs text-muted-foreground line-through">
53
+ {convertToLocale({
54
+ amount: originalPrice,
55
+ currency_code: currencyCode,
56
+ })}
57
+ </span>
58
+ <span className="text-xs font-bold text-success bg-success/10 px-1.5 py-0.5 rounded">
59
+ {labels.free}
60
+ </span>
61
+ </div>
62
+ {description && (
63
+ <p className="text-xs text-muted-foreground mt-1 leading-relaxed">
64
+ {description}
65
+ </p>
66
+ )}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { DualPrice } from "../lib/dual-price"
6
+ import { cn } from "../lib/utils"
7
+ import { useCartDrawer } from "./context"
8
+
9
+ /**
10
+ * CartGiftWrap — inline toggle row for adding gift wrapping to the cart.
11
+ *
12
+ * Ported from `@1click/ui/src/cart-drawer/gift-wrap.tsx` (v2.3.1);
13
+ * originally extracted from mindpages-storefront
14
+ * src/modules/cart-drawer/cart-gift-wrap.tsx. `price` is EUR major units.
15
+ * The store owns the effect: wire `onToggle` to add/remove the store's
16
+ * gift-wrap variant via the provider's `addItem` / `removeItem`.
17
+ */
18
+
19
+ type CartGiftWrapProps = {
20
+ price: number
21
+ currencyCode: string
22
+ enabled?: boolean
23
+ onToggle?: (enabled: boolean) => void
24
+ }
25
+
26
+ export function CartGiftWrap({
27
+ price,
28
+ currencyCode,
29
+ enabled = false,
30
+ onToggle,
31
+ }: CartGiftWrapProps) {
32
+ const { labels } = useCartDrawer()
33
+ const [checked, setChecked] = useState(enabled)
34
+
35
+ const handleToggle = () => {
36
+ const next = !checked
37
+ setChecked(next)
38
+ onToggle?.(next)
39
+ }
40
+
41
+ return (
42
+ <div className="px-5 sm:px-6 py-2">
43
+ <button
44
+ type="button"
45
+ onClick={handleToggle}
46
+ className="flex items-center gap-3 w-full text-left py-2 min-h-[44px] group"
47
+ >
48
+ <div
49
+ className={cn(
50
+ "w-5 h-5 rounded border-[1.5px] flex items-center justify-center transition-all flex-shrink-0",
51
+ checked
52
+ ? "bg-foreground border-text-base"
53
+ : "border-border group-hover:border-text-muted group-active:border-text-muted"
54
+ )}
55
+ >
56
+ {checked && (
57
+ <svg
58
+ width="10"
59
+ height="10"
60
+ viewBox="0 0 10 10"
61
+ fill="none"
62
+ stroke="white"
63
+ strokeWidth="2"
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ >
67
+ <path d="M2 5l2 2 4-4" />
68
+ </svg>
69
+ )}
70
+ </div>
71
+ <span className="text-[13px] text-foreground flex-1">
72
+ {labels.addGiftWrap}
73
+ </span>
74
+ <DualPrice
75
+ amount={price}
76
+ currencyCode={currencyCode}
77
+ className="text-[13px] text-muted-foreground font-medium"
78
+ bgnClassName="text-muted-foreground text-[10px] ml-1"
79
+ />
80
+ </button>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,52 @@
1
+ "use client"
2
+
3
+ import { productItemCount } from "../lib/cart-helpers"
4
+ import { useCartDrawer } from "./context"
5
+
6
+ /**
7
+ * Cart drawer header — store title, item count badge, close button.
8
+ *
9
+ * Ported from `@1click/ui/src/cart-drawer/header.tsx` (v2.3.1). Barter
10
+ * adjustment: the badge counts PRODUCT lines via the shared
11
+ * `productItemCount` predicate (the source summed all lines) so a
12
+ * backend-injected COD-fee line can never inflate the item count —
13
+ * consistent with the provider's auto-open counter.
14
+ */
15
+
16
+ export function CartDrawerHeader() {
17
+ const { close, cart, labels } = useCartDrawer()
18
+ const totalItems = productItemCount(cart?.items)
19
+
20
+ return (
21
+ <div className="flex items-center justify-between px-5 sm:px-6 py-4 sm:py-5 flex-shrink-0 bg-card">
22
+ <div className="flex items-center gap-2.5">
23
+ <h2 className="text-base sm:text-[17px] font-bold text-foreground tracking-tight">
24
+ {labels.yourCart}
25
+ </h2>
26
+ {totalItems > 0 && (
27
+ <span className="text-[11px] font-semibold text-muted-foreground bg-muted rounded-full px-2.5 py-1 leading-none">
28
+ {totalItems} {totalItems === 1 ? labels.item : labels.items}
29
+ </span>
30
+ )}
31
+ </div>
32
+ <button
33
+ onClick={close}
34
+ className="w-11 h-11 -mr-1.5 flex items-center justify-center rounded-full hover:bg-muted active:bg-muted transition-colors text-muted-foreground hover:text-foreground"
35
+ aria-label={labels.closeCart}
36
+ >
37
+ <svg
38
+ width="18"
39
+ height="18"
40
+ viewBox="0 0 16 16"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ strokeWidth="2"
44
+ strokeLinecap="round"
45
+ aria-hidden="true"
46
+ >
47
+ <path d="M4 4l8 8M12 4l-8 8" />
48
+ </svg>
49
+ </button>
50
+ </div>
51
+ )
52
+ }