@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.
- package/CHANGELOG.md +155 -0
- package/README.md +228 -68
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
- 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 +447 -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-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} +119 -85
- 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/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +156 -128
- package/src/lib/contracts-curated.test.ts +47 -29
- package/src/lib/contracts.test.ts +198 -2
- package/src/lib/contracts.ts +410 -16
- 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 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- 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.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- 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 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- 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
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- 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 "{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,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
|
-
})
|