@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,224 @@
1
+ "use client"
2
+
3
+ import type { ChangeEvent } from "react"
4
+
5
+ import type { CartAddress } from "../api/carts"
6
+ import type { CustomerAddress } from "../api/customers"
7
+ import { Field } from "../primitives/field"
8
+ import { SelectField } from "../primitives/select-field"
9
+ import { AddressSelect } from "./address-select"
10
+ import { CompanyDetails } from "./company-details"
11
+ import { useCheckoutLabels } from "./context"
12
+ import { ErrorMessage } from "./error-message"
13
+
14
+ /**
15
+ * CheckoutAddressForm — the delivery address + email section of the
16
+ * checkout. Presentational: state and handlers come from the parent
17
+ * (`CheckoutClient` / `useCheckoutOrchestration`).
18
+ *
19
+ * Ported from `@1click/ui/src/checkout/address-form.tsx` (v2.3.1) with the
20
+ * barter type seam only: `@medusajs/types` → the SDK's `CustomerAddress` /
21
+ * `CartAddress` partials. The `countries` list comes from the orchestration
22
+ * hook's `regionCountries` — barter regions carry NO embedded countries
23
+ * array (api/regions.ts divergence), so the host supplies the list via the
24
+ * hook's `countries` option (single-entry `countryCode` fallback).
25
+ */
26
+
27
+ type CheckoutAddressFormProps = {
28
+ formData: Record<string, string>
29
+ onChange: (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void
30
+ onBlur: () => void
31
+ /** Saved addresses from the customer profile (optional) */
32
+ savedAddresses?: CustomerAddress[]
33
+ /** Handler for selecting a saved address */
34
+ onSelectSavedAddress?: (
35
+ address: Partial<CartAddress> | undefined,
36
+ email?: string
37
+ ) => void
38
+ /** Region countries for the country select */
39
+ countries: Array<{ iso_2: string; display_name: string }>
40
+ /** Current address input (for saved-address match detection) */
41
+ addressInput: Partial<CartAddress> | null
42
+ addressError: string | null
43
+ /**
44
+ * Hide the country field entirely. Useful for single-country stores
45
+ * (BG-only) where the field is redundant. The `country_code` value
46
+ * still lives in formData (initialized from the `countryCode` prop on
47
+ * `useCheckoutOrchestration`), so submissions still carry it.
48
+ * Default false — library-generic.
49
+ */
50
+ hideCountry?: boolean
51
+ /**
52
+ * Set of formData keys that are required-but-empty AND should be
53
+ * visually pulsed (3s-idle attention cue). The orchestration hook
54
+ * computes this; the form just reads it and applies the soft-blue
55
+ * pulse class to matching fields. Empty set / undefined = no cue.
56
+ */
57
+ pulseFields?: Set<string>
58
+ }
59
+
60
+ export function CheckoutAddressForm({
61
+ formData,
62
+ onChange,
63
+ onBlur,
64
+ savedAddresses,
65
+ onSelectSavedAddress,
66
+ countries,
67
+ addressInput,
68
+ addressError,
69
+ hideCountry = false,
70
+ pulseFields,
71
+ }: CheckoutAddressFormProps) {
72
+ const labels = useCheckoutLabels()
73
+ const p = (k: string) => !!pulseFields?.has(k)
74
+
75
+ return (
76
+ <div>
77
+ <h2 className="text-lg font-semibold text-foreground mb-5 tracking-tight">
78
+ {labels.deliveryInfo}
79
+ </h2>
80
+
81
+ {savedAddresses && savedAddresses.length > 0 && onSelectSavedAddress && (
82
+ <div className="mb-5 p-4 bg-muted rounded-xl border border-border">
83
+ <p className="text-sm text-muted-foreground mb-3">{labels.useSavedAddress}</p>
84
+ <AddressSelect
85
+ addresses={savedAddresses}
86
+ addressInput={addressInput}
87
+ onSelect={onSelectSavedAddress}
88
+ />
89
+ </div>
90
+ )}
91
+
92
+ <div className="space-y-2.5">
93
+ <Field
94
+ label={labels.email}
95
+ name="email"
96
+ type="email"
97
+ autoComplete="email"
98
+ value={formData.email ?? ""}
99
+ onChange={onChange}
100
+ onBlur={onBlur}
101
+ required
102
+ pulse={p("email")}
103
+ />
104
+
105
+ {!hideCountry && (
106
+ countries.length === 1 ? (
107
+ // Single-country region — render a readonly field showing
108
+ // the localized country name. (Library-default rendering
109
+ // when `hideCountry` isn't passed.) The actual country_code
110
+ // remains in formData so submission still carries it.
111
+ <Field
112
+ label={labels.country}
113
+ name="shipping_address.country_code_display"
114
+ value={
115
+ labels.singleCountryName ??
116
+ countries[0]?.display_name ??
117
+ ""
118
+ }
119
+ onChange={() => {}}
120
+ readOnly
121
+ />
122
+ ) : (
123
+ <SelectField
124
+ label={labels.country}
125
+ name="shipping_address.country_code"
126
+ autoComplete="country"
127
+ value={formData["shipping_address.country_code"] ?? ""}
128
+ onChange={onChange}
129
+ required
130
+ >
131
+ <option value="" disabled />
132
+ {countries.map((c) => (
133
+ <option key={c.iso_2} value={c.iso_2}>
134
+ {c.display_name}
135
+ </option>
136
+ ))}
137
+ </SelectField>
138
+ )
139
+ )}
140
+
141
+ <div className="grid grid-cols-2 gap-2.5">
142
+ <Field
143
+ label={labels.firstName}
144
+ name="shipping_address.first_name"
145
+ autoComplete="given-name"
146
+ value={formData["shipping_address.first_name"] ?? ""}
147
+ onChange={onChange}
148
+ onBlur={onBlur}
149
+ required
150
+ pulse={p("shipping_address.first_name")}
151
+ />
152
+ <Field
153
+ label={labels.lastName}
154
+ name="shipping_address.last_name"
155
+ autoComplete="family-name"
156
+ value={formData["shipping_address.last_name"] ?? ""}
157
+ onChange={onChange}
158
+ onBlur={onBlur}
159
+ required
160
+ pulse={p("shipping_address.last_name")}
161
+ />
162
+ </div>
163
+
164
+ <Field
165
+ label={labels.address}
166
+ name="shipping_address.address_1"
167
+ autoComplete="address-line1"
168
+ value={formData["shipping_address.address_1"] ?? ""}
169
+ onChange={onChange}
170
+ onBlur={onBlur}
171
+ required
172
+ pulse={p("shipping_address.address_1")}
173
+ />
174
+
175
+ <Field
176
+ label={labels.apartment}
177
+ name="shipping_address.company"
178
+ autoComplete="address-line2"
179
+ value={formData["shipping_address.company"] ?? ""}
180
+ onChange={onChange}
181
+ onBlur={onBlur}
182
+ />
183
+
184
+ <div className="grid grid-cols-2 gap-2.5">
185
+ <Field
186
+ label={labels.postalCode}
187
+ name="shipping_address.postal_code"
188
+ autoComplete="postal-code"
189
+ value={formData["shipping_address.postal_code"] ?? ""}
190
+ onChange={onChange}
191
+ onBlur={onBlur}
192
+ required
193
+ pulse={p("shipping_address.postal_code")}
194
+ />
195
+ <Field
196
+ label={labels.city}
197
+ name="shipping_address.city"
198
+ autoComplete="address-level2"
199
+ value={formData["shipping_address.city"] ?? ""}
200
+ onChange={onChange}
201
+ onBlur={onBlur}
202
+ required
203
+ pulse={p("shipping_address.city")}
204
+ />
205
+ </div>
206
+
207
+ <Field
208
+ label={labels.phone}
209
+ name="shipping_address.phone"
210
+ type="tel"
211
+ autoComplete="tel"
212
+ value={formData["shipping_address.phone"] ?? ""}
213
+ onChange={onChange}
214
+ onBlur={onBlur}
215
+ pulse={p("shipping_address.phone")}
216
+ />
217
+ </div>
218
+
219
+ <CompanyDetails formData={formData} onChange={onChange} onBlur={onBlur} />
220
+
221
+ <ErrorMessage error={addressError} data-testid="address-error-message" />
222
+ </div>
223
+ )
224
+ }
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ import { useMemo } from "react"
4
+
5
+ import type { CartAddress } from "../api/carts"
6
+ import type { CustomerAddress } from "../api/customers"
7
+ import compareAddresses from "./compare-addresses"
8
+ import { cn } from "../lib/utils"
9
+ import { useCheckoutLabels } from "./context"
10
+
11
+ /**
12
+ * AddressSelect — native `<select>` dropdown for picking a saved customer
13
+ * address. Uses a plain HTML select so the library has zero dependency on
14
+ * @headlessui/react. Stores that want a fancier Listbox UX can easily
15
+ * replace this one file locally.
16
+ *
17
+ * Ported from `@1click/ui/src/checkout/address-select.tsx` (v2.3.1) with
18
+ * the barter type seam: `@medusajs/types` address DTOs → the SDK's
19
+ * `CustomerAddress` (GET /api/store/customers/me — addresses embedded,
20
+ * docs/storefront/customers.md) and `CartAddress` partials. Field names are
21
+ * identical across both wires, so the compare/format logic is unchanged.
22
+ */
23
+
24
+ type AddressSelectProps = {
25
+ addresses: CustomerAddress[]
26
+ addressInput: Partial<CartAddress> | null
27
+ onSelect: (address: Partial<CartAddress> | undefined, email?: string) => void
28
+ }
29
+
30
+ export function AddressSelect({
31
+ addresses,
32
+ addressInput,
33
+ onSelect,
34
+ }: AddressSelectProps) {
35
+ const labels = useCheckoutLabels()
36
+
37
+ const selectedId = useMemo(() => {
38
+ const match = addresses.find((a) =>
39
+ compareAddresses(
40
+ a as unknown as Record<string, unknown>,
41
+ addressInput as unknown as Record<string, unknown>
42
+ )
43
+ )
44
+ return match?.id ?? ""
45
+ }, [addresses, addressInput])
46
+
47
+ const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
48
+ const id = e.target.value
49
+ const picked = addresses.find((a) => a.id === id)
50
+ if (picked) onSelect(picked as unknown as Partial<CartAddress>)
51
+ }
52
+
53
+ const formatAddress = (a: CustomerAddress): string => {
54
+ const name = [a.first_name, a.last_name].filter(Boolean).join(" ")
55
+ const loc = [a.address_1, a.city, a.country_code?.toUpperCase()]
56
+ .filter(Boolean)
57
+ .join(", ")
58
+ return `${name} — ${loc}`
59
+ }
60
+
61
+ return (
62
+ <select
63
+ value={selectedId}
64
+ onChange={handleChange}
65
+ className={cn(
66
+ "w-full px-4 py-[10px] text-sm text-left bg-card border border-border rounded-lg",
67
+ "focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
68
+ )}
69
+ data-testid="shipping-address-select"
70
+ >
71
+ <option value="">{labels.chooseAddress}</option>
72
+ {addresses.map((address) => (
73
+ <option key={address.id} value={address.id}>
74
+ {formatAddress(address)}
75
+ </option>
76
+ ))}
77
+ </select>
78
+ )
79
+ }