@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,131 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { useCartDrawer } from "./context"
7
+
8
+ /**
9
+ * CartNotes — pill buttons to open gift/order note textareas.
10
+ *
11
+ * Ported from `@1click/ui/src/cart-drawer/notes.tsx` (v2.3.1);
12
+ * originally extracted from mindpages-storefront
13
+ * src/modules/cart-drawer/cart-notes.tsx. Tokens applied. Icons +
14
+ * placeholders pulled from `labels` context. Persist notes via `onSave`
15
+ * — e.g. `updateCart(client, cartId, { metadata: { ...cart.metadata,
16
+ * gift_note: value } })` (`@cartbase/storefront/api/carts`). NOTE: the update
17
+ * route REPLACES the metadata object wholesale (no server-side merge) —
18
+ * always spread the current `cart.metadata` or you clobber other keys
19
+ * (UTM attribution, gift-wrap flags…). Cart metadata is copied onto the
20
+ * order at complete.
21
+ */
22
+
23
+ type NoteType = "gift" | "order"
24
+
25
+ type CartNotesProps = {
26
+ types?: NoteType[]
27
+ onSave?: (type: NoteType, value: string) => void
28
+ }
29
+
30
+ const iconGift = (
31
+ <svg
32
+ width="14"
33
+ height="14"
34
+ viewBox="0 0 14 14"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="1.3"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ >
41
+ <rect x="1.5" y="5" width="11" height="7.5" rx="1" />
42
+ <rect x="0.5" y="3" width="13" height="2.5" rx="0.5" />
43
+ <path d="M7 3v9.5" />
44
+ </svg>
45
+ )
46
+
47
+ const iconOrder = (
48
+ <svg
49
+ width="14"
50
+ height="14"
51
+ viewBox="0 0 14 14"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ strokeWidth="1.3"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ >
58
+ <path d="M8.5 1.5H4a1 1 0 00-1 1v9a1 1 0 001 1h6a1 1 0 001-1V4L8.5 1.5z" />
59
+ <path d="M8.5 1.5V4H11M5.5 7h3M5.5 9.5h3" />
60
+ </svg>
61
+ )
62
+
63
+ export function CartNotes({
64
+ types = ["gift", "order"],
65
+ onSave,
66
+ }: CartNotesProps) {
67
+ const { labels } = useCartDrawer()
68
+ const [activeNote, setActiveNote] = useState<NoteType | null>(null)
69
+ const [values, setValues] = useState<Record<NoteType, string>>({
70
+ gift: "",
71
+ order: "",
72
+ })
73
+
74
+ const noteConfig: Record<
75
+ NoteType,
76
+ { label: string; placeholder: string; icon: React.ReactNode }
77
+ > = {
78
+ gift: {
79
+ label: labels.giftNote,
80
+ placeholder: labels.giftNotePlaceholder,
81
+ icon: iconGift,
82
+ },
83
+ order: {
84
+ label: labels.orderNote,
85
+ placeholder: labels.orderNotePlaceholder,
86
+ icon: iconOrder,
87
+ },
88
+ }
89
+
90
+ return (
91
+ <div className="px-5 sm:px-6 py-3">
92
+ <div className="flex gap-2">
93
+ {types.map((type) => {
94
+ const config = noteConfig[type]
95
+ const active = activeNote === type
96
+ return (
97
+ <button
98
+ key={type}
99
+ type="button"
100
+ onClick={() => setActiveNote(active ? null : type)}
101
+ className={cn(
102
+ "flex items-center gap-1.5 text-xs font-medium px-4 py-2.5 min-h-[44px] rounded-xl border transition-colors active:scale-[0.97]",
103
+ active
104
+ ? "border-text-base bg-foreground text-card"
105
+ : "border-border text-muted-foreground hover:border-text-muted active:bg-muted"
106
+ )}
107
+ >
108
+ {config.icon}
109
+ {config.label}
110
+ </button>
111
+ )
112
+ })}
113
+ </div>
114
+
115
+ {activeNote && (
116
+ <div className="mt-3">
117
+ <textarea
118
+ value={values[activeNote]}
119
+ onChange={(e) =>
120
+ setValues({ ...values, [activeNote]: e.target.value })
121
+ }
122
+ onBlur={() => onSave?.(activeNote, values[activeNote])}
123
+ placeholder={noteConfig[activeNote].placeholder}
124
+ rows={3}
125
+ className="w-full text-[14px] sm:text-sm border border-border rounded-xl px-4 py-3 resize-none focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary placeholder:text-muted-foreground text-foreground bg-card"
126
+ />
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }
@@ -0,0 +1,96 @@
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+
5
+ /**
6
+ * CartPaymentBadges — row of small payment-method logos shown near the
7
+ * checkout button.
8
+ *
9
+ * Ported from `@1click/ui/src/cart-drawer/payment-badges.tsx` (v2.3.1);
10
+ * originally extracted from mindpages-storefront
11
+ * src/modules/cart-drawer/cart-payment-badges.tsx.
12
+ * Inline SVGs are kept so stores don't need to provide asset files for the
13
+ * default set, but stores can pass their own badges map via the `badges`
14
+ * prop to override.
15
+ */
16
+
17
+ type BadgeKey = "visa" | "mastercard" | "applepay" | "googlepay" | "amex"
18
+
19
+ const defaultBadges: Record<BadgeKey, ReactNode> = {
20
+ visa: (
21
+ <svg viewBox="0 0 48 32" width="42" height="28" fill="none">
22
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#e4e4e7" strokeWidth="1" />
23
+ <path
24
+ d="M20.3 21h-2.7l1.7-10.5h2.7L20.3 21zm-4.7 0l-2.5-7.2-.3-1.5c-.5-1.3-1.7-1.8-1.7-1.8h-.1l-1 .2L7 21h2.8l.6-1.5h3.4l.3 1.5h2.5zm-3.2-3.6l1.4-3.8.8 3.8h-2.2zM32 10.5L29.3 21h-2.5l1.5-8.2-.1-.2-2 8.4h-2l-2-8.4v.3L20.7 21h-2.4l2.8-10.5h3.4l1.7 7.2 1.7-7.2H32zm8.8 0L38 21h-2.5l2.8-10.5h2.5z"
25
+ fill="#1a1f71"
26
+ />
27
+ </svg>
28
+ ),
29
+ mastercard: (
30
+ <svg viewBox="0 0 48 32" width="42" height="28" fill="none">
31
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#e4e4e7" strokeWidth="1" />
32
+ <circle cx="19" cy="16" r="8" fill="#eb001b" />
33
+ <circle cx="29" cy="16" r="8" fill="#f79e1b" />
34
+ <path d="M24 9.8a8 8 0 010 12.4 8 8 0 000-12.4z" fill="#ff5f00" />
35
+ </svg>
36
+ ),
37
+ applepay: (
38
+ <svg viewBox="0 0 48 32" width="42" height="28" fill="none">
39
+ <rect width="48" height="32" rx="4" fill="#000" />
40
+ <g fill="#fff">
41
+ <path d="M15.2 11.8c-.6.7-1.5 1.2-2.4 1.1-.1-1 .4-2 .9-2.6.6-.7 1.5-1.2 2.3-1.2.1 1-.3 2-.8 2.7zm.8 1.4c-1.3-.1-2.5.8-3.1.8s-1.6-.7-2.7-.7c-1.4 0-2.7.8-3.4 2.1-1.4 2.5-.4 6.2 1 8.2.7 1 1.5 2.1 2.6 2s1.4-.7 2.7-.7 1.6.7 2.7.7 1.8-1 2.5-2c.8-1.1 1.1-2.2 1.1-2.3-1.2-.5-2-1.8-2-3.3 0-1.3.7-2.5 1.8-3.1-.7-1-1.8-1.7-3.2-1.7z" />
42
+ <path d="M25 12.5v10.8h1.7v-3.7h2.3c2.1 0 3.6-1.5 3.6-3.6s-1.5-3.5-3.5-3.5H25zm1.7 1.4h1.9c1.5 0 2.3.8 2.3 2.1s-.8 2.1-2.3 2.1h-1.9v-4.2zm8.8 9.5c1.1 0 2.1-.5 2.5-1.4h0v1.3H39.6v-5.2c0-1.8-1.4-2.9-3.5-2.9-2 0-3.4 1.1-3.4 2.8h1.6c.1-.8.9-1.4 1.8-1.4 1.2 0 1.8.5 1.8 1.5v.7l-2.4.1c-2.2.1-3.4 1-3.4 2.6 0 1.6 1.2 2.7 2.9 2.7v-.8zm.4-1.3c-1 0-1.7-.5-1.7-1.3 0-.8.6-1.3 1.8-1.4l2.1-.1v.7c0 1.2-1 2.1-2.2 2.1z" />
43
+ </g>
44
+ </svg>
45
+ ),
46
+ googlepay: (
47
+ <svg viewBox="0 0 48 32" width="42" height="28" fill="none">
48
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#e4e4e7" strokeWidth="1" />
49
+ <path
50
+ d="M23.5 16.7v3.1h-1v-7.6h2.7c.7 0 1.2.2 1.7.6.5.4.7.9.7 1.5s-.2 1.1-.7 1.5c-.5.4-1 .6-1.7.6h-1.7zm0-3.5v2.5h1.7c.4 0 .8-.2 1.1-.4.3-.3.4-.6.4-1s-.1-.7-.4-1c-.3-.3-.6-.4-1.1-.4h-1.7z"
51
+ fill="#3c4043"
52
+ />
53
+ <path
54
+ d="M30.6 14.2c.7 0 1.3.2 1.8.6.4.4.7 1 .7 1.7v3.3h-1v-.7c-.4.6-.9.9-1.6.9-.6 0-1.1-.2-1.5-.5-.4-.3-.6-.8-.6-1.3 0-.5.2-1 .5-1.3.4-.3.9-.5 1.5-.5.6 0 1.1.2 1.4.5v-.3c0-.4-.2-.7-.4-.9-.3-.2-.6-.4-1-.4-.6 0-1 .2-1.3.7l-.8-.5c.5-.7 1.1-1 2-1h.3zm-1 4.6c.3.2.6.3 1 .3s.7-.1 1-.3c.3-.2.4-.5.4-.8s-.1-.6-.4-.8c-.3-.2-.6-.3-1-.3s-.7.1-1 .3c-.3.2-.4.5-.4.8s.2.6.4.8z"
55
+ fill="#3c4043"
56
+ />
57
+ <path d="M37 14.4l-2.4 5.5h-1l.9-1.9-1.6-3.6h1.1l1 2.5 1-2.5H37z" fill="#3c4043" />
58
+ <path d="M18.3 16c0-.3 0-.6-.1-.9h-4.6v1.7h2.7c-.1.6-.4 1.1-.9 1.4v1.2h1.5c.9-.8 1.4-2 1.4-3.4z" fill="#4285f4" />
59
+ <path d="M13.6 19.7c1.2 0 2.2-.4 3-1.1l-1.5-1.2c-.4.3-.9.4-1.5.4-1.2 0-2.1-.8-2.5-1.8H9.7v1.2c.7 1.5 2.2 2.5 3.9 2.5z" fill="#34a853" />
60
+ <path d="M11.1 16c0-.3.1-.7.2-1v-1.2H9.7c-.4.7-.6 1.4-.6 2.2s.2 1.5.6 2.2l1.6-1.2c-.1-.3-.2-.7-.2-1z" fill="#fbbc04" />
61
+ <path d="M13.6 12.2c.7 0 1.3.2 1.8.7l1.3-1.3c-.8-.8-1.9-1.2-3.1-1.2-1.7 0-3.2 1-3.9 2.5l1.6 1.2c.4-1.1 1.3-1.9 2.3-1.9z" fill="#ea4335" />
62
+ </svg>
63
+ ),
64
+ amex: (
65
+ <svg viewBox="0 0 48 32" width="42" height="28" fill="none">
66
+ <rect width="48" height="32" rx="4" fill="#006fcf" />
67
+ <text x="24" y="20" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle" fontFamily="Arial, sans-serif">
68
+ AMEX
69
+ </text>
70
+ </svg>
71
+ ),
72
+ }
73
+
74
+ type CartPaymentBadgesProps = {
75
+ methods?: BadgeKey[]
76
+ badges?: Partial<Record<BadgeKey, ReactNode>>
77
+ }
78
+
79
+ export function CartPaymentBadges({
80
+ methods = ["visa", "mastercard", "googlepay", "applepay"],
81
+ badges,
82
+ }: CartPaymentBadgesProps) {
83
+ const resolved = { ...defaultBadges, ...badges }
84
+ return (
85
+ <div className="flex items-center justify-center gap-2 px-5 py-3">
86
+ {methods.map((method) => (
87
+ <div
88
+ key={method}
89
+ className="opacity-70 hover:opacity-100 transition-opacity"
90
+ >
91
+ {resolved[method]}
92
+ </div>
93
+ ))}
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,43 @@
1
+ "use client"
2
+
3
+ import { cn } from "../lib/utils"
4
+
5
+ /**
6
+ * Cart drawer promo banner — tiny top bar with a message and a variant.
7
+ *
8
+ * Ported from `@1click/ui/src/cart-drawer/promo-banner.tsx` (v2.3.1);
9
+ * originally extracted from mindpages-storefront
10
+ * src/modules/cart-drawer/cart-promo-banner.tsx.
11
+ */
12
+
13
+ type CartPromoBannerProps = {
14
+ message: string
15
+ variant?: "info" | "success" | "warning"
16
+ className?: string
17
+ }
18
+
19
+ const variantStyles = {
20
+ info: "bg-foreground text-card",
21
+ success: "bg-success/10 text-success",
22
+ warning: "bg-warning/10 text-warning",
23
+ }
24
+
25
+ export function CartPromoBanner({
26
+ message,
27
+ variant = "warning",
28
+ className,
29
+ }: CartPromoBannerProps) {
30
+ if (!message) return null
31
+
32
+ return (
33
+ <div
34
+ className={cn(
35
+ "px-6 py-2.5 text-[13px] font-medium flex-shrink-0 text-center",
36
+ variantStyles[variant],
37
+ className
38
+ )}
39
+ >
40
+ {message}
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,78 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { useCartDrawer } from "./context"
7
+
8
+ /**
9
+ * CartRewardsPoints — expandable row that previews how many reward points
10
+ * the current cart earns.
11
+ *
12
+ * Ported from `@1click/ui/src/cart-drawer/rewards-points.tsx` (v2.3.1);
13
+ * originally extracted from mindpages-storefront
14
+ * src/modules/cart-drawer/cart-rewards-points.tsx.
15
+ */
16
+
17
+ type CartRewardsPointsProps = {
18
+ points: number
19
+ label?: string
20
+ details?: string
21
+ }
22
+
23
+ export function CartRewardsPoints({
24
+ points,
25
+ label,
26
+ details,
27
+ }: CartRewardsPointsProps) {
28
+ const { labels } = useCartDrawer()
29
+ const [expanded, setExpanded] = useState(false)
30
+
31
+ if (points <= 0) return null
32
+
33
+ return (
34
+ <div className="mx-5 sm:mx-6 my-3">
35
+ <button
36
+ type="button"
37
+ onClick={() => setExpanded(!expanded)}
38
+ className="w-full flex items-center gap-3 px-4 py-3 min-h-[44px] rounded-xl bg-success/10 text-success hover:bg-success/20 active:bg-success/20 transition-colors"
39
+ >
40
+ <svg
41
+ width="18"
42
+ height="18"
43
+ viewBox="0 0 18 18"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ strokeWidth="1.3"
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ className="flex-shrink-0"
50
+ >
51
+ <path d="M9 1l2.47 5.01 5.53.8-4 3.9.94 5.49L9 13.76l-4.94 2.44.94-5.49-4-3.9 5.53-.8L9 1z" />
52
+ </svg>
53
+ <span className="text-[13px] sm:text-sm font-medium flex-1 text-left">
54
+ {label || labels.estimatedRewardPoints}:{" "}
55
+ <span className="font-bold">{points.toLocaleString()}</span>
56
+ </span>
57
+ <svg
58
+ width="14"
59
+ height="14"
60
+ viewBox="0 0 14 14"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ strokeWidth="1.5"
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ className={cn("transition-transform", expanded && "rotate-180")}
67
+ >
68
+ <path d="M3.5 5.5l3.5 3 3.5-3" />
69
+ </svg>
70
+ </button>
71
+ {expanded && details && (
72
+ <div className="px-4 py-2.5 text-xs text-success leading-relaxed">
73
+ {details}
74
+ </div>
75
+ )}
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,73 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+
5
+ import { DualPrice } from "../lib/dual-price"
6
+ import { productTotal } from "../lib/cart-helpers"
7
+ import { useCartDrawer } from "./context"
8
+
9
+ /**
10
+ * CartStickyFooter — bottom-of-drawer subtotal + checkout CTA with
11
+ * iPhone home-indicator safe-area padding.
12
+ *
13
+ * Ported from `@1click/ui/src/cart-drawer/sticky-footer.tsx` (v2.3.1);
14
+ * originally extracted from mindpages-storefront
15
+ * src/modules/cart-drawer/cart-sticky-footer.tsx. Amounts EUR major units.
16
+ */
17
+
18
+ export function CartStickyFooter() {
19
+ const { cart, close, labels, hrefs } = useCartDrawer()
20
+ if (!cart || !cart.items?.length) return null
21
+
22
+ // Drawer is pre-checkout context — never read `cart.total` here.
23
+ // `cart.total` includes shipping/tax/COD fee, all of which are
24
+ // checkout-context state that must not leak into the shopping
25
+ // drawer. Sum of product line totals only.
26
+ const total = productTotal(cart.items)
27
+ const currencyCode = cart.currency_code
28
+
29
+ return (
30
+ <div
31
+ className="flex-shrink-0 bg-card px-5 sm:px-6 pt-4 sm:pt-5 relative z-10"
32
+ style={{
33
+ boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
34
+ paddingBottom: "max(1.25rem, env(safe-area-inset-bottom, 1.25rem))",
35
+ }}
36
+ >
37
+ <div className="flex items-baseline justify-between mb-1">
38
+ <span className="text-[15px] font-bold text-foreground">
39
+ {labels.subtotal}
40
+ </span>
41
+ <DualPrice
42
+ amount={total}
43
+ currencyCode={currencyCode}
44
+ className="text-xl font-bold text-foreground tracking-tight"
45
+ />
46
+ </div>
47
+ <p className="text-[11px] text-muted-foreground mb-3.5 sm:mb-4">
48
+ {labels.taxAndShipping}
49
+ </p>
50
+ <Link href={hrefs.checkout} onClick={close}>
51
+ <button
52
+ type="button"
53
+ className="w-full h-[52px] bg-foreground hover:bg-foreground/90 active:bg-foreground/95 text-card text-[15px] sm:text-sm font-semibold rounded-[2px] flex items-center justify-center gap-2.5 transition-colors active:scale-[0.98]"
54
+ >
55
+ <svg
56
+ width="16"
57
+ height="16"
58
+ viewBox="0 0 16 16"
59
+ fill="none"
60
+ stroke="currentColor"
61
+ strokeWidth="1.5"
62
+ strokeLinecap="round"
63
+ strokeLinejoin="round"
64
+ >
65
+ <rect x="3" y="6" width="10" height="8" rx="1.5" />
66
+ <path d="M5.5 6V4.5a2.5 2.5 0 015 0V6" />
67
+ </svg>
68
+ {labels.secureCheckout}
69
+ </button>
70
+ </Link>
71
+ </div>
72
+ )
73
+ }
@@ -0,0 +1,196 @@
1
+ "use client"
2
+
3
+ import type { AppliedGiftCard } from "../api/carts"
4
+ import { convertToLocale } from "../lib/money"
5
+ import { useCartDrawer } from "./context"
6
+
7
+ /**
8
+ * CartSummaryBreakdown — full totals breakdown for the drawer (or a cart
9
+ * page): subtotal, discount, shipping, tax, COD fee, total, applied
10
+ * gift-card tender and the remainder. Alternative to the compact
11
+ * `CartStickyFooter` (which deliberately shows a product-only subtotal in
12
+ * pre-checkout context).
13
+ *
14
+ * Ported from `@1click/ui/src/cart-drawer/summary-breakdown.tsx` (v2.3.1)
15
+ * and EXPANDED for barter: the source rendered a client-summed product
16
+ * subtotal only; barter's decorated cart carries server-computed totals on
17
+ * every read (`subtotal`, `discount_total`, `shipping_total`, `tax_total`,
18
+ * `cod_fee_total`/`cod_fee_label`, `gift_cards[]`, `gift_card_total`,
19
+ * `gift_card_remainder`, `total` — all EUR major units), so this component
20
+ * renders EXACTLY what the server returns and computes no money. The only
21
+ * client-side transformation is the display sign on deductions.
22
+ *
23
+ * Row rules (see `selectSummaryRows`):
24
+ * - discount / tax / COD-fee rows appear only when their total is > 0.
25
+ * - COD fee renders the server's `cod_fee_label` (labels.codFee fallback).
26
+ * - shipping renders the amount once a shipping method is set (0 → FREE);
27
+ * before that it shows "calculated at checkout".
28
+ * - gift-card tender renders one row per applied card (masked `last4`,
29
+ * negative display) plus the `gift_card_remainder` row — the amount the
30
+ * remainder provider actually charges. A depleted card stays listed at 0
31
+ * (server keeps it in `gift_cards` at amount 0).
32
+ */
33
+
34
+ /** The decorated-cart fields the breakdown consumes (structural subset). */
35
+ export type SummaryCartTotals = {
36
+ subtotal: number
37
+ discount_total: number
38
+ shipping_total: number
39
+ tax_total: number
40
+ total: number
41
+ cod_fee_total?: number
42
+ cod_fee_label?: string | null
43
+ gift_cards?: AppliedGiftCard[]
44
+ gift_card_total?: number
45
+ gift_card_remainder?: number
46
+ shipping_methods?: ReadonlyArray<unknown>
47
+ }
48
+
49
+ export type SummaryRow =
50
+ | { kind: "subtotal"; amount: number }
51
+ | { kind: "discount"; amount: number }
52
+ | { kind: "shipping"; amount: number }
53
+ | { kind: "shipping_pending" }
54
+ | { kind: "tax"; amount: number }
55
+ | { kind: "cod_fee"; amount: number; label: string | null }
56
+ | { kind: "total"; amount: number }
57
+ | { kind: "gift_card"; last4: string; amount: number }
58
+ | { kind: "remainder"; amount: number }
59
+
60
+ /**
61
+ * Pure row selection over the decorated-cart totals — exported for unit
62
+ * tests. Amounts pass through verbatim (server truth); deduction rows
63
+ * (discount, gift cards) carry a negated amount for display only.
64
+ */
65
+ export function selectSummaryRows(cart: SummaryCartTotals): SummaryRow[] {
66
+ const rows: SummaryRow[] = []
67
+ rows.push({ kind: "subtotal", amount: cart.subtotal })
68
+
69
+ if ((cart.discount_total ?? 0) > 0) {
70
+ rows.push({ kind: "discount", amount: -cart.discount_total })
71
+ }
72
+
73
+ if (cart.shipping_methods && cart.shipping_methods.length > 0) {
74
+ rows.push({ kind: "shipping", amount: cart.shipping_total })
75
+ } else {
76
+ rows.push({ kind: "shipping_pending" })
77
+ }
78
+
79
+ if ((cart.tax_total ?? 0) > 0) {
80
+ rows.push({ kind: "tax", amount: cart.tax_total })
81
+ }
82
+
83
+ if ((cart.cod_fee_total ?? 0) > 0) {
84
+ rows.push({
85
+ kind: "cod_fee",
86
+ amount: cart.cod_fee_total!,
87
+ label: cart.cod_fee_label ?? null,
88
+ })
89
+ }
90
+
91
+ rows.push({ kind: "total", amount: cart.total })
92
+
93
+ if (cart.gift_cards && cart.gift_cards.length > 0) {
94
+ for (const card of cart.gift_cards) {
95
+ // `|| 0` normalizes -0 (a depleted card at amount 0) so Intl never
96
+ // renders a stray "-€0.00".
97
+ rows.push({ kind: "gift_card", last4: card.last4, amount: -card.amount || 0 })
98
+ }
99
+ rows.push({ kind: "remainder", amount: cart.gift_card_remainder ?? 0 })
100
+ }
101
+
102
+ return rows
103
+ }
104
+
105
+ export function CartSummaryBreakdown() {
106
+ const { cart, labels } = useCartDrawer()
107
+ if (!cart) return null
108
+
109
+ const currencyCode = cart.currency_code
110
+ const money = (amount: number) =>
111
+ convertToLocale({ amount, currency_code: currencyCode })
112
+ const rows = selectSummaryRows(cart)
113
+
114
+ return (
115
+ <div className="px-6 py-5 space-y-2">
116
+ {rows.map((row, i) => {
117
+ switch (row.kind) {
118
+ case "subtotal":
119
+ return (
120
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
121
+ <span>{labels.subtotal}</span>
122
+ <span className="tabular-nums">{money(row.amount)}</span>
123
+ </div>
124
+ )
125
+ case "discount":
126
+ return (
127
+ <div key={i} className="flex justify-between items-center text-[13px] text-success">
128
+ <span>{labels.discount}</span>
129
+ <span className="tabular-nums">{money(row.amount)}</span>
130
+ </div>
131
+ )
132
+ case "shipping":
133
+ return (
134
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
135
+ <span>{labels.shipping}</span>
136
+ <span className="tabular-nums">
137
+ {row.amount === 0 ? labels.free : money(row.amount)}
138
+ </span>
139
+ </div>
140
+ )
141
+ case "shipping_pending":
142
+ return (
143
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
144
+ <span>{labels.shipping}</span>
145
+ <span>{labels.calculatedAtCheckout}</span>
146
+ </div>
147
+ )
148
+ case "tax":
149
+ return (
150
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
151
+ <span>{labels.tax}</span>
152
+ <span className="tabular-nums">{money(row.amount)}</span>
153
+ </div>
154
+ )
155
+ case "cod_fee":
156
+ return (
157
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
158
+ <span>{row.label ?? labels.codFee}</span>
159
+ <span className="tabular-nums">{money(row.amount)}</span>
160
+ </div>
161
+ )
162
+ case "total":
163
+ return (
164
+ <div
165
+ key={i}
166
+ className="flex justify-between items-center pt-2 mt-1 border-t border-border"
167
+ >
168
+ <span className="text-[15px] font-bold text-foreground">
169
+ {labels.total}
170
+ </span>
171
+ <span className="text-[17px] font-bold text-foreground tracking-tight tabular-nums">
172
+ {money(row.amount)}
173
+ </span>
174
+ </div>
175
+ )
176
+ case "gift_card":
177
+ return (
178
+ <div key={i} className="flex justify-between items-center text-[13px] text-success">
179
+ <span>
180
+ {labels.giftCard} ••{row.last4}
181
+ </span>
182
+ <span className="tabular-nums">{money(row.amount)}</span>
183
+ </div>
184
+ )
185
+ case "remainder":
186
+ return (
187
+ <div key={i} className="flex justify-between items-center text-[13px] font-semibold text-foreground">
188
+ <span>{labels.remainingToPay}</span>
189
+ <span className="tabular-nums">{money(row.amount)}</span>
190
+ </div>
191
+ )
192
+ }
193
+ })}
194
+ </div>
195
+ )
196
+ }