@thorprovider/blocks 0.1.3 → 2.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 (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  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/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -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
@@ -1,318 +0,0 @@
1
- import { ArrowRight, MapPin } from 'lucide-react'
2
- import type { ComponentProps, ReactNode } from 'react'
3
-
4
- import { Button } from '../../../ui/button'
5
- import { Card, CardContent } from '../../../ui/card'
6
- import { cn } from '../../../lib/utils'
7
- import type { CTA } from '../../../lib/types'
8
-
9
- export interface Department {
10
- /** Unique identifier */
11
- id: string
12
- /** Department name */
13
- name: string
14
- /** Image URL for the department chip */
15
- image: string
16
- /** Accessible image description (falls back to `name`) */
17
- imageAlt?: string
18
- /** Product count label shown under the name */
19
- count?: string
20
- /** Link destination when the department is a link */
21
- href?: string
22
- }
23
-
24
- export interface FeaturedDeal {
25
- /** Unique identifier */
26
- id: string
27
- /** Deal title */
28
- name: string
29
- /** Short supporting description */
30
- description: string
31
- /** Image URL */
32
- image: string
33
- /** Accessible image description (falls back to `name`) */
34
- imageAlt?: string
35
- /** Current price */
36
- price: number
37
- /** Original price, rendered struck-through when present */
38
- originalPrice?: number
39
- /** Optional call to action shown on the card */
40
- cta?: CTA
41
- }
42
-
43
- export interface StoreLocator {
44
- /** Locator section title */
45
- title: string
46
- /** Locator section description */
47
- description: string
48
- /** Locator button label */
49
- ctaLabel: string
50
- /** Optional link destination for the locator button */
51
- ctaHref?: string
52
- }
53
-
54
- export interface StorefrontHero5Props {
55
- /** Small pill shown above the title */
56
- badge?: string
57
- /** Main headline */
58
- title: string
59
- /** Accent text appended to the headline */
60
- titleAccent?: string
61
- /** Supporting paragraph under the headline */
62
- description: string
63
- /** Primary call to action */
64
- primaryCta?: CTA
65
- /** Secondary call to action */
66
- secondaryCta?: CTA
67
- /** Shipping banner text shown under the CTAs */
68
- shippingInfo?: string
69
- /** Department categories rendered as a grid */
70
- departments: Department[]
71
- /** Featured deals rendered as cards */
72
- featuredDeals: FeaturedDeal[]
73
- /** Store locator strip rendered at the bottom */
74
- storeLocator?: StoreLocator
75
- /** Invoked when a department is clicked (departments without `href`) */
76
- onDepartmentClick?: (department: Department) => void
77
- /** Invoked when a deal CTA is clicked (deals whose `cta` has no `href`) */
78
- onShopNow?: (deal: FeaturedDeal) => void
79
- /** Invoked when the locator button is clicked (when `ctaHref` is absent) */
80
- onLocateStores?: () => void
81
- /** Label for the deal call-to-action button */
82
- shopNowLabel?: string
83
- }
84
-
85
- function CtaButton({
86
- cta,
87
- variant,
88
- children,
89
- }: {
90
- cta: CTA
91
- variant?: ComponentProps<typeof Button>['variant']
92
- children: ReactNode
93
- }) {
94
- if (cta.href) {
95
- return (
96
- <Button
97
- variant={variant}
98
- size='lg'
99
- className='group h-10 px-4 cursor-pointer text-base'
100
- asChild
101
- >
102
- <a href={cta.href}>{children}</a>
103
- </Button>
104
- )
105
- }
106
-
107
- return (
108
- <Button
109
- variant={variant}
110
- size='lg'
111
- className='group h-10 px-4 cursor-pointer text-base'
112
- >
113
- {children}
114
- </Button>
115
- )
116
- }
117
-
118
- function StorefrontHero5({
119
- badge,
120
- title,
121
- titleAccent,
122
- description,
123
- primaryCta,
124
- secondaryCta,
125
- shippingInfo,
126
- departments,
127
- featuredDeals,
128
- storeLocator,
129
- onDepartmentClick,
130
- onShopNow,
131
- onLocateStores,
132
- shopNowLabel = 'Comprar ahora',
133
- }: StorefrontHero5Props) {
134
- const departmentChipClasses = cn(
135
- 'flex flex-col items-center gap-4 rounded-lg p-2 text-center outline-none',
136
- 'transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring',
137
- )
138
-
139
- return (
140
- <div className='bg-background relative overflow-hidden'>
141
- <div className='relative z-10'>
142
- <div className='mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20'>
143
- <div className='mx-auto max-w-4xl text-center'>
144
- {badge && (
145
- <div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center justify-center rounded-md border px-4 py-1.5 text-sm'>
146
- {badge}
147
- </div>
148
- )}
149
-
150
- <h1 className='mt-6 text-4xl font-bold tracking-tight text-balance sm:text-5xl md:text-6xl'>
151
- {title}{' '}
152
- {titleAccent && <span className='text-primary'>{titleAccent}</span>}
153
- </h1>
154
-
155
- <p className='text-muted-foreground mx-auto mt-6 max-w-2xl text-lg'>
156
- {description}
157
- </p>
158
-
159
- {(primaryCta || secondaryCta) && (
160
- <div className='mt-8 flex flex-wrap justify-center gap-4'>
161
- {primaryCta && (
162
- <CtaButton cta={primaryCta}>
163
- {primaryCta.label}
164
- <ArrowRight className='transition-transform group-hover:translate-x-1' />
165
- </CtaButton>
166
- )}
167
- {secondaryCta && <CtaButton cta={secondaryCta} variant='outline'>{secondaryCta.label}</CtaButton>}
168
- </div>
169
- )}
170
-
171
- {shippingInfo && (
172
- <div className='mt-6'>
173
- <div className='bg-background/80 text-muted-foreground inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm backdrop-blur-sm'>
174
- <MapPin className='text-primary size-4' aria-hidden='true' />
175
- <span>{shippingInfo}</span>
176
- </div>
177
- </div>
178
- )}
179
-
180
- <div className='mt-12 grid grid-cols-3 gap-8'>
181
- {departments.map(department => {
182
- const chip = (
183
- <>
184
- <img
185
- src={department.image || undefined}
186
- alt={department.imageAlt ?? department.name}
187
- className='bg-card border-border size-16 rounded-full border object-cover'
188
- loading='lazy'
189
- />
190
- <div>
191
- <div className='text-lg font-medium'>{department.name}</div>
192
- {department.count && (
193
- <div className='text-muted-foreground mt-1 text-sm'>{department.count}</div>
194
- )}
195
- </div>
196
- </>
197
- )
198
-
199
- if (department.href) {
200
- return (
201
- <a
202
- key={department.id}
203
- href={department.href}
204
- className={cn(departmentChipClasses, 'cursor-pointer')}
205
- >
206
- {chip}
207
- </a>
208
- )
209
- }
210
-
211
- return (
212
- <button
213
- key={department.id}
214
- type='button'
215
- onClick={onDepartmentClick ? () => onDepartmentClick(department) : undefined}
216
- className={cn(departmentChipClasses, 'cursor-pointer')}
217
- >
218
- {chip}
219
- </button>
220
- )
221
- })}
222
- </div>
223
-
224
- {featuredDeals.length > 0 && (
225
- <div className='mt-16 grid gap-6 sm:grid-cols-2'>
226
- {featuredDeals.map(deal => (
227
- <Card key={deal.id} className='mx-auto h-full w-full overflow-hidden border-0 py-0 shadow-lg'>
228
- <CardContent className='p-0'>
229
- <div className='relative aspect-[3/2] overflow-hidden'>
230
- <img
231
- src={deal.image || undefined}
232
- alt={deal.imageAlt ?? deal.name}
233
- className='size-full object-cover object-center transition-transform duration-700 hover:scale-105'
234
- loading='lazy'
235
- />
236
- <div className='from-background/90 via-background/30 absolute inset-0 bg-gradient-to-t to-transparent' />
237
- <div className='absolute inset-0 flex items-end p-8'>
238
- <div className='w-full'>
239
- <div className='bg-background/80 flex flex-col items-center justify-between gap-6 rounded-lg p-6 backdrop-blur-sm sm:flex-row sm:gap-8'>
240
- <div className='text-center sm:text-left'>
241
- <h3 className='text-2xl font-bold tracking-tight sm:text-3xl'>{deal.name}</h3>
242
- <p className='text-muted-foreground mt-1'>{deal.description}</p>
243
- <div className='mt-2 flex items-baseline justify-center gap-2 sm:justify-start'>
244
- <span className='text-lg font-bold'>${deal.price.toFixed(2)}</span>
245
- {deal.originalPrice && (
246
- <span className='text-muted-foreground text-sm line-through'>
247
- ${deal.originalPrice.toFixed(2)}
248
- </span>
249
- )}
250
- </div>
251
- </div>
252
- {(deal.cta || onShopNow) && (
253
- <div className='flex-shrink-0'>
254
- {deal.cta?.href ? (
255
- <Button size='lg' className='group h-10 px-4 cursor-pointer' asChild>
256
- <a href={deal.cta.href}>
257
- {deal.cta.label}
258
- <ArrowRight className='transition-transform group-hover:translate-x-1' />
259
- </a>
260
- </Button>
261
- ) : (
262
- <Button
263
- size='lg'
264
- className='group h-10 px-4 cursor-pointer'
265
- onClick={onShopNow ? () => onShopNow(deal) : undefined}
266
- >
267
- {deal.cta?.label ?? shopNowLabel}
268
- <ArrowRight className='transition-transform group-hover:translate-x-1' />
269
- </Button>
270
- )}
271
- </div>
272
- )}
273
- </div>
274
- </div>
275
- </div>
276
- </div>
277
- </CardContent>
278
- </Card>
279
- ))}
280
- </div>
281
- )}
282
-
283
- {storeLocator && (
284
- <div className='border-border/50 bg-background/50 mt-8 rounded-lg border p-6 backdrop-blur-sm'>
285
- <div className='flex flex-col items-center gap-6 sm:flex-row sm:justify-between'>
286
- <div className='flex items-center gap-4'>
287
- <div className='bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full'>
288
- <MapPin className='size-6' aria-hidden='true' />
289
- </div>
290
- <div className='text-start'>
291
- <h4 className='text-lg font-medium'>{storeLocator.title}</h4>
292
- <p className='text-muted-foreground'>{storeLocator.description}</p>
293
- </div>
294
- </div>
295
- {storeLocator.ctaHref ? (
296
- <Button variant='outline' className='h-9 cursor-pointer px-4 py-2 whitespace-nowrap' asChild>
297
- <a href={storeLocator.ctaHref}>{storeLocator.ctaLabel}</a>
298
- </Button>
299
- ) : (
300
- <Button
301
- variant='outline'
302
- className='h-9 cursor-pointer px-4 py-2 whitespace-nowrap'
303
- onClick={onLocateStores ? onLocateStores : undefined}
304
- >
305
- {storeLocator.ctaLabel}
306
- </Button>
307
- )}
308
- </div>
309
- </div>
310
- )}
311
- </div>
312
- </div>
313
- </div>
314
- </div>
315
- )
316
- }
317
-
318
- export default StorefrontHero5