@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.
- package/package.json +226 -0
- package/src/api/auth.ts +108 -0
- package/src/api/carts.ts +506 -0
- package/src/api/categories.ts +184 -0
- package/src/api/checkout.ts +440 -0
- package/src/api/collections.ts +130 -0
- package/src/api/consent.ts +75 -0
- package/src/api/content.ts +125 -0
- package/src/api/customers.ts +307 -0
- package/src/api/gift-cards.ts +112 -0
- package/src/api/http.ts +122 -0
- package/src/api/index.ts +29 -0
- package/src/api/integrations.ts +130 -0
- package/src/api/menus.ts +77 -0
- package/src/api/metaobjects.ts +136 -0
- package/src/api/orders.ts +290 -0
- package/src/api/products.ts +303 -0
- package/src/api/redirects.ts +37 -0
- package/src/api/regions.ts +141 -0
- package/src/api/reviews.ts +259 -0
- package/src/api/search.ts +133 -0
- package/src/api/types.ts +91 -0
- package/src/cart-drawer/cart-drawer.tsx +86 -0
- package/src/cart-drawer/context.tsx +569 -0
- package/src/cart-drawer/continue-shopping.tsx +28 -0
- package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
- package/src/cart-drawer/empty.tsx +52 -0
- package/src/cart-drawer/free-gift.tsx +71 -0
- package/src/cart-drawer/gift-wrap.tsx +83 -0
- package/src/cart-drawer/header.tsx +52 -0
- package/src/cart-drawer/index.ts +69 -0
- package/src/cart-drawer/item/index.tsx +164 -0
- package/src/cart-drawer/item/quantity.tsx +100 -0
- package/src/cart-drawer/item/upsell.tsx +110 -0
- package/src/cart-drawer/item/variant.tsx +46 -0
- package/src/cart-drawer/labels-bg.ts +72 -0
- package/src/cart-drawer/labels.ts +119 -0
- package/src/cart-drawer/notes.tsx +131 -0
- package/src/cart-drawer/payment-badges.tsx +96 -0
- package/src/cart-drawer/promo-banner.tsx +43 -0
- package/src/cart-drawer/rewards-points.tsx +78 -0
- package/src/cart-drawer/sticky-footer.tsx +73 -0
- package/src/cart-drawer/summary-breakdown.tsx +196 -0
- package/src/cart-drawer/template.tsx +225 -0
- package/src/cart-drawer/tiered-progress.tsx +168 -0
- package/src/checkout/address-error-copy.ts +119 -0
- package/src/checkout/address-form.tsx +224 -0
- package/src/checkout/address-select.tsx +79 -0
- package/src/checkout/boxnow-locker-selector.tsx +410 -0
- package/src/checkout/checkout-client.tsx +222 -0
- package/src/checkout/company-details.tsx +94 -0
- package/src/checkout/compare-addresses.ts +40 -0
- package/src/checkout/context.tsx +76 -0
- package/src/checkout/discount-section.tsx +218 -0
- package/src/checkout/econt-office-selector.tsx +332 -0
- package/src/checkout/error-message.tsx +25 -0
- package/src/checkout/geocode.ts +154 -0
- package/src/checkout/gift-card-section.tsx +224 -0
- package/src/checkout/index.ts +74 -0
- package/src/checkout/labels-bg.ts +128 -0
- package/src/checkout/labels.ts +263 -0
- package/src/checkout/line-item-card.tsx +152 -0
- package/src/checkout/order-summary.tsx +524 -0
- package/src/checkout/payment-button.tsx +373 -0
- package/src/checkout/payment-error-copy.ts +269 -0
- package/src/checkout/payment-method-list.tsx +365 -0
- package/src/checkout/payment-wrapper.tsx +102 -0
- package/src/checkout/promotion-error-copy.ts +124 -0
- package/src/checkout/shipping-method-list.tsx +335 -0
- package/src/checkout/stripe-wrapper.tsx +165 -0
- package/src/checkout/use-checkout-orchestration.ts +1504 -0
- package/src/common/cart-button-client.tsx +39 -0
- package/src/common/cart-button.tsx +28 -0
- package/src/common/country-select.tsx +65 -0
- package/src/common/delete-button.tsx +66 -0
- package/src/common/index.ts +17 -0
- package/src/common/language-select.tsx +78 -0
- package/src/common/localized-link.tsx +45 -0
- package/src/common/skeleton.tsx +29 -0
- package/src/index.ts +12 -0
- package/src/lib/cart-helpers.ts +113 -0
- package/src/lib/dual-price.tsx +73 -0
- package/src/lib/get-percentage-diff.ts +5 -0
- package/src/lib/get-product-price.ts +133 -0
- package/src/lib/hooks/use-intersection.ts +30 -0
- package/src/lib/hooks/use-toggle-state.ts +25 -0
- package/src/lib/money.ts +73 -0
- package/src/lib/payment-constants.ts +66 -0
- package/src/lib/product.ts +22 -0
- package/src/lib/sort-products.ts +63 -0
- package/src/lib/store-api-error.ts +36 -0
- package/src/lib/utils.ts +16 -0
- package/src/order/context.tsx +32 -0
- package/src/order/index.ts +63 -0
- package/src/order/labels-bg.ts +39 -0
- package/src/order/labels.ts +79 -0
- package/src/order/order-address-card.tsx +47 -0
- package/src/order/order-completed-template.tsx +165 -0
- package/src/order/order-confirmation-header.tsx +65 -0
- package/src/order/order-delivery-card.tsx +258 -0
- package/src/order/order-help-section.tsx +47 -0
- package/src/order/order-item.tsx +201 -0
- package/src/order/order-items-list.tsx +52 -0
- package/src/order/order-payment-card.tsx +95 -0
- package/src/order/order-timeline.tsx +141 -0
- package/src/order/order-totals.tsx +245 -0
- package/src/primitives/field.tsx +125 -0
- package/src/primitives/select-field.tsx +77 -0
- package/src/primitives/ui/accordion.tsx +61 -0
- package/src/primitives/ui/button.tsx +68 -0
- package/src/primitives/ui/collapsible.tsx +16 -0
- package/src/primitives/ui/dialog.tsx +112 -0
- package/src/primitives/ui/input.tsx +30 -0
- package/src/primitives/ui/label.tsx +31 -0
- package/src/primitives/ui/popover.tsx +38 -0
- package/src/primitives/ui/select.tsx +163 -0
- package/src/primitives/ui/sheet.tsx +131 -0
- package/src/primitives/ui/tabs.tsx +62 -0
- package/src/products/context.tsx +34 -0
- package/src/products/image-gallery.tsx +43 -0
- package/src/products/index.ts +44 -0
- package/src/products/labels-bg.ts +35 -0
- package/src/products/labels.ts +57 -0
- package/src/products/mobile-actions.tsx +180 -0
- package/src/products/option-select.tsx +67 -0
- package/src/products/preview-price.tsx +36 -0
- package/src/products/product-actions-wrapper.tsx +58 -0
- package/src/products/product-actions.tsx +217 -0
- package/src/products/product-info.tsx +43 -0
- package/src/products/product-preview.tsx +49 -0
- package/src/products/product-price.tsx +69 -0
- package/src/products/product-tabs.tsx +169 -0
- package/src/products/product-template.tsx +114 -0
- package/src/products/purchase-options.tsx +130 -0
- package/src/products/related-products.tsx +86 -0
- package/src/products/thumbnail.tsx +71 -0
- package/src/products/variant-matching.ts +71 -0
- package/src/reviews-ui/helpers.ts +174 -0
- package/src/reviews-ui/index.ts +74 -0
- package/src/reviews-ui/labels-bg.ts +91 -0
- package/src/reviews-ui/labels.ts +199 -0
- package/src/reviews-ui/photo-upload.tsx +345 -0
- package/src/reviews-ui/review-list.tsx +249 -0
- package/src/reviews-ui/review-widget.tsx +224 -0
- package/src/reviews-ui/review-wizard.tsx +560 -0
- package/src/reviews-ui/star-badge.tsx +104 -0
- package/src/reviews-ui/wizard-state.ts +81 -0
- package/src/store/category-template.tsx +129 -0
- package/src/store/collection-template.tsx +139 -0
- package/src/store/index.ts +41 -0
- package/src/store/labels-bg.ts +22 -0
- package/src/store/labels.ts +52 -0
- package/src/store/paginated-products.tsx +116 -0
- package/src/store/pagination.tsx +103 -0
- package/src/store/search-params.ts +256 -0
- package/src/store/search-template.tsx +249 -0
- package/src/store/skeleton-product-grid.tsx +26 -0
- package/src/store/sort-select.tsx +81 -0
- package/src/store/store-template.tsx +65 -0
- package/src/tracking/attribution.ts +418 -0
- package/src/tracking/consent-banner.tsx +355 -0
- package/src/tracking/consent-init.tsx +44 -0
- package/src/tracking/consent.ts +243 -0
- package/src/tracking/fbq.ts +168 -0
- package/src/tracking/ga4.tsx +49 -0
- package/src/tracking/get-tracking-attribution.ts +224 -0
- package/src/tracking/get-tracking-config.ts +50 -0
- package/src/tracking/gtag.ts +200 -0
- package/src/tracking/index.ts +133 -0
- package/src/tracking/meta-pixel.tsx +166 -0
- package/src/tracking/rybbit-events.ts +242 -0
- package/src/tracking/rybbit.tsx +40 -0
- package/src/tracking/types.ts +185 -0
- package/src/tracking/use-engagement-time.ts +58 -0
- 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 }
|