@thorprovider/blocks 0.1.2 → 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 +108 -23
- 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/navigation/back-to-top.test.tsx +27 -0
- package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -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-list/product-showcase-1.test.tsx +78 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -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} +204 -109
- 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 +56 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +154 -2
- package/src/blocks/marketing/navbars/navbar-4.tsx +152 -32
- package/src/index.ts +93 -46
- package/src/lib/contracts-curated.test.ts +42 -24
- package/src/lib/contracts.test.ts +143 -2
- package/src/lib/contracts.ts +395 -13
- 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 -37
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1584
- package/dist/contracts.d.ts +0 -1584
- package/dist/contracts.js +0 -390
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -334
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -2786
- package/dist/index.d.ts +0 -2786
- package/dist/index.js +0 -17139
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17059
- 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.tsx +0 -131
- 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 -137
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -65
- 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,295 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { ArrowLeft, Shield, ShoppingBag, Tag, Truck } from 'lucide-react'
|
|
5
|
-
import { Button } from '../../../ui/button'
|
|
6
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
7
|
-
import { Input } from '../../../ui/input'
|
|
8
|
-
import { Separator } from '../../../ui/separator'
|
|
9
|
-
import { ShoppingCart2ItemRow } from './shopping-cart-2-item-row'
|
|
10
|
-
import { ShoppingCart2Recommendations } from './shopping-cart-2-recommendations'
|
|
11
|
-
import { ShoppingCart2SpinningCounter } from './shopping-cart-2-spinning-counter'
|
|
12
|
-
|
|
13
|
-
export interface ShoppingCart2Item {
|
|
14
|
-
id: string
|
|
15
|
-
name: string
|
|
16
|
-
brand?: string
|
|
17
|
-
price: number
|
|
18
|
-
originalPrice?: number
|
|
19
|
-
quantity: number
|
|
20
|
-
image: string
|
|
21
|
-
color?: string
|
|
22
|
-
size?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ShoppingCart2Recommendation {
|
|
26
|
-
id: string
|
|
27
|
-
name: string
|
|
28
|
-
price: number
|
|
29
|
-
image: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ShoppingCart2Props {
|
|
33
|
-
items: ShoppingCart2Item[]
|
|
34
|
-
recommendations?: ShoppingCart2Recommendation[]
|
|
35
|
-
taxRate?: number
|
|
36
|
-
shippingCost?: number
|
|
37
|
-
freeShippingThreshold?: number
|
|
38
|
-
validatePromo?: (code: string) => number | null
|
|
39
|
-
titleLabel?: string
|
|
40
|
-
itemsCountLabel?: string
|
|
41
|
-
cartItemsLabel?: string
|
|
42
|
-
orderSummaryLabel?: string
|
|
43
|
-
subtotalLabel?: string
|
|
44
|
-
savingsLabel?: string
|
|
45
|
-
shippingLabel?: string
|
|
46
|
-
freeShippingLabel?: string
|
|
47
|
-
taxLabel?: string
|
|
48
|
-
totalLabel?: string
|
|
49
|
-
promoPlaceholder?: string
|
|
50
|
-
applyPromoLabel?: string
|
|
51
|
-
promoAppliedLabel?: string
|
|
52
|
-
continueShoppingLabel?: string
|
|
53
|
-
emptyCartTitleLabel?: string
|
|
54
|
-
emptyCartDescriptionLabel?: string
|
|
55
|
-
checkoutLabel?: string
|
|
56
|
-
freeShippingNoteLabel?: string
|
|
57
|
-
freeShippingThresholdNoteLabel?: string
|
|
58
|
-
secureCheckoutLabel?: string
|
|
59
|
-
recommendationsLabel?: string
|
|
60
|
-
onQuantityChange?: (id: string, quantity: number) => void
|
|
61
|
-
onRemove?: (id: string) => void
|
|
62
|
-
onAddToWishlist?: (item: ShoppingCart2Item) => void
|
|
63
|
-
onCheckout?: () => void
|
|
64
|
-
onContinueShopping?: () => void
|
|
65
|
-
onRecommendationClick?: (product: ShoppingCart2Recommendation) => void
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function ShoppingCart2({
|
|
69
|
-
items,
|
|
70
|
-
recommendations,
|
|
71
|
-
taxRate = 0.08,
|
|
72
|
-
shippingCost = 9.99,
|
|
73
|
-
freeShippingThreshold = 75,
|
|
74
|
-
validatePromo,
|
|
75
|
-
titleLabel = 'Carrito de compras',
|
|
76
|
-
itemsCountLabel = 'artículos en tu carrito',
|
|
77
|
-
cartItemsLabel = 'Artículos',
|
|
78
|
-
orderSummaryLabel = 'Resumen del pedido',
|
|
79
|
-
subtotalLabel = 'Subtotal',
|
|
80
|
-
savingsLabel = 'Ahorro',
|
|
81
|
-
shippingLabel = 'Envío',
|
|
82
|
-
freeShippingLabel = 'Gratis',
|
|
83
|
-
taxLabel = 'Impuestos',
|
|
84
|
-
totalLabel = 'Total',
|
|
85
|
-
promoPlaceholder = 'Código de descuento',
|
|
86
|
-
applyPromoLabel = 'Aplicar código',
|
|
87
|
-
promoAppliedLabel = 'Código "{code}" aplicado',
|
|
88
|
-
continueShoppingLabel = 'Seguir comprando',
|
|
89
|
-
emptyCartTitleLabel = 'Tu carrito está vacío',
|
|
90
|
-
emptyCartDescriptionLabel = 'Añade algunos artículos para empezar',
|
|
91
|
-
checkoutLabel = 'Finalizar compra',
|
|
92
|
-
freeShippingNoteLabel = 'Envío gratis en este pedido',
|
|
93
|
-
freeShippingThresholdNoteLabel = 'Envío gratis en pedidos superiores a ${threshold}',
|
|
94
|
-
secureCheckoutLabel = 'Pago seguro garantizado',
|
|
95
|
-
recommendationsLabel = 'También te puede gustar',
|
|
96
|
-
onQuantityChange,
|
|
97
|
-
onRemove,
|
|
98
|
-
onAddToWishlist,
|
|
99
|
-
onCheckout,
|
|
100
|
-
onContinueShopping,
|
|
101
|
-
onRecommendationClick,
|
|
102
|
-
}: ShoppingCart2Props) {
|
|
103
|
-
const [promoCode, setPromoCode] = useState('')
|
|
104
|
-
const [appliedPromo, setAppliedPromo] = useState<string | null>(null)
|
|
105
|
-
|
|
106
|
-
const applyPromoCode = () => {
|
|
107
|
-
if (!validatePromo) return
|
|
108
|
-
const code = promoCode.trim()
|
|
109
|
-
const discount = validatePromo(code)
|
|
110
|
-
if (discount !== null && discount > 0) {
|
|
111
|
-
setAppliedPromo(code)
|
|
112
|
-
setPromoCode('')
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
117
|
-
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
118
|
-
const savings = items.reduce((sum, item) => {
|
|
119
|
-
const originalPrice = item.originalPrice ?? item.price
|
|
120
|
-
return sum + (originalPrice - item.price) * item.quantity
|
|
121
|
-
}, 0)
|
|
122
|
-
const promoDiscount = appliedPromo && validatePromo ? subtotal * ((validatePromo(appliedPromo) ?? 0) / 100) : 0
|
|
123
|
-
const shipping = subtotal > freeShippingThreshold ? 0 : shippingCost
|
|
124
|
-
const tax = (subtotal - promoDiscount) * taxRate
|
|
125
|
-
const total = subtotal - promoDiscount + shipping + tax
|
|
126
|
-
|
|
127
|
-
const formatPrice = (value: number) => `$${value.toFixed(2)}`
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<section className='py-8'>
|
|
131
|
-
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
132
|
-
{items.length === 0 ? (
|
|
133
|
-
<Card>
|
|
134
|
-
<CardContent className='py-12 text-center'>
|
|
135
|
-
<ShoppingBag className='text-muted-foreground mx-auto mb-4 size-16' />
|
|
136
|
-
<h2 className='mb-2 text-2xl font-semibold'>{emptyCartTitleLabel}</h2>
|
|
137
|
-
<p className='text-muted-foreground mb-6'>{emptyCartDescriptionLabel}</p>
|
|
138
|
-
<Button size='lg' className='h-10 px-8 cursor-pointer' onClick={onContinueShopping}>
|
|
139
|
-
{continueShoppingLabel}
|
|
140
|
-
</Button>
|
|
141
|
-
</CardContent>
|
|
142
|
-
</Card>
|
|
143
|
-
) : (
|
|
144
|
-
<>
|
|
145
|
-
<div className='mb-8'>
|
|
146
|
-
<Button
|
|
147
|
-
variant='ghost'
|
|
148
|
-
size='sm'
|
|
149
|
-
className='mb-4 h-8 px-3 text-xs cursor-pointer'
|
|
150
|
-
onClick={onContinueShopping}
|
|
151
|
-
>
|
|
152
|
-
<ArrowLeft />
|
|
153
|
-
{continueShoppingLabel}
|
|
154
|
-
</Button>
|
|
155
|
-
<h1 className='text-3xl font-bold tracking-tight text-balance'>{titleLabel}</h1>
|
|
156
|
-
<p className='text-muted-foreground mt-2'>
|
|
157
|
-
{items.length} {itemsCountLabel}
|
|
158
|
-
</p>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
<div className='grid gap-8 lg:grid-cols-3'>
|
|
162
|
-
<div className='lg:col-span-2'>
|
|
163
|
-
<Card>
|
|
164
|
-
<CardHeader>
|
|
165
|
-
<CardTitle>{cartItemsLabel}</CardTitle>
|
|
166
|
-
</CardHeader>
|
|
167
|
-
<CardContent className='flex flex-col gap-6'>
|
|
168
|
-
{items.map((item, index) => (
|
|
169
|
-
<ShoppingCart2ItemRow
|
|
170
|
-
key={item.id}
|
|
171
|
-
item={item}
|
|
172
|
-
isLast={index === items.length - 1}
|
|
173
|
-
onRemove={onRemove}
|
|
174
|
-
onQuantityChange={onQuantityChange}
|
|
175
|
-
onAddToWishlist={onAddToWishlist}
|
|
176
|
-
/>
|
|
177
|
-
))}
|
|
178
|
-
</CardContent>
|
|
179
|
-
</Card>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<div className='lg:col-span-1'>
|
|
183
|
-
<Card className='lg:sticky lg:top-6'>
|
|
184
|
-
<CardHeader>
|
|
185
|
-
<CardTitle>{orderSummaryLabel}</CardTitle>
|
|
186
|
-
</CardHeader>
|
|
187
|
-
<CardContent className='flex flex-col gap-4'>
|
|
188
|
-
<div className='flex flex-col gap-3 text-sm'>
|
|
189
|
-
<div className='flex justify-between gap-4 flex-wrap'>
|
|
190
|
-
<span className='flex-1'>
|
|
191
|
-
{subtotalLabel} ({itemCount} {cartItemsLabel})
|
|
192
|
-
</span>
|
|
193
|
-
<span className='font-medium shrink-0'>{formatPrice(subtotal)}</span>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
{savings > 0 ? (
|
|
197
|
-
<div className='flex justify-between gap-4 text-success flex-wrap'>
|
|
198
|
-
<span className='flex-1'>{savingsLabel}</span>
|
|
199
|
-
<span className='font-medium shrink-0'>-{formatPrice(savings)}</span>
|
|
200
|
-
</div>
|
|
201
|
-
) : null}
|
|
202
|
-
|
|
203
|
-
{appliedPromo ? (
|
|
204
|
-
<div className='flex justify-between gap-4 text-success flex-wrap'>
|
|
205
|
-
<span className='flex-1'>{promoAppliedLabel.replace('{code}', appliedPromo)}</span>
|
|
206
|
-
<span className='font-medium shrink-0'>-{formatPrice(promoDiscount)}</span>
|
|
207
|
-
</div>
|
|
208
|
-
) : null}
|
|
209
|
-
|
|
210
|
-
<div className='flex justify-between gap-4 flex-wrap'>
|
|
211
|
-
<span className='flex-1'>{shippingLabel}</span>
|
|
212
|
-
<span className='font-medium shrink-0'>
|
|
213
|
-
{shipping === 0 ? freeShippingLabel : formatPrice(shipping)}
|
|
214
|
-
</span>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
<div className='flex justify-between gap-4 flex-wrap'>
|
|
218
|
-
<span className='flex-1'>{taxLabel}</span>
|
|
219
|
-
<span className='font-medium shrink-0'>{formatPrice(tax)}</span>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<Separator />
|
|
224
|
-
|
|
225
|
-
<div className='flex justify-between gap-4 text-lg font-semibold flex-wrap'>
|
|
226
|
-
<span>{totalLabel}</span>
|
|
227
|
-
<span className='shrink-0'>
|
|
228
|
-
<ShoppingCart2SpinningCounter value={total} decimals={2} prefix='$' />
|
|
229
|
-
</span>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<div className='flex flex-col gap-3'>
|
|
233
|
-
<Input
|
|
234
|
-
placeholder={promoPlaceholder}
|
|
235
|
-
value={promoCode}
|
|
236
|
-
onChange={e => setPromoCode(e.target.value)}
|
|
237
|
-
aria-label={promoPlaceholder}
|
|
238
|
-
className='h-9 flex-1'
|
|
239
|
-
/>
|
|
240
|
-
<Button
|
|
241
|
-
variant='outline'
|
|
242
|
-
onClick={applyPromoCode}
|
|
243
|
-
className='h-9 cursor-pointer px-4 py-2'
|
|
244
|
-
disabled={!promoCode || !validatePromo}
|
|
245
|
-
aria-label={applyPromoLabel}
|
|
246
|
-
>
|
|
247
|
-
<Tag />
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
{appliedPromo ? (
|
|
252
|
-
<div className='flex items-center gap-2 text-sm text-success'>
|
|
253
|
-
<Tag className='size-4' />
|
|
254
|
-
{promoAppliedLabel.replace('{code}', appliedPromo)}
|
|
255
|
-
</div>
|
|
256
|
-
) : null}
|
|
257
|
-
|
|
258
|
-
<Button size='lg' className='h-10 w-full px-8 cursor-pointer' onClick={onCheckout}>
|
|
259
|
-
{checkoutLabel}
|
|
260
|
-
</Button>
|
|
261
|
-
|
|
262
|
-
<div className='flex flex-col gap-3 text-sm'>
|
|
263
|
-
<div className='flex items-center gap-2 text-success'>
|
|
264
|
-
<Truck className='size-4' />
|
|
265
|
-
<span>
|
|
266
|
-
{shipping === 0
|
|
267
|
-
? freeShippingNoteLabel
|
|
268
|
-
: freeShippingThresholdNoteLabel.replace('${threshold}', String(freeShippingThreshold))}
|
|
269
|
-
</span>
|
|
270
|
-
</div>
|
|
271
|
-
<div className='text-muted-foreground flex items-center gap-2'>
|
|
272
|
-
<Shield className='size-4' />
|
|
273
|
-
<span>{secureCheckoutLabel}</span>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
</CardContent>
|
|
277
|
-
</Card>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
{recommendations && recommendations.length > 0 ? (
|
|
282
|
-
<ShoppingCart2Recommendations
|
|
283
|
-
recommendations={recommendations}
|
|
284
|
-
title={recommendationsLabel}
|
|
285
|
-
onRecommendationClick={onRecommendationClick}
|
|
286
|
-
/>
|
|
287
|
-
) : null}
|
|
288
|
-
</>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
</section>
|
|
292
|
-
)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export default ShoppingCart2
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
|
|
5
|
-
import { cn } from '../../../lib/utils'
|
|
6
|
-
|
|
7
|
-
const CELL_SIZE = 20
|
|
8
|
-
const DURATION = 450
|
|
9
|
-
const STAGGER = 60
|
|
10
|
-
const SPINS = 1
|
|
11
|
-
const SPIN_BLUR = 2
|
|
12
|
-
|
|
13
|
-
const REEL_DIGITS = Array.from({ length: (SPINS + 1) * 10 }, (_, i) => i % 10)
|
|
14
|
-
|
|
15
|
-
type ShoppingCart3SpinningCounterProps = {
|
|
16
|
-
value: number
|
|
17
|
-
decimals?: number
|
|
18
|
-
prefix?: string
|
|
19
|
-
className?: string
|
|
20
|
-
digitClassName?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function ShoppingCart3SpinningCounter({
|
|
24
|
-
value,
|
|
25
|
-
decimals = 0,
|
|
26
|
-
prefix,
|
|
27
|
-
className,
|
|
28
|
-
digitClassName,
|
|
29
|
-
}: ShoppingCart3SpinningCounterProps) {
|
|
30
|
-
const safeValue = Math.max(0, value)
|
|
31
|
-
const formatted = safeValue.toFixed(decimals)
|
|
32
|
-
const chars = formatted.split('')
|
|
33
|
-
const containerRef = useRef<HTMLSpanElement>(null)
|
|
34
|
-
const isFirstRender = useRef(true)
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const container = containerRef.current
|
|
38
|
-
if (!container) return
|
|
39
|
-
|
|
40
|
-
const strips = Array.from(container.querySelectorAll<HTMLElement>('[data-reel-strip]'))
|
|
41
|
-
const timeouts: number[] = []
|
|
42
|
-
|
|
43
|
-
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
44
|
-
|
|
45
|
-
strips.forEach((strip, index) => {
|
|
46
|
-
const digit = Number(strip.dataset.digit)
|
|
47
|
-
const delay = (strips.length - 1 - index) * STAGGER
|
|
48
|
-
|
|
49
|
-
if (isFirstRender.current || !strip.dataset.initialized || prefersReducedMotion) {
|
|
50
|
-
strip.style.transition = 'none'
|
|
51
|
-
strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
|
|
52
|
-
strip.style.filter = 'blur(0px)'
|
|
53
|
-
strip.dataset.initialized = 'true'
|
|
54
|
-
void strip.offsetHeight
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const landingIndex = SPINS * 10 + digit
|
|
59
|
-
strip.style.transition = `transform ${DURATION}ms cubic-bezier(0.16, 1, 0.3, 1) ${delay}ms, filter ${DURATION}ms ease-out ${delay}ms`
|
|
60
|
-
strip.style.transform = `translateY(-${landingIndex * CELL_SIZE}px)`
|
|
61
|
-
strip.style.filter = `blur(${SPIN_BLUR}px)`
|
|
62
|
-
|
|
63
|
-
const settleTimeout = window.setTimeout(() => {
|
|
64
|
-
strip.style.filter = 'blur(0px)'
|
|
65
|
-
}, delay + 30)
|
|
66
|
-
|
|
67
|
-
const resetTimeout = window.setTimeout(() => {
|
|
68
|
-
strip.style.transition = 'none'
|
|
69
|
-
strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
|
|
70
|
-
void strip.offsetHeight
|
|
71
|
-
}, delay + DURATION + 30)
|
|
72
|
-
|
|
73
|
-
timeouts.push(settleTimeout, resetTimeout)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
isFirstRender.current = false
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
timeouts.forEach(window.clearTimeout)
|
|
80
|
-
}
|
|
81
|
-
}, [formatted])
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<span className='relative inline-flex items-center'>
|
|
85
|
-
<span className='sr-only'>{prefix}{formatted}</span>
|
|
86
|
-
<span
|
|
87
|
-
ref={containerRef}
|
|
88
|
-
className={cn('inline-flex items-center font-[inherit] tabular-nums', className)}
|
|
89
|
-
style={{ height: CELL_SIZE }}
|
|
90
|
-
aria-hidden='true'
|
|
91
|
-
>
|
|
92
|
-
{prefix ? (
|
|
93
|
-
<span
|
|
94
|
-
className={digitClassName}
|
|
95
|
-
style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
|
|
96
|
-
>
|
|
97
|
-
{prefix}
|
|
98
|
-
</span>
|
|
99
|
-
) : null}
|
|
100
|
-
{chars.map((char, index) => {
|
|
101
|
-
if (!/\d/.test(char)) {
|
|
102
|
-
return (
|
|
103
|
-
<span
|
|
104
|
-
key={index}
|
|
105
|
-
className={digitClassName}
|
|
106
|
-
style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
|
|
107
|
-
>
|
|
108
|
-
{char}
|
|
109
|
-
</span>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<span
|
|
115
|
-
key={index}
|
|
116
|
-
className='relative inline-block overflow-hidden'
|
|
117
|
-
style={{
|
|
118
|
-
height: CELL_SIZE,
|
|
119
|
-
maskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
|
|
120
|
-
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
<span data-reel-strip data-digit={char} className='flex flex-col will-change-[transform,filter]'>
|
|
124
|
-
{REEL_DIGITS.map((cellDigit, cellIndex) => (
|
|
125
|
-
<span
|
|
126
|
-
key={cellIndex}
|
|
127
|
-
className={cn('flex items-center justify-center', digitClassName)}
|
|
128
|
-
style={{ height: CELL_SIZE }}
|
|
129
|
-
>
|
|
130
|
-
{cellDigit}
|
|
131
|
-
</span>
|
|
132
|
-
))}
|
|
133
|
-
</span>
|
|
134
|
-
</span>
|
|
135
|
-
)
|
|
136
|
-
})}
|
|
137
|
-
</span>
|
|
138
|
-
</span>
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export default ShoppingCart3SpinningCounter
|