@cartbase/storefront 0.17.0 → 0.18.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.
- package/LICENSE +21 -21
- package/package.json +3 -1
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/products.ts +43 -1
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +259 -259
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +367 -168
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/index.ts +73 -68
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/quantity.tsx +8 -14
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -0
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/checkout/use-checkout-orchestration.ts +1604 -1592
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/cookie-names.ts +14 -0
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/bg.ts +467 -467
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +466 -466
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/index.ts +68 -59
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-actions-wrapper.tsx +63 -62
- package/src/products/product-actions.tsx +151 -150
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/product-template.tsx +163 -162
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -0
- package/src/products/use-product-actions.ts +199 -164
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +73 -73
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
- package/theme/theme.css +150 -150
- package/theme/tokens.css +106 -106
|
@@ -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
|
+
}
|
|
@@ -0,0 +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,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
|
+
}
|