@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,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
|
+
}
|