@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +108 -23
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
  7. package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
  8. package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
  9. package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
  10. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
  11. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
  12. package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
  13. package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
  14. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  15. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  16. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  17. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  18. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  19. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  20. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  21. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  22. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  23. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  24. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  25. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  26. package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
  27. package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
  28. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  29. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  30. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  31. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  32. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  33. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  34. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  35. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  36. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +204 -109
  37. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  38. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  39. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  40. package/src/blocks/marketing/features/feature.tsx +195 -0
  41. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +56 -0
  42. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
  43. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
  44. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
  45. package/src/blocks/marketing/navbars/navbar-4.test.tsx +154 -2
  46. package/src/blocks/marketing/navbars/navbar-4.tsx +152 -32
  47. package/src/index.ts +93 -46
  48. package/src/lib/contracts-curated.test.ts +42 -24
  49. package/src/lib/contracts.test.ts +143 -2
  50. package/src/lib/contracts.ts +395 -13
  51. package/src/ui/button-group.tsx +1 -1
  52. package/src/ui/carousel.tsx +1 -1
  53. package/src/ui/dialog.tsx +1 -1
  54. package/src/ui/field.tsx +2 -2
  55. package/src/ui/input-group.tsx +3 -3
  56. package/src/ui/pagination.tsx +1 -1
  57. package/src/ui/sidebar.tsx +7 -7
  58. package/src/ui/toggle-group.tsx +1 -1
  59. package/tsconfig.build.json +12 -0
  60. package/tsup.config.ts +11 -7
  61. package/.turbo/turbo-build.log +0 -24
  62. package/.turbo/turbo-test.log +0 -37
  63. package/.turbo/turbo-type-check.log +0 -0
  64. package/dist/.tsbuildinfo +0 -1
  65. package/dist/contracts.d.mts +0 -1584
  66. package/dist/contracts.d.ts +0 -1584
  67. package/dist/contracts.js +0 -390
  68. package/dist/contracts.js.map +0 -1
  69. package/dist/contracts.mjs +0 -334
  70. package/dist/contracts.mjs.map +0 -1
  71. package/dist/index.d.mts +0 -2786
  72. package/dist/index.d.ts +0 -2786
  73. package/dist/index.js +0 -17139
  74. package/dist/index.js.map +0 -1
  75. package/dist/index.mjs +0 -17059
  76. package/dist/index.mjs.map +0 -1
  77. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  78. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  79. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  80. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  81. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  82. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  83. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -131
  84. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  85. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  86. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  87. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  88. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  89. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  90. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  91. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  92. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  93. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  94. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  95. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  96. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  97. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  98. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  99. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -137
  105. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -65
  106. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  107. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  108. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  109. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  110. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  111. 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