@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,263 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { motion, useReducedMotion } from 'motion/react'
|
|
4
|
-
|
|
5
|
-
import { Badge } from '../../../ui/badge'
|
|
6
|
-
import { Button } from '../../../ui/button'
|
|
7
|
-
import { ShoppingBag, Star, ArrowRight } from 'lucide-react'
|
|
8
|
-
import type { ComponentProps, ReactNode } from 'react'
|
|
9
|
-
import type { CTA } from '../../../lib/types'
|
|
10
|
-
|
|
11
|
-
export interface FeaturedProduct {
|
|
12
|
-
id: string
|
|
13
|
-
name: string
|
|
14
|
-
brand: string
|
|
15
|
-
price: number
|
|
16
|
-
originalPrice: number
|
|
17
|
-
rating: number
|
|
18
|
-
reviewCount: number
|
|
19
|
-
inStock: boolean
|
|
20
|
-
isNew: boolean
|
|
21
|
-
isOnSale: boolean
|
|
22
|
-
image: string
|
|
23
|
-
colors: string[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface HeroImage {
|
|
27
|
-
src: string
|
|
28
|
-
alt: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface StorefrontHero1Props {
|
|
32
|
-
title: string
|
|
33
|
-
description: string
|
|
34
|
-
products: FeaturedProduct[]
|
|
35
|
-
titleAccent?: string
|
|
36
|
-
badge?: string
|
|
37
|
-
primaryCta?: CTA
|
|
38
|
-
secondaryCta?: CTA
|
|
39
|
-
/**
|
|
40
|
-
* Optional editorial image. Rendered as a native `<img>` when `src` is a
|
|
41
|
-
* non-empty string; omitted entirely when absent or blank (never an empty
|
|
42
|
-
* `src`). The product grid is unaffected.
|
|
43
|
-
*/
|
|
44
|
-
heroImage?: HeroImage
|
|
45
|
-
onAddToCart?: (product: FeaturedProduct) => void
|
|
46
|
-
onBuyNow?: (product: FeaturedProduct) => void
|
|
47
|
-
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
48
|
-
addToCartLabel?: string
|
|
49
|
-
buyNowLabel?: string
|
|
50
|
-
newLabel?: string
|
|
51
|
-
outOfStockLabel?: string
|
|
52
|
-
colorsLabel?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function CtaButton({
|
|
56
|
-
cta,
|
|
57
|
-
variant,
|
|
58
|
-
children,
|
|
59
|
-
}: {
|
|
60
|
-
cta: CTA
|
|
61
|
-
variant?: ComponentProps<typeof Button>['variant']
|
|
62
|
-
children: ReactNode
|
|
63
|
-
}) {
|
|
64
|
-
if (cta.href) {
|
|
65
|
-
return (
|
|
66
|
-
<Button
|
|
67
|
-
variant={variant}
|
|
68
|
-
size='lg'
|
|
69
|
-
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
70
|
-
asChild
|
|
71
|
-
>
|
|
72
|
-
<a href={cta.href}>{children}</a>
|
|
73
|
-
</Button>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<Button
|
|
79
|
-
variant={variant}
|
|
80
|
-
size='lg'
|
|
81
|
-
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
82
|
-
>
|
|
83
|
-
{children}
|
|
84
|
-
</Button>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function StorefrontHero1({
|
|
89
|
-
title,
|
|
90
|
-
description,
|
|
91
|
-
products,
|
|
92
|
-
titleAccent,
|
|
93
|
-
badge,
|
|
94
|
-
primaryCta,
|
|
95
|
-
secondaryCta,
|
|
96
|
-
heroImage,
|
|
97
|
-
onAddToCart,
|
|
98
|
-
onBuyNow,
|
|
99
|
-
onSelectColor,
|
|
100
|
-
addToCartLabel = 'Agregar al carrito',
|
|
101
|
-
buyNowLabel = 'Comprar ahora',
|
|
102
|
-
newLabel = 'Nuevo',
|
|
103
|
-
outOfStockLabel = 'Agotado',
|
|
104
|
-
colorsLabel = 'Colores:',
|
|
105
|
-
}: StorefrontHero1Props) {
|
|
106
|
-
const reduceMotion = useReducedMotion()
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
|
|
110
|
-
<div className='relative z-10 mx-auto w-full max-w-7xl flex flex-col items-center justify-between gap-12 px-4 sm:px-6 lg:px-8 py-16 lg:py-20 xl:flex-row xl:items-start'>
|
|
111
|
-
<motion.div
|
|
112
|
-
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
113
|
-
animate={{ opacity: 1, y: 0 }}
|
|
114
|
-
transition={{ duration: 0.5, ease: 'easeOut' }}
|
|
115
|
-
className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
|
|
116
|
-
>
|
|
117
|
-
{badge && (
|
|
118
|
-
<div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-full border px-4 py-1.5 text-sm font-medium backdrop-blur-sm'>
|
|
119
|
-
{badge}
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
<h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
|
|
123
|
-
{title}{' '}
|
|
124
|
-
{titleAccent && (
|
|
125
|
-
<span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
|
|
126
|
-
{titleAccent}
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
129
|
-
</h1>
|
|
130
|
-
<p className='text-muted-foreground mt-8 text-lg'>{description}</p>
|
|
131
|
-
{(primaryCta || secondaryCta) && (
|
|
132
|
-
<div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
|
|
133
|
-
{primaryCta && (
|
|
134
|
-
<CtaButton cta={primaryCta}>
|
|
135
|
-
<span className='relative z-10 flex items-center gap-2'>
|
|
136
|
-
{primaryCta.label}
|
|
137
|
-
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
138
|
-
</span>
|
|
139
|
-
<span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
|
|
140
|
-
</CtaButton>
|
|
141
|
-
)}
|
|
142
|
-
{secondaryCta && (
|
|
143
|
-
<CtaButton cta={secondaryCta} variant='outline'>
|
|
144
|
-
<span className='relative z-10 flex items-center gap-2'>
|
|
145
|
-
<ShoppingBag />
|
|
146
|
-
{secondaryCta.label}
|
|
147
|
-
</span>
|
|
148
|
-
</CtaButton>
|
|
149
|
-
)}
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
{heroImage && heroImage.src.trim().length > 0 ? (
|
|
153
|
-
<img
|
|
154
|
-
src={heroImage.src}
|
|
155
|
-
alt={heroImage.alt}
|
|
156
|
-
className='mt-10 aspect-video w-full rounded-lg object-cover'
|
|
157
|
-
/>
|
|
158
|
-
) : null}
|
|
159
|
-
</motion.div>
|
|
160
|
-
|
|
161
|
-
<div className='w-full max-w-2xl lg:mt-0'>
|
|
162
|
-
<div className='grid gap-6 sm:grid-cols-2'>
|
|
163
|
-
{products.map((product, index) => (
|
|
164
|
-
<motion.div
|
|
165
|
-
key={product.id}
|
|
166
|
-
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
167
|
-
whileInView={{ opacity: 1, y: 0 }}
|
|
168
|
-
viewport={{ once: true, margin: '-40px' }}
|
|
169
|
-
transition={{
|
|
170
|
-
duration: 0.45,
|
|
171
|
-
delay: reduceMotion ? 0 : index * 0.1,
|
|
172
|
-
ease: 'easeOut',
|
|
173
|
-
}}
|
|
174
|
-
className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'
|
|
175
|
-
>
|
|
176
|
-
<div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
|
|
177
|
-
{product.isNew && (
|
|
178
|
-
<Badge variant='secondary' className='rounded-full'>{newLabel}</Badge>
|
|
179
|
-
)}
|
|
180
|
-
{product.isOnSale && (
|
|
181
|
-
<Badge variant='destructive' className='rounded-full'>
|
|
182
|
-
{Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF
|
|
183
|
-
</Badge>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div className='relative aspect-square overflow-hidden'>
|
|
188
|
-
<img
|
|
189
|
-
src={product.image || undefined}
|
|
190
|
-
alt={`${product.brand} ${product.name}`}
|
|
191
|
-
className='size-full object-cover'
|
|
192
|
-
loading='lazy'
|
|
193
|
-
/>
|
|
194
|
-
{!product.inStock && (
|
|
195
|
-
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
|
|
196
|
-
<span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{outOfStockLabel}</span>
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<div className='p-4'>
|
|
202
|
-
<div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
|
|
203
|
-
<h3 className='mb-2 font-medium'>{product.name}</h3>
|
|
204
|
-
|
|
205
|
-
<div className='mb-3'>
|
|
206
|
-
<span className='text-lg font-bold'>${product.price.toFixed(2)}</span>
|
|
207
|
-
{product.isOnSale && (
|
|
208
|
-
<span className='text-muted-foreground ms-2 text-sm line-through'>
|
|
209
|
-
${product.originalPrice.toFixed(2)}
|
|
210
|
-
</span>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<div className='mb-4 flex items-center'>
|
|
215
|
-
<span className='text-muted-foreground me-2 text-sm'>{colorsLabel}</span>
|
|
216
|
-
<div className='flex gap-1'>
|
|
217
|
-
{product.colors.map((color, i) => (
|
|
218
|
-
<button
|
|
219
|
-
key={i}
|
|
220
|
-
className='size-5 rounded-full border'
|
|
221
|
-
style={{ backgroundColor: color }}
|
|
222
|
-
aria-label={`Opción de color ${i + 1}`}
|
|
223
|
-
onClick={onSelectColor ? () => onSelectColor(product, i, product.colors[i]) : undefined}
|
|
224
|
-
/>
|
|
225
|
-
))}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div className='mb-4 flex items-center'>
|
|
230
|
-
<div className='flex'>
|
|
231
|
-
{[...Array(5)].map((_, i) => (
|
|
232
|
-
<Star
|
|
233
|
-
key={i}
|
|
234
|
-
className={`size-4 ${i < Math.floor(product.rating) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
|
|
235
|
-
/>
|
|
236
|
-
))}
|
|
237
|
-
</div>
|
|
238
|
-
<span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
<div className='flex flex-col gap-2'>
|
|
242
|
-
<Button variant='outline' className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
|
|
243
|
-
<ShoppingBag />
|
|
244
|
-
{addToCartLabel}
|
|
245
|
-
</Button>
|
|
246
|
-
<Button className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onBuyNow ? () => onBuyNow(product) : undefined}>
|
|
247
|
-
{buyNowLabel}
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
</motion.div>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
<div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
|
|
258
|
-
<div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
|
|
259
|
-
</div>
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export default StorefrontHero1
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
4
|
-
import { ArrowRight, Flame, Search, ShoppingBag, Star, TrendingUp } from 'lucide-react'
|
|
5
|
-
import type { ComponentProps, ReactNode } from 'react'
|
|
6
|
-
|
|
7
|
-
import { Badge } from '../../../ui/badge'
|
|
8
|
-
import { Button } from '../../../ui/button'
|
|
9
|
-
import { Card, CardContent } from '../../../ui/card'
|
|
10
|
-
import { Carousel, CarouselContent, CarouselItem } from '../../../ui/carousel'
|
|
11
|
-
import type { CarouselApi } from '../../../ui/carousel'
|
|
12
|
-
import { Input } from '../../../ui/input'
|
|
13
|
-
import type { CTA } from '../../../lib/types'
|
|
14
|
-
import { cn } from '../../../lib/utils'
|
|
15
|
-
|
|
16
|
-
interface FeaturedProduct {
|
|
17
|
-
/** Unique identifier for the product. */
|
|
18
|
-
id: string
|
|
19
|
-
/** Product display name. */
|
|
20
|
-
name: string
|
|
21
|
-
/** Image URL or path for the product. */
|
|
22
|
-
image: string
|
|
23
|
-
/** Current selling price in the store currency. */
|
|
24
|
-
price: number
|
|
25
|
-
/** Average rating from 0 to 5. */
|
|
26
|
-
rating: number
|
|
27
|
-
/** Number of reviews the product has received. */
|
|
28
|
-
reviews: number
|
|
29
|
-
/** Whether the product is flagged as trending. */
|
|
30
|
-
trending: boolean
|
|
31
|
-
/** Badge label shown on the slide, e.g. "Best Seller". */
|
|
32
|
-
tag: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface StorefrontHero2Props {
|
|
36
|
-
/** Main heading displayed in the hero. */
|
|
37
|
-
title: string
|
|
38
|
-
/** Supporting description under the heading. */
|
|
39
|
-
description: string
|
|
40
|
-
/** Products rendered in the auto-rotating carousel. */
|
|
41
|
-
products: FeaturedProduct[]
|
|
42
|
-
/** Small pill badge above the heading. */
|
|
43
|
-
badge?: string
|
|
44
|
-
/** Placeholder text for the search input. */
|
|
45
|
-
searchPlaceholder?: string
|
|
46
|
-
/** Label of the search submit button. */
|
|
47
|
-
searchLabel?: string
|
|
48
|
-
/** Primary call to action; rendered as a link when it has an href. */
|
|
49
|
-
primaryCta?: CTA
|
|
50
|
-
/** Secondary call to action; rendered as a link when it has an href. */
|
|
51
|
-
secondaryCta?: CTA
|
|
52
|
-
/** Copy shown inside each product slide below the product name. */
|
|
53
|
-
featuredDescription?: string
|
|
54
|
-
/** Label of the call to action inside each product slide. */
|
|
55
|
-
productCtaLabel?: string
|
|
56
|
-
/** Label shown next to the review count. */
|
|
57
|
-
reviewsLabel?: string
|
|
58
|
-
/** Label of the trending badge on product slides. */
|
|
59
|
-
trendingLabel?: string
|
|
60
|
-
/** Called with the current query when the search is submitted. */
|
|
61
|
-
onSearch?: (query: string) => void
|
|
62
|
-
/** Called when a product slide call to action is clicked. */
|
|
63
|
-
onProductClick?: (product: FeaturedProduct) => void
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function CtaButton({
|
|
67
|
-
cta,
|
|
68
|
-
variant,
|
|
69
|
-
children,
|
|
70
|
-
}: {
|
|
71
|
-
cta: CTA
|
|
72
|
-
variant?: ComponentProps<typeof Button>['variant']
|
|
73
|
-
children: ReactNode
|
|
74
|
-
}) {
|
|
75
|
-
const className = 'h-10 cursor-pointer rounded-full px-4'
|
|
76
|
-
|
|
77
|
-
if (cta.href) {
|
|
78
|
-
return (
|
|
79
|
-
<Button variant={variant} size='lg' className={className} asChild>
|
|
80
|
-
<a href={cta.href}>{children}</a>
|
|
81
|
-
</Button>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<Button variant={variant} size='lg' className={className}>
|
|
87
|
-
{children}
|
|
88
|
-
</Button>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function StorefrontHero2({
|
|
93
|
-
title,
|
|
94
|
-
description,
|
|
95
|
-
products,
|
|
96
|
-
badge,
|
|
97
|
-
searchPlaceholder = 'Buscar productos...',
|
|
98
|
-
searchLabel = 'Buscar',
|
|
99
|
-
primaryCta,
|
|
100
|
-
secondaryCta,
|
|
101
|
-
featuredDescription,
|
|
102
|
-
productCtaLabel = 'Comprar ahora',
|
|
103
|
-
reviewsLabel = 'reseñas',
|
|
104
|
-
trendingLabel = 'Tendencia',
|
|
105
|
-
onSearch,
|
|
106
|
-
onProductClick,
|
|
107
|
-
}: StorefrontHero2Props) {
|
|
108
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
109
|
-
const [api, setApi] = useState<CarouselApi>()
|
|
110
|
-
const [currentSlide, setCurrentSlide] = useState(0)
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (!api || products.length <= 1) return
|
|
114
|
-
|
|
115
|
-
const interval = setInterval(() => {
|
|
116
|
-
const nextSlide = (currentSlide + 1) % products.length
|
|
117
|
-
api.scrollTo(nextSlide)
|
|
118
|
-
setCurrentSlide(nextSlide)
|
|
119
|
-
}, 5000)
|
|
120
|
-
|
|
121
|
-
return () => clearInterval(interval)
|
|
122
|
-
}, [api, currentSlide, products.length])
|
|
123
|
-
|
|
124
|
-
const handleSearch = () => {
|
|
125
|
-
if (onSearch) onSearch(searchQuery)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<section className='from-background to-accent/20 relative bg-linear-to-b'>
|
|
130
|
-
<div className='relative mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20'>
|
|
131
|
-
<div className='grid grid-cols-1 items-center gap-12 lg:grid-cols-2'>
|
|
132
|
-
<header className='flex flex-col gap-8'>
|
|
133
|
-
{badge && (
|
|
134
|
-
<Badge variant='outline' className='flex h-auto w-fit items-center gap-2 rounded-full px-4 py-2 font-semibold'>
|
|
135
|
-
<TrendingUp className='size-4' />
|
|
136
|
-
{badge}
|
|
137
|
-
</Badge>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
<h1 className='text-5xl leading-tight font-bold text-balance md:text-6xl lg:text-7xl'>{title}</h1>
|
|
141
|
-
|
|
142
|
-
<p className='text-muted-foreground max-w-lg text-xl text-balance'>{description}</p>
|
|
143
|
-
|
|
144
|
-
<div className='relative max-w-md'>
|
|
145
|
-
<Input
|
|
146
|
-
type='search'
|
|
147
|
-
placeholder={searchPlaceholder}
|
|
148
|
-
value={searchQuery}
|
|
149
|
-
onChange={e => setSearchQuery(e.target.value)}
|
|
150
|
-
onKeyDown={e => {
|
|
151
|
-
if (e.key === 'Enter') handleSearch()
|
|
152
|
-
}}
|
|
153
|
-
className='h-14 rounded-full pe-4 pl-12 text-lg'
|
|
154
|
-
aria-label={searchPlaceholder}
|
|
155
|
-
/>
|
|
156
|
-
<Search className='text-muted-foreground absolute top-1/2 start-4 size-5 -translate-y-1/2' />
|
|
157
|
-
<Button
|
|
158
|
-
size='lg'
|
|
159
|
-
className='absolute top-1/2 end-2 h-10 -translate-y-1/2 cursor-pointer rounded-full px-6'
|
|
160
|
-
onClick={handleSearch}
|
|
161
|
-
>
|
|
162
|
-
{searchLabel}
|
|
163
|
-
</Button>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{(primaryCta || secondaryCta) && (
|
|
167
|
-
<div className='flex items-center gap-4'>
|
|
168
|
-
{primaryCta && (
|
|
169
|
-
<CtaButton cta={primaryCta}>
|
|
170
|
-
<span className='flex items-center gap-2'>
|
|
171
|
-
{primaryCta.label}
|
|
172
|
-
<ArrowRight />
|
|
173
|
-
</span>
|
|
174
|
-
</CtaButton>
|
|
175
|
-
)}
|
|
176
|
-
{secondaryCta && (
|
|
177
|
-
<CtaButton cta={secondaryCta} variant='outline'>
|
|
178
|
-
<span className='flex items-center gap-2'>
|
|
179
|
-
<ShoppingBag />
|
|
180
|
-
{secondaryCta.label}
|
|
181
|
-
</span>
|
|
182
|
-
</CtaButton>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
186
|
-
</header>
|
|
187
|
-
|
|
188
|
-
{products.length > 0 && (
|
|
189
|
-
<div className='flex flex-col gap-4'>
|
|
190
|
-
<div className='relative h-[500px] w-full border-0'>
|
|
191
|
-
<Carousel
|
|
192
|
-
className='group size-full'
|
|
193
|
-
setApi={setApi}
|
|
194
|
-
opts={{
|
|
195
|
-
align: 'start',
|
|
196
|
-
loop: true,
|
|
197
|
-
duration: 20,
|
|
198
|
-
skipSnaps: true,
|
|
199
|
-
}}
|
|
200
|
-
>
|
|
201
|
-
<CarouselContent className='h-full'>
|
|
202
|
-
{products.map(product => (
|
|
203
|
-
<CarouselItem key={product.id} className='h-full'>
|
|
204
|
-
<Card className='relative size-full overflow-hidden border-1 py-4'>
|
|
205
|
-
<CardContent className='px-4'>
|
|
206
|
-
<div className='relative size-full overflow-hidden rounded-md'>
|
|
207
|
-
<img
|
|
208
|
-
src={product.image || undefined}
|
|
209
|
-
alt={product.name}
|
|
210
|
-
className='h-[500px] w-full object-cover'
|
|
211
|
-
loading='lazy'
|
|
212
|
-
/>
|
|
213
|
-
</div>
|
|
214
|
-
<div className='from-background/90 via-background/30 absolute inset-0 bg-linear-to-t to-transparent' />
|
|
215
|
-
|
|
216
|
-
<div className='text-background-foreground absolute inset-0 flex flex-col justify-end p-8'>
|
|
217
|
-
<div className='relative z-10 flex max-w-md flex-col gap-4'>
|
|
218
|
-
<Badge className='w-fit rounded-full px-2.5 py-0.5 font-semibold'>{product.tag}</Badge>
|
|
219
|
-
<h2 className='text-4xl font-bold'>{product.name}</h2>
|
|
220
|
-
{featuredDescription && (
|
|
221
|
-
<p className='text-background-foreground/80 text-lg'>{featuredDescription}</p>
|
|
222
|
-
)}
|
|
223
|
-
<span className='text-3xl font-bold'>${product.price.toFixed(2)}</span>
|
|
224
|
-
<div className='flex items-center gap-4 pt-2'>
|
|
225
|
-
<Button
|
|
226
|
-
size='lg'
|
|
227
|
-
className='h-10 cursor-pointer rounded-full px-8'
|
|
228
|
-
onClick={onProductClick ? () => onProductClick(product) : undefined}
|
|
229
|
-
>
|
|
230
|
-
{productCtaLabel}
|
|
231
|
-
</Button>
|
|
232
|
-
<div className='text-foreground flex items-center gap-1'>
|
|
233
|
-
<Star className='fill-foreground size-5' />
|
|
234
|
-
<span className='font-medium'>{product.rating}</span>
|
|
235
|
-
<span className='text-foreground/80'>
|
|
236
|
-
({product.reviews} {reviewsLabel})
|
|
237
|
-
</span>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
{product.trending && (
|
|
244
|
-
<div className='text-background-foreground bg-foreground/10 dark:bg-background/20 absolute top-8 end-8 flex items-center gap-1 rounded-full px-3 py-1 text-sm font-medium backdrop-blur-xs'>
|
|
245
|
-
<Flame className='size-4' /> {trendingLabel}
|
|
246
|
-
</div>
|
|
247
|
-
)}
|
|
248
|
-
</CardContent>
|
|
249
|
-
</Card>
|
|
250
|
-
</CarouselItem>
|
|
251
|
-
))}
|
|
252
|
-
</CarouselContent>
|
|
253
|
-
</Carousel>
|
|
254
|
-
</div>
|
|
255
|
-
|
|
256
|
-
<div className='relative mt-8 flex justify-center gap-3'>
|
|
257
|
-
{products.map((_, index) => (
|
|
258
|
-
<button
|
|
259
|
-
key={index}
|
|
260
|
-
onClick={() => {
|
|
261
|
-
api?.scrollTo(index)
|
|
262
|
-
setCurrentSlide(index)
|
|
263
|
-
}}
|
|
264
|
-
className={cn(
|
|
265
|
-
'relative size-3 rounded-full transition-all',
|
|
266
|
-
currentSlide === index ? 'bg-primary' : 'bg-foreground/20 hover:bg-foreground/40'
|
|
267
|
-
)}
|
|
268
|
-
aria-label={`Ir a la diapositiva ${index + 1}`}
|
|
269
|
-
aria-current={currentSlide === index ? 'step' : undefined}
|
|
270
|
-
/>
|
|
271
|
-
))}
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
)}
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
</section>
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export { StorefrontHero2 }
|
|
282
|
-
export type { FeaturedProduct, StorefrontHero2Props }
|
|
283
|
-
export default StorefrontHero2
|