@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,47 @@
1
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
2
+
3
+ /**
4
+ * Contact-info card (name, phone, email). Ported from
5
+ * `@1click/ui/src/order/order-address-card.tsx` (v2.3.1); data seam:
6
+ * structural pick — barter's `StoreOrderDetail` (with its embedded
7
+ * `order_addresses` row) satisfies it directly.
8
+ */
9
+ export interface OrderContactData {
10
+ email: string | null
11
+ shipping_address?: {
12
+ first_name?: string | null
13
+ last_name?: string | null
14
+ phone?: string | null
15
+ [key: string]: unknown
16
+ } | null
17
+ [key: string]: unknown
18
+ }
19
+
20
+ type OrderAddressCardProps = {
21
+ order: OrderContactData
22
+ labels?: Pick<OrderLabels, "contactInfo">
23
+ }
24
+
25
+ export function OrderAddressCard({ order, labels }: OrderAddressCardProps) {
26
+ const l = { ...defaultOrderLabels, ...labels }
27
+ const addr = order.shipping_address
28
+
29
+ return (
30
+ <div className="bg-card rounded-xl p-5 shadow-sm">
31
+ <h3 className="text-sm font-semibold text-foreground mb-3">
32
+ {l.contactInfo}
33
+ </h3>
34
+ <div className="space-y-1 text-sm text-muted-foreground">
35
+ {addr && (
36
+ <p className="font-medium text-foreground">
37
+ {addr.first_name} {addr.last_name}
38
+ </p>
39
+ )}
40
+ {addr?.phone && <p>{addr.phone}</p>}
41
+ <p>{order.email}</p>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ export { type OrderAddressCardProps }
@@ -0,0 +1,165 @@
1
+ import Link from "next/link"
2
+ import { ChevronLeft } from "lucide-react"
3
+ import type { StoreOrderDetail, StoreOrderItem } from "../api/orders"
4
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
5
+ import {
6
+ OrderConfirmationHeader,
7
+ type OrderHeaderData,
8
+ } from "./order-confirmation-header"
9
+ import { deriveFulfillmentStatus, OrderTimeline } from "./order-timeline"
10
+ import { OrderItemsList } from "./order-items-list"
11
+ import {
12
+ displayItemFromOrderItem,
13
+ type OrderDisplayItem,
14
+ } from "./order-item"
15
+ import { OrderTotals, type OrderTotalsSource } from "./order-totals"
16
+ import { OrderAddressCard, type OrderContactData } from "./order-address-card"
17
+ import {
18
+ OrderDeliveryCard,
19
+ type OrderDeliveryData,
20
+ type ShippingMethodDisplay,
21
+ } from "./order-delivery-card"
22
+ import { OrderPaymentCard } from "./order-payment-card"
23
+ import { OrderHelpSection } from "./order-help-section"
24
+
25
+ /**
26
+ * Full order-confirmation page. Ported from
27
+ * `@1click/ui/src/order/order-completed-template.tsx` (v2.3.1) with the
28
+ * barter data seam: Medusa's `StoreOrder` was one object carrying items
29
+ * with totals, order totals, shipping methods and payments — barter splits
30
+ * those across surfaces, so the template takes them as separate props:
31
+ *
32
+ * - `order` — barter `StoreOrderDetail` (or any structural equivalent:
33
+ * id/display_id/email/created_at/currency_code/metadata/addresses/
34
+ * fulfillments). The fulfillment timeline is derived from
35
+ * `order.fulfillments` internally.
36
+ * - `totals` — an `OrderTotalsSource`: pass the decorated cart from the
37
+ * just-completed checkout (`api/carts` `Cart` satisfies it), or
38
+ * `orderTotalsFromSummary(completeCart().order.summary)`.
39
+ * - `items` — normalized display lines; when omitted they are built from
40
+ * `order.items` via `displayItemFromOrderItem` (prefer passing
41
+ * cart-derived lines via `displayItemFromCartLine` right after checkout
42
+ * to keep server-computed per-line discounts).
43
+ * - `shippingMethod` / `paymentProviderId` / `cardLast4` — from the
44
+ * checkout state (decorated cart / prepare-checkout response); the
45
+ * delivery and payment cards render only what they are given.
46
+ *
47
+ * The Purchase tracking trio (fbq/gtag/rybbit, deduped by
48
+ * `order.display_id`) stays APP-OWNED: fire it from the confirmation
49
+ * route, not from this template — see docs/storefront/components.md
50
+ * (tracking family) for the dedupe contract.
51
+ */
52
+ type OrderCompletedTemplateProps = {
53
+ order: OrderHeaderData &
54
+ OrderContactData &
55
+ OrderDeliveryData & {
56
+ currency_code: string
57
+ items?: StoreOrderItem[] | null
58
+ }
59
+ totals: OrderTotalsSource
60
+ /** Normalized lines; defaults to `order.items` via the pure converter. */
61
+ items?: OrderDisplayItem[]
62
+ shippingMethod?: ShippingMethodDisplay | null
63
+ paymentProviderId?: string | null
64
+ cardLast4?: string | null
65
+ labels?: OrderLabels
66
+ locale?: string
67
+ contactHref?: string
68
+ returnsHref?: string
69
+ /** Href for the bottom "continue shopping" CTA (source hardcoded /store). */
70
+ storeHref?: string
71
+ /**
72
+ * Per-store override for the cash-on-delivery fee row in the totals
73
+ * breakdown. Optional — falls back to the server's `cod_fee_label`,
74
+ * then the fee line's title, then the translated `labels.codFee`.
75
+ */
76
+ codFeeLabel?: string
77
+ }
78
+
79
+ export function OrderCompletedTemplate({
80
+ order,
81
+ totals,
82
+ items,
83
+ shippingMethod,
84
+ paymentProviderId,
85
+ cardLast4,
86
+ labels,
87
+ locale,
88
+ contactHref,
89
+ returnsHref,
90
+ storeHref = "/store",
91
+ codFeeLabel,
92
+ }: OrderCompletedTemplateProps) {
93
+ const l = { ...defaultOrderLabels, ...labels }
94
+ const displayItems =
95
+ items ?? (order.items ?? []).map(displayItemFromOrderItem)
96
+
97
+ return (
98
+ <div className="min-h-[calc(100vh-64px)] bg-muted/50 py-8 sm:py-12">
99
+ <div className="max-w-5xl mx-auto px-4">
100
+ <OrderConfirmationHeader order={order} labels={l} locale={locale} />
101
+
102
+ <div className="mb-6 mt-6">
103
+ <OrderTimeline
104
+ fulfillmentStatus={deriveFulfillmentStatus(order.fulfillments)}
105
+ labels={l}
106
+ />
107
+ </div>
108
+
109
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
110
+ <div className="lg:col-span-3">
111
+ <div className="bg-card rounded-xl p-5 sm:p-6 shadow-sm">
112
+ <OrderItemsList
113
+ items={displayItems}
114
+ currencyCode={order.currency_code}
115
+ labels={l}
116
+ />
117
+ <OrderTotals
118
+ totals={totals}
119
+ currencyCode={order.currency_code}
120
+ items={displayItems}
121
+ labels={l}
122
+ codFeeLabel={codFeeLabel}
123
+ />
124
+ </div>
125
+ </div>
126
+
127
+ <div className="lg:col-span-2 flex flex-col gap-4">
128
+ <OrderAddressCard order={order} labels={l} />
129
+ <OrderDeliveryCard
130
+ order={order}
131
+ shippingMethod={shippingMethod}
132
+ currencyCode={order.currency_code}
133
+ labels={l}
134
+ />
135
+ <OrderPaymentCard
136
+ providerId={paymentProviderId}
137
+ cardLast4={cardLast4}
138
+ labels={l}
139
+ />
140
+ <OrderHelpSection
141
+ labels={l}
142
+ contactHref={contactHref}
143
+ returnsHref={returnsHref}
144
+ />
145
+ </div>
146
+ </div>
147
+
148
+ <div className="text-center mt-8">
149
+ <Link
150
+ href={storeHref}
151
+ className="inline-flex items-center gap-2 px-6 py-3 bg-foreground text-card text-sm font-semibold rounded-xl hover:opacity-90 active:opacity-100 transition-opacity"
152
+ >
153
+ <ChevronLeft className="w-4 h-4" />
154
+ {l.continueShopping}
155
+ </Link>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )
160
+ }
161
+
162
+ /** Re-exported so template callers can type their page data without extra imports. */
163
+ export type { StoreOrderDetail }
164
+
165
+ export { type OrderCompletedTemplateProps }
@@ -0,0 +1,65 @@
1
+ import { Check } from "lucide-react"
2
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
3
+
4
+ /**
5
+ * Confirmation hero (check mark + order number + date). Ported from
6
+ * `@1click/ui/src/order/order-confirmation-header.tsx` (v2.3.1); data
7
+ * seam: structural pick instead of Medusa's `StoreOrder` — both barter's
8
+ * `StoreOrderDetail` and `completeCart()`'s `CompletedOrder` satisfy it.
9
+ */
10
+ export interface OrderHeaderData {
11
+ display_id?: number | null
12
+ email: string | null
13
+ created_at: string
14
+ [key: string]: unknown
15
+ }
16
+
17
+ type OrderConfirmationHeaderProps = {
18
+ order: OrderHeaderData
19
+ labels?: OrderLabels
20
+ locale?: string
21
+ }
22
+
23
+ export function OrderConfirmationHeader({
24
+ order,
25
+ labels,
26
+ locale = "en-US",
27
+ }: OrderConfirmationHeaderProps) {
28
+ const l = { ...defaultOrderLabels, ...labels }
29
+ const orderDate = new Date(order.created_at).toLocaleDateString(locale, {
30
+ day: "numeric",
31
+ month: "long",
32
+ year: "numeric",
33
+ })
34
+
35
+ return (
36
+ <div className="text-center pt-8 pb-4 sm:pt-10 sm:pb-5">
37
+ <div className="mx-auto mb-5 w-[72px] h-[72px] rounded-full bg-success flex items-center justify-center">
38
+ <Check className="w-9 h-9 text-white" strokeWidth={2.5} />
39
+ </div>
40
+
41
+ <h1 className="text-2xl sm:text-3xl font-bold text-foreground tracking-tight mb-2">
42
+ {l.orderConfirmed}
43
+ </h1>
44
+ <p className="text-sm text-muted-foreground mb-6">
45
+ {l.confirmationSent}{" "}
46
+ <span className="font-medium text-foreground">{order.email}</span>
47
+ </p>
48
+
49
+ <div className="flex items-center justify-center gap-3 sm:gap-4">
50
+ <div className="flex items-center gap-2 px-4 py-2 bg-muted rounded-lg">
51
+ <span className="text-xs text-muted-foreground">{l.orderNumber}</span>
52
+ <span className="text-sm font-semibold text-foreground tabular-nums">
53
+ #{order.display_id}
54
+ </span>
55
+ </div>
56
+ <div className="flex items-center gap-2 px-4 py-2 bg-muted rounded-lg">
57
+ <span className="text-xs text-muted-foreground">{l.orderDate}</span>
58
+ <span className="text-sm font-medium text-foreground">{orderDate}</span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export { type OrderConfirmationHeaderProps }
@@ -0,0 +1,258 @@
1
+ import { ExternalLink, MapPin, PackageSearch, Truck } from "lucide-react"
2
+ import { DualPrice } from "../lib/dual-price"
3
+ import type { StoreOrderFulfillment } from "../api/orders"
4
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
5
+
6
+ /**
7
+ * Delivery card: pickup point / address, shipping method row, and — new in
8
+ * the barter port — the fulfillment tracking labels. Ported from
9
+ * `@1click/ui/src/order/order-delivery-card.tsx` (v2.3.1) with these seam
10
+ * translations:
11
+ *
12
+ * - Medusa's `order.shipping_methods[0]` embed does not exist on barter's
13
+ * `StoreOrderDetail`; the method comes via the `shippingMethod` prop
14
+ * (structural — `api/carts` `CartShippingMethod` from the decorated
15
+ * cart on the confirmation page satisfies it; `data` still carries the
16
+ * fulfillment-option id used for the pickup detection below).
17
+ * - `order.fulfillments[].fulfillment.labels` (tracking_number /
18
+ * tracking_url) ARE on the barter surface (docs/storefront/orders.md);
19
+ * `pickTrackingLabels()` selects the renderable ones and the card shows
20
+ * them under a `labels.tracking` heading.
21
+ * - Pickup detection preserved verbatim: `metadata.econt_office_code` (the
22
+ * Econt-office selector writes cart metadata, copied to the order) or a
23
+ * stable fulfillment-option id. Prefer the id over name parsing:
24
+ * "До точен адрес с ЕКОНТ" contains "еконт" but is address delivery,
25
+ * not pickup — the id disambiguates cleanly (production fix).
26
+ */
27
+
28
+ /** Structural shipping-method display shape (CartShippingMethod fits). */
29
+ export interface ShippingMethodDisplay {
30
+ name?: string | null
31
+ /** SERVER-computed method total (falls back to `amount`). */
32
+ total?: number | null
33
+ amount?: number | null
34
+ data?: Record<string, unknown> | null
35
+ [key: string]: unknown
36
+ }
37
+
38
+ export interface TrackingLabel {
39
+ tracking_number: string
40
+ tracking_url: string | null
41
+ }
42
+
43
+ /**
44
+ * Pure selection of renderable tracking labels from the order's
45
+ * fulfillments: skips canceled fulfillments, drops labels without a
46
+ * tracking number, and dedupes by tracking number (a re-printed label
47
+ * must not render twice). Unit-tested.
48
+ */
49
+ export function pickTrackingLabels(
50
+ fulfillments: readonly StoreOrderFulfillment[] | null | undefined
51
+ ): TrackingLabel[] {
52
+ if (!fulfillments?.length) return []
53
+ const seen = new Set<string>()
54
+ const out: TrackingLabel[] = []
55
+ for (const row of fulfillments) {
56
+ const f = row?.fulfillment
57
+ if (!f || f.canceled_at) continue
58
+ for (const label of f.labels ?? []) {
59
+ const num = label?.tracking_number
60
+ if (!num || seen.has(num)) continue
61
+ seen.add(num)
62
+ out.push({ tracking_number: num, tracking_url: label.tracking_url ?? null })
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+ export interface OrderDeliveryData {
69
+ metadata?: Record<string, unknown> | null
70
+ shipping_address?: {
71
+ address_1?: string | null
72
+ address_2?: string | null
73
+ city?: string | null
74
+ postal_code?: string | null
75
+ country_code?: string | null
76
+ [key: string]: unknown
77
+ } | null
78
+ fulfillments?: StoreOrderFulfillment[] | null
79
+ [key: string]: unknown
80
+ }
81
+
82
+ type OrderDeliveryCardProps = {
83
+ order: OrderDeliveryData
84
+ /** The chosen shipping method (from the decorated cart / checkout state). */
85
+ shippingMethod?: ShippingMethodDisplay | null
86
+ currencyCode: string
87
+ labels?: Pick<OrderLabels, "delivery" | "free" | "tracking">
88
+ }
89
+
90
+ export function OrderDeliveryCard({
91
+ order,
92
+ shippingMethod,
93
+ currencyCode,
94
+ labels,
95
+ }: OrderDeliveryCardProps) {
96
+ const l = { ...defaultOrderLabels, ...labels }
97
+ const method = shippingMethod ?? null
98
+ const metadata = order.metadata || {}
99
+ const addr = order.shipping_address
100
+ const tracking = pickTrackingLabels(order.fulfillments)
101
+
102
+ const hasEcontMetadata = !!metadata.econt_office_code
103
+ // Prefer the stable fulfillment-option id (shipping_option data.id)
104
+ // over name parsing — see the header comment.
105
+ const fulfillmentOptionId =
106
+ ((method?.data as { id?: string } | undefined | null)?.id as
107
+ | string
108
+ | undefined) ?? null
109
+ const isPickupById =
110
+ fulfillmentOptionId === "econt-office" ||
111
+ fulfillmentOptionId === "boxnow-locker"
112
+ const isPickup = hasEcontMetadata || isPickupById
113
+
114
+ return (
115
+ <div className="bg-card rounded-xl p-5 shadow-sm">
116
+ <h3 className="text-sm font-semibold text-foreground mb-3">
117
+ {l.delivery}
118
+ </h3>
119
+
120
+ {isPickup ? (
121
+ <div className="space-y-3">
122
+ {hasEcontMetadata ? (
123
+ <div className="flex items-start gap-2.5">
124
+ <div className="w-9 h-9 rounded-lg bg-success/10 flex items-center justify-center flex-shrink-0">
125
+ <MapPin className="w-[18px] h-[18px] text-success" />
126
+ </div>
127
+ <div>
128
+ <p className="text-sm font-medium text-foreground">
129
+ {String(metadata.econt_office_name ?? "")}
130
+ </p>
131
+ <p className="text-xs text-muted-foreground mt-0.5">
132
+ {String(metadata.econt_office_address ?? "")}
133
+ {metadata.econt_office_city
134
+ ? `, ${String(metadata.econt_office_city)}`
135
+ : ""}
136
+ </p>
137
+ {Boolean(metadata.econt_office_phone) && (
138
+ <p className="text-xs text-muted-foreground mt-0.5">
139
+ {String(metadata.econt_office_phone)}
140
+ </p>
141
+ )}
142
+ </div>
143
+ </div>
144
+ ) : (
145
+ <div className="flex items-start gap-2.5">
146
+ <div className="w-9 h-9 rounded-lg bg-success/10 flex items-center justify-center flex-shrink-0">
147
+ <MapPin className="w-[18px] h-[18px] text-success" />
148
+ </div>
149
+ <p className="text-sm font-medium text-foreground">
150
+ {method?.name}
151
+ </p>
152
+ </div>
153
+ )}
154
+
155
+ {method && (
156
+ <>
157
+ <div className="h-px bg-border" />
158
+ <ShippingMethodRow
159
+ method={method}
160
+ currencyCode={currencyCode}
161
+ labels={l}
162
+ />
163
+ </>
164
+ )}
165
+ </div>
166
+ ) : (
167
+ <div className="space-y-3">
168
+ {addr && (
169
+ <div className="space-y-1 text-sm text-muted-foreground">
170
+ <p>
171
+ {addr.address_1}
172
+ {addr.address_2 ? `, ${addr.address_2}` : ""}
173
+ </p>
174
+ <p>
175
+ {addr.postal_code} {addr.city}
176
+ </p>
177
+ <p>{addr.country_code?.toUpperCase()}</p>
178
+ </div>
179
+ )}
180
+ {method && (
181
+ <>
182
+ <div className="h-px bg-border" />
183
+ <ShippingMethodRow
184
+ method={method}
185
+ currencyCode={currencyCode}
186
+ labels={l}
187
+ />
188
+ </>
189
+ )}
190
+ </div>
191
+ )}
192
+
193
+ {tracking.length > 0 && (
194
+ <div className="mt-3 space-y-2">
195
+ <div className="h-px bg-border" />
196
+ <p className="text-xs font-medium text-muted-foreground">
197
+ {l.tracking}
198
+ </p>
199
+ {tracking.map((t) => (
200
+ <div key={t.tracking_number} className="flex items-center gap-2.5">
201
+ <div className="w-9 h-9 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
202
+ <PackageSearch className="w-[18px] h-[18px] text-muted-foreground" />
203
+ </div>
204
+ {t.tracking_url ? (
205
+ <a
206
+ href={t.tracking_url}
207
+ target="_blank"
208
+ rel="noopener noreferrer"
209
+ className="inline-flex items-center gap-1.5 text-sm text-foreground underline underline-offset-2 hover:text-foreground/80 tabular-nums"
210
+ >
211
+ {t.tracking_number}
212
+ <ExternalLink className="w-3.5 h-3.5" />
213
+ </a>
214
+ ) : (
215
+ <span className="text-sm text-foreground tabular-nums">
216
+ {t.tracking_number}
217
+ </span>
218
+ )}
219
+ </div>
220
+ ))}
221
+ </div>
222
+ )}
223
+ </div>
224
+ )
225
+ }
226
+
227
+ function ShippingMethodRow({
228
+ method,
229
+ currencyCode,
230
+ labels,
231
+ }: {
232
+ method: ShippingMethodDisplay
233
+ currencyCode: string
234
+ labels: Pick<OrderLabels, "free">
235
+ }) {
236
+ const amount = Number(method.total ?? method.amount ?? 0)
237
+ return (
238
+ <div className="flex items-center justify-between">
239
+ <div className="flex items-center gap-2.5">
240
+ <div className="w-9 h-9 rounded-lg bg-muted flex items-center justify-center">
241
+ <Truck className="w-[18px] h-[18px] text-muted-foreground" />
242
+ </div>
243
+ <span className="text-sm text-foreground">{method.name}</span>
244
+ </div>
245
+ {amount === 0 ? (
246
+ <span className="text-sm font-medium text-success">{labels.free}</span>
247
+ ) : (
248
+ <DualPrice
249
+ amount={amount}
250
+ currencyCode={currencyCode}
251
+ className="text-sm font-medium text-foreground"
252
+ />
253
+ )}
254
+ </div>
255
+ )
256
+ }
257
+
258
+ export { type OrderDeliveryCardProps }
@@ -0,0 +1,47 @@
1
+ import Link from "next/link"
2
+ import { MessageSquare, Undo2 } from "lucide-react"
3
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
4
+
5
+ /**
6
+ * "Need help?" links card. Verbatim port of
7
+ * `@1click/ui/src/order/order-help-section.tsx` (v2.3.1) — no data seam.
8
+ */
9
+ type OrderHelpSectionProps = {
10
+ labels?: Pick<OrderLabels, "needHelp" | "contactUs" | "returnsExchanges">
11
+ contactHref?: string
12
+ returnsHref?: string
13
+ }
14
+
15
+ export function OrderHelpSection({
16
+ labels,
17
+ contactHref = "/contact",
18
+ returnsHref = "/returns",
19
+ }: OrderHelpSectionProps) {
20
+ const l = { ...defaultOrderLabels, ...labels }
21
+
22
+ return (
23
+ <div className="bg-card rounded-xl p-5 shadow-sm">
24
+ <h3 className="text-sm font-semibold text-foreground mb-3">
25
+ {l.needHelp}
26
+ </h3>
27
+ <div className="flex flex-col gap-2">
28
+ <Link
29
+ href={contactHref}
30
+ className="flex items-center gap-2.5 text-sm text-muted-foreground hover:text-foreground transition-colors py-1"
31
+ >
32
+ <MessageSquare className="w-4 h-4" />
33
+ {l.contactUs}
34
+ </Link>
35
+ <Link
36
+ href={returnsHref}
37
+ className="flex items-center gap-2.5 text-sm text-muted-foreground hover:text-foreground transition-colors py-1"
38
+ >
39
+ <Undo2 className="w-4 h-4" />
40
+ {l.returnsExchanges}
41
+ </Link>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ export { type OrderHelpSectionProps }