@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,59 +0,0 @@
|
|
|
1
|
-
import { Camera } from 'lucide-react'
|
|
2
|
-
import { Badge } from '../../../ui/badge'
|
|
3
|
-
import { cn } from '../../../lib/utils'
|
|
4
|
-
|
|
5
|
-
import type { ProductOverview5Product } from './product-overview-5'
|
|
6
|
-
|
|
7
|
-
export interface ProductOverview5GalleryProps {
|
|
8
|
-
product: ProductOverview5Product
|
|
9
|
-
badgeLabel?: string
|
|
10
|
-
selectedImage: number
|
|
11
|
-
onSelectImage: (index: number) => void
|
|
12
|
-
photosLabel?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ProductOverview5Gallery({
|
|
16
|
-
product,
|
|
17
|
-
badgeLabel,
|
|
18
|
-
selectedImage,
|
|
19
|
-
onSelectImage,
|
|
20
|
-
photosLabel = 'fotos',
|
|
21
|
-
}: ProductOverview5GalleryProps) {
|
|
22
|
-
return (
|
|
23
|
-
<div>
|
|
24
|
-
<div className='bg-muted relative aspect-square overflow-hidden rounded-xl'>
|
|
25
|
-
<img src={product.mainImages[selectedImage] || undefined} alt={product.name} className='size-full object-cover' />
|
|
26
|
-
{badgeLabel && (
|
|
27
|
-
<Badge className='px-2.5 py-0.5 font-semibold absolute top-4 left-4'>
|
|
28
|
-
{badgeLabel}
|
|
29
|
-
</Badge>
|
|
30
|
-
)}
|
|
31
|
-
<div className='absolute right-4 bottom-4 rounded-full bg-black/60 px-3 py-1.5 text-sm text-white backdrop-blur-sm'>
|
|
32
|
-
<Camera className='mr-1 inline size-4' />
|
|
33
|
-
{product.mainImages.length} {photosLabel}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
{product.mainImages.length > 1 && (
|
|
38
|
-
<div className='mt-4 flex gap-3 overflow-x-auto pb-2'>
|
|
39
|
-
{product.mainImages.map((image, index) => (
|
|
40
|
-
<button
|
|
41
|
-
type='button'
|
|
42
|
-
key={index}
|
|
43
|
-
onClick={() => onSelectImage(index)}
|
|
44
|
-
className={cn(
|
|
45
|
-
'relative aspect-square size-20 flex-shrink-0 cursor-pointer overflow-hidden rounded-lg border-2 transition-all',
|
|
46
|
-
selectedImage === index ? 'border-primary ring-primary/20 shadow-md ring-2' : 'border-muted hover:border-muted-foreground/50',
|
|
47
|
-
)}
|
|
48
|
-
aria-label={`Mostrar imagen ${index + 1} de ${product.name}`}
|
|
49
|
-
>
|
|
50
|
-
<img src={image || undefined} alt={`${product.name} ${index + 1}`} className='size-full object-cover' />
|
|
51
|
-
</button>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
)}
|
|
55
|
-
</div>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export default ProductOverview5Gallery
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { Camera, Heart, Info, MessageSquare, Package, Share2, ShoppingCart, Star } from 'lucide-react'
|
|
5
|
-
import { Card, CardContent } from '../../../ui/card'
|
|
6
|
-
import { Button } from '../../../ui/button'
|
|
7
|
-
import { Separator } from '../../../ui/separator'
|
|
8
|
-
import { ReactionToggle } from '../../shared/reaction-toggle'
|
|
9
|
-
import { ProductOverview5Gallery } from './product-overview-5-gallery'
|
|
10
|
-
import { ProductOverview5LightboxTab } from './product-overview-5-lightbox-tab'
|
|
11
|
-
import { ProductOverview5ReviewsTab } from './product-overview-5-reviews-tab'
|
|
12
|
-
import {
|
|
13
|
-
ProductOverview5Tabs,
|
|
14
|
-
ProductOverview5TabsContentFade,
|
|
15
|
-
ProductOverview5TabsIndicator,
|
|
16
|
-
ProductOverview5TabsList,
|
|
17
|
-
ProductOverview5TabsTrigger,
|
|
18
|
-
} from './product-overview-5-tabs'
|
|
19
|
-
|
|
20
|
-
export interface ProductOverview5Review {
|
|
21
|
-
id: number
|
|
22
|
-
author: string
|
|
23
|
-
rating: number
|
|
24
|
-
comment: string
|
|
25
|
-
date: string
|
|
26
|
-
helpful: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ProductOverview5Product {
|
|
30
|
-
name: string
|
|
31
|
-
price: number
|
|
32
|
-
originalPrice: null | number
|
|
33
|
-
rating: number
|
|
34
|
-
reviewCount: number
|
|
35
|
-
sku: string
|
|
36
|
-
mainImages: string[]
|
|
37
|
-
galleryImages: string[]
|
|
38
|
-
description: string
|
|
39
|
-
features: string[]
|
|
40
|
-
specifications: Record<string, string>
|
|
41
|
-
userReviews: ProductOverview5Review[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ProductOverview5Props {
|
|
45
|
-
product: ProductOverview5Product
|
|
46
|
-
badgeLabel?: string
|
|
47
|
-
availabilityLabel?: string
|
|
48
|
-
currencySymbol?: string
|
|
49
|
-
skuLabel?: string
|
|
50
|
-
overviewLabel?: string
|
|
51
|
-
specsLabel?: string
|
|
52
|
-
featuresLabel?: string
|
|
53
|
-
specificationsLabel?: string
|
|
54
|
-
reviewsLabel?: string
|
|
55
|
-
galleryLabel?: string
|
|
56
|
-
saveLabel?: string
|
|
57
|
-
savedLabel?: string
|
|
58
|
-
shareLabel?: string
|
|
59
|
-
addToCartLabel?: string
|
|
60
|
-
basedOnReviewsLabel?: string
|
|
61
|
-
foundHelpfulLabel?: string
|
|
62
|
-
ofCounterLabel?: string
|
|
63
|
-
photosLabel?: string
|
|
64
|
-
onAddToCart?: (product: ProductOverview5Product) => void
|
|
65
|
-
onToggleWishlist?: (product: ProductOverview5Product, isWishlisted: boolean) => void
|
|
66
|
-
onShare?: (product: ProductOverview5Product) => void
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function renderStars(rating: number) {
|
|
70
|
-
return Array.from({ length: 5 }, (_, i) => (
|
|
71
|
-
<Star key={i} className={`size-4 ${i < Math.floor(rating) ? 'fill-foreground text-foreground' : i < rating ? 'text-foreground' : ''}`} />
|
|
72
|
-
))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default function ProductOverview5({
|
|
76
|
-
product,
|
|
77
|
-
badgeLabel = 'Destacado',
|
|
78
|
-
availabilityLabel = 'Disponible',
|
|
79
|
-
currencySymbol = '$',
|
|
80
|
-
skuLabel = 'SKU',
|
|
81
|
-
overviewLabel = 'Resumen',
|
|
82
|
-
specsLabel = 'Especificaciones',
|
|
83
|
-
featuresLabel = 'Características clave',
|
|
84
|
-
specificationsLabel = 'Especificaciones técnicas',
|
|
85
|
-
reviewsLabel = 'Reseñas',
|
|
86
|
-
galleryLabel = 'Galería',
|
|
87
|
-
saveLabel = 'Guardar',
|
|
88
|
-
savedLabel = 'Guardado',
|
|
89
|
-
shareLabel = 'Compartir',
|
|
90
|
-
addToCartLabel = 'Agregar al carrito',
|
|
91
|
-
basedOnReviewsLabel = 'Basado en {count} reseñas',
|
|
92
|
-
foundHelpfulLabel = '{count} personas encontraron esto útil',
|
|
93
|
-
ofCounterLabel = '{current} de {total}',
|
|
94
|
-
photosLabel = 'fotos',
|
|
95
|
-
onAddToCart,
|
|
96
|
-
onToggleWishlist,
|
|
97
|
-
onShare,
|
|
98
|
-
}: ProductOverview5Props) {
|
|
99
|
-
const [selectedImage, setSelectedImage] = useState(0)
|
|
100
|
-
const [isWishlisted, setIsWishlisted] = useState(false)
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<section className='py-12'>
|
|
104
|
-
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
105
|
-
<div className='grid gap-12 lg:grid-cols-2'>
|
|
106
|
-
<ProductOverview5Gallery
|
|
107
|
-
product={product}
|
|
108
|
-
badgeLabel={badgeLabel}
|
|
109
|
-
selectedImage={selectedImage}
|
|
110
|
-
onSelectImage={setSelectedImage}
|
|
111
|
-
photosLabel={photosLabel}
|
|
112
|
-
/>
|
|
113
|
-
|
|
114
|
-
<div className='flex flex-col gap-8'>
|
|
115
|
-
<div>
|
|
116
|
-
<h1 className='text-3xl leading-tight font-bold text-balance'>{product.name}</h1>
|
|
117
|
-
<div className='mt-4 flex flex-wrap items-center gap-4'>
|
|
118
|
-
<div className='flex items-center gap-2'>
|
|
119
|
-
<div className='flex'>{renderStars(product.rating)}</div>
|
|
120
|
-
<span className='text-sm font-medium'>{product.rating}</span>
|
|
121
|
-
</div>
|
|
122
|
-
<span className='text-muted-foreground text-sm'>
|
|
123
|
-
{product.userReviews.length} {reviewsLabel.toLowerCase()}
|
|
124
|
-
</span>
|
|
125
|
-
<Separator orientation='vertical' className='h-4' />
|
|
126
|
-
<span className='text-muted-foreground text-sm'>
|
|
127
|
-
{skuLabel}: {product.sku}
|
|
128
|
-
</span>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div className='flex items-baseline gap-3'>
|
|
133
|
-
<span className='text-4xl font-bold'>
|
|
134
|
-
{currencySymbol}
|
|
135
|
-
{product.price}
|
|
136
|
-
</span>
|
|
137
|
-
{product.originalPrice && (
|
|
138
|
-
<span className='text-muted-foreground text-xl line-through'>
|
|
139
|
-
{currencySymbol}
|
|
140
|
-
{product.originalPrice}
|
|
141
|
-
</span>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<div className='inline-flex items-center w-fit gap-2 rounded-full bg-success/10 px-3 py-1.5'>
|
|
146
|
-
<div className='size-2 rounded-full bg-success' />
|
|
147
|
-
<span className='text-sm font-medium text-success'>{availabilityLabel}</span>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
<p className='text-muted-foreground leading-relaxed'>{product.description}</p>
|
|
151
|
-
|
|
152
|
-
<div className='flex flex-col gap-4'>
|
|
153
|
-
<Button
|
|
154
|
-
size='lg'
|
|
155
|
-
className='h-12 w-full cursor-pointer gap-2'
|
|
156
|
-
onClick={onAddToCart ? () => onAddToCart(product) : undefined}
|
|
157
|
-
>
|
|
158
|
-
<ShoppingCart />
|
|
159
|
-
{addToCartLabel}
|
|
160
|
-
</Button>
|
|
161
|
-
<div className='flex gap-3'>
|
|
162
|
-
<Button
|
|
163
|
-
variant='outline'
|
|
164
|
-
size='lg'
|
|
165
|
-
className='h-12 flex-1 cursor-pointer gap-2'
|
|
166
|
-
aria-pressed={isWishlisted}
|
|
167
|
-
onClick={() => {
|
|
168
|
-
const next = !isWishlisted
|
|
169
|
-
setIsWishlisted(next)
|
|
170
|
-
onToggleWishlist?.(product, next)
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
<ReactionToggle active={isWishlisted} icon={Heart} activeColorClassName='text-destructive' />
|
|
174
|
-
{isWishlisted ? savedLabel : saveLabel}
|
|
175
|
-
</Button>
|
|
176
|
-
<Button
|
|
177
|
-
variant='outline'
|
|
178
|
-
size='lg'
|
|
179
|
-
className='h-12 flex-1 cursor-pointer gap-2'
|
|
180
|
-
onClick={onShare ? () => onShare(product) : undefined}
|
|
181
|
-
>
|
|
182
|
-
<Share2 />
|
|
183
|
-
{shareLabel}
|
|
184
|
-
</Button>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
<div className='mt-16'>
|
|
191
|
-
<ProductOverview5Tabs defaultValue='overview' className='w-full'>
|
|
192
|
-
<ProductOverview5TabsList className='bg-muted/50 relative grid !h-12 w-full grid-cols-4 rounded-xl p-1'>
|
|
193
|
-
<ProductOverview5TabsIndicator />
|
|
194
|
-
<ProductOverview5TabsTrigger
|
|
195
|
-
value='overview'
|
|
196
|
-
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
197
|
-
>
|
|
198
|
-
<Info className='size-4 shrink-0' />
|
|
199
|
-
<span className='hidden sm:inline'>{overviewLabel}</span>
|
|
200
|
-
</ProductOverview5TabsTrigger>
|
|
201
|
-
<ProductOverview5TabsTrigger
|
|
202
|
-
value='specs'
|
|
203
|
-
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
204
|
-
>
|
|
205
|
-
<Package className='size-4 shrink-0' />
|
|
206
|
-
<span className='hidden sm:inline'>{specsLabel}</span>
|
|
207
|
-
</ProductOverview5TabsTrigger>
|
|
208
|
-
<ProductOverview5TabsTrigger
|
|
209
|
-
value='reviews'
|
|
210
|
-
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
211
|
-
>
|
|
212
|
-
<MessageSquare className='size-4 shrink-0' />
|
|
213
|
-
<span className='hidden sm:inline'>
|
|
214
|
-
{reviewsLabel} ({product.userReviews.length})
|
|
215
|
-
</span>
|
|
216
|
-
</ProductOverview5TabsTrigger>
|
|
217
|
-
<ProductOverview5TabsTrigger
|
|
218
|
-
value='gallery'
|
|
219
|
-
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
220
|
-
>
|
|
221
|
-
<Camera className='size-4 shrink-0' />
|
|
222
|
-
<span className='hidden sm:inline'>{galleryLabel}</span>
|
|
223
|
-
</ProductOverview5TabsTrigger>
|
|
224
|
-
</ProductOverview5TabsList>
|
|
225
|
-
<ProductOverview5TabsContentFade value='overview' className='mt-8'>
|
|
226
|
-
<Card className='border shadow-none'>
|
|
227
|
-
<CardContent className='p-8 py-0'>
|
|
228
|
-
<h3 className='mb-6 text-xl font-semibold text-balance'>{featuresLabel}</h3>
|
|
229
|
-
<div className='grid gap-4 sm:grid-cols-2'>
|
|
230
|
-
{product.features.map(feature => (
|
|
231
|
-
<div key={feature} className='group flex items-center gap-3'>
|
|
232
|
-
<div className='bg-primary/20 size-2 rounded-full transition-colors' />
|
|
233
|
-
<span className='text-sm'>{feature}</span>
|
|
234
|
-
</div>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
</CardContent>
|
|
238
|
-
</Card>
|
|
239
|
-
</ProductOverview5TabsContentFade>
|
|
240
|
-
<ProductOverview5TabsContentFade value='specs' className='mt-8'>
|
|
241
|
-
<Card className='border shadow-none'>
|
|
242
|
-
<CardContent className='p-8 py-0'>
|
|
243
|
-
<h3 className='mb-6 text-xl font-semibold text-balance'>{specificationsLabel}</h3>
|
|
244
|
-
<div className='flex flex-col gap-4'>
|
|
245
|
-
{Object.entries(product.specifications).map(([key, value]) => (
|
|
246
|
-
<div key={key} className='border-muted grid grid-cols-1 gap-2 border-b py-3 last:border-b-0 md:grid-cols-3 md:gap-4'>
|
|
247
|
-
<span className='text-muted-foreground text-sm font-medium'>{key}</span>
|
|
248
|
-
<span className='col-span-1 text-sm md:col-span-2'>{value}</span>
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
</div>
|
|
252
|
-
</CardContent>
|
|
253
|
-
</Card>
|
|
254
|
-
</ProductOverview5TabsContentFade>
|
|
255
|
-
<ProductOverview5TabsContentFade value='reviews' className='mt-8'>
|
|
256
|
-
<ProductOverview5ReviewsTab
|
|
257
|
-
product={product}
|
|
258
|
-
basedOnReviewsLabel={basedOnReviewsLabel}
|
|
259
|
-
foundHelpfulLabel={foundHelpfulLabel}
|
|
260
|
-
/>
|
|
261
|
-
</ProductOverview5TabsContentFade>
|
|
262
|
-
<ProductOverview5TabsContentFade value='gallery' className='mt-8'>
|
|
263
|
-
<ProductOverview5LightboxTab
|
|
264
|
-
product={product}
|
|
265
|
-
galleryLabel={galleryLabel}
|
|
266
|
-
ofCounterLabel={ofCounterLabel}
|
|
267
|
-
/>
|
|
268
|
-
</ProductOverview5TabsContentFade>
|
|
269
|
-
</ProductOverview5Tabs>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
</section>
|
|
273
|
-
)
|
|
274
|
-
}
|
|
@@ -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 ShoppingCart1SpinningCounterProps = {
|
|
16
|
-
value: number
|
|
17
|
-
decimals?: number
|
|
18
|
-
prefix?: string
|
|
19
|
-
className?: string
|
|
20
|
-
digitClassName?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function ShoppingCart1SpinningCounter({
|
|
24
|
-
value,
|
|
25
|
-
decimals = 0,
|
|
26
|
-
prefix,
|
|
27
|
-
className,
|
|
28
|
-
digitClassName,
|
|
29
|
-
}: ShoppingCart1SpinningCounterProps) {
|
|
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 ShoppingCart1SpinningCounter
|