@thorprovider/blocks 0.1.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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Camera, Heart, Info, MessageSquare, Package, Share2, ShoppingCart, Star } from 'lucide-react'
|
|
5
|
+
import { Card, CardContent } from '../../../ui/card'
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import { Separator } from '../../../ui/separator'
|
|
8
|
+
import { ReactionToggle } from '../../shared/reaction-toggle'
|
|
9
|
+
import { ProductOverview5Gallery } from './product-overview-5-gallery'
|
|
10
|
+
import { ProductOverview5LightboxTab } from './product-overview-5-lightbox-tab'
|
|
11
|
+
import { ProductOverview5ReviewsTab } from './product-overview-5-reviews-tab'
|
|
12
|
+
import {
|
|
13
|
+
ProductOverview5Tabs,
|
|
14
|
+
ProductOverview5TabsContentFade,
|
|
15
|
+
ProductOverview5TabsIndicator,
|
|
16
|
+
ProductOverview5TabsList,
|
|
17
|
+
ProductOverview5TabsTrigger,
|
|
18
|
+
} from './product-overview-5-tabs'
|
|
19
|
+
|
|
20
|
+
export interface ProductOverview5Review {
|
|
21
|
+
id: number
|
|
22
|
+
author: string
|
|
23
|
+
rating: number
|
|
24
|
+
comment: string
|
|
25
|
+
date: string
|
|
26
|
+
helpful: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProductOverview5Product {
|
|
30
|
+
name: string
|
|
31
|
+
price: number
|
|
32
|
+
originalPrice: null | number
|
|
33
|
+
rating: number
|
|
34
|
+
reviewCount: number
|
|
35
|
+
sku: string
|
|
36
|
+
mainImages: string[]
|
|
37
|
+
galleryImages: string[]
|
|
38
|
+
description: string
|
|
39
|
+
features: string[]
|
|
40
|
+
specifications: Record<string, string>
|
|
41
|
+
userReviews: ProductOverview5Review[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProductOverview5Props {
|
|
45
|
+
product: ProductOverview5Product
|
|
46
|
+
badgeLabel?: string
|
|
47
|
+
availabilityLabel?: string
|
|
48
|
+
currencySymbol?: string
|
|
49
|
+
skuLabel?: string
|
|
50
|
+
overviewLabel?: string
|
|
51
|
+
specsLabel?: string
|
|
52
|
+
featuresLabel?: string
|
|
53
|
+
specificationsLabel?: string
|
|
54
|
+
reviewsLabel?: string
|
|
55
|
+
galleryLabel?: string
|
|
56
|
+
saveLabel?: string
|
|
57
|
+
savedLabel?: string
|
|
58
|
+
shareLabel?: string
|
|
59
|
+
addToCartLabel?: string
|
|
60
|
+
basedOnReviewsLabel?: string
|
|
61
|
+
foundHelpfulLabel?: string
|
|
62
|
+
ofCounterLabel?: string
|
|
63
|
+
photosLabel?: string
|
|
64
|
+
onAddToCart?: (product: ProductOverview5Product) => void
|
|
65
|
+
onToggleWishlist?: (product: ProductOverview5Product, isWishlisted: boolean) => void
|
|
66
|
+
onShare?: (product: ProductOverview5Product) => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderStars(rating: number) {
|
|
70
|
+
return Array.from({ length: 5 }, (_, i) => (
|
|
71
|
+
<Star key={i} className={`size-4 ${i < Math.floor(rating) ? 'fill-foreground text-foreground' : i < rating ? 'text-foreground' : ''}`} />
|
|
72
|
+
))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default function ProductOverview5({
|
|
76
|
+
product,
|
|
77
|
+
badgeLabel = 'Destacado',
|
|
78
|
+
availabilityLabel = 'Disponible',
|
|
79
|
+
currencySymbol = '$',
|
|
80
|
+
skuLabel = 'SKU',
|
|
81
|
+
overviewLabel = 'Resumen',
|
|
82
|
+
specsLabel = 'Especificaciones',
|
|
83
|
+
featuresLabel = 'Características clave',
|
|
84
|
+
specificationsLabel = 'Especificaciones técnicas',
|
|
85
|
+
reviewsLabel = 'Reseñas',
|
|
86
|
+
galleryLabel = 'Galería',
|
|
87
|
+
saveLabel = 'Guardar',
|
|
88
|
+
savedLabel = 'Guardado',
|
|
89
|
+
shareLabel = 'Compartir',
|
|
90
|
+
addToCartLabel = 'Agregar al carrito',
|
|
91
|
+
basedOnReviewsLabel = 'Basado en {count} reseñas',
|
|
92
|
+
foundHelpfulLabel = '{count} personas encontraron esto útil',
|
|
93
|
+
ofCounterLabel = '{current} de {total}',
|
|
94
|
+
photosLabel = 'fotos',
|
|
95
|
+
onAddToCart,
|
|
96
|
+
onToggleWishlist,
|
|
97
|
+
onShare,
|
|
98
|
+
}: ProductOverview5Props) {
|
|
99
|
+
const [selectedImage, setSelectedImage] = useState(0)
|
|
100
|
+
const [isWishlisted, setIsWishlisted] = useState(false)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<section className='py-12'>
|
|
104
|
+
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
105
|
+
<div className='grid gap-12 lg:grid-cols-2'>
|
|
106
|
+
<ProductOverview5Gallery
|
|
107
|
+
product={product}
|
|
108
|
+
badgeLabel={badgeLabel}
|
|
109
|
+
selectedImage={selectedImage}
|
|
110
|
+
onSelectImage={setSelectedImage}
|
|
111
|
+
photosLabel={photosLabel}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<div className='flex flex-col gap-8'>
|
|
115
|
+
<div>
|
|
116
|
+
<h1 className='text-3xl leading-tight font-bold text-balance'>{product.name}</h1>
|
|
117
|
+
<div className='mt-4 flex flex-wrap items-center gap-4'>
|
|
118
|
+
<div className='flex items-center gap-2'>
|
|
119
|
+
<div className='flex'>{renderStars(product.rating)}</div>
|
|
120
|
+
<span className='text-sm font-medium'>{product.rating}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<span className='text-muted-foreground text-sm'>
|
|
123
|
+
{product.userReviews.length} {reviewsLabel.toLowerCase()}
|
|
124
|
+
</span>
|
|
125
|
+
<Separator orientation='vertical' className='h-4' />
|
|
126
|
+
<span className='text-muted-foreground text-sm'>
|
|
127
|
+
{skuLabel}: {product.sku}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className='flex items-baseline gap-3'>
|
|
133
|
+
<span className='text-4xl font-bold'>
|
|
134
|
+
{currencySymbol}
|
|
135
|
+
{product.price}
|
|
136
|
+
</span>
|
|
137
|
+
{product.originalPrice && (
|
|
138
|
+
<span className='text-muted-foreground text-xl line-through'>
|
|
139
|
+
{currencySymbol}
|
|
140
|
+
{product.originalPrice}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className='inline-flex items-center w-fit gap-2 rounded-full bg-success/10 px-3 py-1.5'>
|
|
146
|
+
<div className='size-2 rounded-full bg-success' />
|
|
147
|
+
<span className='text-sm font-medium text-success'>{availabilityLabel}</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<p className='text-muted-foreground leading-relaxed'>{product.description}</p>
|
|
151
|
+
|
|
152
|
+
<div className='flex flex-col gap-4'>
|
|
153
|
+
<Button
|
|
154
|
+
size='lg'
|
|
155
|
+
className='h-12 w-full cursor-pointer gap-2'
|
|
156
|
+
onClick={onAddToCart ? () => onAddToCart(product) : undefined}
|
|
157
|
+
>
|
|
158
|
+
<ShoppingCart />
|
|
159
|
+
{addToCartLabel}
|
|
160
|
+
</Button>
|
|
161
|
+
<div className='flex gap-3'>
|
|
162
|
+
<Button
|
|
163
|
+
variant='outline'
|
|
164
|
+
size='lg'
|
|
165
|
+
className='h-12 flex-1 cursor-pointer gap-2'
|
|
166
|
+
aria-pressed={isWishlisted}
|
|
167
|
+
onClick={() => {
|
|
168
|
+
const next = !isWishlisted
|
|
169
|
+
setIsWishlisted(next)
|
|
170
|
+
onToggleWishlist?.(product, next)
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<ReactionToggle active={isWishlisted} icon={Heart} activeColorClassName='text-destructive' />
|
|
174
|
+
{isWishlisted ? savedLabel : saveLabel}
|
|
175
|
+
</Button>
|
|
176
|
+
<Button
|
|
177
|
+
variant='outline'
|
|
178
|
+
size='lg'
|
|
179
|
+
className='h-12 flex-1 cursor-pointer gap-2'
|
|
180
|
+
onClick={onShare ? () => onShare(product) : undefined}
|
|
181
|
+
>
|
|
182
|
+
<Share2 />
|
|
183
|
+
{shareLabel}
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className='mt-16'>
|
|
191
|
+
<ProductOverview5Tabs defaultValue='overview' className='w-full'>
|
|
192
|
+
<ProductOverview5TabsList className='bg-muted/50 relative grid !h-12 w-full grid-cols-4 rounded-xl p-1'>
|
|
193
|
+
<ProductOverview5TabsIndicator />
|
|
194
|
+
<ProductOverview5TabsTrigger
|
|
195
|
+
value='overview'
|
|
196
|
+
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
197
|
+
>
|
|
198
|
+
<Info className='size-4 shrink-0' />
|
|
199
|
+
<span className='hidden sm:inline'>{overviewLabel}</span>
|
|
200
|
+
</ProductOverview5TabsTrigger>
|
|
201
|
+
<ProductOverview5TabsTrigger
|
|
202
|
+
value='specs'
|
|
203
|
+
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
204
|
+
>
|
|
205
|
+
<Package className='size-4 shrink-0' />
|
|
206
|
+
<span className='hidden sm:inline'>{specsLabel}</span>
|
|
207
|
+
</ProductOverview5TabsTrigger>
|
|
208
|
+
<ProductOverview5TabsTrigger
|
|
209
|
+
value='reviews'
|
|
210
|
+
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
211
|
+
>
|
|
212
|
+
<MessageSquare className='size-4 shrink-0' />
|
|
213
|
+
<span className='hidden sm:inline'>
|
|
214
|
+
{reviewsLabel} ({product.userReviews.length})
|
|
215
|
+
</span>
|
|
216
|
+
</ProductOverview5TabsTrigger>
|
|
217
|
+
<ProductOverview5TabsTrigger
|
|
218
|
+
value='gallery'
|
|
219
|
+
className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
|
|
220
|
+
>
|
|
221
|
+
<Camera className='size-4 shrink-0' />
|
|
222
|
+
<span className='hidden sm:inline'>{galleryLabel}</span>
|
|
223
|
+
</ProductOverview5TabsTrigger>
|
|
224
|
+
</ProductOverview5TabsList>
|
|
225
|
+
<ProductOverview5TabsContentFade value='overview' className='mt-8'>
|
|
226
|
+
<Card className='border shadow-none'>
|
|
227
|
+
<CardContent className='p-8 py-0'>
|
|
228
|
+
<h3 className='mb-6 text-xl font-semibold text-balance'>{featuresLabel}</h3>
|
|
229
|
+
<div className='grid gap-4 sm:grid-cols-2'>
|
|
230
|
+
{product.features.map(feature => (
|
|
231
|
+
<div key={feature} className='group flex items-center gap-3'>
|
|
232
|
+
<div className='bg-primary/20 size-2 rounded-full transition-colors' />
|
|
233
|
+
<span className='text-sm'>{feature}</span>
|
|
234
|
+
</div>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
</CardContent>
|
|
238
|
+
</Card>
|
|
239
|
+
</ProductOverview5TabsContentFade>
|
|
240
|
+
<ProductOverview5TabsContentFade value='specs' className='mt-8'>
|
|
241
|
+
<Card className='border shadow-none'>
|
|
242
|
+
<CardContent className='p-8 py-0'>
|
|
243
|
+
<h3 className='mb-6 text-xl font-semibold text-balance'>{specificationsLabel}</h3>
|
|
244
|
+
<div className='flex flex-col gap-4'>
|
|
245
|
+
{Object.entries(product.specifications).map(([key, value]) => (
|
|
246
|
+
<div key={key} className='border-muted grid grid-cols-1 gap-2 border-b py-3 last:border-b-0 md:grid-cols-3 md:gap-4'>
|
|
247
|
+
<span className='text-muted-foreground text-sm font-medium'>{key}</span>
|
|
248
|
+
<span className='col-span-1 text-sm md:col-span-2'>{value}</span>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
</ProductOverview5TabsContentFade>
|
|
255
|
+
<ProductOverview5TabsContentFade value='reviews' className='mt-8'>
|
|
256
|
+
<ProductOverview5ReviewsTab
|
|
257
|
+
product={product}
|
|
258
|
+
basedOnReviewsLabel={basedOnReviewsLabel}
|
|
259
|
+
foundHelpfulLabel={foundHelpfulLabel}
|
|
260
|
+
/>
|
|
261
|
+
</ProductOverview5TabsContentFade>
|
|
262
|
+
<ProductOverview5TabsContentFade value='gallery' className='mt-8'>
|
|
263
|
+
<ProductOverview5LightboxTab
|
|
264
|
+
product={product}
|
|
265
|
+
galleryLabel={galleryLabel}
|
|
266
|
+
ofCounterLabel={ofCounterLabel}
|
|
267
|
+
/>
|
|
268
|
+
</ProductOverview5TabsContentFade>
|
|
269
|
+
</ProductOverview5Tabs>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</section>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Eye, Star } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { Badge } from '../../../ui/badge'
|
|
6
|
+
import { Card, CardContent } from '../../../ui/card'
|
|
7
|
+
import { cn } from '../../../lib/utils'
|
|
8
|
+
|
|
9
|
+
import type { QuickViewProduct } from './product-quick-view'
|
|
10
|
+
|
|
11
|
+
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One product in the grid, and the thing a quick view is meant to keep you inside of.
|
|
15
|
+
*
|
|
16
|
+
* The whole card is the button. A card that carries a separate "Quick view" link has two targets
|
|
17
|
+
* for one intention, and on a touch screen the hover-revealed one is a target that never appears
|
|
18
|
+
* at all - so the control is drawn on hover and focus as an *affordance*, while the press area
|
|
19
|
+
* is the entire card at every input type.
|
|
20
|
+
*/
|
|
21
|
+
export function ProductQuickViewCard({
|
|
22
|
+
product,
|
|
23
|
+
isActive,
|
|
24
|
+
onOpen,
|
|
25
|
+
}: {
|
|
26
|
+
product: QuickViewProduct
|
|
27
|
+
/** Whether this is the product currently open in the quick view. */
|
|
28
|
+
isActive: boolean
|
|
29
|
+
onOpen: () => void
|
|
30
|
+
}) {
|
|
31
|
+
const swatches = product.variants.find(variant => variant.options.some(option => option.swatch))?.options ?? []
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Card
|
|
35
|
+
className={cn(
|
|
36
|
+
'group/card relative gap-0 overflow-hidden py-0 transition-shadow',
|
|
37
|
+
// The mark that survives behind the dialog: when the reader closes it, the card they
|
|
38
|
+
// were looking at is the one still lit, so the grid tells them where they got to.
|
|
39
|
+
isActive ? 'ring-primary ring-2' : 'hover:shadow-lg'
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<div className='bg-muted relative aspect-4/5 w-full overflow-hidden'>
|
|
43
|
+
<img
|
|
44
|
+
src={product.image || undefined}
|
|
45
|
+
alt={product.imageAlt}
|
|
46
|
+
className='size-full object-cover transition-transform duration-500 ease-out group-hover/card:scale-[1.03]'
|
|
47
|
+
loading='lazy'
|
|
48
|
+
decoding='async'
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
{product.compareAt ? (
|
|
52
|
+
<Badge className='absolute top-3 left-3 rounded-full'>
|
|
53
|
+
{`${Math.round((1 - product.price / product.compareAt) * 100)}% de descuento`}
|
|
54
|
+
</Badge>
|
|
55
|
+
) : null}
|
|
56
|
+
|
|
57
|
+
{/*
|
|
58
|
+
Drawn on hover and focus, and never the thing that has to be hit. `pointer-events-none`
|
|
59
|
+
keeps every press on the card's own trigger underneath, so there is exactly one target
|
|
60
|
+
however the reader is pointing at it.
|
|
61
|
+
*/}
|
|
62
|
+
<div
|
|
63
|
+
aria-hidden='true'
|
|
64
|
+
className={cn(
|
|
65
|
+
'pointer-events-none absolute inset-x-0 bottom-0 flex justify-center p-3',
|
|
66
|
+
'translate-y-2 opacity-0 transition-[opacity,transform] duration-200 ease-out',
|
|
67
|
+
'group-hover/card:translate-y-0 group-hover/card:opacity-100',
|
|
68
|
+
'group-has-focus-visible/card:translate-y-0 group-has-focus-visible/card:opacity-100'
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<span className='bg-background/90 text-foreground ring-border flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium ring-1 backdrop-blur'>
|
|
72
|
+
<Eye className='size-4' />
|
|
73
|
+
Vista rápida
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/*
|
|
79
|
+
The card's one interactive element, and it covers the card.
|
|
80
|
+
|
|
81
|
+
A stretched link would leave only the title actually pressable: the picture sits over a
|
|
82
|
+
pseudo-element and swallows every click on the image and on the Quick view pill. A real
|
|
83
|
+
element in the DOM, on top, with an explicit z-index, is unambiguous at every input type.
|
|
84
|
+
It is empty and carries an accessible name, so the price and rating underneath are not
|
|
85
|
+
read as part of the button's label.
|
|
86
|
+
*/}
|
|
87
|
+
<button
|
|
88
|
+
type='button'
|
|
89
|
+
onClick={onOpen}
|
|
90
|
+
aria-haspopup='dialog'
|
|
91
|
+
className='focus-visible:ring-ring/50 absolute inset-0 z-10 cursor-pointer rounded-xl outline-none focus-visible:ring-[3px]'
|
|
92
|
+
>
|
|
93
|
+
<span className='sr-only'>{`Vista rápida: ${product.name}`}</span>
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
<CardContent className='flex flex-col gap-1 p-4'>
|
|
97
|
+
<div className='flex items-start justify-between gap-3'>
|
|
98
|
+
<p className='text-base font-medium'>{product.name}</p>
|
|
99
|
+
|
|
100
|
+
<p className='shrink-0 text-base font-medium tabular-nums'>{money.format(product.price)}</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<p className='text-muted-foreground text-sm'>{product.tagline}</p>
|
|
104
|
+
|
|
105
|
+
<div className='mt-2 flex items-center justify-between gap-3'>
|
|
106
|
+
<p className='text-muted-foreground flex items-center gap-1.5 text-xs'>
|
|
107
|
+
{/* Foreground token per theming contract, never the only carrier of the rating. */}
|
|
108
|
+
<Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />
|
|
109
|
+
<span className='text-foreground font-medium tabular-nums'>{product.rating}</span>
|
|
110
|
+
<span className='tabular-nums'>({product.reviewCount})</span>
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
{swatches.length > 0 ? (
|
|
114
|
+
<span className='flex items-center gap-1'>
|
|
115
|
+
{/*
|
|
116
|
+
Finish colours are product data, so they stay literal rather than tokenised, and
|
|
117
|
+
they are decoration here - the same information is in words inside the panel.
|
|
118
|
+
*/}
|
|
119
|
+
{swatches.map(option => (
|
|
120
|
+
<span
|
|
121
|
+
key={option.value}
|
|
122
|
+
aria-hidden='true'
|
|
123
|
+
className='ring-border size-3 rounded-full ring-1'
|
|
124
|
+
style={{ backgroundColor: option.swatch }}
|
|
125
|
+
/>
|
|
126
|
+
))}
|
|
127
|
+
</span>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default ProductQuickViewCard
|