@cartbase/storefront 0.19.0 → 0.20.1

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 (105) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
@@ -1,123 +1,123 @@
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). Cartbase
10
- * additions: the summary-breakdown row labels (`discount`, `tax`, `paymentMethodFee`,
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 (Cartbase decorated-cart fields)
59
- discount: string
60
- tax: string
61
- /** Fallback only — the server's `payment_method_fee_label` wins when present. */
62
- paymentMethodFee: 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
- // Neutral and true on every store: it states WHEN the numbers are worked
103
- // out, not which tax regime applies. It read "VAT included. Shipping
104
- // calculated at checkout." until 2026-09-13, and the first half is false
105
- // wherever tax is added at checkout instead of sitting in the price.
106
- taxAndShipping: "Taxes and shipping calculated at checkout.",
107
- shipping: "Shipping",
108
- free: "FREE",
109
- calculatedAtCheckout: "Calculated at checkout",
110
- total: "Total",
111
- secureCheckout: "Checkout",
112
- shippingAndTaxCalculatedAtCheckout: "Shipping and tax calculated at checkout",
113
-
114
- discount: "Discount",
115
- tax: "VAT",
116
- paymentMethodFee: "Payment method fee",
117
- giftCard: "Gift card",
118
- remainingToPay: "Remaining to pay",
119
-
120
- continueShopping: "Continue shopping",
121
-
122
- remove: "Remove",
123
- }
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). Cartbase
10
+ * additions: the summary-breakdown row labels (`discount`, `tax`, `paymentMethodFee`,
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 (Cartbase decorated-cart fields)
59
+ discount: string
60
+ tax: string
61
+ /** Fallback only — the server's `payment_method_fee_label` wins when present. */
62
+ paymentMethodFee: 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
+ // Neutral and true on every store: it states WHEN the numbers are worked
103
+ // out, not which tax regime applies. It read "VAT included. Shipping
104
+ // calculated at checkout." until 2026-09-13, and the first half is false
105
+ // wherever tax is added at checkout instead of sitting in the price.
106
+ taxAndShipping: "Taxes and shipping calculated at checkout.",
107
+ shipping: "Shipping",
108
+ free: "FREE",
109
+ calculatedAtCheckout: "Calculated at checkout",
110
+ total: "Total",
111
+ secureCheckout: "Checkout",
112
+ shippingAndTaxCalculatedAtCheckout: "Shipping and tax calculated at checkout",
113
+
114
+ discount: "Discount",
115
+ tax: "VAT",
116
+ paymentMethodFee: "Payment method fee",
117
+ giftCard: "Gift card",
118
+ remainingToPay: "Remaining to pay",
119
+
120
+ continueShopping: "Continue shopping",
121
+
122
+ remove: "Remove",
123
+ }
@@ -1,78 +1,78 @@
1
- /**
2
- * THE CART'S CHANGES, ONE AT A TIME (the instant cart, 2026-09-16).
3
- *
4
- * The drawer shows every change at once and the platform confirms behind
5
- * it. The platform's answers are whole carts, so two changes in flight
6
- * together can answer out of order and the older cart would overwrite the
7
- * newer one; and a second add before the first has created the cart would
8
- * create a second cart. So the provider sends its changes through this
9
- * queue: one call at a time, each starting after the one before has
10
- * answered, each reading the cart as the answers left it.
11
- *
12
- * Quantity clicks on one line are combined: while a change for a line waits
13
- * its turn, a new click only moves the number it will send, so ten fast
14
- * clicks send at most two calls (the one already in flight and the last
15
- * number). Pure module, no React.
16
- */
17
-
18
- export interface CartMutationQueue {
19
- /** Run a change after every change before it has answered. A failure never blocks the next. */
20
- run<T>(job: () => Promise<T>): Promise<T>
21
- /**
22
- * A quantity change for one line. While an earlier change for the same
23
- * line still waits its turn, only its number moves and its promise is
24
- * returned; otherwise `send` is queued and called with the latest number
25
- * when its turn comes.
26
- */
27
- quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T>
28
- /** A waiting quantity change for this line is dropped (the line is being removed). */
29
- forget(lineKey: string): void
30
- /** True while any change is queued or in flight. */
31
- busy(): boolean
32
- }
33
-
34
- export function createCartMutationQueue(): CartMutationQueue {
35
- let tail: Promise<unknown> = Promise.resolve()
36
- let pending = 0
37
- const waiting = new Map<string, { quantity: number; dropped: boolean; job: Promise<unknown> }>()
38
-
39
- function run<T>(job: () => Promise<T>): Promise<T> {
40
- pending++
41
- const result = tail.then(job, job)
42
- const settled = result.then(
43
- () => undefined,
44
- () => undefined
45
- )
46
- tail = settled
47
- void settled.then(() => {
48
- pending--
49
- })
50
- return result
51
- }
52
-
53
- function quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T> {
54
- const queued = waiting.get(lineKey)
55
- if (queued && !queued.dropped) {
56
- queued.quantity = quantity
57
- return queued.job as Promise<T>
58
- }
59
- const entry = { quantity, dropped: false, job: Promise.resolve() as Promise<unknown> }
60
- entry.job = run(async () => {
61
- // The turn has come: later clicks start a new change from here on.
62
- if (waiting.get(lineKey) === entry) waiting.delete(lineKey)
63
- if (entry.dropped) return undefined as T
64
- return send(entry.quantity)
65
- })
66
- waiting.set(lineKey, entry)
67
- return entry.job as Promise<T>
68
- }
69
-
70
- function forget(lineKey: string): void {
71
- const queued = waiting.get(lineKey)
72
- if (!queued) return
73
- queued.dropped = true
74
- waiting.delete(lineKey)
75
- }
76
-
77
- return { run, quantity, forget, busy: () => pending > 0 }
78
- }
1
+ /**
2
+ * THE CART'S CHANGES, ONE AT A TIME (the instant cart, 2026-09-16).
3
+ *
4
+ * The drawer shows every change at once and the platform confirms behind
5
+ * it. The platform's answers are whole carts, so two changes in flight
6
+ * together can answer out of order and the older cart would overwrite the
7
+ * newer one; and a second add before the first has created the cart would
8
+ * create a second cart. So the provider sends its changes through this
9
+ * queue: one call at a time, each starting after the one before has
10
+ * answered, each reading the cart as the answers left it.
11
+ *
12
+ * Quantity clicks on one line are combined: while a change for a line waits
13
+ * its turn, a new click only moves the number it will send, so ten fast
14
+ * clicks send at most two calls (the one already in flight and the last
15
+ * number). Pure module, no React.
16
+ */
17
+
18
+ export interface CartMutationQueue {
19
+ /** Run a change after every change before it has answered. A failure never blocks the next. */
20
+ run<T>(job: () => Promise<T>): Promise<T>
21
+ /**
22
+ * A quantity change for one line. While an earlier change for the same
23
+ * line still waits its turn, only its number moves and its promise is
24
+ * returned; otherwise `send` is queued and called with the latest number
25
+ * when its turn comes.
26
+ */
27
+ quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T>
28
+ /** A waiting quantity change for this line is dropped (the line is being removed). */
29
+ forget(lineKey: string): void
30
+ /** True while any change is queued or in flight. */
31
+ busy(): boolean
32
+ }
33
+
34
+ export function createCartMutationQueue(): CartMutationQueue {
35
+ let tail: Promise<unknown> = Promise.resolve()
36
+ let pending = 0
37
+ const waiting = new Map<string, { quantity: number; dropped: boolean; job: Promise<unknown> }>()
38
+
39
+ function run<T>(job: () => Promise<T>): Promise<T> {
40
+ pending++
41
+ const result = tail.then(job, job)
42
+ const settled = result.then(
43
+ () => undefined,
44
+ () => undefined
45
+ )
46
+ tail = settled
47
+ void settled.then(() => {
48
+ pending--
49
+ })
50
+ return result
51
+ }
52
+
53
+ function quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T> {
54
+ const queued = waiting.get(lineKey)
55
+ if (queued && !queued.dropped) {
56
+ queued.quantity = quantity
57
+ return queued.job as Promise<T>
58
+ }
59
+ const entry = { quantity, dropped: false, job: Promise.resolve() as Promise<unknown> }
60
+ entry.job = run(async () => {
61
+ // The turn has come: later clicks start a new change from here on.
62
+ if (waiting.get(lineKey) === entry) waiting.delete(lineKey)
63
+ if (entry.dropped) return undefined as T
64
+ return send(entry.quantity)
65
+ })
66
+ waiting.set(lineKey, entry)
67
+ return entry.job as Promise<T>
68
+ }
69
+
70
+ function forget(lineKey: string): void {
71
+ const queued = waiting.get(lineKey)
72
+ if (!queued) return
73
+ queued.dropped = true
74
+ waiting.delete(lineKey)
75
+ }
76
+
77
+ return { run, quantity, forget, busy: () => pending > 0 }
78
+ }
@@ -1,131 +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
- }
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
+ }