@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,120 +0,0 @@
1
- import { Card } from '../../../ui/card'
2
- import { Badge } from '../../../ui/badge'
3
- import { Button } from '../../../ui/button'
4
- import { ArrowRight, ShoppingBag } from 'lucide-react'
5
- import type { CTA } from '../../../lib/types'
6
-
7
- export interface ProductCategory2Category {
8
- id: string
9
- handle?: string
10
- name: string
11
- description: string
12
- /**
13
- * Number of items in the category. Optional: when omitted, or when it is not
14
- * a finite non-negative number, no count is rendered — an unknown projection
15
- * must not be shown as a fabricated `0`. A real `0` is preserved.
16
- */
17
- itemCount?: number
18
- image: string
19
- trending: boolean
20
- }
21
-
22
- export interface ProductCategory2Props {
23
- title?: string
24
- description?: string
25
- categories: ProductCategory2Category[]
26
- cta?: CTA
27
- onBrowse?: (category: ProductCategory2Category) => void
28
- }
29
-
30
- /**
31
- * Formats a category item count only when it is a supplied, finite,
32
- * non-negative number. Returns `undefined` for unknown or invalid values so the
33
- * caller omits the count entirely (a real `0` is preserved).
34
- */
35
- function formatItemCount(itemCount: number | undefined): string | undefined {
36
- if (itemCount === undefined || !Number.isFinite(itemCount) || itemCount < 0) {
37
- return undefined
38
- }
39
- return `${itemCount.toLocaleString()} items`
40
- }
41
-
42
- export function ProductCategory2({
43
- title,
44
- description,
45
- categories,
46
- cta,
47
- onBrowse,
48
- }: ProductCategory2Props) {
49
- return (
50
- <section className='py-12'>
51
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
52
- {(title || description) && (
53
- <div className='mb-12 text-center'>
54
- {title ? <h2 className='text-3xl font-bold tracking-tight text-balance'>{title}</h2> : null}
55
- {description ? (
56
- <p className='text-muted-foreground mt-4 text-lg'>{description}</p>
57
- ) : null}
58
- </div>
59
- )}
60
-
61
- <div className='grid gap-6 sm:grid-cols-2 lg:grid-cols-3'>
62
- {categories.map(category => {
63
- const itemCountLabel = formatItemCount(category.itemCount)
64
- return (
65
- <Card
66
- key={category.id}
67
- className='group cursor-pointer overflow-hidden py-0 transition-all duration-500 hover:shadow-lg'
68
- >
69
- <div className='relative aspect-[5/4] overflow-hidden'>
70
- <img
71
- src={category.image || undefined}
72
- alt={category.name}
73
- className='size-full object-cover transition-transform duration-500 group-hover:scale-105'
74
- />
75
- <div className='absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent' />
76
-
77
- {category.trending ? (
78
- <Badge className='absolute top-4 left-4 rounded-sm px-2.5 py-0.5 font-semibold'>
79
- Trending
80
- </Badge>
81
- ) : null}
82
-
83
- <div className='absolute right-0 bottom-0 left-0 p-6 text-white'>
84
- <h3 className='mb-1 text-xl font-bold'>{category.name}</h3>
85
- <p className='mb-3 text-sm text-white/90'>{category.description}</p>
86
- <div className='flex items-center justify-between'>
87
- {itemCountLabel ? <span className='text-sm'>{itemCountLabel}</span> : null}
88
- <Button
89
- size='sm'
90
- variant='secondary'
91
- className='ms-auto h-8 cursor-pointer border-white/30 bg-white/20 px-3 text-xs text-white backdrop-blur-sm hover:bg-white/30'
92
- onClick={onBrowse ? () => onBrowse(category) : undefined}
93
- >
94
- Browse
95
- <ArrowRight />
96
- </Button>
97
- </div>
98
- </div>
99
- </div>
100
- </Card>
101
- )
102
- })}
103
- </div>
104
-
105
- {cta ? (
106
- <div className='mt-12 text-center'>
107
- <Button size='lg' className='h-10 cursor-pointer gap-2 px-4' asChild>
108
- <a href={cta.href}>
109
- <ShoppingBag className='size-5' />
110
- {cta.label}
111
- </a>
112
- </Button>
113
- </div>
114
- ) : null}
115
- </div>
116
- </section>
117
- )
118
- }
119
-
120
- export default ProductCategory2
@@ -1,129 +0,0 @@
1
- 'use client'
2
-
3
- import type { LucideIcon } from 'lucide-react'
4
- import { Users, ShoppingBag } from 'lucide-react'
5
-
6
- import { Card, CardContent } from '../../../ui/card'
7
- import { Badge } from '../../../ui/badge'
8
- import { Avatar, AvatarImage, AvatarFallback } from '../../../ui/avatar'
9
- import { AvatarGroup } from './avatar-group'
10
-
11
- export interface ProductCategory4Category {
12
- id: string
13
- title: string
14
- description: string
15
- icon: LucideIcon
16
- customers: string
17
- products: number
18
- avatarImages?: string[]
19
- }
20
-
21
- export interface ProductCategory4Props {
22
- title?: string
23
- description?: string
24
- categories: ProductCategory4Category[]
25
- productsLabel?: string
26
- customersLabel?: string
27
- onCategoryClick?: (category: ProductCategory4Category) => void
28
- }
29
-
30
- function getInitials(title: string): string {
31
- return title
32
- .split(/\s+/)
33
- .filter(Boolean)
34
- .slice(0, 2)
35
- .map(word => word.charAt(0).toUpperCase())
36
- .join('')
37
- }
38
-
39
- export function ProductCategory4({
40
- title = 'Compra por categoría',
41
- description = 'Explora nuestra selección de repuestos y accesorios',
42
- categories,
43
- productsLabel = 'productos',
44
- customersLabel = 'clientes',
45
- onCategoryClick,
46
- }: ProductCategory4Props) {
47
- return (
48
- <section className='bg-background w-full py-12 md:py-16'>
49
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
50
- <div className='mx-auto mb-12 max-w-3xl text-center'>
51
- <h2 className='text-foreground mb-4 text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl'>
52
- {title}
53
- </h2>
54
- <p className='text-muted-foreground text-lg md:text-xl'>{description}</p>
55
- </div>
56
-
57
- <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3'>
58
- {categories.map(category => {
59
- const Icon = category.icon
60
- const clickable = Boolean(onCategoryClick)
61
- return (
62
- <Card
63
- key={category.id}
64
- className={`group relative overflow-hidden transition-all hover:shadow-md ${
65
- clickable ? 'cursor-pointer' : ''
66
- }`}
67
- onClick={onCategoryClick ? () => onCategoryClick(category) : undefined}
68
- aria-label={clickable ? category.title : undefined}
69
- >
70
- <CardContent>
71
- <div className='mb-4 flex items-center justify-between'>
72
- <div className='bg-accent text-primary rounded-lg p-2.5' aria-hidden='true'>
73
- <Icon className='size-5' />
74
- </div>
75
- <Badge
76
- variant='secondary'
77
- className='bg-muted/50 rounded-sm px-2 py-1 font-semibold'
78
- aria-label={`${category.products} ${productsLabel}`}
79
- >
80
- <ShoppingBag className='me-1 size-3' />
81
- {category.products} {productsLabel}
82
- </Badge>
83
- </div>
84
-
85
- <h3 className='text-foreground mb-2 text-lg font-semibold'>{category.title}</h3>
86
- <p className='text-muted-foreground mb-4 text-sm'>{category.description}</p>
87
-
88
- <div className='flex items-center justify-between'>
89
- <div className='flex items-center gap-2'>
90
- {category.avatarImages?.length ? (
91
- <>
92
- <AvatarGroup>
93
- {category.avatarImages.slice(0, 3).map((src, i) => (
94
- <Avatar key={i} className='bg-muted size-6 border'>
95
- <AvatarImage src={src || undefined} alt='' />
96
- <AvatarFallback>{getInitials(category.title)}</AvatarFallback>
97
- </Avatar>
98
- ))}
99
- </AvatarGroup>
100
- <span
101
- className='text-muted-foreground flex items-center gap-1 text-sm'
102
- aria-label={`${category.customers} ${customersLabel}`}
103
- >
104
- <Users className='h-3.5 w-3.5' aria-hidden='true' />
105
- {category.customers} {customersLabel}
106
- </span>
107
- </>
108
- ) : (
109
- <span
110
- className='text-muted-foreground flex items-center gap-1 text-sm'
111
- aria-label={`${category.customers} ${customersLabel}`}
112
- >
113
- <Users className='h-3.5 w-3.5' aria-hidden='true' />
114
- {category.customers} {customersLabel}
115
- </span>
116
- )}
117
- </div>
118
- </div>
119
- </CardContent>
120
- </Card>
121
- )
122
- })}
123
- </div>
124
- </div>
125
- </section>
126
- )
127
- }
128
-
129
- export default ProductCategory4
@@ -1,218 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import { Card, CardContent } from '../../../ui/card'
5
- import { Badge } from '../../../ui/badge'
6
- import { Button } from '../../../ui/button'
7
- import { Input } from '../../../ui/input'
8
- import {
9
- ArrowRight,
10
- Search,
11
- Laptop,
12
- Shirt,
13
- Home,
14
- Dumbbell,
15
- BookOpen,
16
- Heart,
17
- Car,
18
- Utensils,
19
- Gamepad2,
20
- Baby,
21
- Gift,
22
- Wrench,
23
- } from 'lucide-react'
24
- import { cn } from '../../../lib/utils'
25
-
26
- export type CategoryIcon =
27
- | 'electronics'
28
- | 'fashion'
29
- | 'home'
30
- | 'sports'
31
- | 'books'
32
- | 'beauty'
33
- | 'automotive'
34
- | 'food'
35
- | 'gaming'
36
- | 'baby'
37
- | 'gifts'
38
- | 'tools'
39
-
40
- export interface ProductCategory5Category {
41
- id: string
42
- name: string
43
- icon: CategoryIcon
44
- description: string
45
- itemCount: number
46
- subcategories: string[]
47
- featured?: boolean
48
- }
49
-
50
- export interface ProductCategory5Props {
51
- title?: string
52
- description?: string
53
- categories: ProductCategory5Category[]
54
- searchPlaceholder?: string
55
- productsLabel?: string
56
- featuredLabel?: string
57
- browseLabel?: string
58
- viewAllLabel?: string
59
- notFoundLabel?: string
60
- onBrowse?: (category: ProductCategory5Category) => void
61
- onViewAll?: () => void
62
- }
63
-
64
- const categoryIcons: Record<CategoryIcon, typeof Laptop> = {
65
- electronics: Laptop,
66
- fashion: Shirt,
67
- home: Home,
68
- sports: Dumbbell,
69
- books: BookOpen,
70
- beauty: Heart,
71
- automotive: Car,
72
- food: Utensils,
73
- gaming: Gamepad2,
74
- baby: Baby,
75
- gifts: Gift,
76
- tools: Wrench,
77
- }
78
-
79
- export function ProductCategory5({
80
- title = 'Explora categorías',
81
- description = 'Encuentra lo que necesitas en nuestras categorías organizadas',
82
- categories,
83
- searchPlaceholder = 'Buscar categorías...',
84
- productsLabel = 'productos disponibles',
85
- featuredLabel = 'Destacado',
86
- browseLabel = 'Explorar',
87
- viewAllLabel = 'Ver todos los productos',
88
- notFoundLabel = '¿No encuentras lo que buscas?',
89
- onBrowse,
90
- onViewAll,
91
- }: ProductCategory5Props) {
92
- const [query, setQuery] = useState('')
93
-
94
- const filteredCategories = categories.filter(category => {
95
- const q = query.trim().toLowerCase()
96
- if (!q) return true
97
- return (
98
- category.name.toLowerCase().includes(q) ||
99
- category.subcategories.some(sub => sub.toLowerCase().includes(q))
100
- )
101
- })
102
-
103
- const totalCount = categories.reduce((sum, category) => sum + category.itemCount, 0)
104
-
105
- return (
106
- <section className='py-12'>
107
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
108
- <div className='mb-8 text-center'>
109
- <h2 className='mb-4 text-3xl font-bold tracking-tight text-balance'>{title}</h2>
110
- <p className='text-muted-foreground mb-6 text-lg'>{description}</p>
111
-
112
- <div className='relative mx-auto max-w-md'>
113
- <Search
114
- className='text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2'
115
- aria-hidden='true'
116
- />
117
- <Input
118
- placeholder={searchPlaceholder}
119
- value={query}
120
- onChange={e => setQuery(e.target.value)}
121
- className='ps-9 h-9'
122
- aria-label='Buscar categorías'
123
- />
124
- </div>
125
- </div>
126
-
127
- {filteredCategories.length === 0 ? (
128
- <p className='text-muted-foreground py-10 text-center text-sm'>
129
- Sin resultados para &quot;{query.trim()}&quot;
130
- </p>
131
- ) : (
132
- <div className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
133
- {filteredCategories.map(category => {
134
- const Icon = categoryIcons[category.icon]
135
-
136
- return (
137
- <Card
138
- key={category.id}
139
- className={cn(
140
- 'group hover:border-primary relative py-0 transition-all duration-200 hover:shadow-md'
141
- )}
142
- >
143
- <CardContent className='p-6'>
144
- <div className='flex items-start gap-4'>
145
- <div className='flex-shrink-0'>
146
- <div className='bg-primary/10 group-hover:bg-primary/20 flex size-12 items-center justify-center rounded-lg transition-colors'>
147
- <Icon className='text-primary size-6' aria-hidden='true' />
148
- </div>
149
- </div>
150
-
151
- <div className='min-w-0 flex-1'>
152
- <div className='flex items-start justify-between'>
153
- <div className='flex-1'>
154
- <div className='mb-2 flex items-center gap-2'>
155
- <h3 className='text-lg font-semibold transition-colors'>{category.name}</h3>
156
- {category.featured ? (
157
- <Badge
158
- variant='secondary'
159
- className='rounded-sm px-2 py-0.5 font-semibold'
160
- >
161
- {featuredLabel}
162
- </Badge>
163
- ) : null}
164
- </div>
165
- <p className='text-muted-foreground mb-3 line-clamp-2 text-sm'>
166
- {category.description}
167
- </p>
168
-
169
- <div className='mb-3 flex flex-wrap gap-2'>
170
- {category.subcategories.map((sub, index) => (
171
- <span
172
- key={index}
173
- className='bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs'
174
- >
175
- {sub}
176
- </span>
177
- ))}
178
- </div>
179
-
180
- <div className='text-muted-foreground text-sm'>
181
- {category.itemCount.toLocaleString()} {productsLabel}
182
- </div>
183
- </div>
184
- </div>
185
- </div>
186
- </div>
187
- <Button
188
- variant='ghost'
189
- size='sm'
190
- className='absolute end-4 bottom-4 h-8 cursor-pointer px-3 text-xs opacity-0 transition-opacity group-hover:opacity-100'
191
- onClick={onBrowse ? () => onBrowse(category) : undefined}
192
- >
193
- {browseLabel}
194
- <ArrowRight />
195
- </Button>
196
- </CardContent>
197
- </Card>
198
- )
199
- })}
200
- </div>
201
- )}
202
-
203
- <div className='mt-8 text-center'>
204
- <p className='text-muted-foreground mb-4 text-sm'>{notFoundLabel}</p>
205
- <Button
206
- variant='outline'
207
- className='h-9 cursor-pointer px-4 py-2'
208
- onClick={onViewAll}
209
- >
210
- {viewAllLabel} {totalCount.toLocaleString()}
211
- </Button>
212
- </div>
213
- </div>
214
- </section>
215
- )
216
- }
217
-
218
- export default ProductCategory5
@@ -1,88 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import { ProductCard1, type ProductCard1Product } from './product-card-1'
4
-
5
- const legacy: ProductCard1Product = {
6
- id: 'legacy-1',
7
- title: 'Taladro percutor',
8
- price: 49.99,
9
- originalPrice: 69.99,
10
- image: '/demo/drill.svg',
11
- }
12
-
13
- const canonical: ProductCard1Product = {
14
- id: 'sku-1',
15
- title: 'Llave de impacto',
16
- price: { amount: '129.50', currencyCode: 'EUR' },
17
- compareAtPrice: { amount: '159.00', currencyCode: 'EUR' },
18
- image: '/demo/impact.svg',
19
- imageAlt: 'Llave de impacto inalámbrica',
20
- rating: 4.5,
21
- badge: 'Nuevo',
22
- }
23
-
24
- const render = (products: ProductCard1Product[], extra: Record<string, unknown> = {}) =>
25
- renderToStaticMarkup(<ProductCard1 products={products} {...extra} />)
26
-
27
- describe('ProductCard1 — legacy numeric path unchanged', () => {
28
- it('keeps the legacy dollar output and originalPrice', () => {
29
- const html = render([legacy])
30
- expect(html).toContain('$49.99')
31
- expect(html).toContain('$69.99')
32
- })
33
- })
34
-
35
- describe('ProductCard1 — canonical Money path', () => {
36
- it('formats the canonical price in its own currency (no dollars)', () => {
37
- const html = render([canonical])
38
- expect(html).not.toContain('$')
39
- expect(html).toContain('129')
40
- })
41
-
42
- it('renders a comparison price only when it is strictly higher', () => {
43
- expect(render([canonical])).toContain('159')
44
- const equal = render([
45
- { ...canonical, compareAtPrice: { amount: '129.50', currencyCode: 'EUR' } },
46
- ])
47
- const lower = render([
48
- { ...canonical, compareAtPrice: { amount: '99.00', currencyCode: 'EUR' } },
49
- ])
50
- expect(equal).not.toContain('159')
51
- expect(lower).not.toContain('99')
52
- })
53
-
54
- it('treats a valid zero as a real price', () => {
55
- const html = render([
56
- { ...canonical, price: { amount: '0.00', currencyCode: 'USD' }, compareAtPrice: undefined },
57
- ])
58
- expect(html).toContain('$0.00')
59
- expect(html).not.toContain('Precio no disponible')
60
- })
61
-
62
- it('shows the unavailable label for an unresolved price (never $0)', () => {
63
- const html = render([
64
- { ...canonical, price: { amount: '', currencyCode: '' }, compareAtPrice: undefined },
65
- ])
66
- expect(html).toContain('Precio no disponible')
67
- expect(html).not.toContain('$0')
68
- })
69
-
70
- it('renders the rating and badge only from real data', () => {
71
- expect(render([canonical])).toContain('4.5')
72
- expect(render([canonical])).toContain('Nuevo')
73
- const withoutRating = render([{ ...canonical, rating: undefined, badge: undefined }])
74
- expect(withoutRating).not.toContain('4.5')
75
- expect(withoutRating).not.toContain('Nuevo')
76
- })
77
-
78
- it('omits the image source entirely when there is no image (no empty src)', () => {
79
- const html = render([{ ...canonical, image: undefined }])
80
- expect(html).not.toContain('src=""')
81
- expect(html).toContain('Llave de impacto inalámbrica')
82
- })
83
-
84
- it('falls back to the title when no image alt is provided', () => {
85
- const html = render([{ ...canonical, imageAlt: undefined }])
86
- expect(html).toContain('alt="Llave de impacto"')
87
- })
88
- })