@thorprovider/blocks 0.1.3 → 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 (99) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +73 -29
  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/product-categories/category-card-grid.test.tsx +122 -0
  7. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  8. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  9. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  10. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  11. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  12. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  13. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  14. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  15. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  16. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  17. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  18. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  19. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  20. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  21. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  22. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  23. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  24. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  25. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  26. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  27. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  28. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  29. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  30. package/src/blocks/marketing/features/feature.tsx +195 -0
  31. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
  32. package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
  33. package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
  34. package/src/index.ts +77 -48
  35. package/src/lib/contracts-curated.test.ts +42 -24
  36. package/src/lib/contracts.test.ts +143 -2
  37. package/src/lib/contracts.ts +346 -14
  38. package/src/ui/button-group.tsx +1 -1
  39. package/src/ui/carousel.tsx +1 -1
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/field.tsx +2 -2
  42. package/src/ui/input-group.tsx +3 -3
  43. package/src/ui/pagination.tsx +1 -1
  44. package/src/ui/sidebar.tsx +7 -7
  45. package/src/ui/toggle-group.tsx +1 -1
  46. package/tsconfig.build.json +12 -0
  47. package/tsup.config.ts +11 -7
  48. package/.turbo/turbo-build.log +0 -24
  49. package/.turbo/turbo-test.log +0 -47
  50. package/.turbo/turbo-type-check.log +0 -0
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/contracts.d.mts +0 -1732
  53. package/dist/contracts.d.ts +0 -1732
  54. package/dist/contracts.js +0 -423
  55. package/dist/contracts.js.map +0 -1
  56. package/dist/contracts.mjs +0 -365
  57. package/dist/contracts.mjs.map +0 -1
  58. package/dist/index.d.mts +0 -3068
  59. package/dist/index.d.ts +0 -3068
  60. package/dist/index.js +0 -17834
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.mjs +0 -17740
  63. package/dist/index.mjs.map +0 -1
  64. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  65. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  66. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  67. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  68. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  69. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  70. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  71. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  72. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  73. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  74. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  75. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  76. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  77. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  78. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  79. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  80. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  81. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  82. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  83. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  84. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  85. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  86. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  87. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  88. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  89. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  90. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  91. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  92. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  93. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  94. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  95. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  96. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  97. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  98. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  99. package/src/blocks/marketing/features/feature-2.tsx +0 -82
@@ -1,229 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import { ShoppingCart4, type ShoppingCart4Item, type ShoppingCart4Totals } from './shopping-cart-4'
4
-
5
- const items: ShoppingCart4Item[] = [
6
- {
7
- id: 'line_1',
8
- name: 'Taladro',
9
- price: 45,
10
- quantity: 2,
11
- image: 'https://cdn.example/taladro.jpg',
12
- lineTotal: { amount: '90.00', currencyCode: 'USD' },
13
- },
14
- { id: 'line_2', name: 'Batería', price: 20, quantity: 1 },
15
- ]
16
-
17
- const totals: ShoppingCart4Totals = {
18
- subtotal: { amount: '110.00', currencyCode: 'USD' },
19
- discount: { amount: '10.00', currencyCode: 'USD' },
20
- tax: { amount: '8.00', currencyCode: 'USD' },
21
- shipping: { amount: '0', currencyCode: 'USD' },
22
- total: { amount: '108.00', currencyCode: 'USD' },
23
- }
24
-
25
- const formatMoney = (money: { amount: string; currencyCode: string }) =>
26
- `${money.currencyCode} ${money.amount}`
27
-
28
- describe('ShoppingCart4 — backend-authoritative totals', () => {
29
- it('renders supplied backend totals verbatim (no local arithmetic)', () => {
30
- const html = renderToStaticMarkup(
31
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
32
- )
33
- expect(html).toContain('USD 110.00')
34
- expect(html).toContain('USD 10.00')
35
- expect(html).toContain('USD 8.00')
36
- expect(html).toContain('Gratis')
37
- expect(html).toContain('USD 108.00')
38
- })
39
-
40
- it('does not invent subtotal or total when none are supplied', () => {
41
- const html = renderToStaticMarkup(
42
- <ShoppingCart4 items={items} promotions={[]} formatMoney={formatMoney} />,
43
- )
44
- expect(html).not.toContain('Subtotal')
45
- expect(html).not.toContain('Total')
46
- })
47
-
48
- it('renders a controlled pending state', () => {
49
- const html = renderToStaticMarkup(
50
- <ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
51
- )
52
- expect(html).toContain('Actualizando totales')
53
- })
54
-
55
- it('renders a controlled error state with a recovery action', () => {
56
- const html = renderToStaticMarkup(
57
- <ShoppingCart4
58
- items={items}
59
- promotions={[]}
60
- totalsState='error'
61
- onRetryTotals={() => {}}
62
- />,
63
- )
64
- expect(html).toContain('No se pudieron actualizar los totales')
65
- expect(html).toContain('Reintentar')
66
- })
67
-
68
- it('omits an absent line image without emitting an empty src', () => {
69
- const html = renderToStaticMarkup(
70
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
71
- )
72
- expect(html).not.toMatch(/src=""/)
73
- })
74
-
75
- it('does not fabricate a delivery estimate or express payment controls', () => {
76
- const html = renderToStaticMarkup(
77
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
78
- )
79
- expect(html).not.toContain('Entrega estimada')
80
- expect(html).not.toContain('Apple Pay')
81
- expect(html).not.toContain('Google Pay')
82
- expect(html).not.toContain('Compra rápida')
83
- })
84
-
85
- it('never renders a fake zero for absent or malformed required totals', () => {
86
- const html = renderToStaticMarkup(
87
- <ShoppingCart4
88
- items={items}
89
- promotions={[]}
90
- totals={{
91
- subtotal: { amount: '', currencyCode: 'USD' },
92
- total: { amount: 'abc', currencyCode: 'USD' },
93
- }}
94
- formatMoney={formatMoney}
95
- />,
96
- )
97
- expect(html).not.toContain('USD 0')
98
- expect(html).toContain('Totales no disponibles')
99
- })
100
-
101
- it('preserves a legitimate zero total', () => {
102
- const html = renderToStaticMarkup(
103
- <ShoppingCart4
104
- items={items}
105
- promotions={[]}
106
- totals={{
107
- subtotal: { amount: '0', currencyCode: 'USD' },
108
- total: { amount: '0', currencyCode: 'USD' },
109
- }}
110
- formatMoney={formatMoney}
111
- />,
112
- )
113
- expect(html).toContain('USD 0')
114
- expect(html).not.toContain('Totales no disponibles')
115
- })
116
-
117
- it('shows an unavailable marker (not zero) for a supplied but malformed line total', () => {
118
- const html = renderToStaticMarkup(
119
- <ShoppingCart4
120
- items={[{ id: 'line_x', name: 'Pieza', price: 10, quantity: 1, lineTotal: { amount: '', currencyCode: 'USD' } }]}
121
- promotions={[]}
122
- totals={totals}
123
- formatMoney={formatMoney}
124
- />,
125
- )
126
- expect(html).toContain('—')
127
- expect(html).not.toContain('USD 0')
128
- })
129
-
130
- it('renders absolute quantities from props (cart contract, not a delta)', () => {
131
- const html = renderToStaticMarkup(
132
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
133
- )
134
- expect(html).toContain('>2<')
135
- expect(html).toContain('>1<')
136
- })
137
- })
138
-
139
- describe('ShoppingCart4 — presentation and action availability', () => {
140
- it('defaults to the compact collapsible card', () => {
141
- const html = renderToStaticMarkup(
142
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
143
- )
144
- expect(html).toContain('max-w-md')
145
- expect(html).toContain('Contraer carrito')
146
- expect(html).toContain('max-h-64')
147
- })
148
-
149
- it('renders page presentation as a wide, always-expanded review with a heading', () => {
150
- const html = renderToStaticMarkup(
151
- <ShoppingCart4
152
- items={items}
153
- promotions={[]}
154
- totals={totals}
155
- formatMoney={formatMoney}
156
- presentation='page'
157
- />,
158
- )
159
- expect(html).toContain('max-w-3xl')
160
- expect(html).toContain('<h1')
161
- expect(html).not.toContain('Contraer carrito')
162
- expect(html).not.toContain('max-h-64')
163
- })
164
-
165
- it('offers a continue-shopping action in page presentation', () => {
166
- const html = renderToStaticMarkup(
167
- <ShoppingCart4
168
- items={items}
169
- promotions={[]}
170
- totals={totals}
171
- formatMoney={formatMoney}
172
- presentation='page'
173
- onContinueShopping={() => {}}
174
- />,
175
- )
176
- expect(html).toContain('Seguir comprando')
177
- })
178
-
179
- it('suppresses checkout when the checkout feature is unavailable', () => {
180
- const html = renderToStaticMarkup(
181
- <ShoppingCart4
182
- items={items}
183
- promotions={[]}
184
- totals={totals}
185
- formatMoney={formatMoney}
186
- presentation='page'
187
- checkoutUnavailable
188
- />,
189
- )
190
- expect(html).not.toContain('Ver carrito y finalizar')
191
- })
192
-
193
- it('disables checkout while the cart is not ready', () => {
194
- const html = renderToStaticMarkup(
195
- <ShoppingCart4
196
- items={items}
197
- promotions={[]}
198
- totals={totals}
199
- formatMoney={formatMoney}
200
- presentation='page'
201
- checkoutDisabled
202
- />,
203
- )
204
- expect(html).toMatch(/<button[^>]*disabled[^>]*>[^<]*Ver carrito y finalizar/)
205
- })
206
- })
207
-
208
- describe('ShoppingCart4 — accessibility', () => {
209
- it('names every line mutation control', () => {
210
- const html = renderToStaticMarkup(
211
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
212
- )
213
- expect(html).toContain('aria-label="Disminuir cantidad, Taladro"')
214
- expect(html).toContain('aria-label="Aumentar cantidad, Taladro"')
215
- expect(html).toContain('aria-label="Quitar Taladro"')
216
- })
217
-
218
- it('announces pending totals politely and errors as an alert', () => {
219
- const pending = renderToStaticMarkup(
220
- <ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
221
- )
222
- expect(pending).toContain('aria-live="polite"')
223
- expect(pending).toContain('Actualizando totales')
224
- const error = renderToStaticMarkup(
225
- <ShoppingCart4 items={items} promotions={[]} totalsState='error' />,
226
- )
227
- expect(error).toMatch(/role="alert"/)
228
- })
229
- })
@@ -1,72 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import StorefrontHero1, { type FeaturedProduct } from './storefront-hero-1'
4
-
5
- const product: FeaturedProduct = {
6
- id: 'p1',
7
- name: 'Taladro percutor',
8
- brand: 'INGCO',
9
- price: 69.99,
10
- originalPrice: 99.99,
11
- rating: 4.8,
12
- reviewCount: 236,
13
- inStock: true,
14
- isNew: false,
15
- isOnSale: true,
16
- image: '/products/drill.png',
17
- colors: ['#111111'],
18
- }
19
-
20
- const base = {
21
- title: 'Potencia para cada proyecto',
22
- description: 'Herramientas profesionales',
23
- products: [],
24
- }
25
-
26
- describe('StorefrontHero1 — optional editorial image', () => {
27
- it('renders no editorial image when none is supplied', () => {
28
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} />)
29
- expect(html).not.toContain('<img')
30
- })
31
-
32
- it('renders a native editorial image with its src and alt text', () => {
33
- const html = renderToStaticMarkup(
34
- <StorefrontHero1 {...base} heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico y accesorios' }} />,
35
- )
36
- expect(html).toContain('src="/hero-tools.png"')
37
- expect(html).toContain('alt="Taladro inalámbrico y accesorios"')
38
- })
39
-
40
- it('never renders an empty image src', () => {
41
- const blank = renderToStaticMarkup(
42
- <StorefrontHero1 {...base} heroImage={{ src: ' ', alt: 'Vacío' }} />,
43
- )
44
- expect(blank).not.toContain('<img')
45
- })
46
-
47
- it('preserves the product grid when the editorial image is omitted', () => {
48
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
49
- expect(html).toContain('Taladro percutor')
50
- expect(html).toContain('src="/products/drill.png"')
51
- expect(html).not.toContain('/hero-tools.png')
52
- })
53
-
54
- it('keeps the product grid when the editorial image is supplied', () => {
55
- const html = renderToStaticMarkup(
56
- <StorefrontHero1
57
- {...base}
58
- products={[product]}
59
- heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico' }}
60
- />,
61
- )
62
- expect(html).toContain('src="/hero-tools.png"')
63
- expect(html).toContain('Taladro percutor')
64
- })
65
- })
66
-
67
- describe('StorefrontHero1 — product card secondary action label', () => {
68
- it('defaults the second product-card action to "Ver producto"', () => {
69
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
70
- expect(html).toContain('Ver producto')
71
- })
72
- })
@@ -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 = 'Ver producto',
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