@thorprovider/blocks 0.1.2 → 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.
- package/CHANGELOG.md +107 -0
- package/README.md +108 -23
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
- package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +204 -109
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +154 -2
- package/src/blocks/marketing/navbars/navbar-4.tsx +152 -32
- package/src/index.ts +93 -46
- package/src/lib/contracts-curated.test.ts +42 -24
- package/src/lib/contracts.test.ts +143 -2
- package/src/lib/contracts.ts +395 -13
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -37
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1584
- package/dist/contracts.d.ts +0 -1584
- package/dist/contracts.js +0 -390
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -334
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -2786
- package/dist/index.d.ts +0 -2786
- package/dist/index.js +0 -17139
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17059
- package/dist/index.mjs.map +0 -1
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -131
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -137
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -65
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
|
@@ -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 "{query.trim()}"
|
|
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,131 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { motion, useReducedMotion } from 'motion/react'
|
|
4
|
-
|
|
5
|
-
import { Button } from '../../../ui/button'
|
|
6
|
-
import { Card, CardContent, CardFooter } from '../../../ui/card'
|
|
7
|
-
|
|
8
|
-
export interface Product {
|
|
9
|
-
id: string | number
|
|
10
|
-
title: string
|
|
11
|
-
price: number
|
|
12
|
-
originalPrice?: number
|
|
13
|
-
image: string
|
|
14
|
-
badge?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ProductCard1Props {
|
|
18
|
-
products: Product[]
|
|
19
|
-
heading?: string
|
|
20
|
-
maxItems?: number
|
|
21
|
-
onAddToCart?: (product: Product) => void
|
|
22
|
-
onProductClick?: (product: Product) => void
|
|
23
|
-
addToCartLabel?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function ProductCard1({
|
|
27
|
-
products,
|
|
28
|
-
heading,
|
|
29
|
-
maxItems = 6,
|
|
30
|
-
onAddToCart,
|
|
31
|
-
onProductClick,
|
|
32
|
-
addToCartLabel = 'Agregar al carrito',
|
|
33
|
-
}: ProductCard1Props) {
|
|
34
|
-
const reduceMotion = useReducedMotion()
|
|
35
|
-
const displayedProducts = products.slice(0, maxItems)
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<section className='py-12'>
|
|
39
|
-
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
40
|
-
{heading && (
|
|
41
|
-
<h2 className='mb-8 text-center text-2xl font-bold text-balance md:text-3xl'>{heading}</h2>
|
|
42
|
-
)}
|
|
43
|
-
|
|
44
|
-
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6'>
|
|
45
|
-
{displayedProducts.map((product, index) => (
|
|
46
|
-
<motion.div
|
|
47
|
-
key={product.id}
|
|
48
|
-
initial={reduceMotion ? false : { opacity: 0, y: 16 }}
|
|
49
|
-
whileInView={{ opacity: 1, y: 0 }}
|
|
50
|
-
viewport={{ once: true, margin: '-40px' }}
|
|
51
|
-
whileHover={reduceMotion ? undefined : { y: -4 }}
|
|
52
|
-
transition={{
|
|
53
|
-
duration: 0.35,
|
|
54
|
-
delay: reduceMotion ? 0 : index * 0.05,
|
|
55
|
-
ease: 'easeOut',
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
<Card
|
|
59
|
-
className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${
|
|
60
|
-
onProductClick ? 'cursor-pointer' : ''
|
|
61
|
-
}`}
|
|
62
|
-
onClick={onProductClick ? () => onProductClick(product) : undefined}
|
|
63
|
-
>
|
|
64
|
-
<CardContent className='flex flex-1 flex-col gap-4 px-4 '>
|
|
65
|
-
<div className='relative aspect-square overflow-hidden rounded-md'>
|
|
66
|
-
{product.badge && (
|
|
67
|
-
<span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>
|
|
68
|
-
{product.badge}
|
|
69
|
-
</span>
|
|
70
|
-
)}
|
|
71
|
-
<img
|
|
72
|
-
src={product.image || undefined}
|
|
73
|
-
alt={product.title}
|
|
74
|
-
className='size-full rounded-md object-contain'
|
|
75
|
-
loading='lazy'
|
|
76
|
-
width={400}
|
|
77
|
-
height={400}
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<div className='flex flex-1 flex-col'>
|
|
82
|
-
<h2 className='mb-1 font-medium text-balance'>
|
|
83
|
-
{onProductClick ? (
|
|
84
|
-
<button
|
|
85
|
-
type='button'
|
|
86
|
-
onClick={(e) => {
|
|
87
|
-
e.stopPropagation()
|
|
88
|
-
onProductClick(product)
|
|
89
|
-
}}
|
|
90
|
-
className='hover:text-primary cursor-pointer text-left transition-colors'
|
|
91
|
-
>
|
|
92
|
-
{product.title}
|
|
93
|
-
</button>
|
|
94
|
-
) : (
|
|
95
|
-
product.title
|
|
96
|
-
)}
|
|
97
|
-
</h2>
|
|
98
|
-
<div className='mt-auto flex items-baseline gap-2'>
|
|
99
|
-
<p className='font-semibold'>${product.price.toFixed(2)}</p>
|
|
100
|
-
{product.originalPrice && (
|
|
101
|
-
<p className='text-muted-foreground text-sm line-through md:text-base xl:text-sm 2xl:text-base'>
|
|
102
|
-
${product.originalPrice.toFixed(2)}
|
|
103
|
-
</p>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</CardContent>
|
|
108
|
-
|
|
109
|
-
<CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>
|
|
110
|
-
<Button
|
|
111
|
-
variant='outline'
|
|
112
|
-
size='sm'
|
|
113
|
-
className='h-8 w-full cursor-pointer px-3 text-sm text-xs'
|
|
114
|
-
onClick={(e) => {
|
|
115
|
-
e.stopPropagation()
|
|
116
|
-
onAddToCart?.(product)
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
{addToCartLabel}
|
|
120
|
-
</Button>
|
|
121
|
-
</CardFooter>
|
|
122
|
-
</Card>
|
|
123
|
-
</motion.div>
|
|
124
|
-
))}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</section>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export default ProductCard1
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Star } from 'lucide-react'
|
|
2
|
-
import { Card, CardContent, CardTitle } from '../../../ui/card'
|
|
3
|
-
|
|
4
|
-
export interface ProductCard2Product {
|
|
5
|
-
id: string | number
|
|
6
|
-
title: string
|
|
7
|
-
image: string
|
|
8
|
-
price: number
|
|
9
|
-
rating: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ProductCard2Props {
|
|
13
|
-
products: ProductCard2Product[]
|
|
14
|
-
heading?: string
|
|
15
|
-
description?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function ProductCard2({ products, heading, description }: ProductCard2Props) {
|
|
19
|
-
return (
|
|
20
|
-
<section className='py-12'>
|
|
21
|
-
<div className='mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 sm:px-6 lg:px-8'>
|
|
22
|
-
{heading || description ? (
|
|
23
|
-
<header className='flex flex-col gap-2'>
|
|
24
|
-
{heading && <h2 className='text-3xl font-bold text-balance sm:text-4xl'>{heading}</h2>}
|
|
25
|
-
{description && (
|
|
26
|
-
<p className='text-muted-foreground max-w-[60ch] text-balance'>{description}</p>
|
|
27
|
-
)}
|
|
28
|
-
</header>
|
|
29
|
-
) : null}
|
|
30
|
-
|
|
31
|
-
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
|
|
32
|
-
{products.map(product => (
|
|
33
|
-
<Card key={product.id} className='group overflow-hidden transition-all hover:shadow-lg'>
|
|
34
|
-
<CardContent className='flex flex-col gap-4'>
|
|
35
|
-
<div className='overflow-hidden rounded-md'>
|
|
36
|
-
<img
|
|
37
|
-
src={product.image || undefined}
|
|
38
|
-
alt={product.title}
|
|
39
|
-
width={400}
|
|
40
|
-
height={400}
|
|
41
|
-
className='aspect-square w-full object-cover transition-transform duration-300 group-hover:scale-105'
|
|
42
|
-
/>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div className='flex flex-col gap-2'>
|
|
46
|
-
<CardTitle className='line-clamp-1 text-lg font-semibold text-balance sm:text-xl'>
|
|
47
|
-
{product.title}
|
|
48
|
-
</CardTitle>
|
|
49
|
-
|
|
50
|
-
<div
|
|
51
|
-
className='flex items-center gap-0.5'
|
|
52
|
-
aria-label={`${product.rating} out of 5 stars`}
|
|
53
|
-
role='img'
|
|
54
|
-
>
|
|
55
|
-
{Array.from({ length: product.rating }).map((_, i) => (
|
|
56
|
-
<Star key={i} className='fill-foreground text-foreground size-4 sm:size-5' />
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<p className='text-lg font-semibold sm:text-xl'>${product.price.toFixed(2)}</p>
|
|
61
|
-
</div>
|
|
62
|
-
</CardContent>
|
|
63
|
-
</Card>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</section>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export default ProductCard2
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState, type ComponentType } from 'react'
|
|
4
|
-
|
|
5
|
-
import { cn } from '../../../lib/utils'
|
|
6
|
-
|
|
7
|
-
const PARTICLE_COUNT = 8
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The pop/burst keyframes travel with the component instead of living in the
|
|
11
|
-
* app's stylesheet, so this file is self-contained: drop it in and the
|
|
12
|
-
* animation works with no Tailwind config or global CSS to copy over.
|
|
13
|
-
* Injected once per page, guarded by the marker attribute.
|
|
14
|
-
*/
|
|
15
|
-
const STYLE_MARKER = 'data-reaction-toggle-keyframes'
|
|
16
|
-
const STYLES = `
|
|
17
|
-
@keyframes reaction-toggle-pop {
|
|
18
|
-
0% { transform: scale(1); }
|
|
19
|
-
30% { transform: scale(0.82); }
|
|
20
|
-
100% { transform: scale(1); }
|
|
21
|
-
}
|
|
22
|
-
@keyframes reaction-toggle-burst {
|
|
23
|
-
0% { opacity: 0; transform: translate(0, 0) scale(0.4); }
|
|
24
|
-
20% { opacity: 1; transform: translate(calc(var(--rx) * 0.25), calc(var(--ry) * 0.25)) scale(1); }
|
|
25
|
-
100% { opacity: 0; transform: translate(var(--rx), var(--ry)) scale(0.6); }
|
|
26
|
-
}
|
|
27
|
-
.reaction-toggle-pop {
|
|
28
|
-
animation: reaction-toggle-pop 350ms cubic-bezier(0.34, 1.96, 0.64, 1);
|
|
29
|
-
}
|
|
30
|
-
.reaction-toggle-burst {
|
|
31
|
-
animation-name: reaction-toggle-burst;
|
|
32
|
-
animation-timing-function: ease-out;
|
|
33
|
-
animation-fill-mode: forwards;
|
|
34
|
-
}
|
|
35
|
-
@media (prefers-reduced-motion: reduce) {
|
|
36
|
-
.reaction-toggle-pop,
|
|
37
|
-
.reaction-toggle-burst { animation: none !important; }
|
|
38
|
-
}
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
function useReactionToggleStyles() {
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (document.head.querySelector(`[${STYLE_MARKER}]`)) return
|
|
44
|
-
|
|
45
|
-
const style = document.createElement('style')
|
|
46
|
-
style.setAttribute(STYLE_MARKER, '')
|
|
47
|
-
style.textContent = STYLES
|
|
48
|
-
document.head.append(style)
|
|
49
|
-
}, [])
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type ReactionIconProps = {
|
|
53
|
-
className?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type Particle = {
|
|
57
|
-
rx: number
|
|
58
|
-
ry: number
|
|
59
|
-
duration: number
|
|
60
|
-
delay: number
|
|
61
|
-
size: number
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function buildParticles(): Particle[] {
|
|
65
|
-
return Array.from({ length: PARTICLE_COUNT }, (_, i) => {
|
|
66
|
-
const angle = (Math.PI * 2 * i) / PARTICLE_COUNT + (Math.random() - 0.5) * 0.4
|
|
67
|
-
const dist = 16 + Math.random() * 10
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
rx: Math.round(Math.cos(angle) * dist * 10) / 10,
|
|
71
|
-
ry: Math.round(Math.sin(angle) * dist * 10) / 10,
|
|
72
|
-
duration: Math.round(500 + Math.random() * 200),
|
|
73
|
-
delay: Math.round(Math.random() * 60),
|
|
74
|
-
size: Math.round((0.7 + Math.random() * 0.8) * 100) / 100,
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type ReactionToggleProps = {
|
|
80
|
-
/** Whether the reaction is currently active (liked/helpful/etc). */
|
|
81
|
-
active: boolean
|
|
82
|
-
/** Icon component to render, e.g. `Heart` or `ThumbsUp` from lucide-react. */
|
|
83
|
-
icon: ComponentType<ReactionIconProps>
|
|
84
|
-
/** Wrapper class - sizing/positioning of the whole control. */
|
|
85
|
-
className?: string
|
|
86
|
-
/** Class applied to the icon itself (e.g. `size-5`). */
|
|
87
|
-
iconClassName?: string
|
|
88
|
-
/**
|
|
89
|
-
* Tailwind color class (e.g. `text-primary`, `text-destructive`) applied to the
|
|
90
|
-
* icon and particles while active. Drives `fill-current`/`bg-current`, so it
|
|
91
|
-
* stays in sync with the design system instead of a hardcoded brand color.
|
|
92
|
-
*/
|
|
93
|
-
activeColorClassName?: string
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Heart/thumbs-up style reaction control: the icon pops with an overshoot scale
|
|
98
|
-
* and sprinkles 8 particles outward the moment it becomes active.
|
|
99
|
-
*/
|
|
100
|
-
export function ProductCard3ReactionToggle({
|
|
101
|
-
active,
|
|
102
|
-
icon: Icon,
|
|
103
|
-
className,
|
|
104
|
-
iconClassName,
|
|
105
|
-
activeColorClassName = 'text-primary',
|
|
106
|
-
}: ReactionToggleProps) {
|
|
107
|
-
const [isBursting, setIsBursting] = useState(false)
|
|
108
|
-
const wasActive = useRef(active)
|
|
109
|
-
const particlesRef = useRef<Particle[]>(buildParticles())
|
|
110
|
-
|
|
111
|
-
useReactionToggleStyles()
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (active && !wasActive.current) {
|
|
115
|
-
particlesRef.current = buildParticles()
|
|
116
|
-
setIsBursting(true)
|
|
117
|
-
const timeout = setTimeout(() => setIsBursting(false), 700)
|
|
118
|
-
wasActive.current = active
|
|
119
|
-
return () => clearTimeout(timeout)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
wasActive.current = active
|
|
123
|
-
}, [active])
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<span className={cn('relative inline-flex', className)}>
|
|
127
|
-
<span className={cn('inline-flex', active && 'reaction-toggle-pop')}>
|
|
128
|
-
<Icon
|
|
129
|
-
className={cn(
|
|
130
|
-
'transition-[fill,stroke,color] duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]',
|
|
131
|
-
active ? cn('fill-current', activeColorClassName) : 'fill-transparent',
|
|
132
|
-
iconClassName
|
|
133
|
-
)}
|
|
134
|
-
/>
|
|
135
|
-
</span>
|
|
136
|
-
|
|
137
|
-
{isBursting && (
|
|
138
|
-
<span
|
|
139
|
-
aria-hidden='true'
|
|
140
|
-
className={cn('pointer-events-none absolute top-1/2 left-1/2 size-0', activeColorClassName)}
|
|
141
|
-
>
|
|
142
|
-
{particlesRef.current.map((particle, index) => (
|
|
143
|
-
<i
|
|
144
|
-
key={index}
|
|
145
|
-
className='reaction-toggle-burst absolute rounded-full bg-current opacity-0'
|
|
146
|
-
style={
|
|
147
|
-
{
|
|
148
|
-
'--rx': `${particle.rx}px`,
|
|
149
|
-
'--ry': `${particle.ry}px`,
|
|
150
|
-
animationDuration: `${particle.duration}ms`,
|
|
151
|
-
animationDelay: `${particle.delay}ms`,
|
|
152
|
-
width: `${2.5 * particle.size}px`,
|
|
153
|
-
height: `${2.5 * particle.size}px`,
|
|
154
|
-
left: `${-1.25 * particle.size}px`,
|
|
155
|
-
top: `${-1.25 * particle.size}px`,
|
|
156
|
-
} as React.CSSProperties
|
|
157
|
-
}
|
|
158
|
-
/>
|
|
159
|
-
))}
|
|
160
|
-
</span>
|
|
161
|
-
)}
|
|
162
|
-
</span>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export default ProductCard3ReactionToggle
|