@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,219 +0,0 @@
1
- 'use client'
2
-
3
- import type { JSX } from 'react'
4
- import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
5
- import { Flame, ShoppingBag, Sparkles, Star, Trophy } from 'lucide-react'
6
-
7
- import { Badge } from '../../../ui/badge'
8
- import { Button } from '../../../ui/button'
9
- import { Card, CardContent } from '../../../ui/card'
10
- import { cn } from '../../../lib/utils'
11
-
12
- import { Navbar4LikeButton } from './navbar-4-like-button'
13
-
14
- export interface CatalogProduct {
15
- id: string
16
- title: string
17
- subtitle: string
18
- image: string
19
- price: number
20
- originalPrice: number
21
- rating: number
22
- reviewCount: number
23
- discountPercentage: number
24
- isLowStock?: boolean
25
- badge?: {
26
- text: string
27
- variant: 'trending' | 'bestseller' | 'new'
28
- }
29
- }
30
-
31
- export interface Navbar4ProductGridProps {
32
- products: CatalogProduct[]
33
- searchQuery: string
34
- /** When omitted, the section heading is hidden. */
35
- heading?: string
36
- currencySymbol?: string
37
- onAddToCart?: (product: CatalogProduct) => void
38
- }
39
-
40
- const filterEase = [0.22, 1, 0.36, 1] as const
41
-
42
- interface BadgeStyle {
43
- bg: string
44
- icon: JSX.Element | null
45
- }
46
-
47
- const badgeStyles: Record<string, BadgeStyle> = {
48
- trending: {
49
- bg: 'bg-black',
50
- icon: <Flame className='size-3' />,
51
- },
52
- bestseller: {
53
- bg: 'bg-black',
54
- icon: <Trophy className='size-3' />,
55
- },
56
- new: {
57
- bg: 'bg-black',
58
- icon: <Sparkles className='size-3' />,
59
- },
60
- }
61
-
62
- interface Navbar4ProductCardProps {
63
- product: CatalogProduct
64
- currencySymbol?: string
65
- onAddToCart?: (product: CatalogProduct) => void
66
- }
67
-
68
- function Navbar4ProductCard({
69
- product,
70
- currencySymbol = '$',
71
- onAddToCart,
72
- }: Navbar4ProductCardProps) {
73
- const badgeStyle = product.badge ? badgeStyles[product.badge.variant] || badgeStyles.new : null
74
-
75
- return (
76
- <Card className='relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md'>
77
- <CardContent className='px-0'>
78
- <div className='relative aspect-square w-full overflow-hidden'>
79
- <img src={product.image || undefined} alt={product.title} className='size-full object-cover' loading='lazy' />
80
-
81
- {product.badge && badgeStyle ? (
82
- <div className='absolute start-2 top-2 z-20'>
83
- <Badge
84
- className={cn(
85
- 'flex h-auto items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs',
86
- badgeStyle.bg,
87
- )}
88
- >
89
- {badgeStyle.icon}
90
- <span className='text-xs font-medium'>{product.badge.text}</span>
91
- </Badge>
92
- </div>
93
- ) : null}
94
-
95
- {product.rating > 0 ? (
96
- <div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>
97
- <Star className='fill-foreground text-foreground size-4' />
98
- <span>{product.rating}</span>
99
- {product.reviewCount > 0 ? (
100
- <span className='text-muted-foreground'>
101
- |{' '}
102
- {product.reviewCount >= 1000
103
- ? `${(product.reviewCount / 1000).toFixed(1)}k`
104
- : product.reviewCount}
105
- </span>
106
- ) : null}
107
- </div>
108
- ) : null}
109
-
110
- <Navbar4LikeButton className='absolute end-2 top-2 z-20' />
111
- </div>
112
-
113
- <div className='flex flex-col gap-3 p-4'>
114
- <div className='flex flex-col gap-1.5'>
115
- <h3 className='font-medium text-balance'>{product.title}</h3>
116
- <p className='text-muted-foreground text-sm text-balance'>{product.subtitle}</p>
117
- </div>
118
-
119
- <div className='flex items-center gap-2'>
120
- <span className='font-semibold'>
121
- {currencySymbol}
122
- {product.price}
123
- </span>
124
- <span className='text-muted-foreground text-sm line-through'>
125
- {currencySymbol}
126
- {product.originalPrice}
127
- </span>
128
- <span className='text-sm'>({product.discountPercentage}% OFF)</span>
129
- </div>
130
-
131
- {product.isLowStock ? <p className='text-destructive text-sm'>Only few items left!</p> : null}
132
-
133
- <Button className='w-full' onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
134
- <ShoppingBag data-icon='inline-start' />
135
- Add to cart
136
- </Button>
137
- </div>
138
- </CardContent>
139
- </Card>
140
- )
141
- }
142
-
143
- export function Navbar4ProductGrid({
144
- products,
145
- searchQuery,
146
- heading,
147
- currencySymbol,
148
- onAddToCart,
149
- }: Navbar4ProductGridProps) {
150
- const reduceMotion = useReducedMotion()
151
- const query = searchQuery.trim().toLowerCase()
152
- const filtered = query
153
- ? products.filter(
154
- product =>
155
- product.title.toLowerCase().includes(query) || product.subtitle.toLowerCase().includes(query),
156
- )
157
- : products
158
-
159
- return (
160
- <section aria-label='Featured products' className='mx-auto w-full max-w-7xl px-4 py-10 sm:px-6 lg:px-8'>
161
- <div className='mb-6 flex items-end justify-between gap-4'>
162
- <div>
163
- {heading ? (
164
- <h2 className='text-lg font-semibold tracking-tight'>{heading}</h2>
165
- ) : null}
166
- <p className='text-muted-foreground text-sm'>
167
- {query
168
- ? `${filtered.length} ${filtered.length === 1 ? 'result' : 'results'} for “${searchQuery.trim()}”`
169
- : 'Shop the latest pieces — add anything to your bag.'}
170
- </p>
171
- </div>
172
- </div>
173
-
174
- {filtered.length === 0 ? (
175
- <div className='border-border bg-muted/30 flex min-h-64 items-center justify-center rounded-xl border border-dashed px-6 text-center'>
176
- <div>
177
- <p className='font-medium'>No products match your search</p>
178
- <p className='text-muted-foreground mt-1 text-sm'>Try a different keyword or clear the search.</p>
179
- </div>
180
- </div>
181
- ) : (
182
- <motion.ul
183
- layout
184
- className='grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'
185
- transition={{ duration: 0.28, ease: filterEase }}
186
- >
187
- <AnimatePresence mode='popLayout'>
188
- {filtered.map(product => (
189
- <motion.li
190
- key={product.id}
191
- layout={!reduceMotion}
192
- initial={reduceMotion ? false : { opacity: 0, y: 12, scale: 0.97 }}
193
- animate={{ opacity: 1, y: 0, scale: 1 }}
194
- exit={
195
- reduceMotion
196
- ? undefined
197
- : {
198
- opacity: 0,
199
- y: -8,
200
- scale: 0.98,
201
- filter: 'blur(4px)',
202
- transition: { duration: 0.2, ease: 'easeIn' },
203
- }
204
- }
205
- transition={{ duration: 0.28, ease: filterEase }}
206
- >
207
- <Navbar4ProductCard
208
- product={product}
209
- currencySymbol={currencySymbol}
210
- onAddToCart={onAddToCart}
211
- />
212
- </motion.li>
213
- ))}
214
- </AnimatePresence>
215
- </motion.ul>
216
- )}
217
- </section>
218
- )
219
- }
@@ -1,141 +0,0 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import { Star } from 'lucide-react'
5
- import { cn } from '../../../lib/utils'
6
- import { Card, CardContent, CardHeader, CardFooter, CardTitle } from '../../../ui/card'
7
- import {
8
- Carousel,
9
- CarouselContent,
10
- CarouselItem,
11
- CarouselNext,
12
- CarouselPrevious,
13
- type CarouselApi,
14
- } from '../../../ui/carousel'
15
- import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
16
- import { Button } from '../../../ui/button'
17
-
18
- export interface Testimonial1Item {
19
- id: string
20
- name: string
21
- role: string
22
- avatar: string
23
- content: string
24
- rating: number
25
- }
26
-
27
- export interface Testimonial1Props {
28
- items: Testimonial1Item[]
29
- title?: string
30
- prevLabel?: string
31
- nextLabel?: string
32
- slideAriaLabel?: string
33
- }
34
-
35
- function RatingStars({ rating }: { rating: number }) {
36
- return (
37
- <div className='flex items-center gap-1'>
38
- {[...Array(5)].map((_, index) => (
39
- <Star
40
- key={index}
41
- className={cn('size-4', index < Math.floor(rating) ? 'fill-foreground' : 'fill-none')}
42
- />
43
- ))}
44
- <span className='text-muted-foreground ms-2 text-sm'>({rating})</span>
45
- </div>
46
- )
47
- }
48
-
49
- export function Testimonial1({
50
- items,
51
- title = 'Lo que dicen nuestros clientes',
52
- prevLabel = 'Anterior',
53
- nextLabel = 'Siguiente',
54
- slideAriaLabel = 'Ir a la diapositiva {n}',
55
- }: Testimonial1Props) {
56
- const [api, setApi] = React.useState<CarouselApi | null>(null)
57
- const [current, setCurrent] = React.useState(0)
58
-
59
- React.useEffect(() => {
60
- if (!api) return
61
-
62
- const onSelect = () => {
63
- setCurrent(api.selectedScrollSnap())
64
- }
65
-
66
- api.on('select', onSelect)
67
- return () => {
68
- api.off('select', onSelect)
69
- }
70
- }, [api])
71
-
72
- return (
73
- <section className='py-12 lg:py-20'>
74
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
75
- <header className='mb-16 text-center'>
76
- <h2 className='text-3xl font-bold text-balance md:text-4xl'>{title}</h2>
77
- </header>
78
-
79
- <Carousel className='w-full' setApi={setApi} opts={{ align: 'start', loop: true }}>
80
- <CarouselContent className='-ml-1'>
81
- {items.map(testimonial => (
82
- <CarouselItem
83
- key={testimonial.id}
84
- className='basis-full px-4 last:pe-0 sm:basis-1/2 lg:basis-1/3'
85
- >
86
- <Card className='h-full overflow-hidden border border-border py-6'>
87
- <CardHeader className='gap-0 px-6'>
88
- <div className='flex items-center gap-4'>
89
- <Avatar className='bg-muted size-12'>
90
- <AvatarImage src={testimonial.avatar || undefined} alt={testimonial.name} className='size-12' />
91
- <AvatarFallback className='bg-card'>{testimonial.name[0]}</AvatarFallback>
92
- </Avatar>
93
- <div>
94
- <CardTitle className='text-foreground font-semibold'>{testimonial.name}</CardTitle>
95
- <p className='text-muted-foreground text-sm'>{testimonial.role}</p>
96
- </div>
97
- </div>
98
- </CardHeader>
99
- <CardContent className='px-6'>
100
- <p className='text-muted-foreground text-base'>{testimonial.content}</p>
101
- </CardContent>
102
- <CardFooter className='bg-transparent border-t-0 px-6 pb-6'>
103
- <RatingStars rating={testimonial.rating} />
104
- </CardFooter>
105
- </Card>
106
- </CarouselItem>
107
- ))}
108
- </CarouselContent>
109
- <CarouselPrevious
110
- variant='outline'
111
- className='hidden cursor-pointer lg:flex'
112
- aria-label={prevLabel}
113
- />
114
- <CarouselNext
115
- variant='outline'
116
- className='hidden cursor-pointer lg:flex'
117
- aria-label={nextLabel}
118
- />
119
- <div className='mt-8 flex items-center justify-center gap-2'>
120
- {items.map((_, index) => (
121
- <Button
122
- variant='ghost'
123
- key={index}
124
- onClick={() => api?.scrollTo(index)}
125
- className={cn(
126
- 'h-9 px-4 py-2',
127
- 'size-2 cursor-pointer rounded-full !p-0 transition-all',
128
- current === index ? 'bg-foreground w-6' : 'bg-muted',
129
- )}
130
- aria-label={slideAriaLabel.replace('{n}', String(index + 1))}
131
- aria-current={current === index ? 'true' : 'false'}
132
- />
133
- ))}
134
- </div>
135
- </Carousel>
136
- </div>
137
- </section>
138
- )
139
- }
140
-
141
- export default Testimonial1
@@ -1,108 +0,0 @@
1
- import { Quote } from 'lucide-react'
2
- import { Card, CardContent, CardHeader } from '../../../ui/card'
3
- import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
4
-
5
- export interface Testimonial2Item {
6
- id: string
7
- name: string
8
- role: string
9
- avatar: string
10
- content: string
11
- }
12
-
13
- export interface Testimonial2Props {
14
- items: Testimonial2Item[]
15
- title?: string
16
- description?: string
17
- }
18
-
19
- const bentoGridClasses = [
20
- 'grid grid-rows-[auto_1fr] gap-8 ring-1 ring-foreground/10 sm:p-6 md:col-span-2 lg:col-span-2 lg:row-span-2 shadow-sm',
21
- 'ring-1 ring-foreground/10 md:col-span-2 lg:col-span-2 py-6 shadow-sm',
22
- 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
23
- 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
24
- ]
25
-
26
- const contentClasses = [
27
- '',
28
- 'h-full px-6',
29
- 'h-full pt-6 px-6',
30
- 'h-full pt-6 px-6',
31
- ]
32
-
33
- const textClasses = [
34
- 'text-xl text-balance',
35
- 'text-base text-balance',
36
- 'text-base text-balance',
37
- 'text-base text-balance',
38
- ]
39
-
40
- function initials(name: string) {
41
- return name
42
- .split(' ')
43
- .map(n => n[0])
44
- .join('')
45
- }
46
-
47
- export function Testimonial2({
48
- items,
49
- title = 'Con la confianza de quienes construyen',
50
- description,
51
- }: Testimonial2Props) {
52
- /** El diseño bento consume exactamente 4 tarjetas: la primera ocupa la celda
53
- * grande (lg:row-span-2) y las tres restantes completan la cuadrícula. */
54
- const bentoItems = items.slice(0, 4)
55
-
56
- return (
57
- <section className='py-12 md:py-16'>
58
- <div className='mx-auto w-full max-w-7xl flex flex-col gap-8 px-4 sm:px-6 lg:px-8 md:gap-16'>
59
- <div className='relative z-10 mx-auto max-w-4xl flex flex-col gap-6 text-center md:gap-12'>
60
- <h2 className='text-4xl font-medium max-lg:text-balance lg:text-5xl'>{title}</h2>
61
- {description && (
62
- <p className='text-muted-foreground mx-auto max-w-2xl max-lg:text-balance'>
63
- {description}
64
- </p>
65
- )}
66
- </div>
67
-
68
- <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4 lg:grid-rows-2'>
69
- {bentoItems.map((testimonial, index) => (
70
- <Card key={testimonial.id} className={bentoGridClasses[index]}>
71
- {index === 0 && (
72
- <CardHeader className='pb-6'>
73
- <div className='flex items-center justify-start'>
74
- <Quote className='text-muted-foreground/30 size-16 rotate-180' />
75
- </div>
76
- </CardHeader>
77
- )}
78
- <CardContent className={contentClasses[index]}>
79
- <blockquote className='grid h-full grid-rows-[1fr_auto] gap-6'>
80
- <p className={textClasses[index]}>{testimonial.content}</p>
81
-
82
- <div className='grid grid-cols-[auto_1fr] items-center gap-3'>
83
- <Avatar className='bg-muted size-12'>
84
- <AvatarImage
85
- src={testimonial.avatar || undefined}
86
- alt={testimonial.name}
87
- height='400'
88
- width='400'
89
- loading='lazy'
90
- />
91
- <AvatarFallback>{initials(testimonial.name)}</AvatarFallback>
92
- </Avatar>
93
- <div>
94
- <cite className='text-sm font-medium not-italic'>{testimonial.name}</cite>
95
- <span className='text-muted-foreground block text-sm'>{testimonial.role}</span>
96
- </div>
97
- </div>
98
- </blockquote>
99
- </CardContent>
100
- </Card>
101
- ))}
102
- </div>
103
- </div>
104
- </section>
105
- )
106
- }
107
-
108
- export default Testimonial2