@thorprovider/blocks 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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Check, Clock, MapPin, Truck } from 'lucide-react'
|
|
5
|
+
import { Button } from '../../../ui/button'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
7
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'
|
|
8
|
+
import { cn } from '../../../lib/utils'
|
|
9
|
+
import type { FulfilmentStage, FulfilmentStageDetail } from './checkout-confirmation'
|
|
10
|
+
|
|
11
|
+
const longDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
|
|
12
|
+
const stampDate = new Intl.DateTimeFormat('en-US', {
|
|
13
|
+
dateStyle: 'medium',
|
|
14
|
+
timeStyle: 'short',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const STAGE_LABELS: Record<FulfilmentStage, string> = {
|
|
18
|
+
Confirmed: 'Confirmado',
|
|
19
|
+
Packed: 'Empaquetado',
|
|
20
|
+
Shipped: 'Enviado',
|
|
21
|
+
Delivered: 'Entregado',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CheckoutConfirmationFulfilmentProgressProps {
|
|
25
|
+
order: {
|
|
26
|
+
currentStage: FulfilmentStage
|
|
27
|
+
estimatedDelivery: string
|
|
28
|
+
trackingHref?: string
|
|
29
|
+
}
|
|
30
|
+
/** Ordered by `Object.keys(fulfilmentDetails)` from the orchestrator. */
|
|
31
|
+
stages: FulfilmentStageDetail[]
|
|
32
|
+
progressLabel?: string
|
|
33
|
+
estimatedDeliveryLabel?: string
|
|
34
|
+
currentStageLabel?: string
|
|
35
|
+
inProgressLabel?: string
|
|
36
|
+
expectedByLabel?: string
|
|
37
|
+
completedLabel?: string
|
|
38
|
+
statusLabel?: string
|
|
39
|
+
locationLabel?: string
|
|
40
|
+
trackOrderLabel?: string
|
|
41
|
+
/** Kept for API symmetry with the orchestrator; the fulfilment card renders no prices. */
|
|
42
|
+
formatPrice?: (value: number) => string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The four fulfilment stages, as tabs that actually open.
|
|
47
|
+
*
|
|
48
|
+
* This was a static `<ol>` that looked exactly like a tab strip — four labels with an underline
|
|
49
|
+
* under each — and did nothing when pressed. A control that looks pressable and is not is worse
|
|
50
|
+
* than no control: the reader tries it, nothing happens, and they conclude the page is broken
|
|
51
|
+
* rather than that the row was decoration.
|
|
52
|
+
*
|
|
53
|
+
* So it is a real `Tabs`, and each stage opens the one thing a reader on this screen wants that
|
|
54
|
+
* the label alone cannot give them: when it happened, where, and what it means. Radix Tabs
|
|
55
|
+
* supplies roving focus, arrow-key movement and the tab/panel wiring; a row of buttons with a
|
|
56
|
+
* `useState` would have to fake all three.
|
|
57
|
+
*
|
|
58
|
+
* Every stage is reachable, including ones still ahead — a reader is entitled to look at what
|
|
59
|
+
* "Delivered" will mean before it happens. What a future stage does not get is a fabricated
|
|
60
|
+
* timestamp; it says what it is waiting for instead.
|
|
61
|
+
*/
|
|
62
|
+
export function CheckoutConfirmationFulfilmentProgress({
|
|
63
|
+
order,
|
|
64
|
+
stages,
|
|
65
|
+
progressLabel = 'Progreso',
|
|
66
|
+
estimatedDeliveryLabel = 'Entrega estimada',
|
|
67
|
+
currentStageLabel = 'Etapa actual',
|
|
68
|
+
inProgressLabel = 'En curso',
|
|
69
|
+
expectedByLabel = 'Se espera para',
|
|
70
|
+
completedLabel = 'Completada',
|
|
71
|
+
statusLabel = 'Estado',
|
|
72
|
+
locationLabel = 'Ubicación',
|
|
73
|
+
trackOrderLabel = 'Seguir este pedido',
|
|
74
|
+
}: CheckoutConfirmationFulfilmentProgressProps) {
|
|
75
|
+
const currentIndex = stages.findIndex(stage => stage.stage === order.currentStage)
|
|
76
|
+
const [selected, setSelected] = useState<string>(order.currentStage)
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Card className='mt-10'>
|
|
80
|
+
<CardHeader>
|
|
81
|
+
<CardTitle className='flex flex-wrap items-center justify-between gap-2 text-base'>
|
|
82
|
+
{progressLabel}
|
|
83
|
+
<span className='text-muted-foreground text-sm font-normal'>
|
|
84
|
+
{`${estimatedDeliveryLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
|
|
85
|
+
</span>
|
|
86
|
+
</CardTitle>
|
|
87
|
+
</CardHeader>
|
|
88
|
+
|
|
89
|
+
<CardContent>
|
|
90
|
+
<Tabs value={selected} onValueChange={value => setSelected(String(value))}>
|
|
91
|
+
{/*
|
|
92
|
+
`variant='line'` because these are stages along a route, not a segmented control —
|
|
93
|
+
the underline reads as progress in a way a filled pill does not.
|
|
94
|
+
|
|
95
|
+
`h-auto!` because `TabsList` pins itself to `h-8` through a
|
|
96
|
+
`group-data-horizontal/tabs:` variant, which a plain `h-auto` cannot outrank and
|
|
97
|
+
tailwind-merge does not treat as the same utility. Left alone it clipped the 24px
|
|
98
|
+
stage markers. The list scrolls rather than wrapping on a narrow screen, so the four
|
|
99
|
+
stages stay in one order.
|
|
100
|
+
|
|
101
|
+
`pb-2` is load-bearing, not spacing. `TabsTrigger` draws its active underline at
|
|
102
|
+
`bottom-[-5px]`, i.e. outside its own box, and CSS forces an axis to `auto` when the
|
|
103
|
+
other one is not `visible` — so asking for `overflow-x-auto` silently made the
|
|
104
|
+
vertical axis scrollable too, and those 7px of underline became a stray vertical
|
|
105
|
+
scrollbar beside the tabs at every width. The padding puts the underline back inside
|
|
106
|
+
the box, which leaves nothing to scroll.
|
|
107
|
+
*/}
|
|
108
|
+
<TabsList variant='line' className='h-auto! w-full justify-start gap-2 overflow-x-auto p-0 pb-2'>
|
|
109
|
+
{stages.map((stage, index) => {
|
|
110
|
+
const done = index <= currentIndex
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<TabsTrigger key={stage.stage} value={stage.stage} className='h-auto flex-1 justify-start gap-2 px-1 py-2'>
|
|
114
|
+
<span
|
|
115
|
+
aria-hidden='true'
|
|
116
|
+
className={cn(
|
|
117
|
+
'flex size-6 shrink-0 items-center justify-center rounded-full text-xs',
|
|
118
|
+
done ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{done ? <Check className='size-3.5' /> : index + 1}
|
|
122
|
+
</span>
|
|
123
|
+
{STAGE_LABELS[stage.stage]}
|
|
124
|
+
{/*
|
|
125
|
+
The stage the order is actually at, marked on the strip itself. Without it
|
|
126
|
+
the only sign of "where we are" would be whichever tab happens to be open,
|
|
127
|
+
which changes the moment the reader looks at another one.
|
|
128
|
+
*/}
|
|
129
|
+
{index === currentIndex ? <span className='sr-only'>{currentStageLabel}</span> : null}
|
|
130
|
+
{index === currentIndex ? (
|
|
131
|
+
<span aria-hidden='true' className='bg-primary size-1.5 shrink-0 rounded-full' />
|
|
132
|
+
) : null}
|
|
133
|
+
</TabsTrigger>
|
|
134
|
+
)
|
|
135
|
+
})}
|
|
136
|
+
</TabsList>
|
|
137
|
+
|
|
138
|
+
{stages.map((detail, index) => {
|
|
139
|
+
const done = index <= currentIndex
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<TabsContent key={detail.stage} value={detail.stage} className='pt-4'>
|
|
143
|
+
{index === currentIndex ? (
|
|
144
|
+
<p className='text-primary mb-2 text-xs font-medium tracking-[0.12em] uppercase'>
|
|
145
|
+
{currentStageLabel}
|
|
146
|
+
</p>
|
|
147
|
+
) : null}
|
|
148
|
+
<p className='text-sm'>{detail.summary}</p>
|
|
149
|
+
|
|
150
|
+
<dl className='text-muted-foreground mt-3 flex flex-col gap-1.5 text-sm sm:flex-row sm:gap-6'>
|
|
151
|
+
<div className='flex items-center gap-2'>
|
|
152
|
+
<dt className='sr-only'>{detail.at ? completedLabel : statusLabel}</dt>
|
|
153
|
+
<Clock className='size-4 shrink-0' aria-hidden='true' />
|
|
154
|
+
<dd className='tabular-nums'>
|
|
155
|
+
{detail.at
|
|
156
|
+
? stampDate.format(new Date(detail.at))
|
|
157
|
+
: done
|
|
158
|
+
? inProgressLabel
|
|
159
|
+
: `${expectedByLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
|
|
160
|
+
</dd>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className='flex items-center gap-2'>
|
|
164
|
+
<dt className='sr-only'>{locationLabel}</dt>
|
|
165
|
+
<MapPin className='size-4 shrink-0' aria-hidden='true' />
|
|
166
|
+
<dd>{detail.location}</dd>
|
|
167
|
+
</div>
|
|
168
|
+
</dl>
|
|
169
|
+
</TabsContent>
|
|
170
|
+
)
|
|
171
|
+
})}
|
|
172
|
+
</Tabs>
|
|
173
|
+
|
|
174
|
+
{/*
|
|
175
|
+
Render as an anchor through Radix `asChild` when a real tracking URL exists; with no
|
|
176
|
+
href the button is omitted rather than pointing at a fabricated destination.
|
|
177
|
+
*/}
|
|
178
|
+
{order.trackingHref ? (
|
|
179
|
+
<Button className='mt-6 w-full sm:w-auto' asChild>
|
|
180
|
+
<a href={order.trackingHref}>
|
|
181
|
+
<Truck />
|
|
182
|
+
{trackOrderLabel}
|
|
183
|
+
</a>
|
|
184
|
+
</Button>
|
|
185
|
+
) : null}
|
|
186
|
+
</CardContent>
|
|
187
|
+
</Card>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { CircleCheck } from 'lucide-react'
|
|
2
|
+
import { Button } from '../../../ui/button'
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
4
|
+
import { Separator } from '../../../ui/separator'
|
|
5
|
+
import { CheckoutConfirmationFulfilmentProgress } from './checkout-confirmation-fulfilment-progress'
|
|
6
|
+
|
|
7
|
+
export type FulfilmentStage = 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
|
|
8
|
+
|
|
9
|
+
export interface OrderLine {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
variant: string
|
|
13
|
+
quantity: number
|
|
14
|
+
price: number
|
|
15
|
+
image: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FulfilmentStageDetail {
|
|
19
|
+
stage: FulfilmentStage
|
|
20
|
+
/** What happened, in a sentence. Present tense for a stage that has not arrived yet. */
|
|
21
|
+
summary: string
|
|
22
|
+
/** Where it happened, or where it will. */
|
|
23
|
+
location: string
|
|
24
|
+
/**
|
|
25
|
+
* When it happened. `null` for a stage still ahead, which the panel renders as an
|
|
26
|
+
* expectation rather than as a fact — a date with no event behind it is a promise, and
|
|
27
|
+
* printing it in the same voice as a completed step is how a tracker starts lying.
|
|
28
|
+
*/
|
|
29
|
+
at: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OrderAddress {
|
|
33
|
+
name: string
|
|
34
|
+
lines: string[]
|
|
35
|
+
method: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OrderTotals {
|
|
39
|
+
subtotal: number
|
|
40
|
+
shipping: number
|
|
41
|
+
tax: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Order {
|
|
45
|
+
number: string
|
|
46
|
+
/** ISO date */
|
|
47
|
+
placedAt: string
|
|
48
|
+
email: string
|
|
49
|
+
currentStage: FulfilmentStage
|
|
50
|
+
/** ISO date */
|
|
51
|
+
estimatedDelivery: string
|
|
52
|
+
trackingHref?: string
|
|
53
|
+
shipping: OrderAddress
|
|
54
|
+
billing: OrderAddress
|
|
55
|
+
totals: OrderTotals
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CheckoutConfirmationProps {
|
|
59
|
+
order: Order
|
|
60
|
+
orderLines: OrderLine[]
|
|
61
|
+
/**
|
|
62
|
+
* Keyed by `FulfilmentStage`. The stage strip is ordered by `Object.keys`,
|
|
63
|
+
* so define the record in the canonical `Confirmed → Packed → Shipped → Delivered` order.
|
|
64
|
+
*/
|
|
65
|
+
fulfilmentDetails: Record<FulfilmentStage, FulfilmentStageDetail>
|
|
66
|
+
/** Override price formatting (locale, currency). Defaults to USD. */
|
|
67
|
+
formatPrice?: (value: number) => string
|
|
68
|
+
titleLabel?: string
|
|
69
|
+
/** Template with `{number}`/`{date}`/`{email}` placeholders, resolved internally. */
|
|
70
|
+
orderPlacedLabel?: string
|
|
71
|
+
itemsLabel?: string
|
|
72
|
+
quantityLabel?: string
|
|
73
|
+
subtotalLabel?: string
|
|
74
|
+
shippingLabel?: string
|
|
75
|
+
taxLabel?: string
|
|
76
|
+
totalLabel?: string
|
|
77
|
+
freeLabel?: string
|
|
78
|
+
shippingAddressLabel?: string
|
|
79
|
+
billingAddressLabel?: string
|
|
80
|
+
continueShoppingLabel?: string
|
|
81
|
+
/** Prefix of the support line; the `supportLabel` link renders right after it. */
|
|
82
|
+
contactSupportLabel?: string
|
|
83
|
+
supportLabel?: string
|
|
84
|
+
progressLabel?: string
|
|
85
|
+
estimatedDeliveryLabel?: string
|
|
86
|
+
currentStageLabel?: string
|
|
87
|
+
inProgressLabel?: string
|
|
88
|
+
expectedByLabel?: string
|
|
89
|
+
completedLabel?: string
|
|
90
|
+
statusLabel?: string
|
|
91
|
+
locationLabel?: string
|
|
92
|
+
trackOrderLabel?: string
|
|
93
|
+
continueShoppingHref?: string
|
|
94
|
+
supportHref?: string
|
|
95
|
+
onContinueShopping?: () => void
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
|
|
99
|
+
const longDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The screen that closes the purchase journey the checkout blocks open.
|
|
103
|
+
*
|
|
104
|
+
* This file has no hooks or handlers, so it still renders as a server component straight from
|
|
105
|
+
* your order record. Only the progress tabs are interactive, and they carry their own
|
|
106
|
+
* `'use client'` — the boundary is one child rather than the whole page.
|
|
107
|
+
*/
|
|
108
|
+
export function CheckoutConfirmation({
|
|
109
|
+
order,
|
|
110
|
+
orderLines,
|
|
111
|
+
fulfilmentDetails,
|
|
112
|
+
formatPrice,
|
|
113
|
+
titleLabel = '¡Gracias! Tu pedido está en camino',
|
|
114
|
+
orderPlacedLabel = 'Pedido {number} realizado el {date}. Enviamos la confirmación a {email}.',
|
|
115
|
+
itemsLabel = 'Artículos',
|
|
116
|
+
quantityLabel = 'Cantidad',
|
|
117
|
+
subtotalLabel = 'Subtotal',
|
|
118
|
+
shippingLabel = 'Envío',
|
|
119
|
+
taxLabel = 'Impuestos',
|
|
120
|
+
totalLabel = 'Total',
|
|
121
|
+
freeLabel = 'Gratis',
|
|
122
|
+
shippingAddressLabel = 'Envío',
|
|
123
|
+
billingAddressLabel = 'Facturación',
|
|
124
|
+
continueShoppingLabel = 'Seguir comprando',
|
|
125
|
+
contactSupportLabel = '¿Algo falla con este pedido?',
|
|
126
|
+
supportLabel = 'Contacta con soporte',
|
|
127
|
+
progressLabel = 'Progreso',
|
|
128
|
+
estimatedDeliveryLabel = 'Entrega estimada',
|
|
129
|
+
currentStageLabel = 'Etapa actual',
|
|
130
|
+
inProgressLabel = 'En curso',
|
|
131
|
+
expectedByLabel = 'Se espera para',
|
|
132
|
+
completedLabel = 'Completada',
|
|
133
|
+
statusLabel = 'Estado',
|
|
134
|
+
locationLabel = 'Ubicación',
|
|
135
|
+
trackOrderLabel = 'Seguir este pedido',
|
|
136
|
+
continueShoppingHref,
|
|
137
|
+
supportHref,
|
|
138
|
+
onContinueShopping,
|
|
139
|
+
}: CheckoutConfirmationProps) {
|
|
140
|
+
const total = order.totals.subtotal + order.totals.shipping + order.totals.tax
|
|
141
|
+
const format = formatPrice ?? (value => usdFormatter.format(value))
|
|
142
|
+
const fulfilmentStages = Object.keys(fulfilmentDetails) as FulfilmentStage[]
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className='w-full py-16 sm:py-24'>
|
|
146
|
+
<div className='mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8'>
|
|
147
|
+
<div className='text-center'>
|
|
148
|
+
<span
|
|
149
|
+
aria-hidden='true'
|
|
150
|
+
className='bg-primary/10 text-primary mx-auto flex size-12 items-center justify-center rounded-full'
|
|
151
|
+
>
|
|
152
|
+
<CircleCheck className='size-6' />
|
|
153
|
+
</span>
|
|
154
|
+
<h1 className='mt-4 text-3xl font-semibold tracking-tight'>{titleLabel}</h1>
|
|
155
|
+
<p className='text-muted-foreground mt-3'>
|
|
156
|
+
{orderPlacedLabel
|
|
157
|
+
.replace('{number}', order.number)
|
|
158
|
+
.replace('{date}', longDate.format(new Date(order.placedAt)))
|
|
159
|
+
.replace('{email}', order.email)}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<CheckoutConfirmationFulfilmentProgress
|
|
164
|
+
order={{
|
|
165
|
+
currentStage: order.currentStage,
|
|
166
|
+
estimatedDelivery: order.estimatedDelivery,
|
|
167
|
+
trackingHref: order.trackingHref,
|
|
168
|
+
}}
|
|
169
|
+
stages={fulfilmentStages.map(stage => fulfilmentDetails[stage])}
|
|
170
|
+
progressLabel={progressLabel}
|
|
171
|
+
estimatedDeliveryLabel={estimatedDeliveryLabel}
|
|
172
|
+
currentStageLabel={currentStageLabel}
|
|
173
|
+
inProgressLabel={inProgressLabel}
|
|
174
|
+
expectedByLabel={expectedByLabel}
|
|
175
|
+
completedLabel={completedLabel}
|
|
176
|
+
statusLabel={statusLabel}
|
|
177
|
+
locationLabel={locationLabel}
|
|
178
|
+
trackOrderLabel={trackOrderLabel}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<Card className='mt-6'>
|
|
182
|
+
<CardHeader>
|
|
183
|
+
<CardTitle className='text-base'>{itemsLabel}</CardTitle>
|
|
184
|
+
</CardHeader>
|
|
185
|
+
<CardContent>
|
|
186
|
+
<ul className='divide-border divide-y'>
|
|
187
|
+
{orderLines.map(line => (
|
|
188
|
+
<li key={line.id} className='flex gap-4 py-4 first:pt-0 last:pb-0'>
|
|
189
|
+
<img
|
|
190
|
+
src={line.image || undefined}
|
|
191
|
+
alt={line.name}
|
|
192
|
+
width={72}
|
|
193
|
+
height={88}
|
|
194
|
+
className='bg-muted h-22 w-18 shrink-0 rounded-md object-cover'
|
|
195
|
+
/>
|
|
196
|
+
<div className='min-w-0 flex-1'>
|
|
197
|
+
<p className='text-sm font-medium'>{line.name}</p>
|
|
198
|
+
<p className='text-muted-foreground text-sm'>{line.variant}</p>
|
|
199
|
+
<p className='text-muted-foreground text-sm tabular-nums'>
|
|
200
|
+
{`${quantityLabel} ${line.quantity}`}
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
<p className='text-sm font-medium tabular-nums'>
|
|
204
|
+
{format(line.price * line.quantity)}
|
|
205
|
+
</p>
|
|
206
|
+
</li>
|
|
207
|
+
))}
|
|
208
|
+
</ul>
|
|
209
|
+
|
|
210
|
+
<Separator className='my-4' />
|
|
211
|
+
|
|
212
|
+
<dl className='flex flex-col gap-2 text-sm'>
|
|
213
|
+
<div className='flex justify-between'>
|
|
214
|
+
<dt className='text-muted-foreground'>{subtotalLabel}</dt>
|
|
215
|
+
<dd className='tabular-nums'>{format(order.totals.subtotal)}</dd>
|
|
216
|
+
</div>
|
|
217
|
+
<div className='flex justify-between'>
|
|
218
|
+
<dt className='text-muted-foreground'>{shippingLabel}</dt>
|
|
219
|
+
<dd className='tabular-nums'>
|
|
220
|
+
{order.totals.shipping === 0 ? freeLabel : format(order.totals.shipping)}
|
|
221
|
+
</dd>
|
|
222
|
+
</div>
|
|
223
|
+
<div className='flex justify-between'>
|
|
224
|
+
<dt className='text-muted-foreground'>{taxLabel}</dt>
|
|
225
|
+
<dd className='tabular-nums'>{format(order.totals.tax)}</dd>
|
|
226
|
+
</div>
|
|
227
|
+
<div className='flex justify-between pt-2 text-base font-semibold'>
|
|
228
|
+
<dt>{totalLabel}</dt>
|
|
229
|
+
<dd className='tabular-nums'>{format(total)}</dd>
|
|
230
|
+
</div>
|
|
231
|
+
</dl>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
<div className='mt-6 grid gap-6 sm:grid-cols-2'>
|
|
236
|
+
{[
|
|
237
|
+
{ title: shippingAddressLabel, address: order.shipping },
|
|
238
|
+
{ title: billingAddressLabel, address: order.billing },
|
|
239
|
+
].map(section => (
|
|
240
|
+
<Card key={section.title}>
|
|
241
|
+
<CardHeader>
|
|
242
|
+
<CardTitle className='text-base'>{section.title}</CardTitle>
|
|
243
|
+
</CardHeader>
|
|
244
|
+
<CardContent className='text-sm'>
|
|
245
|
+
<address className='text-muted-foreground not-italic'>
|
|
246
|
+
<span className='text-foreground block font-medium'>{section.address.name}</span>
|
|
247
|
+
{section.address.lines.map(line => (
|
|
248
|
+
<span key={line} className='block'>
|
|
249
|
+
{line}
|
|
250
|
+
</span>
|
|
251
|
+
))}
|
|
252
|
+
</address>
|
|
253
|
+
<p className='text-muted-foreground mt-3'>{section.address.method}</p>
|
|
254
|
+
</CardContent>
|
|
255
|
+
</Card>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div className='mt-8 flex flex-col items-center gap-2'>
|
|
260
|
+
{continueShoppingHref ? (
|
|
261
|
+
<Button variant='ghost' asChild>
|
|
262
|
+
<a href={continueShoppingHref}>{continueShoppingLabel}</a>
|
|
263
|
+
</Button>
|
|
264
|
+
) : onContinueShopping ? (
|
|
265
|
+
<Button variant='ghost' onClick={onContinueShopping}>
|
|
266
|
+
{continueShoppingLabel}
|
|
267
|
+
</Button>
|
|
268
|
+
) : null}
|
|
269
|
+
{supportHref ? (
|
|
270
|
+
<p className='text-muted-foreground text-sm'>
|
|
271
|
+
{contactSupportLabel}{' '}
|
|
272
|
+
<a href={supportHref} className='underline underline-offset-4'>
|
|
273
|
+
{supportLabel}
|
|
274
|
+
</a>
|
|
275
|
+
</p>
|
|
276
|
+
) : null}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default CheckoutConfirmation
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FieldDescription, FieldError } from '../../../ui/field'
|
|
4
|
+
import type { CheckoutFieldError } from './checkout-form-types'
|
|
5
|
+
|
|
6
|
+
export function checkoutFieldAria(id: string, errors?: CheckoutFieldError[]) {
|
|
7
|
+
const invalid = !!errors?.length
|
|
8
|
+
return {
|
|
9
|
+
'aria-invalid': invalid || undefined,
|
|
10
|
+
'aria-describedby': invalid ? `${id}-error` : undefined,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CheckoutFieldFeedback({
|
|
15
|
+
id,
|
|
16
|
+
errors,
|
|
17
|
+
description,
|
|
18
|
+
}: {
|
|
19
|
+
id: string
|
|
20
|
+
errors?: CheckoutFieldError[]
|
|
21
|
+
description?: string
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{description && <FieldDescription id={`${id}-description`}>{description}</FieldDescription>}
|
|
26
|
+
{errors?.length ? <FieldError id={`${id}-error`} errors={errors} /> : null}
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
|
|
4
|
+
import { Input } from '../../../ui/input'
|
|
5
|
+
import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
|
|
6
|
+
import type {
|
|
7
|
+
CheckoutForm1ChangeHandler,
|
|
8
|
+
CheckoutForm1FieldDescriptions,
|
|
9
|
+
CheckoutForm1FieldErrors,
|
|
10
|
+
CheckoutForm1FormData,
|
|
11
|
+
} from './checkout-form-1'
|
|
12
|
+
|
|
13
|
+
export interface CheckoutForm1ContactStepProps {
|
|
14
|
+
formData: CheckoutForm1FormData
|
|
15
|
+
onChange: CheckoutForm1ChangeHandler
|
|
16
|
+
fieldErrors?: CheckoutForm1FieldErrors
|
|
17
|
+
fieldDescriptions?: CheckoutForm1FieldDescriptions
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CheckoutForm1ContactStep({
|
|
21
|
+
formData,
|
|
22
|
+
onChange,
|
|
23
|
+
fieldErrors,
|
|
24
|
+
fieldDescriptions,
|
|
25
|
+
}: CheckoutForm1ContactStepProps) {
|
|
26
|
+
return (
|
|
27
|
+
<FieldGroup className="grid gap-4">
|
|
28
|
+
<Field data-invalid={!!fieldErrors?.email?.length}>
|
|
29
|
+
<FieldLabel htmlFor="checkout-email">Email</FieldLabel>
|
|
30
|
+
<Input
|
|
31
|
+
id="checkout-email"
|
|
32
|
+
type="text"
|
|
33
|
+
inputMode="email"
|
|
34
|
+
placeholder="john@example.com"
|
|
35
|
+
value={formData.email}
|
|
36
|
+
onChange={(e) => onChange('email', e.target.value)}
|
|
37
|
+
{...checkoutFieldAria('checkout-email', fieldErrors?.email)}
|
|
38
|
+
/>
|
|
39
|
+
<CheckoutFieldFeedback
|
|
40
|
+
id="checkout-email"
|
|
41
|
+
errors={fieldErrors?.email}
|
|
42
|
+
description={fieldDescriptions?.email}
|
|
43
|
+
/>
|
|
44
|
+
</Field>
|
|
45
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
46
|
+
<Field data-invalid={!!fieldErrors?.firstName?.length}>
|
|
47
|
+
<FieldLabel htmlFor="checkout-first-name">First name</FieldLabel>
|
|
48
|
+
<Input
|
|
49
|
+
id="checkout-first-name"
|
|
50
|
+
placeholder="John"
|
|
51
|
+
value={formData.firstName}
|
|
52
|
+
onChange={(e) => onChange('firstName', e.target.value)}
|
|
53
|
+
{...checkoutFieldAria('checkout-first-name', fieldErrors?.firstName)}
|
|
54
|
+
/>
|
|
55
|
+
<CheckoutFieldFeedback
|
|
56
|
+
id="checkout-first-name"
|
|
57
|
+
errors={fieldErrors?.firstName}
|
|
58
|
+
description={fieldDescriptions?.firstName}
|
|
59
|
+
/>
|
|
60
|
+
</Field>
|
|
61
|
+
<Field data-invalid={!!fieldErrors?.lastName?.length}>
|
|
62
|
+
<FieldLabel htmlFor="checkout-last-name">Last name</FieldLabel>
|
|
63
|
+
<Input
|
|
64
|
+
id="checkout-last-name"
|
|
65
|
+
placeholder="Doe"
|
|
66
|
+
value={formData.lastName}
|
|
67
|
+
onChange={(e) => onChange('lastName', e.target.value)}
|
|
68
|
+
{...checkoutFieldAria('checkout-last-name', fieldErrors?.lastName)}
|
|
69
|
+
/>
|
|
70
|
+
<CheckoutFieldFeedback
|
|
71
|
+
id="checkout-last-name"
|
|
72
|
+
errors={fieldErrors?.lastName}
|
|
73
|
+
description={fieldDescriptions?.lastName}
|
|
74
|
+
/>
|
|
75
|
+
</Field>
|
|
76
|
+
</div>
|
|
77
|
+
<Field data-invalid={!!fieldErrors?.phone?.length}>
|
|
78
|
+
<FieldLabel htmlFor="checkout-phone">Phone (optional)</FieldLabel>
|
|
79
|
+
<Input
|
|
80
|
+
id="checkout-phone"
|
|
81
|
+
type="text"
|
|
82
|
+
inputMode="tel"
|
|
83
|
+
placeholder="+1 (555) 000-0000"
|
|
84
|
+
value={formData.phone}
|
|
85
|
+
onChange={(e) => onChange('phone', e.target.value)}
|
|
86
|
+
{...checkoutFieldAria('checkout-phone', fieldErrors?.phone)}
|
|
87
|
+
/>
|
|
88
|
+
<CheckoutFieldFeedback
|
|
89
|
+
id="checkout-phone"
|
|
90
|
+
errors={fieldErrors?.phone}
|
|
91
|
+
description={fieldDescriptions?.phone}
|
|
92
|
+
/>
|
|
93
|
+
</Field>
|
|
94
|
+
</FieldGroup>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Field, FieldLabel } from '../../../ui/field'
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectGroup,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from '../../../ui/select'
|
|
12
|
+
import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
|
|
13
|
+
import type { CheckoutFieldError } from './checkout-form-types'
|
|
14
|
+
import type { CheckoutForm1CountryOption } from './checkout-form-1'
|
|
15
|
+
|
|
16
|
+
export interface CheckoutForm1CountryFieldProps {
|
|
17
|
+
countries: CheckoutForm1CountryOption[]
|
|
18
|
+
value: string
|
|
19
|
+
onValueChange: (value: string) => void
|
|
20
|
+
errors?: CheckoutFieldError[]
|
|
21
|
+
description?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CheckoutForm1CountryField({
|
|
25
|
+
countries,
|
|
26
|
+
value,
|
|
27
|
+
onValueChange,
|
|
28
|
+
errors,
|
|
29
|
+
description,
|
|
30
|
+
}: CheckoutForm1CountryFieldProps) {
|
|
31
|
+
return (
|
|
32
|
+
<Field data-invalid={!!errors?.length}>
|
|
33
|
+
<FieldLabel htmlFor="checkout-country">Country</FieldLabel>
|
|
34
|
+
<Select value={value} onValueChange={onValueChange}>
|
|
35
|
+
<SelectTrigger id="checkout-country" {...checkoutFieldAria('checkout-country', errors)}>
|
|
36
|
+
<SelectValue placeholder="Select a country" />
|
|
37
|
+
</SelectTrigger>
|
|
38
|
+
<SelectContent>
|
|
39
|
+
<SelectGroup>
|
|
40
|
+
{countries.map((country) => (
|
|
41
|
+
<SelectItem key={country.value} value={country.value}>
|
|
42
|
+
{country.label}
|
|
43
|
+
</SelectItem>
|
|
44
|
+
))}
|
|
45
|
+
</SelectGroup>
|
|
46
|
+
</SelectContent>
|
|
47
|
+
</Select>
|
|
48
|
+
<CheckoutFieldFeedback id="checkout-country" errors={errors} description={description} />
|
|
49
|
+
</Field>
|
|
50
|
+
)
|
|
51
|
+
}
|