@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,291 +0,0 @@
|
|
|
1
|
-
import { Minus, MoveRight, Package, Plus, Shield, ShoppingBag, Store, Trash2, CreditCard } from 'lucide-react'
|
|
2
|
-
import { Button } from '../../../ui/button'
|
|
3
|
-
import { Separator } from '../../../ui/separator'
|
|
4
|
-
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '../../../ui/card'
|
|
5
|
-
import { ShoppingCart1SpinningCounter } from './shopping-cart-1-spinning-counter'
|
|
6
|
-
import { cn } from '../../../lib/utils'
|
|
7
|
-
|
|
8
|
-
export interface ShoppingCart1CartItem {
|
|
9
|
-
id: string
|
|
10
|
-
name: string
|
|
11
|
-
color: string
|
|
12
|
-
size: string
|
|
13
|
-
price: number
|
|
14
|
-
originalPrice: number
|
|
15
|
-
quantity: number
|
|
16
|
-
image: string
|
|
17
|
-
estimatedDelivery: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ShoppingCart1Shipping {
|
|
21
|
-
freeThreshold?: number
|
|
22
|
-
cost?: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ShoppingCart1Props {
|
|
26
|
-
items: ShoppingCart1CartItem[]
|
|
27
|
-
shipping?: ShoppingCart1Shipping
|
|
28
|
-
title?: string
|
|
29
|
-
itemCountLabel?: (count: number) => string
|
|
30
|
-
emptyTitle?: string
|
|
31
|
-
emptyDescription?: string
|
|
32
|
-
continueShoppingLabel?: string
|
|
33
|
-
removeItemLabel?: (name: string) => string
|
|
34
|
-
decreaseQuantityLabel?: (name: string) => string
|
|
35
|
-
increaseQuantityLabel?: (name: string) => string
|
|
36
|
-
estimatedDeliveryLabel?: string
|
|
37
|
-
orderSummaryLabel?: string
|
|
38
|
-
subtotalLabel?: string
|
|
39
|
-
shippingLabel?: string
|
|
40
|
-
freeShippingLabel?: string
|
|
41
|
-
youSaveLabel?: string
|
|
42
|
-
totalLabel?: string
|
|
43
|
-
taxNoteLabel?: string
|
|
44
|
-
checkoutLabel?: string
|
|
45
|
-
securePaymentLabel?: string
|
|
46
|
-
secureCheckoutLabel?: string
|
|
47
|
-
secureCheckoutDescription?: string
|
|
48
|
-
onUpdateQuantity?: (id: string, quantity: number) => void
|
|
49
|
-
onRemoveItem?: (id: string) => void
|
|
50
|
-
onCheckout?: () => void
|
|
51
|
-
onContinueShopping?: () => void
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function ShoppingCart1({
|
|
55
|
-
items,
|
|
56
|
-
shipping,
|
|
57
|
-
title = 'Tu carrito de compras',
|
|
58
|
-
itemCountLabel = (count) => `${count} ${count === 1 ? 'artículo' : 'artículos'} en tu carrito`,
|
|
59
|
-
emptyTitle = 'Tu carrito está vacío',
|
|
60
|
-
emptyDescription = 'Agrega productos para empezar',
|
|
61
|
-
continueShoppingLabel = 'Seguir comprando',
|
|
62
|
-
removeItemLabel = (name) => `Quitar ${name}`,
|
|
63
|
-
decreaseQuantityLabel = (name) => `Disminuir cantidad de ${name}`,
|
|
64
|
-
increaseQuantityLabel = (name) => `Aumentar cantidad de ${name}`,
|
|
65
|
-
estimatedDeliveryLabel = 'Entrega estimada',
|
|
66
|
-
orderSummaryLabel = 'Resumen del pedido',
|
|
67
|
-
subtotalLabel = 'Subtotal',
|
|
68
|
-
shippingLabel = 'Envío',
|
|
69
|
-
freeShippingLabel = 'Gratis',
|
|
70
|
-
youSaveLabel = 'Ahorras',
|
|
71
|
-
totalLabel = 'Total',
|
|
72
|
-
taxNoteLabel = 'incluye IVA si corresponde',
|
|
73
|
-
checkoutLabel = 'Finalizar compra',
|
|
74
|
-
securePaymentLabel = 'Pago seguro con cifrado SSL',
|
|
75
|
-
secureCheckoutLabel = 'Pago seguro',
|
|
76
|
-
secureCheckoutDescription = 'Tu información de pago está cifrada y protegida.',
|
|
77
|
-
onUpdateQuantity,
|
|
78
|
-
onRemoveItem,
|
|
79
|
-
onCheckout,
|
|
80
|
-
onContinueShopping,
|
|
81
|
-
}: ShoppingCart1Props) {
|
|
82
|
-
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
83
|
-
const savings = items.reduce((sum, item) => sum + (item.originalPrice - item.price) * item.quantity, 0)
|
|
84
|
-
const freeThreshold = shipping?.freeThreshold ?? Infinity
|
|
85
|
-
const shippingCost = shipping?.cost ?? 0
|
|
86
|
-
const shippingTotal = subtotal >= freeThreshold ? 0 : shippingCost
|
|
87
|
-
const total = subtotal + shippingTotal
|
|
88
|
-
|
|
89
|
-
const changeQuantity = (id: string, quantity: number, delta: number) => {
|
|
90
|
-
onUpdateQuantity?.(id, Math.max(1, quantity + delta))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div className='mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8'>
|
|
95
|
-
<div className='mb-8 flex flex-col gap-2 text-center'>
|
|
96
|
-
<h1 className='text-3xl font-bold tracking-tight sm:text-4xl'>{title}</h1>
|
|
97
|
-
<p className='text-muted-foreground'>
|
|
98
|
-
{itemCountLabel(items.length)} •{' '}
|
|
99
|
-
<span className='text-foreground font-semibold'>${subtotal.toFixed(2)}</span>
|
|
100
|
-
</p>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div className='flex flex-col gap-8 lg:flex-row'>
|
|
104
|
-
<div className='flex flex-1 flex-col gap-6'>
|
|
105
|
-
{items.length === 0 ? (
|
|
106
|
-
<Card className='border-dashed'>
|
|
107
|
-
<CardContent className='flex flex-col items-center justify-center py-12 text-center'>
|
|
108
|
-
<ShoppingBag className='text-muted-foreground/50 mb-4 size-12' />
|
|
109
|
-
<h3 className='text-lg font-medium'>{emptyTitle}</h3>
|
|
110
|
-
<p className='text-muted-foreground mt-1 text-sm'>{emptyDescription}</p>
|
|
111
|
-
<Button
|
|
112
|
-
className='mt-4 h-9 cursor-pointer px-4 py-2'
|
|
113
|
-
variant='outline'
|
|
114
|
-
onClick={onContinueShopping}
|
|
115
|
-
>
|
|
116
|
-
{continueShoppingLabel}
|
|
117
|
-
</Button>
|
|
118
|
-
</CardContent>
|
|
119
|
-
</Card>
|
|
120
|
-
) : (
|
|
121
|
-
items.map(item => (
|
|
122
|
-
<Card key={item.id} className='gap-0 overflow-hidden py-0'>
|
|
123
|
-
<div className='flex flex-col sm:flex-row'>
|
|
124
|
-
<div className='relative h-auto w-full sm:w-40'>
|
|
125
|
-
<img
|
|
126
|
-
src={item.image || undefined}
|
|
127
|
-
alt={item.name}
|
|
128
|
-
className='h-36 w-full object-cover object-center'
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div className='flex-1 p-4'>
|
|
133
|
-
<div className='flex items-start justify-between'>
|
|
134
|
-
<div>
|
|
135
|
-
<h3 className='text-foreground text-lg font-medium'>{item.name}</h3>
|
|
136
|
-
<p className='text-muted-foreground mt-1 text-sm'>
|
|
137
|
-
{item.color} {item.size ? `• ${item.size}` : ''}
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
140
|
-
<Button
|
|
141
|
-
variant='ghost'
|
|
142
|
-
size='icon'
|
|
143
|
-
className='size-8 cursor-pointer text-muted-foreground hover:bg-destructive/10 hover:text-destructive'
|
|
144
|
-
onClick={onRemoveItem ? () => onRemoveItem(item.id) : undefined}
|
|
145
|
-
aria-label={removeItemLabel(item.name)}
|
|
146
|
-
>
|
|
147
|
-
<Trash2 />
|
|
148
|
-
</Button>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div className='mt-4 flex items-center justify-between'>
|
|
152
|
-
<div className='flex items-center gap-2'>
|
|
153
|
-
<Button
|
|
154
|
-
variant='outline'
|
|
155
|
-
size='icon'
|
|
156
|
-
className='size-8 cursor-pointer'
|
|
157
|
-
onClick={
|
|
158
|
-
onUpdateQuantity
|
|
159
|
-
? () => changeQuantity(item.id, item.quantity, -1)
|
|
160
|
-
: undefined
|
|
161
|
-
}
|
|
162
|
-
disabled={item.quantity <= 1}
|
|
163
|
-
aria-label={decreaseQuantityLabel(item.name)}
|
|
164
|
-
>
|
|
165
|
-
<Minus />
|
|
166
|
-
</Button>
|
|
167
|
-
<span className='w-8 text-center text-sm font-medium'>{item.quantity}</span>
|
|
168
|
-
<Button
|
|
169
|
-
variant='outline'
|
|
170
|
-
size='icon'
|
|
171
|
-
className='size-8 cursor-pointer'
|
|
172
|
-
onClick={
|
|
173
|
-
onUpdateQuantity
|
|
174
|
-
? () => changeQuantity(item.id, item.quantity, 1)
|
|
175
|
-
: undefined
|
|
176
|
-
}
|
|
177
|
-
aria-label={increaseQuantityLabel(item.name)}
|
|
178
|
-
>
|
|
179
|
-
<Plus />
|
|
180
|
-
</Button>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<div className='text-end'>
|
|
184
|
-
<p className='text-lg font-semibold'>${(item.price * item.quantity).toFixed(2)}</p>
|
|
185
|
-
{item.originalPrice > item.price && (
|
|
186
|
-
<p className='text-muted-foreground text-xs line-through'>
|
|
187
|
-
${item.originalPrice.toFixed(2)}
|
|
188
|
-
</p>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<CardFooter className='bg-muted/20 border-t px-4 !py-2'>
|
|
196
|
-
<div className='text-muted-foreground flex items-center text-sm'>
|
|
197
|
-
<Package className='me-2 size-4' />
|
|
198
|
-
<span>{estimatedDeliveryLabel}: {item.estimatedDelivery}</span>
|
|
199
|
-
</div>
|
|
200
|
-
</CardFooter>
|
|
201
|
-
</Card>
|
|
202
|
-
))
|
|
203
|
-
)}
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div className='flex w-full flex-col gap-4 lg:w-96'>
|
|
207
|
-
<Card className='sticky top-4 gap-0'>
|
|
208
|
-
<CardHeader className='pb-4'>
|
|
209
|
-
<CardTitle className='text-xl'>{orderSummaryLabel}</CardTitle>
|
|
210
|
-
</CardHeader>
|
|
211
|
-
<CardContent className='flex flex-col gap-4'>
|
|
212
|
-
<div className='flex flex-col gap-3'>
|
|
213
|
-
<div className='flex justify-between text-sm'>
|
|
214
|
-
<span className='text-muted-foreground'>{subtotalLabel}</span>
|
|
215
|
-
<span>${subtotal.toFixed(2)}</span>
|
|
216
|
-
</div>
|
|
217
|
-
<div className='flex justify-between text-sm'>
|
|
218
|
-
<span className='text-muted-foreground'>{shippingLabel}</span>
|
|
219
|
-
<span className={cn(shippingTotal === 0 && 'text-success')}>
|
|
220
|
-
{shippingTotal === 0 ? freeShippingLabel : `$${shippingTotal.toFixed(2)}`}
|
|
221
|
-
</span>
|
|
222
|
-
</div>
|
|
223
|
-
{savings > 0 && (
|
|
224
|
-
<div className='flex justify-between text-sm font-medium'>
|
|
225
|
-
<span>{youSaveLabel}</span>
|
|
226
|
-
<span>-${savings.toFixed(2)}</span>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
<Separator className='my-2' />
|
|
232
|
-
|
|
233
|
-
<div className='flex items-center justify-between text-base font-medium'>
|
|
234
|
-
<span>{totalLabel}</span>
|
|
235
|
-
<div className='text-end'>
|
|
236
|
-
<p className='text-xl font-bold'>
|
|
237
|
-
<ShoppingCart1SpinningCounter value={total} decimals={2} prefix='$' />
|
|
238
|
-
</p>
|
|
239
|
-
<p className='text-muted-foreground text-xs'>{taxNoteLabel}</p>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<Button
|
|
244
|
-
size='lg'
|
|
245
|
-
className='mt-4 h-10 w-full cursor-pointer px-8 text-base font-medium'
|
|
246
|
-
disabled={items.length === 0}
|
|
247
|
-
onClick={onCheckout}
|
|
248
|
-
>
|
|
249
|
-
<ShoppingBag data-icon='inline-start' />
|
|
250
|
-
{checkoutLabel}
|
|
251
|
-
</Button>
|
|
252
|
-
|
|
253
|
-
<div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
|
|
254
|
-
<CreditCard className='size-3.5' />
|
|
255
|
-
<span>{securePaymentLabel}</span>
|
|
256
|
-
</div>
|
|
257
|
-
</CardContent>
|
|
258
|
-
</Card>
|
|
259
|
-
|
|
260
|
-
<Card className='border-dashed py-4'>
|
|
261
|
-
<CardContent className='px-4'>
|
|
262
|
-
<div className='flex items-start gap-3'>
|
|
263
|
-
<div className='flex size-10 items-center justify-center rounded-full bg-warning/10 text-warning'>
|
|
264
|
-
<Shield className='size-5' />
|
|
265
|
-
</div>
|
|
266
|
-
<div>
|
|
267
|
-
<h4 className='font-medium'>{secureCheckoutLabel}</h4>
|
|
268
|
-
<p className='text-muted-foreground mt-1 text-xs'>
|
|
269
|
-
{secureCheckoutDescription}
|
|
270
|
-
</p>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
</CardContent>
|
|
274
|
-
</Card>
|
|
275
|
-
|
|
276
|
-
<Button
|
|
277
|
-
variant='outline'
|
|
278
|
-
className='h-9 w-full cursor-pointer px-4 py-2'
|
|
279
|
-
onClick={onContinueShopping}
|
|
280
|
-
>
|
|
281
|
-
<Store data-icon='inline-start' />
|
|
282
|
-
{continueShoppingLabel}
|
|
283
|
-
<MoveRight data-icon='inline-end' />
|
|
284
|
-
</Button>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export default ShoppingCart1
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { Heart, Minus, Plus, Trash2 } from 'lucide-react'
|
|
2
|
-
import { Badge } from '../../../ui/badge'
|
|
3
|
-
import { Button } from '../../../ui/button'
|
|
4
|
-
import { Separator } from '../../../ui/separator'
|
|
5
|
-
import type { ShoppingCart2Item } from './shopping-cart-2'
|
|
6
|
-
|
|
7
|
-
export interface ShoppingCart2ItemRowProps {
|
|
8
|
-
item: ShoppingCart2Item
|
|
9
|
-
isLast: boolean
|
|
10
|
-
onRemove?: (id: string) => void
|
|
11
|
-
onQuantityChange?: (id: string, quantity: number) => void
|
|
12
|
-
onAddToWishlist?: (item: ShoppingCart2Item) => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ShoppingCart2ItemRow({
|
|
16
|
-
item,
|
|
17
|
-
isLast,
|
|
18
|
-
onRemove,
|
|
19
|
-
onQuantityChange,
|
|
20
|
-
onAddToWishlist,
|
|
21
|
-
}: ShoppingCart2ItemRowProps) {
|
|
22
|
-
const formatPrice = (value: number) => `$${value.toFixed(2)}`
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div>
|
|
26
|
-
<div className='flex flex-col sm:flex-row sm:items-start gap-4'>
|
|
27
|
-
<img src={item.image || undefined} alt={item.name} className='w-20 h-20 rounded-lg object-cover shrink-0 sm:w-20 sm:h-20' />
|
|
28
|
-
|
|
29
|
-
<div className='flex-1 flex flex-col gap-3 min-w-0'>
|
|
30
|
-
<div className='flex items-start justify-between gap-2'>
|
|
31
|
-
<div className='flex-1 min-w-0'>
|
|
32
|
-
<h3 className='line-clamp-2 font-medium'>{item.name}</h3>
|
|
33
|
-
{item.brand ? <p className='text-muted-foreground text-sm'>{item.brand}</p> : null}
|
|
34
|
-
<div className='text-muted-foreground mt-1 flex gap-3 text-sm flex-wrap'>
|
|
35
|
-
{item.color ? <span className='whitespace-nowrap'>Color: {item.color}</span> : null}
|
|
36
|
-
{item.size ? <span className='whitespace-nowrap'>Talla: {item.size}</span> : null}
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
<Button
|
|
40
|
-
variant='ghost'
|
|
41
|
-
size='sm'
|
|
42
|
-
onClick={onRemove ? () => onRemove(item.id) : undefined}
|
|
43
|
-
className='h-8 shrink-0 px-3 text-xs cursor-pointer text-destructive'
|
|
44
|
-
aria-label={`Quitar ${item.name}`}
|
|
45
|
-
>
|
|
46
|
-
<Trash2 />
|
|
47
|
-
</Button>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'>
|
|
51
|
-
<div className='flex items-center gap-2 flex-wrap'>
|
|
52
|
-
<span className='font-semibold text-base'>{formatPrice(item.price)}</span>
|
|
53
|
-
{item.originalPrice ? (
|
|
54
|
-
<span className='text-muted-foreground text-sm line-through'>
|
|
55
|
-
{formatPrice(item.originalPrice)}
|
|
56
|
-
</span>
|
|
57
|
-
) : null}
|
|
58
|
-
{item.originalPrice ? (
|
|
59
|
-
<Badge variant='destructive' className='rounded-sm px-2 py-0.5 font-semibold text-xs'>
|
|
60
|
-
Ahorra {formatPrice(item.originalPrice - item.price)}
|
|
61
|
-
</Badge>
|
|
62
|
-
) : null}
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div className='flex items-center gap-2 justify-between sm:justify-end'>
|
|
66
|
-
<Button
|
|
67
|
-
variant='ghost'
|
|
68
|
-
size='sm'
|
|
69
|
-
className='h-8 cursor-pointer px-2 text-xs'
|
|
70
|
-
onClick={onAddToWishlist ? () => onAddToWishlist(item) : undefined}
|
|
71
|
-
aria-label={`Añadir ${item.name} a la lista de deseos`}
|
|
72
|
-
>
|
|
73
|
-
<Heart className='size-4' />
|
|
74
|
-
</Button>
|
|
75
|
-
<div className='flex items-center rounded-md border'>
|
|
76
|
-
<Button
|
|
77
|
-
variant='ghost'
|
|
78
|
-
size='sm'
|
|
79
|
-
className='h-8 cursor-pointer px-2 text-xs'
|
|
80
|
-
onClick={
|
|
81
|
-
onQuantityChange ? () => onQuantityChange(item.id, item.quantity - 1) : undefined
|
|
82
|
-
}
|
|
83
|
-
disabled={item.quantity <= 1}
|
|
84
|
-
aria-label={`Disminuir cantidad de ${item.name}`}
|
|
85
|
-
>
|
|
86
|
-
<Minus className='size-4' />
|
|
87
|
-
</Button>
|
|
88
|
-
<span className='min-w-[2rem] px-2 py-1 text-center text-sm'>{item.quantity}</span>
|
|
89
|
-
<Button
|
|
90
|
-
variant='ghost'
|
|
91
|
-
size='sm'
|
|
92
|
-
className='h-8 cursor-pointer px-2 text-xs'
|
|
93
|
-
onClick={
|
|
94
|
-
onQuantityChange ? () => onQuantityChange(item.id, item.quantity + 1) : undefined
|
|
95
|
-
}
|
|
96
|
-
aria-label={`Aumentar cantidad de ${item.name}`}
|
|
97
|
-
>
|
|
98
|
-
<Plus className='size-4' />
|
|
99
|
-
</Button>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
{!isLast ? <Separator className='mt-6' /> : null}
|
|
106
|
-
</div>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default ShoppingCart2ItemRow
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent } from '../../../ui/card'
|
|
2
|
-
import { cn } from '../../../lib/utils'
|
|
3
|
-
import type { ShoppingCart2Recommendation } from './shopping-cart-2'
|
|
4
|
-
|
|
5
|
-
export interface ShoppingCart2RecommendationsProps {
|
|
6
|
-
recommendations: ShoppingCart2Recommendation[]
|
|
7
|
-
title?: string
|
|
8
|
-
onRecommendationClick?: (product: ShoppingCart2Recommendation) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ShoppingCart2Recommendations({
|
|
12
|
-
recommendations,
|
|
13
|
-
title = 'También te puede gustar',
|
|
14
|
-
onRecommendationClick,
|
|
15
|
-
}: ShoppingCart2RecommendationsProps) {
|
|
16
|
-
if (recommendations.length === 0) return null
|
|
17
|
-
|
|
18
|
-
const formatPrice = (value: number) => `$${value.toFixed(2)}`
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className='mt-12'>
|
|
22
|
-
<h3 className='mb-4 text-lg font-semibold'>{title}</h3>
|
|
23
|
-
<div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
|
|
24
|
-
{recommendations.map(product => (
|
|
25
|
-
<Card
|
|
26
|
-
key={product.id}
|
|
27
|
-
className={cn(
|
|
28
|
-
'group py-0 transition-shadow',
|
|
29
|
-
onRecommendationClick && 'cursor-pointer hover:shadow-md'
|
|
30
|
-
)}
|
|
31
|
-
onClick={onRecommendationClick ? () => onRecommendationClick(product) : undefined}
|
|
32
|
-
>
|
|
33
|
-
<CardContent className='p-4'>
|
|
34
|
-
<div className='mb-3 aspect-square overflow-hidden'>
|
|
35
|
-
<img
|
|
36
|
-
src={product.image || undefined}
|
|
37
|
-
alt={product.name}
|
|
38
|
-
className='size-full rounded-md object-cover'
|
|
39
|
-
/>
|
|
40
|
-
</div>
|
|
41
|
-
<h4 className='mb-1 line-clamp-2 text-sm font-medium'>{product.name}</h4>
|
|
42
|
-
<p className='text-sm font-semibold'>{formatPrice(product.price)}</p>
|
|
43
|
-
</CardContent>
|
|
44
|
-
</Card>
|
|
45
|
-
))}
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default ShoppingCart2Recommendations
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
|
|
5
|
-
import { cn } from '../../../lib/utils'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Spinning counter: each digit is a clipped vertical reel of 0-9 cells that
|
|
9
|
-
* spins forward and lands on the target digit. Adapted from Transitions.dev
|
|
10
|
-
* "Spinning counter" (26-spinning-counter.md), re-tuned for cart totals:
|
|
11
|
-
* one loop instead of several, and a snappier duration so rapid +/- clicks
|
|
12
|
-
* don't queue up sluggish animations. The spec's directional SVG blur is
|
|
13
|
-
* approximated here with a CSS `filter: blur()` transition for simplicity.
|
|
14
|
-
*
|
|
15
|
-
* Non-digit characters (currency symbol, decimal point, thousands separator)
|
|
16
|
-
* render as static text so only the digits spin.
|
|
17
|
-
*/
|
|
18
|
-
const CELL_SIZE = 20 // px
|
|
19
|
-
const DURATION = 450 // ms
|
|
20
|
-
const STAGGER = 60 // ms per column
|
|
21
|
-
const SPINS = 1 // extra full loops before landing
|
|
22
|
-
const SPIN_BLUR = 2 // px
|
|
23
|
-
|
|
24
|
-
const REEL_DIGITS = Array.from({ length: (SPINS + 1) * 10 }, (_, i) => i % 10)
|
|
25
|
-
|
|
26
|
-
type ShoppingCart2SpinningCounterProps = {
|
|
27
|
-
value: number
|
|
28
|
-
/** Fixed decimal places to format the value with, e.g. 2 for currency. */
|
|
29
|
-
decimals?: number
|
|
30
|
-
/** Static text rendered before the formatted value, e.g. '$'. */
|
|
31
|
-
prefix?: string
|
|
32
|
-
className?: string
|
|
33
|
-
digitClassName?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function ShoppingCart2SpinningCounter({ value, decimals = 0, prefix, className, digitClassName }: ShoppingCart2SpinningCounterProps) {
|
|
37
|
-
const safeValue = Math.max(0, value)
|
|
38
|
-
const formatted = safeValue.toFixed(decimals)
|
|
39
|
-
const chars = formatted.split('')
|
|
40
|
-
const containerRef = useRef<HTMLSpanElement>(null)
|
|
41
|
-
const isFirstRender = useRef(true)
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
const container = containerRef.current
|
|
45
|
-
if (!container) return
|
|
46
|
-
|
|
47
|
-
const strips = Array.from(container.querySelectorAll<HTMLElement>('[data-reel-strip]'))
|
|
48
|
-
const timeouts: number[] = []
|
|
49
|
-
|
|
50
|
-
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
51
|
-
|
|
52
|
-
strips.forEach((strip, index) => {
|
|
53
|
-
const digit = Number(strip.dataset.digit)
|
|
54
|
-
const delay = (strips.length - 1 - index) * STAGGER
|
|
55
|
-
|
|
56
|
-
if (isFirstRender.current || !strip.dataset.initialized || prefersReducedMotion) {
|
|
57
|
-
strip.style.transition = 'none'
|
|
58
|
-
strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
|
|
59
|
-
strip.style.filter = 'blur(0px)'
|
|
60
|
-
strip.dataset.initialized = 'true'
|
|
61
|
-
void strip.offsetHeight
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const landingIndex = SPINS * 10 + digit
|
|
66
|
-
strip.style.transition = `transform ${DURATION}ms cubic-bezier(0.16, 1, 0.3, 1) ${delay}ms, filter ${DURATION}ms ease-out ${delay}ms`
|
|
67
|
-
strip.style.transform = `translateY(-${landingIndex * CELL_SIZE}px)`
|
|
68
|
-
strip.style.filter = `blur(${SPIN_BLUR}px)`
|
|
69
|
-
|
|
70
|
-
const settleTimeout = window.setTimeout(() => {
|
|
71
|
-
strip.style.filter = 'blur(0px)'
|
|
72
|
-
}, delay + 30)
|
|
73
|
-
|
|
74
|
-
const resetTimeout = window.setTimeout(() => {
|
|
75
|
-
strip.style.transition = 'none'
|
|
76
|
-
strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
|
|
77
|
-
void strip.offsetHeight
|
|
78
|
-
}, delay + DURATION + 30)
|
|
79
|
-
|
|
80
|
-
timeouts.push(settleTimeout, resetTimeout)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
isFirstRender.current = false
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
timeouts.forEach(window.clearTimeout)
|
|
87
|
-
}
|
|
88
|
-
}, [formatted])
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<span className='relative inline-flex items-center'>
|
|
92
|
-
<span className='sr-only'>{prefix}{formatted}</span>
|
|
93
|
-
<span
|
|
94
|
-
ref={containerRef}
|
|
95
|
-
className={cn('inline-flex items-center font-[inherit] tabular-nums', className)}
|
|
96
|
-
style={{ height: CELL_SIZE }}
|
|
97
|
-
aria-hidden='true'
|
|
98
|
-
>
|
|
99
|
-
{prefix ? (
|
|
100
|
-
<span className={digitClassName} style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}>
|
|
101
|
-
{prefix}
|
|
102
|
-
</span>
|
|
103
|
-
) : null}
|
|
104
|
-
{chars.map((char, index) => {
|
|
105
|
-
if (!/\d/.test(char)) {
|
|
106
|
-
return (
|
|
107
|
-
<span
|
|
108
|
-
key={index}
|
|
109
|
-
className={digitClassName}
|
|
110
|
-
style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
|
|
111
|
-
>
|
|
112
|
-
{char}
|
|
113
|
-
</span>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<span
|
|
119
|
-
key={index}
|
|
120
|
-
className='relative inline-block overflow-hidden'
|
|
121
|
-
style={{
|
|
122
|
-
height: CELL_SIZE,
|
|
123
|
-
maskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
|
|
124
|
-
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
|
|
125
|
-
}}
|
|
126
|
-
>
|
|
127
|
-
<span data-reel-strip data-digit={char} className='flex flex-col will-change-[transform,filter]'>
|
|
128
|
-
{REEL_DIGITS.map((cellDigit, cellIndex) => (
|
|
129
|
-
<span
|
|
130
|
-
key={cellIndex}
|
|
131
|
-
className={cn('flex items-center justify-center', digitClassName)}
|
|
132
|
-
style={{ height: CELL_SIZE }}
|
|
133
|
-
>
|
|
134
|
-
{cellDigit}
|
|
135
|
-
</span>
|
|
136
|
-
))}
|
|
137
|
-
</span>
|
|
138
|
-
</span>
|
|
139
|
-
)
|
|
140
|
-
})}
|
|
141
|
-
</span>
|
|
142
|
-
</span>
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export default ShoppingCart2SpinningCounter
|