@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,272 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect } from 'react'
4
- import { Button } from '../../../ui/button'
5
- import { cn } from '../../../lib/utils'
6
- import { Heart, ShoppingBag, Truck, Star, Check } from 'lucide-react'
7
- import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from '../../../ui/carousel'
8
- import { ReactionToggle } from '../../shared/reaction-toggle'
9
-
10
- export interface ProductOverview2ProductImage {
11
- src: string
12
- alt: string
13
- }
14
-
15
- export interface ProductOverview2ProductColor {
16
- name: string
17
- value: string
18
- }
19
-
20
- export interface ProductOverview2ProductSize {
21
- eu: number
22
- uk: number
23
- us: number
24
- }
25
-
26
- export interface ProductOverview2Product {
27
- id: string
28
- brand: string
29
- name: string
30
- sku: string
31
- price: number
32
- currency: string
33
- rating: number
34
- reviews: number
35
- images: ProductOverview2ProductImage[]
36
- sizes: ProductOverview2ProductSize[]
37
- colors: ProductOverview2ProductColor[]
38
- }
39
-
40
- export interface ProductOverview2Props {
41
- product: ProductOverview2Product
42
- sizesLabel?: string
43
- colorLabel?: string
44
- reviewsLabel?: string
45
- addToCartLabel?: string
46
- removeFromWishlistLabel?: string
47
- addToWishlistLabel?: string
48
- sizeGuideLabel?: string
49
- freeDeliveryLabel?: string
50
- onAddToCart?: (product: ProductOverview2Product) => void
51
- onToggleWishlist?: (product: ProductOverview2Product, isWishlisted: boolean) => void
52
- }
53
-
54
- export default function ProductOverview2({
55
- product,
56
- sizesLabel = 'Tallas',
57
- colorLabel = 'Color',
58
- reviewsLabel = 'Reseñas',
59
- addToCartLabel = 'Agregar al carrito',
60
- removeFromWishlistLabel = 'Quitar de favoritos',
61
- addToWishlistLabel = 'Agregar a favoritos',
62
- sizeGuideLabel = 'Guía de tallas',
63
- freeDeliveryLabel = 'Envío gratis en pedidos superiores a $30.00',
64
- onAddToCart,
65
- onToggleWishlist,
66
- }: ProductOverview2Props) {
67
- const [selectedImage, setSelectedImage] = useState(0)
68
- const [selectedSize, setSelectedSize] = useState<number | null>(product.sizes[1]?.eu ?? null)
69
- const [selectedColor, setSelectedColor] = useState(product.colors[0])
70
- const [isWishlisted, setIsWishlisted] = useState(false)
71
- const [sizeType, setSizeType] = useState<'eu' | 'uk' | 'us'>('eu')
72
- const [carouselApi, setCarouselApi] = useState<CarouselApi>()
73
-
74
- useEffect(() => {
75
- if (!carouselApi) return
76
-
77
- // Set carousel to the selected image
78
- carouselApi.scrollTo(selectedImage)
79
-
80
- // Update selected image when carousel changes
81
- const handleSelect = () => {
82
- const currentIndex = carouselApi.selectedScrollSnap()
83
- setSelectedImage(currentIndex)
84
- }
85
-
86
- carouselApi.on('select', handleSelect)
87
- return () => {
88
- carouselApi.off('select', handleSelect)
89
- }
90
- }, [carouselApi, selectedImage])
91
-
92
- return (
93
- <section className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
94
- <div className='grid grid-cols-1 gap-8 py-4 md:py-6 lg:grid-cols-2 lg:py-12'>
95
- {/* Product Gallery */}
96
- <div className='flex flex-col gap-4'>
97
- {/* Main Image - Carousel */}
98
- <Carousel setApi={setCarouselApi} className='w-full'>
99
- <CarouselContent>
100
- {product.images.map((image, index) => (
101
- <CarouselItem key={index}>
102
- <div className='flex h-130 w-full items-center justify-center overflow-hidden rounded-lg bg-muted'>
103
- <img src={image.src || undefined} alt={image.alt} className='h-full w-full object-cover' />
104
- </div>
105
- </CarouselItem>
106
- ))}
107
- </CarouselContent>
108
- </Carousel>
109
-
110
- {/* Thumbnails */}
111
- <div className='flex flex-wrap gap-4'>
112
- {product.images.map((image, index) => (
113
- <button
114
- type='button'
115
- key={index}
116
- onClick={() => setSelectedImage(index)}
117
- aria-label={`Mostrar imagen ${index + 1} de ${product.name}`}
118
- className='aspect-square size-16 cursor-pointer'
119
- >
120
- <img
121
- src={image.src || undefined}
122
- alt={image.alt}
123
- className={cn(
124
- 'size-full cursor-pointer rounded-md object-cover ring-offset-2 transition-all',
125
- selectedImage === index ? 'ring-foreground ring-2' : '',
126
- )}
127
- />
128
- </button>
129
- ))}
130
- </div>
131
- </div>
132
-
133
- {/* Product Details */}
134
- <div className='flex flex-col gap-6'>
135
- {/* Product Header */}
136
- <header className='flex flex-col gap-4'>
137
- <div className='flex items-center'>
138
- <Button variant='ghost' size='icon' className='bg-primary size-6 scale-75 cursor-pointer rounded-full'>
139
- <Check className='text-primary-foreground size-4' />
140
- </Button>
141
- <span className='ms-2 font-medium'>{product.brand}</span>
142
- <span className='text-muted-foreground ms-auto'>{product.sku}</span>
143
- </div>
144
- <h1 className='text-xl font-bold text-balance xl:text-3xl'>{product.name}</h1>
145
-
146
- {/* Ratings */}
147
- <div className='flex items-center gap-2'>
148
- <div className='flex'>
149
- {[...Array(5)].map((_, i) => (
150
- <Star
151
- key={i}
152
- className={cn(
153
- 'size-5',
154
- i < product.rating ? 'fill-foreground text-foreground' : 'text-muted-foreground',
155
- )}
156
- />
157
- ))}
158
- </div>
159
- <span className='text-muted-foreground text-sm'>
160
- {product.reviews} {reviewsLabel}
161
- </span>
162
- </div>
163
- </header>
164
-
165
- {/* Price */}
166
- <p className='text-4xl font-bold'>
167
- {product.currency}
168
- {product.price.toFixed(2)}
169
- </p>
170
- {/* Color Selection */}
171
- <div>
172
- <div className='mb-4 flex items-center text-sm'>
173
- <span className='font-medium'>{colorLabel}:</span>
174
- <span className='text-muted-foreground ms-1'>{selectedColor.name}</span>
175
- </div>
176
- <div className='flex gap-3'>
177
- {product.colors.map(color => (
178
- <Button
179
- key={color.name}
180
- onClick={() => setSelectedColor(color)}
181
- className={cn(
182
- 'h-9 px-4 py-2',
183
- 'ring-offset-background size-8 cursor-pointer rounded-sm ring-offset-2 transition-all',
184
- selectedColor.name === color.name && 'ring-foreground ring-2',
185
- selectedColor.name !== color.name &&
186
- ['Black', 'White'].includes(color.name) &&
187
- 'outline-muted outline-solid',
188
- )}
189
- style={{ backgroundColor: color.value }}
190
- aria-label={`Seleccionar color ${color.name}`}
191
- aria-selected={selectedColor.name === color.name}
192
- />
193
- ))}
194
- </div>
195
- </div>
196
-
197
- {/* Size Selection */}
198
- <div>
199
- <div className='mb-4 flex items-center justify-between'>
200
- <div className='flex items-center'>
201
- <span className='me-4 text-sm font-medium'>{sizesLabel}</span>
202
- <div className='border-input flex rounded-sm border p-1'>
203
- {['eu', 'uk', 'us'].map(type => (
204
- <Button
205
- key={type}
206
- variant='ghost'
207
- size='sm'
208
- onClick={() => setSizeType(type as 'eu' | 'uk' | 'us')}
209
- className={cn(
210
- 'cursor-pointer rounded-sm px-2 text-xs',
211
- sizeType === type && 'bg-secondary text-secondary-foreground',
212
- )}
213
- aria-label={`Seleccionar talla ${type.toUpperCase()}`}
214
- aria-selected={sizeType === type}
215
- >
216
- {type.toUpperCase()}
217
- </Button>
218
- ))}
219
- </div>
220
- </div>
221
- <Button variant='ghost' className='h-9 px-4 py-2 text-primary cursor-pointer text-sm hover:underline'>
222
- {sizeGuideLabel}
223
- </Button>
224
- </div>
225
- <div className='grid grid-cols-4 gap-2 xl:grid-cols-5'>
226
- {product.sizes.map(size => (
227
- <Button
228
- key={size.eu}
229
- variant={selectedSize === size.eu ? 'secondary' : 'outline'}
230
- onClick={() => setSelectedSize(size.eu)}
231
- className='h-12 cursor-pointer rounded-full shadow-none'
232
- >
233
- {sizeType === 'eu' ? size.eu : sizeType === 'uk' ? size.uk : size.us}
234
- </Button>
235
- ))}
236
- </div>
237
- </div>
238
- {/* Add to Cart */}
239
- <div>
240
- <div className='mb-4 flex gap-4'>
241
- <Button
242
- className='h-12 flex-1 cursor-pointer rounded-md'
243
- size='lg'
244
- onClick={onAddToCart ? () => onAddToCart(product) : undefined}
245
- >
246
- <ShoppingBag />
247
- {addToCartLabel}
248
- </Button>
249
- <Button
250
- variant='outline'
251
- size='icon'
252
- className='border-input size-12 cursor-pointer rounded-md transition-colors'
253
- onClick={() => {
254
- const next = !isWishlisted
255
- setIsWishlisted(next)
256
- onToggleWishlist?.(product, next)
257
- }}
258
- aria-label={isWishlisted ? removeFromWishlistLabel : addToWishlistLabel}
259
- >
260
- <ReactionToggle active={isWishlisted} icon={Heart} iconClassName='size-5' activeColorClassName='text-primary' />
261
- </Button>
262
- </div>
263
- <div className='text-muted-foreground flex items-center justify-center gap-2 text-sm'>
264
- <Truck className='size-4' />
265
- <span>{freeDeliveryLabel}</span>
266
- </div>
267
- </div>
268
- </div>
269
- </div>
270
- </section>
271
- )
272
- }
@@ -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
- }