@thorprovider/blocks 0.1.3 → 1.0.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/CHANGELOG.md +107 -0
- package/README.md +73 -29
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
- package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
- package/src/index.ts +77 -48
- package/src/lib/contracts-curated.test.ts +42 -24
- package/src/lib/contracts.test.ts +143 -2
- package/src/lib/contracts.ts +346 -14
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -1
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
|
@@ -1,74 +1,48 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { ShoppingCart, Zap, Clock, CreditCard, Wallet, Minus, Plus, X } from 'lucide-react'
|
|
3
|
+
import { useId, useState } from 'react'
|
|
4
|
+
import { ShoppingCart as ShoppingCartIcon, Zap, Clock, CreditCard, Wallet, Minus, Plus, X } from 'lucide-react'
|
|
5
5
|
import type { LucideIcon } from 'lucide-react'
|
|
6
6
|
import type { Money } from '@thorprovider/types'
|
|
7
7
|
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
8
8
|
import { Button } from '../../../ui/button'
|
|
9
|
+
import { Input } from '../../../ui/input'
|
|
9
10
|
import { Separator } from '../../../ui/separator'
|
|
10
11
|
import { Progress } from '../../../ui/progress'
|
|
11
12
|
import { cn } from '../../../lib/utils'
|
|
12
13
|
import { formatMoney, isAvailableMoney } from '../../../lib/money'
|
|
14
|
+
import type { CartTotals, ShoppingCartItem, ShoppingCartSuggestion } from '../../../lib/contracts'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
id: string
|
|
17
|
-
name: string
|
|
18
|
-
price: number
|
|
19
|
-
quantity: number
|
|
20
|
-
/** Optional; an absent image is omitted rather than serialized as `""`. */
|
|
21
|
-
image?: string
|
|
22
|
-
variant?: string
|
|
23
|
-
/** Canonical backend per-unit price; preferred for display when present. */
|
|
24
|
-
unitPrice?: Money
|
|
25
|
-
/** Canonical backend line total; preferred over `price * quantity`. */
|
|
26
|
-
lineTotal?: Money
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ShoppingCart4Totals {
|
|
30
|
-
subtotal: Money
|
|
31
|
-
discount?: Money
|
|
32
|
-
tax?: Money
|
|
33
|
-
shipping?: Money
|
|
34
|
-
total: Money
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type ShoppingCart4TotalsState = 'idle' | 'pending' | 'error'
|
|
38
|
-
|
|
39
|
-
export interface ShoppingCart4Promotion {
|
|
16
|
+
/** Host-supplied promotion definition. Icons are React-side only. */
|
|
17
|
+
export interface ShoppingCartPromotion {
|
|
40
18
|
threshold: number
|
|
41
19
|
reward: string
|
|
42
20
|
icon: LucideIcon
|
|
43
21
|
}
|
|
44
22
|
|
|
45
|
-
export
|
|
46
|
-
id: string
|
|
47
|
-
name: string
|
|
48
|
-
price: number
|
|
49
|
-
image?: string
|
|
50
|
-
}
|
|
23
|
+
export type ShoppingCartTotalsState = 'idle' | 'pending' | 'error'
|
|
51
24
|
|
|
52
|
-
export type
|
|
25
|
+
export type ShoppingCartPresentation = 'compact' | 'page'
|
|
53
26
|
|
|
54
|
-
export interface
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
27
|
+
export interface ShoppingCartProps {
|
|
28
|
+
/** Canonical cart lines keyed by backend cart line-item id. */
|
|
29
|
+
items: ShoppingCartItem[]
|
|
30
|
+
promotions?: ShoppingCartPromotion[]
|
|
31
|
+
recentlyViewed?: ShoppingCartSuggestion[]
|
|
58
32
|
/**
|
|
59
33
|
* Layout presentation. `compact` (default) is the collapsible quick-cart
|
|
60
34
|
* card; `page` is a wide, always-expanded review layout with a primary
|
|
61
35
|
* heading, no collapse control, and normal page scrolling. Both use the same
|
|
62
36
|
* line controls and authoritative monetary rendering.
|
|
63
37
|
*/
|
|
64
|
-
presentation?:
|
|
38
|
+
presentation?: ShoppingCartPresentation
|
|
65
39
|
/**
|
|
66
|
-
* Backend-authoritative totals. The
|
|
67
|
-
*
|
|
40
|
+
* Backend-authoritative totals. The cart renders these verbatim; it never
|
|
41
|
+
* computes subtotal/tax/shipping/total locally.
|
|
68
42
|
*/
|
|
69
|
-
totals?:
|
|
43
|
+
totals?: CartTotals
|
|
70
44
|
/** Controlled state of the totals fetch/mutation. */
|
|
71
|
-
totalsState?:
|
|
45
|
+
totalsState?: ShoppingCartTotalsState
|
|
72
46
|
totalsErrorLabel?: string
|
|
73
47
|
/** Shown when a required total amount is absent or malformed (never as zero). */
|
|
74
48
|
totalsUnavailableLabel?: string
|
|
@@ -78,10 +52,6 @@ export interface ShoppingCart4Props {
|
|
|
78
52
|
onRetryTotals?: () => void
|
|
79
53
|
/** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
|
|
80
54
|
formatMoney?: (money: Money) => string
|
|
81
|
-
/** @deprecated legacy numeric formatter, used only when `totals` is absent. */
|
|
82
|
-
formatPrice?: (value: number) => string
|
|
83
|
-
/** @deprecated legacy local tax rate, used only when `totals` is absent. */
|
|
84
|
-
taxRate?: number
|
|
85
55
|
// labels (Spanish defaults):
|
|
86
56
|
titleLabel?: string // 'Carrito de compras'
|
|
87
57
|
subtotalLabel?: string // 'Subtotal'
|
|
@@ -100,6 +70,9 @@ export interface ShoppingCart4Props {
|
|
|
100
70
|
estimatedDeliveryLabel?: string
|
|
101
71
|
secureCheckoutLabel?: string // 'Pago seguro • SSL cifrado'
|
|
102
72
|
recentlyViewedLabel?: string // 'Vistos recientemente'
|
|
73
|
+
promoPromptLabel?: string // '¿Tienes un código de descuento?'
|
|
74
|
+
promoPlaceholderLabel?: string // 'Introduce tu código'
|
|
75
|
+
promoApplyLabel?: string // 'Aplicar'
|
|
103
76
|
collapseLabel?: string // 'Contraer carrito'
|
|
104
77
|
expandLabel?: string // 'Expandir carrito'
|
|
105
78
|
checkoutLabel?: string // 'Ver carrito y finalizar'
|
|
@@ -127,12 +100,19 @@ export interface ShoppingCart4Props {
|
|
|
127
100
|
onQuickCheckout?: () => void
|
|
128
101
|
onPayWithApple?: () => void
|
|
129
102
|
onPayWithGoogle?: () => void
|
|
103
|
+
/**
|
|
104
|
+
* Report an entered promotion code to the host. The block never validates
|
|
105
|
+
* or resolves codes; the authoritative result arrives through `totals`
|
|
106
|
+
* (discount) and `promotions`. When absent, the promo affordance is not
|
|
107
|
+
* rendered.
|
|
108
|
+
*/
|
|
109
|
+
onApplyPromo?: (code: string) => void
|
|
130
110
|
onRecentlyViewedClick?: (id: string) => void
|
|
131
111
|
/** Page presentation only: navigate back to browsing. */
|
|
132
112
|
onContinueShopping?: () => void
|
|
133
113
|
}
|
|
134
114
|
|
|
135
|
-
const
|
|
115
|
+
const fallbackFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
|
|
136
116
|
|
|
137
117
|
const interpolate = (template: string, values: Record<string, string>) =>
|
|
138
118
|
template.replace(/\$\{(\w+)\}|\{(\w+)\}/g, (match, dollarKey, braceKey) => {
|
|
@@ -140,7 +120,7 @@ const interpolate = (template: string, values: Record<string, string>) =>
|
|
|
140
120
|
return values[key] ?? match
|
|
141
121
|
})
|
|
142
122
|
|
|
143
|
-
export function
|
|
123
|
+
export function ShoppingCart({
|
|
144
124
|
items,
|
|
145
125
|
promotions,
|
|
146
126
|
recentlyViewed,
|
|
@@ -153,7 +133,6 @@ export function ShoppingCart4({
|
|
|
153
133
|
retryLabel = 'Reintentar',
|
|
154
134
|
onRetryTotals,
|
|
155
135
|
formatMoney: formatMoneyProp,
|
|
156
|
-
formatPrice,
|
|
157
136
|
titleLabel = 'Carrito de compras',
|
|
158
137
|
subtotalLabel = 'Subtotal',
|
|
159
138
|
discountLabel = 'Descuento',
|
|
@@ -171,6 +150,9 @@ export function ShoppingCart4({
|
|
|
171
150
|
estimatedDeliveryLabel,
|
|
172
151
|
secureCheckoutLabel = 'Pago seguro • SSL cifrado',
|
|
173
152
|
recentlyViewedLabel = 'Vistos recientemente',
|
|
153
|
+
promoPromptLabel = '¿Tienes un código de descuento?',
|
|
154
|
+
promoPlaceholderLabel = 'Introduce tu código',
|
|
155
|
+
promoApplyLabel = 'Aplicar',
|
|
174
156
|
collapseLabel = 'Contraer carrito',
|
|
175
157
|
expandLabel = 'Expandir carrito',
|
|
176
158
|
checkoutLabel = 'Ver carrito y finalizar',
|
|
@@ -189,16 +171,29 @@ export function ShoppingCart4({
|
|
|
189
171
|
onQuickCheckout,
|
|
190
172
|
onPayWithApple,
|
|
191
173
|
onPayWithGoogle,
|
|
174
|
+
onApplyPromo,
|
|
192
175
|
onRecentlyViewedClick,
|
|
193
176
|
onContinueShopping,
|
|
194
|
-
}:
|
|
177
|
+
}: ShoppingCartProps) {
|
|
195
178
|
const [isExpanded, setIsExpanded] = useState(true)
|
|
196
179
|
const [quickCheckout, setQuickCheckout] = useState(false)
|
|
180
|
+
const [promoCode, setPromoCode] = useState('')
|
|
181
|
+
const promoInputId = useId()
|
|
197
182
|
|
|
198
183
|
const isPage = presentation === 'page'
|
|
199
184
|
// The page presentation is always expanded; only the compact card collapses.
|
|
200
185
|
const expanded = isPage ? true : isExpanded
|
|
201
|
-
|
|
186
|
+
// Display currency derives from the authoritative Money subtotal; the USD
|
|
187
|
+
// formatter is only the fallback when no authoritative currency exists.
|
|
188
|
+
const format = formatMoneyProp
|
|
189
|
+
? (value: number) => {
|
|
190
|
+
const money: Money = { amount: String(value), currencyCode: totals?.subtotal?.currencyCode ?? 'USD' }
|
|
191
|
+
return formatMoneyProp(money)
|
|
192
|
+
}
|
|
193
|
+
: totals?.subtotal?.currencyCode && totals.subtotal.currencyCode !== 'USD'
|
|
194
|
+
? (value: number) =>
|
|
195
|
+
new Intl.NumberFormat('en-US', { style: 'currency', currency: totals.subtotal.currencyCode }).format(value)
|
|
196
|
+
: (value: number) => fallbackFormatter.format(value)
|
|
202
197
|
const money = (value: Money | null | undefined) => formatMoney(value, formatMoneyProp)
|
|
203
198
|
const subtotalAmount = isAvailableMoney(totals?.subtotal) ? Number(totals.subtotal.amount) : 0
|
|
204
199
|
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
@@ -207,7 +202,7 @@ export function ShoppingCart4({
|
|
|
207
202
|
// conflicting edits and checkout until the backend responds.
|
|
208
203
|
const mutationsDisabled = totalsState === 'pending'
|
|
209
204
|
|
|
210
|
-
const sortedPromotions = [...promotions].sort((a, b) => a.threshold - b.threshold)
|
|
205
|
+
const sortedPromotions = [...(promotions ?? [])].sort((a, b) => a.threshold - b.threshold)
|
|
211
206
|
const currentPromo = (() => {
|
|
212
207
|
if (!isAvailableMoney(totals?.subtotal)) return null
|
|
213
208
|
for (let i = sortedPromotions.length - 1; i >= 0; i--) {
|
|
@@ -223,11 +218,12 @@ export function ShoppingCart4({
|
|
|
223
218
|
? Math.min(100, (subtotalAmount / currentPromo.threshold) * 100)
|
|
224
219
|
: 100
|
|
225
220
|
|
|
226
|
-
const lineAmount = (item:
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
const lineAmount = (item: ShoppingCartItem): string | null => {
|
|
222
|
+
// The backend-authoritative line total renders verbatim. A malformed amount
|
|
223
|
+
// is unavailable (never a fabricated zero); `unitPrice` is never multiplied
|
|
224
|
+
// locally — the cart contract forbids local `unit × qty` arithmetic.
|
|
225
|
+
if (item.lineTotal === undefined) return null
|
|
226
|
+
return money(item.lineTotal)
|
|
231
227
|
}
|
|
232
228
|
|
|
233
229
|
const renderTotals = () => {
|
|
@@ -305,6 +301,36 @@ export function ShoppingCart4({
|
|
|
305
301
|
)
|
|
306
302
|
}
|
|
307
303
|
|
|
304
|
+
const renderPromoEntry = () =>
|
|
305
|
+
onApplyPromo ? (
|
|
306
|
+
<form
|
|
307
|
+
className='flex flex-col gap-2'
|
|
308
|
+
onSubmit={event => {
|
|
309
|
+
event.preventDefault()
|
|
310
|
+
const code = promoCode.trim()
|
|
311
|
+
if (!code) return
|
|
312
|
+
onApplyPromo(code)
|
|
313
|
+
setPromoCode('')
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
<label className='text-sm' htmlFor={promoInputId}>
|
|
317
|
+
{promoPromptLabel}
|
|
318
|
+
</label>
|
|
319
|
+
<div className='flex gap-2'>
|
|
320
|
+
<Input
|
|
321
|
+
id={promoInputId}
|
|
322
|
+
value={promoCode}
|
|
323
|
+
onChange={event => setPromoCode(event.target.value)}
|
|
324
|
+
placeholder={promoPlaceholderLabel}
|
|
325
|
+
className='h-9 flex-1'
|
|
326
|
+
/>
|
|
327
|
+
<Button type='submit' variant='outline' size='sm' className='h-9' disabled={mutationsDisabled}>
|
|
328
|
+
{promoApplyLabel}
|
|
329
|
+
</Button>
|
|
330
|
+
</div>
|
|
331
|
+
</form>
|
|
332
|
+
) : null
|
|
333
|
+
|
|
308
334
|
return (
|
|
309
335
|
<section className='py-8'>
|
|
310
336
|
<div className={cn('mx-auto px-4', isPage ? 'max-w-3xl' : 'max-w-md')}>
|
|
@@ -315,7 +341,7 @@ export function ShoppingCart4({
|
|
|
315
341
|
{/* Header */}
|
|
316
342
|
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-3'>
|
|
317
343
|
<CardTitle className='flex items-center gap-2'>
|
|
318
|
-
<
|
|
344
|
+
<ShoppingCartIcon className='size-5' />
|
|
319
345
|
{titleLabel} ({itemCount})
|
|
320
346
|
</CardTitle>
|
|
321
347
|
{!isPage ? (
|
|
@@ -335,7 +361,7 @@ export function ShoppingCart4({
|
|
|
335
361
|
<CardContent className='flex flex-col gap-6'>
|
|
336
362
|
{items.length === 0 ? (
|
|
337
363
|
<div className='flex flex-col items-center justify-center gap-4 py-6 text-center'>
|
|
338
|
-
<
|
|
364
|
+
<ShoppingCartIcon className='text-muted-foreground/50 size-10' />
|
|
339
365
|
<p className='text-muted-foreground text-sm'>{emptyLabel}</p>
|
|
340
366
|
{isPage && onContinueShopping ? (
|
|
341
367
|
<Button variant='outline' onClick={onContinueShopping}>
|
|
@@ -452,6 +478,9 @@ export function ShoppingCart4({
|
|
|
452
478
|
{/* Order Summary — backend-authoritative only */}
|
|
453
479
|
<div className='flex flex-col gap-2'>{renderTotals()}</div>
|
|
454
480
|
|
|
481
|
+
{/* Promo code entry — reports to the host, never resolves locally */}
|
|
482
|
+
{renderPromoEntry()}
|
|
483
|
+
|
|
455
484
|
{/* Quick Actions */}
|
|
456
485
|
<div className='flex flex-col gap-3'>
|
|
457
486
|
{/* Express Checkout Toggle — only when the host supplies a handler */}
|
|
@@ -551,34 +580,39 @@ export function ShoppingCart4({
|
|
|
551
580
|
</div>
|
|
552
581
|
)}
|
|
553
582
|
|
|
554
|
-
{/* Recently Viewed */}
|
|
583
|
+
{/* Recently Viewed — host-supplied suggestions only */}
|
|
555
584
|
{expanded && recentlyViewed && recentlyViewed.length > 0 && (
|
|
556
585
|
<div className='flex flex-col gap-3'>
|
|
557
586
|
<h4 className='text-sm font-medium'>{recentlyViewedLabel}</h4>
|
|
558
587
|
<div className='grid grid-cols-3 gap-2'>
|
|
559
|
-
{recentlyViewed.map(item =>
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
'
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
588
|
+
{recentlyViewed.map(item => {
|
|
589
|
+
const suggestionPrice = money(item.unitPrice)
|
|
590
|
+
return (
|
|
591
|
+
<button
|
|
592
|
+
key={item.id}
|
|
593
|
+
type='button'
|
|
594
|
+
className={cn(
|
|
595
|
+
'group text-start',
|
|
596
|
+
onRecentlyViewedClick && 'cursor-pointer',
|
|
597
|
+
)}
|
|
598
|
+
onClick={onRecentlyViewedClick ? () => onRecentlyViewedClick(item.id) : undefined}
|
|
599
|
+
>
|
|
600
|
+
<div className='bg-muted mb-1 aspect-square overflow-hidden'>
|
|
601
|
+
{item.image ? (
|
|
602
|
+
<img
|
|
603
|
+
src={item.image || undefined}
|
|
604
|
+
alt={item.name}
|
|
605
|
+
className='size-full rounded-md object-cover'
|
|
606
|
+
/>
|
|
607
|
+
) : null}
|
|
608
|
+
</div>
|
|
609
|
+
<p className='text-muted-foreground truncate text-xs'>{item.name}</p>
|
|
610
|
+
{suggestionPrice !== null ? (
|
|
611
|
+
<p className='text-xs font-medium tabular-nums'>{suggestionPrice}</p>
|
|
576
612
|
) : null}
|
|
577
|
-
</
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
</button>
|
|
581
|
-
))}
|
|
613
|
+
</button>
|
|
614
|
+
)
|
|
615
|
+
})}
|
|
582
616
|
</div>
|
|
583
617
|
</div>
|
|
584
618
|
)}
|
|
@@ -591,4 +625,4 @@ export function ShoppingCart4({
|
|
|
591
625
|
)
|
|
592
626
|
}
|
|
593
627
|
|
|
594
|
-
export default
|
|
628
|
+
export default ShoppingCart
|