@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
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { motion, useReducedMotion } from 'motion/react'
|
|
5
|
+
import { ArrowRight, MapPin, Search, ShoppingBag, Star } from 'lucide-react'
|
|
6
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
7
|
+
|
|
8
|
+
import { Badge } from '../../../ui/badge'
|
|
9
|
+
import { Button } from '../../../ui/button'
|
|
10
|
+
import { Card, CardContent } from '../../../ui/card'
|
|
11
|
+
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from '../../../ui/carousel'
|
|
12
|
+
import { Input } from '../../../ui/input'
|
|
13
|
+
import { cn } from '../../../lib/utils'
|
|
14
|
+
import type { CTA } from '../../../lib/types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Display contract for products featured in the hero. Every popularity,
|
|
18
|
+
* review, or availability field is host-supplied evidence: omitted fields
|
|
19
|
+
* render nothing (honest omission) instead of fabricated defaults.
|
|
20
|
+
*/
|
|
21
|
+
export interface FeaturedProduct {
|
|
22
|
+
id: string
|
|
23
|
+
name: string
|
|
24
|
+
/** Current selling price. */
|
|
25
|
+
price: number
|
|
26
|
+
/** Real compare-at price. Discount treatments render only from a real `originalPrice > price` pair. */
|
|
27
|
+
originalPrice?: number
|
|
28
|
+
/** Image URL or path; omitted entirely when absent or blank (never an empty `src`). */
|
|
29
|
+
image?: string
|
|
30
|
+
/** Accessible image description (falls back to `name`). */
|
|
31
|
+
imageAlt?: string
|
|
32
|
+
/** Host-supplied brand label; the row renders only when supplied. */
|
|
33
|
+
brand?: string
|
|
34
|
+
/** Host-supplied swatch colors; the row renders only when supplied. */
|
|
35
|
+
colors?: string[]
|
|
36
|
+
/** Host-supplied rating (0–5); rendered only when supplied. */
|
|
37
|
+
rating?: number
|
|
38
|
+
/** Host-supplied review count; rendered only when supplied. */
|
|
39
|
+
reviewCount?: number
|
|
40
|
+
/** Host-supplied availability. Absence makes no stock claim; an explicit `false` disables purchase. */
|
|
41
|
+
inStock?: boolean
|
|
42
|
+
/** Host-supplied "new" flag; the badge renders only when true. */
|
|
43
|
+
isNew?: boolean
|
|
44
|
+
/** Host-supplied badge label for showcase slides (e.g. a category); rendered only when supplied. */
|
|
45
|
+
tag?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HeroImage {
|
|
49
|
+
src: string
|
|
50
|
+
alt: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface HeroDepartment {
|
|
54
|
+
id: string
|
|
55
|
+
name: string
|
|
56
|
+
/** Image URL; omitted entirely when absent or blank. */
|
|
57
|
+
image?: string
|
|
58
|
+
/** Accessible image description (falls back to `name`). */
|
|
59
|
+
imageAlt?: string
|
|
60
|
+
/** Host-supplied count label; rendered only when supplied. */
|
|
61
|
+
count?: string
|
|
62
|
+
/** Link destination; renders the chip as a link when present. */
|
|
63
|
+
href?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface HeroFeaturedDeal {
|
|
67
|
+
id: string
|
|
68
|
+
name: string
|
|
69
|
+
/** Host-supplied supporting description; rendered only when supplied. */
|
|
70
|
+
description?: string
|
|
71
|
+
/** Image URL; omitted entirely when absent or blank. */
|
|
72
|
+
image?: string
|
|
73
|
+
/** Accessible image description (falls back to `name`). */
|
|
74
|
+
imageAlt?: string
|
|
75
|
+
price: number
|
|
76
|
+
/** Real compare-at price; rendered struck-through only when greater than `price`. */
|
|
77
|
+
originalPrice?: number
|
|
78
|
+
/** Optional call to action shown on the card. */
|
|
79
|
+
cta?: CTA
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface HeroStoreLocator {
|
|
83
|
+
title: string
|
|
84
|
+
description: string
|
|
85
|
+
ctaLabel: string
|
|
86
|
+
/** Link destination for the locator button; falls back to `onLocateStores`. */
|
|
87
|
+
ctaHref?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface HeroDeal {
|
|
91
|
+
id: string
|
|
92
|
+
/** Deal name shown in the deals bar badge, e.g. a campaign name. */
|
|
93
|
+
title: string
|
|
94
|
+
/** Host-supplied discount label (from a real compare-at price); rendered only when supplied. */
|
|
95
|
+
discount?: string
|
|
96
|
+
/** Host-supplied countdown text; rendered only when supplied — the block never invents one. */
|
|
97
|
+
endTime?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface HeroCategory {
|
|
101
|
+
id: string
|
|
102
|
+
name: string
|
|
103
|
+
/** Image URL; omitted entirely when absent or blank. */
|
|
104
|
+
image?: string
|
|
105
|
+
/** Accessible image description (falls back to `name`). */
|
|
106
|
+
imageAlt?: string
|
|
107
|
+
/** Host-supplied count label; rendered only when supplied. */
|
|
108
|
+
count?: string
|
|
109
|
+
/** Link destination; renders the card arrow as a link when present. */
|
|
110
|
+
href?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The bounded presentation surface, reusing the registry's archetype intent
|
|
115
|
+
* vocabulary: `compact` (text-forward hero + product grid), `showcase`
|
|
116
|
+
* (search affordance + rotating product carousel), `editorial` (department
|
|
117
|
+
* navigation + optional deals/locator), `promotional` (deals bar + category
|
|
118
|
+
* grid). The value changes structure, never the data contract.
|
|
119
|
+
*/
|
|
120
|
+
export type StorefrontHeroPresentation = 'compact' | 'showcase' | 'editorial' | 'promotional'
|
|
121
|
+
|
|
122
|
+
export interface StorefrontHeroProps {
|
|
123
|
+
title: string
|
|
124
|
+
description: string
|
|
125
|
+
/** Bounded presentation option; defaults to `compact`. */
|
|
126
|
+
presentation?: StorefrontHeroPresentation
|
|
127
|
+
/** ISO 4217 code used to format prices via `Intl.NumberFormat`; never hardcoded. */
|
|
128
|
+
currency?: string
|
|
129
|
+
/** Featured products. An empty list renders the hero text-forward with no empty grid chrome. */
|
|
130
|
+
products: FeaturedProduct[]
|
|
131
|
+
badge?: string
|
|
132
|
+
titleAccent?: string
|
|
133
|
+
primaryCta?: CTA
|
|
134
|
+
secondaryCta?: CTA
|
|
135
|
+
/**
|
|
136
|
+
* Optional editorial image. Rendered as a native `<img>` when `src` is a
|
|
137
|
+
* non-empty string; omitted entirely when absent or blank (never an empty
|
|
138
|
+
* `src`).
|
|
139
|
+
*/
|
|
140
|
+
heroImage?: HeroImage
|
|
141
|
+
/** `showcase` only. The search input renders only when a handler is supplied. */
|
|
142
|
+
onSearch?: (query: string) => void
|
|
143
|
+
/** `editorial` only: department navigation grid. */
|
|
144
|
+
departments?: HeroDepartment[]
|
|
145
|
+
/** `editorial` only: featured deal cards. */
|
|
146
|
+
featuredDeals?: HeroFeaturedDeal[]
|
|
147
|
+
/** `editorial` only: store locator strip. */
|
|
148
|
+
storeLocator?: HeroStoreLocator
|
|
149
|
+
/** `editorial` only: shipping banner text under the CTAs. */
|
|
150
|
+
shippingInfo?: string
|
|
151
|
+
/** `promotional` only: rotating deals bar. */
|
|
152
|
+
deals?: HeroDeal[]
|
|
153
|
+
/** `promotional` only: category card grid. */
|
|
154
|
+
categories?: HeroCategory[]
|
|
155
|
+
/** Milliseconds between rotations (showcase carousel and promotional deals bar; default 3000). */
|
|
156
|
+
rotationInterval?: number
|
|
157
|
+
onAddToCart?: (product: FeaturedProduct) => void
|
|
158
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
159
|
+
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
160
|
+
onDepartmentClick?: (department: HeroDepartment) => void
|
|
161
|
+
onShopNow?: (item: HeroFeaturedDeal | HeroDeal) => void
|
|
162
|
+
onLocateStores?: () => void
|
|
163
|
+
onCategoryClick?: (category: HeroCategory) => void
|
|
164
|
+
/** Detail/purchase affordance on product cards and showcase slides. */
|
|
165
|
+
addToCartLabel?: string
|
|
166
|
+
buyNowLabel?: string
|
|
167
|
+
newLabel?: string
|
|
168
|
+
outOfStockLabel?: string
|
|
169
|
+
colorsLabel?: string
|
|
170
|
+
/** Suffix appended to the computed discount percentage (default `% OFF`). */
|
|
171
|
+
discountSuffixLabel?: string
|
|
172
|
+
/** `showcase` labels. */
|
|
173
|
+
searchPlaceholder?: string
|
|
174
|
+
searchLabel?: string
|
|
175
|
+
/** `showcase` copy under each slide's product name. */
|
|
176
|
+
featuredDescription?: string
|
|
177
|
+
/** `editorial`/`promotional` labels. */
|
|
178
|
+
shopNowLabel?: string
|
|
179
|
+
endsInLabel?: string
|
|
180
|
+
productsLabel?: string
|
|
181
|
+
viewLabel?: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hasRealDiscount(item: Pick<FeaturedProduct, 'price' | 'originalPrice'>): boolean {
|
|
185
|
+
return (
|
|
186
|
+
typeof item.originalPrice === 'number' &&
|
|
187
|
+
Number.isFinite(item.originalPrice) &&
|
|
188
|
+
item.originalPrice > item.price &&
|
|
189
|
+
item.price >= 0
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function discountPercent(price: number, originalPrice: number): number {
|
|
194
|
+
return Math.round(((originalPrice - price) / originalPrice) * 100)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatAmount(amount: number, currency?: string): string {
|
|
198
|
+
if (currency) {
|
|
199
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
|
|
200
|
+
}
|
|
201
|
+
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(amount)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function CtaButton({
|
|
205
|
+
cta,
|
|
206
|
+
variant,
|
|
207
|
+
className = 'group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium',
|
|
208
|
+
children,
|
|
209
|
+
}: {
|
|
210
|
+
cta: CTA
|
|
211
|
+
variant?: ComponentProps<typeof Button>['variant']
|
|
212
|
+
className?: string
|
|
213
|
+
children: ReactNode
|
|
214
|
+
}) {
|
|
215
|
+
if (cta.href) {
|
|
216
|
+
return (
|
|
217
|
+
<Button variant={variant} size='lg' className={className} asChild>
|
|
218
|
+
<a href={cta.href}>{children}</a>
|
|
219
|
+
</Button>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<Button variant={variant} size='lg' className={className}>
|
|
225
|
+
{children}
|
|
226
|
+
</Button>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function HeroHeading({
|
|
231
|
+
title,
|
|
232
|
+
titleAccent,
|
|
233
|
+
badge,
|
|
234
|
+
}: {
|
|
235
|
+
title: string
|
|
236
|
+
titleAccent?: string
|
|
237
|
+
badge?: string
|
|
238
|
+
}) {
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
{badge && (
|
|
242
|
+
<div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-full border px-4 py-1.5 text-sm font-medium backdrop-blur-sm'>
|
|
243
|
+
{badge}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
<h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
|
|
247
|
+
{title}{' '}
|
|
248
|
+
{titleAccent && (
|
|
249
|
+
<span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
|
|
250
|
+
{titleAccent}
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
</h1>
|
|
254
|
+
</>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function EditorialHeading({
|
|
259
|
+
title,
|
|
260
|
+
titleAccent,
|
|
261
|
+
badge,
|
|
262
|
+
}: {
|
|
263
|
+
title: string
|
|
264
|
+
titleAccent?: string
|
|
265
|
+
badge?: string
|
|
266
|
+
}) {
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
{badge && (
|
|
270
|
+
<div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center justify-center rounded-md border px-4 py-1.5 text-sm'>
|
|
271
|
+
{badge}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
<h1 className='mt-6 text-4xl font-bold tracking-tight text-balance sm:text-5xl md:text-6xl'>
|
|
275
|
+
{title} {titleAccent && <span className='text-primary'>{titleAccent}</span>}
|
|
276
|
+
</h1>
|
|
277
|
+
</>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface HeroLabels {
|
|
282
|
+
addToCartLabel: string
|
|
283
|
+
buyNowLabel: string
|
|
284
|
+
newLabel: string
|
|
285
|
+
outOfStockLabel: string
|
|
286
|
+
colorsLabel: string
|
|
287
|
+
discountSuffixLabel: string
|
|
288
|
+
searchPlaceholder: string
|
|
289
|
+
searchLabel: string
|
|
290
|
+
featuredDescription?: string
|
|
291
|
+
shopNowLabel: string
|
|
292
|
+
endsInLabel: string
|
|
293
|
+
productsLabel: string
|
|
294
|
+
viewLabel: string
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function CompactProductCard({
|
|
298
|
+
product,
|
|
299
|
+
currency,
|
|
300
|
+
labels,
|
|
301
|
+
onAddToCart,
|
|
302
|
+
onBuyNow,
|
|
303
|
+
onSelectColor,
|
|
304
|
+
hasCustomBuyNowLabel,
|
|
305
|
+
}: {
|
|
306
|
+
product: FeaturedProduct
|
|
307
|
+
currency?: string
|
|
308
|
+
labels: HeroLabels
|
|
309
|
+
onAddToCart?: (product: FeaturedProduct) => void
|
|
310
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
311
|
+
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
312
|
+
hasCustomBuyNowLabel: boolean
|
|
313
|
+
}) {
|
|
314
|
+
const outOfStock = product.inStock === false
|
|
315
|
+
const onSale = hasRealDiscount(product)
|
|
316
|
+
// A host-supplied custom action label is a purchase affordance and requires
|
|
317
|
+
// its handler; the block's default label is a detail affordance that may
|
|
318
|
+
// render without one. Never infer this from the label's language.
|
|
319
|
+
const showBuyNow = Boolean(onBuyNow) || !hasCustomBuyNowLabel
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'>
|
|
323
|
+
<div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
|
|
324
|
+
{product.isNew && (
|
|
325
|
+
<Badge variant='secondary' className='rounded-full'>{labels.newLabel}</Badge>
|
|
326
|
+
)}
|
|
327
|
+
{onSale && (
|
|
328
|
+
<Badge variant='destructive' className='rounded-full'>
|
|
329
|
+
{discountPercent(product.price, product.originalPrice as number)}
|
|
330
|
+
{labels.discountSuffixLabel}
|
|
331
|
+
</Badge>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div className='relative aspect-square overflow-hidden'>
|
|
336
|
+
{product.image && product.image.trim().length > 0 ? (
|
|
337
|
+
<img
|
|
338
|
+
src={product.image}
|
|
339
|
+
alt={product.imageAlt ?? product.name}
|
|
340
|
+
className='size-full object-cover'
|
|
341
|
+
loading='lazy'
|
|
342
|
+
/>
|
|
343
|
+
) : null}
|
|
344
|
+
{outOfStock && (
|
|
345
|
+
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
|
|
346
|
+
<span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{labels.outOfStockLabel}</span>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<div className='p-4'>
|
|
352
|
+
{product.brand && product.brand.trim().length > 0 && (
|
|
353
|
+
<div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
|
|
354
|
+
)}
|
|
355
|
+
<h3 className='mb-2 font-medium'>{product.name}</h3>
|
|
356
|
+
|
|
357
|
+
<div className='mb-3'>
|
|
358
|
+
<span className='text-lg font-bold'>{formatAmount(product.price, currency)}</span>
|
|
359
|
+
{onSale && (
|
|
360
|
+
<span className='text-muted-foreground ms-2 text-sm line-through'>
|
|
361
|
+
{formatAmount(product.originalPrice as number, currency)}
|
|
362
|
+
</span>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{product.colors && product.colors.length > 0 && (
|
|
367
|
+
<div className='mb-4 flex items-center'>
|
|
368
|
+
<span className='text-muted-foreground me-2 text-sm'>{labels.colorsLabel}</span>
|
|
369
|
+
<div className='flex gap-1'>
|
|
370
|
+
{product.colors.map((color, i) => (
|
|
371
|
+
<button
|
|
372
|
+
key={i}
|
|
373
|
+
className='size-5 rounded-full border'
|
|
374
|
+
style={{ backgroundColor: color }}
|
|
375
|
+
aria-label={`Opción de color ${i + 1}`}
|
|
376
|
+
onClick={onSelectColor ? () => onSelectColor(product, i, color) : undefined}
|
|
377
|
+
/>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{typeof product.rating === 'number' && (
|
|
384
|
+
<div className='mb-4 flex items-center'>
|
|
385
|
+
<div className='flex'>
|
|
386
|
+
{[...Array(5)].map((_, i) => (
|
|
387
|
+
<Star
|
|
388
|
+
key={i}
|
|
389
|
+
className={`size-4 ${i < Math.floor(product.rating ?? 0) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
|
|
390
|
+
/>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
{typeof product.reviewCount === 'number' && (
|
|
394
|
+
<span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
<div className='flex flex-col gap-2'>
|
|
400
|
+
{onAddToCart && (
|
|
401
|
+
<Button
|
|
402
|
+
variant='outline'
|
|
403
|
+
className='h-9 cursor-pointer px-4 py-2'
|
|
404
|
+
disabled={outOfStock}
|
|
405
|
+
onClick={() => onAddToCart(product)}
|
|
406
|
+
>
|
|
407
|
+
<ShoppingBag />
|
|
408
|
+
{labels.addToCartLabel}
|
|
409
|
+
</Button>
|
|
410
|
+
)}
|
|
411
|
+
{showBuyNow && (
|
|
412
|
+
<Button
|
|
413
|
+
className='h-9 cursor-pointer px-4 py-2'
|
|
414
|
+
disabled={outOfStock}
|
|
415
|
+
onClick={onBuyNow ? () => onBuyNow(product) : undefined}
|
|
416
|
+
>
|
|
417
|
+
{labels.buyNowLabel}
|
|
418
|
+
</Button>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function CompactPresentation({
|
|
427
|
+
title,
|
|
428
|
+
description,
|
|
429
|
+
products,
|
|
430
|
+
badge,
|
|
431
|
+
titleAccent,
|
|
432
|
+
primaryCta,
|
|
433
|
+
secondaryCta,
|
|
434
|
+
heroImage,
|
|
435
|
+
currency,
|
|
436
|
+
labels,
|
|
437
|
+
onAddToCart,
|
|
438
|
+
onBuyNow,
|
|
439
|
+
onSelectColor,
|
|
440
|
+
hasCustomBuyNowLabel,
|
|
441
|
+
}: {
|
|
442
|
+
title: string
|
|
443
|
+
description: string
|
|
444
|
+
products: FeaturedProduct[]
|
|
445
|
+
badge?: string
|
|
446
|
+
titleAccent?: string
|
|
447
|
+
primaryCta?: CTA
|
|
448
|
+
secondaryCta?: CTA
|
|
449
|
+
heroImage?: HeroImage
|
|
450
|
+
currency?: string
|
|
451
|
+
labels: HeroLabels
|
|
452
|
+
onAddToCart?: (product: FeaturedProduct) => void
|
|
453
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
454
|
+
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
455
|
+
hasCustomBuyNowLabel: boolean
|
|
456
|
+
}) {
|
|
457
|
+
const reduceMotion = useReducedMotion()
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
|
|
461
|
+
<div className='relative z-10 mx-auto w-full max-w-7xl flex flex-col items-center justify-between gap-12 px-4 sm:px-6 lg:px-8 py-16 lg:py-20 xl:flex-row xl:items-start'>
|
|
462
|
+
<motion.div
|
|
463
|
+
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
464
|
+
animate={{ opacity: 1, y: 0 }}
|
|
465
|
+
transition={{ duration: 0.5, ease: 'easeOut' }}
|
|
466
|
+
className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
|
|
467
|
+
>
|
|
468
|
+
<HeroHeading title={title} titleAccent={titleAccent} badge={badge} />
|
|
469
|
+
<p className='text-muted-foreground mt-8 text-lg'>{description}</p>
|
|
470
|
+
{(primaryCta || secondaryCta) && (
|
|
471
|
+
<div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
|
|
472
|
+
{primaryCta && (
|
|
473
|
+
<CtaButton cta={primaryCta}>
|
|
474
|
+
<span className='relative z-10 flex items-center gap-2'>
|
|
475
|
+
{primaryCta.label}
|
|
476
|
+
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
477
|
+
</span>
|
|
478
|
+
<span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
|
|
479
|
+
</CtaButton>
|
|
480
|
+
)}
|
|
481
|
+
{secondaryCta && (
|
|
482
|
+
<CtaButton cta={secondaryCta} variant='outline'>
|
|
483
|
+
<span className='relative z-10 flex items-center gap-2'>
|
|
484
|
+
<ShoppingBag />
|
|
485
|
+
{secondaryCta.label}
|
|
486
|
+
</span>
|
|
487
|
+
</CtaButton>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
{heroImage && heroImage.src.trim().length > 0 ? (
|
|
492
|
+
<img
|
|
493
|
+
src={heroImage.src}
|
|
494
|
+
alt={heroImage.alt}
|
|
495
|
+
className='mt-10 aspect-video w-full rounded-lg object-cover'
|
|
496
|
+
/>
|
|
497
|
+
) : null}
|
|
498
|
+
</motion.div>
|
|
499
|
+
|
|
500
|
+
{products.length > 0 && (
|
|
501
|
+
<div className='w-full max-w-2xl lg:mt-0'>
|
|
502
|
+
<div className='grid gap-6 sm:grid-cols-2'>
|
|
503
|
+
{products.map((product, index) => (
|
|
504
|
+
<motion.div
|
|
505
|
+
key={product.id}
|
|
506
|
+
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
507
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
508
|
+
viewport={{ once: true, margin: '-40px' }}
|
|
509
|
+
transition={{
|
|
510
|
+
duration: 0.45,
|
|
511
|
+
delay: reduceMotion ? 0 : index * 0.1,
|
|
512
|
+
ease: 'easeOut',
|
|
513
|
+
}}
|
|
514
|
+
>
|
|
515
|
+
<CompactProductCard
|
|
516
|
+
product={product}
|
|
517
|
+
currency={currency}
|
|
518
|
+
labels={labels}
|
|
519
|
+
onAddToCart={onAddToCart}
|
|
520
|
+
onBuyNow={onBuyNow}
|
|
521
|
+
onSelectColor={onSelectColor}
|
|
522
|
+
hasCustomBuyNowLabel={hasCustomBuyNowLabel}
|
|
523
|
+
/>
|
|
524
|
+
</motion.div>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
|
|
532
|
+
<div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
|
|
533
|
+
</div>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function ShowcaseSlide({
|
|
538
|
+
product,
|
|
539
|
+
currency,
|
|
540
|
+
labels,
|
|
541
|
+
onBuyNow,
|
|
542
|
+
}: {
|
|
543
|
+
product: FeaturedProduct
|
|
544
|
+
currency?: string
|
|
545
|
+
labels: HeroLabels
|
|
546
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
547
|
+
}) {
|
|
548
|
+
const outOfStock = product.inStock === false
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<Card className='relative size-full overflow-hidden border-1 py-4'>
|
|
552
|
+
<CardContent className='px-4'>
|
|
553
|
+
<div className='relative size-full overflow-hidden rounded-md'>
|
|
554
|
+
{product.image && product.image.trim().length > 0 ? (
|
|
555
|
+
<img
|
|
556
|
+
src={product.image}
|
|
557
|
+
alt={product.imageAlt ?? product.name}
|
|
558
|
+
className='h-[500px] w-full object-cover'
|
|
559
|
+
loading='lazy'
|
|
560
|
+
/>
|
|
561
|
+
) : null}
|
|
562
|
+
</div>
|
|
563
|
+
<div className='from-background/90 via-background/30 absolute inset-0 bg-linear-to-t to-transparent' />
|
|
564
|
+
|
|
565
|
+
<div className='text-background-foreground absolute inset-0 flex flex-col justify-end p-8'>
|
|
566
|
+
<div className='relative z-10 flex max-w-md flex-col gap-4'>
|
|
567
|
+
{product.tag && (
|
|
568
|
+
<Badge className='w-fit rounded-full px-2.5 py-0.5 font-semibold'>{product.tag}</Badge>
|
|
569
|
+
)}
|
|
570
|
+
<h2 className='text-4xl font-bold'>{product.name}</h2>
|
|
571
|
+
{labels.featuredDescription && (
|
|
572
|
+
<p className='text-background-foreground/80 text-lg'>{labels.featuredDescription}</p>
|
|
573
|
+
)}
|
|
574
|
+
<span className='text-3xl font-bold'>{formatAmount(product.price, currency)}</span>
|
|
575
|
+
<div className='flex items-center gap-4 pt-2'>
|
|
576
|
+
{onBuyNow && (
|
|
577
|
+
<Button
|
|
578
|
+
size='lg'
|
|
579
|
+
className='h-10 cursor-pointer rounded-full px-8'
|
|
580
|
+
disabled={outOfStock}
|
|
581
|
+
onClick={() => onBuyNow(product)}
|
|
582
|
+
>
|
|
583
|
+
{labels.buyNowLabel}
|
|
584
|
+
</Button>
|
|
585
|
+
)}
|
|
586
|
+
{typeof product.rating === 'number' && (
|
|
587
|
+
<div className='text-foreground flex items-center gap-1'>
|
|
588
|
+
<Star className='fill-foreground size-5' />
|
|
589
|
+
<span className='font-medium'>{product.rating}</span>
|
|
590
|
+
{typeof product.reviewCount === 'number' && (
|
|
591
|
+
<span className='text-foreground/80'>({product.reviewCount})</span>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
</CardContent>
|
|
599
|
+
</Card>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function ShowcasePresentation({
|
|
604
|
+
title,
|
|
605
|
+
description,
|
|
606
|
+
products,
|
|
607
|
+
badge,
|
|
608
|
+
primaryCta,
|
|
609
|
+
secondaryCta,
|
|
610
|
+
currency,
|
|
611
|
+
labels,
|
|
612
|
+
onSearch,
|
|
613
|
+
onBuyNow,
|
|
614
|
+
rotationInterval,
|
|
615
|
+
}: {
|
|
616
|
+
title: string
|
|
617
|
+
description: string
|
|
618
|
+
products: FeaturedProduct[]
|
|
619
|
+
badge?: string
|
|
620
|
+
primaryCta?: CTA
|
|
621
|
+
secondaryCta?: CTA
|
|
622
|
+
currency?: string
|
|
623
|
+
labels: HeroLabels
|
|
624
|
+
onSearch?: (query: string) => void
|
|
625
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
626
|
+
rotationInterval: number
|
|
627
|
+
}) {
|
|
628
|
+
const reduceMotion = useReducedMotion()
|
|
629
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
630
|
+
const [api, setApi] = useState<CarouselApi>()
|
|
631
|
+
const [currentSlide, setCurrentSlide] = useState(0)
|
|
632
|
+
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
if (!api || products.length <= 1 || reduceMotion) return
|
|
635
|
+
|
|
636
|
+
const interval = setInterval(() => {
|
|
637
|
+
const nextSlide = (currentSlide + 1) % products.length
|
|
638
|
+
api.scrollTo(nextSlide)
|
|
639
|
+
setCurrentSlide(nextSlide)
|
|
640
|
+
}, rotationInterval)
|
|
641
|
+
|
|
642
|
+
return () => clearInterval(interval)
|
|
643
|
+
}, [api, currentSlide, products.length, reduceMotion, rotationInterval])
|
|
644
|
+
|
|
645
|
+
const handleSearch = () => {
|
|
646
|
+
if (onSearch) onSearch(searchQuery)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
<section className='from-background to-accent/20 relative bg-linear-to-b'>
|
|
651
|
+
<div className='relative mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20'>
|
|
652
|
+
<div className='grid grid-cols-1 items-center gap-12 lg:grid-cols-2'>
|
|
653
|
+
<header className='flex flex-col gap-8'>
|
|
654
|
+
{badge && (
|
|
655
|
+
<Badge variant='outline' className='flex h-auto w-fit items-center rounded-full px-4 py-2 font-semibold'>
|
|
656
|
+
{badge}
|
|
657
|
+
</Badge>
|
|
658
|
+
)}
|
|
659
|
+
|
|
660
|
+
<h1 className='text-5xl leading-tight font-bold text-balance md:text-6xl lg:text-7xl'>{title}</h1>
|
|
661
|
+
|
|
662
|
+
<p className='text-muted-foreground max-w-lg text-xl text-balance'>{description}</p>
|
|
663
|
+
|
|
664
|
+
{onSearch && (
|
|
665
|
+
<div className='relative max-w-md'>
|
|
666
|
+
<Input
|
|
667
|
+
type='search'
|
|
668
|
+
placeholder={labels.searchPlaceholder}
|
|
669
|
+
value={searchQuery}
|
|
670
|
+
onChange={e => setSearchQuery(e.target.value)}
|
|
671
|
+
onKeyDown={e => {
|
|
672
|
+
if (e.key === 'Enter') handleSearch()
|
|
673
|
+
}}
|
|
674
|
+
className='h-14 rounded-full pe-4 pl-12 text-lg'
|
|
675
|
+
aria-label={labels.searchPlaceholder}
|
|
676
|
+
/>
|
|
677
|
+
<Search className='text-muted-foreground absolute top-1/2 start-4 size-5 -translate-y-1/2' />
|
|
678
|
+
<Button
|
|
679
|
+
size='lg'
|
|
680
|
+
className='absolute top-1/2 end-2 h-10 -translate-y-1/2 cursor-pointer rounded-full px-6'
|
|
681
|
+
onClick={handleSearch}
|
|
682
|
+
>
|
|
683
|
+
{labels.searchLabel}
|
|
684
|
+
</Button>
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
|
|
688
|
+
{(primaryCta || secondaryCta) && (
|
|
689
|
+
<div className='flex items-center gap-4'>
|
|
690
|
+
{primaryCta && (
|
|
691
|
+
<CtaButton cta={primaryCta} className='h-10 cursor-pointer rounded-full px-4'>
|
|
692
|
+
<span className='flex items-center gap-2'>
|
|
693
|
+
{primaryCta.label}
|
|
694
|
+
<ArrowRight />
|
|
695
|
+
</span>
|
|
696
|
+
</CtaButton>
|
|
697
|
+
)}
|
|
698
|
+
{secondaryCta && (
|
|
699
|
+
<CtaButton cta={secondaryCta} variant='outline' className='h-10 cursor-pointer rounded-full px-4'>
|
|
700
|
+
<span className='flex items-center gap-2'>
|
|
701
|
+
<ShoppingBag />
|
|
702
|
+
{secondaryCta.label}
|
|
703
|
+
</span>
|
|
704
|
+
</CtaButton>
|
|
705
|
+
)}
|
|
706
|
+
</div>
|
|
707
|
+
)}
|
|
708
|
+
</header>
|
|
709
|
+
|
|
710
|
+
{products.length > 0 && (
|
|
711
|
+
<div className='flex flex-col gap-4'>
|
|
712
|
+
<div className='relative h-[500px] w-full border-0'>
|
|
713
|
+
<Carousel
|
|
714
|
+
className='group size-full'
|
|
715
|
+
setApi={setApi}
|
|
716
|
+
opts={{
|
|
717
|
+
align: 'start',
|
|
718
|
+
loop: true,
|
|
719
|
+
duration: 20,
|
|
720
|
+
skipSnaps: true,
|
|
721
|
+
}}
|
|
722
|
+
>
|
|
723
|
+
<CarouselContent className='h-full'>
|
|
724
|
+
{products.map(product => (
|
|
725
|
+
<CarouselItem key={product.id} className='h-full'>
|
|
726
|
+
<ShowcaseSlide product={product} currency={currency} labels={labels} onBuyNow={onBuyNow} />
|
|
727
|
+
</CarouselItem>
|
|
728
|
+
))}
|
|
729
|
+
</CarouselContent>
|
|
730
|
+
</Carousel>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<div className='relative mt-8 flex justify-center gap-3'>
|
|
734
|
+
{products.map((product, index) => (
|
|
735
|
+
<button
|
|
736
|
+
key={product.id}
|
|
737
|
+
type='button'
|
|
738
|
+
onClick={() => {
|
|
739
|
+
api?.scrollTo(index)
|
|
740
|
+
setCurrentSlide(index)
|
|
741
|
+
}}
|
|
742
|
+
className={cn(
|
|
743
|
+
'relative size-3 rounded-full transition-all',
|
|
744
|
+
currentSlide === index ? 'bg-primary' : 'bg-foreground/20 hover:bg-foreground/40'
|
|
745
|
+
)}
|
|
746
|
+
aria-label={`Ir a la diapositiva ${index + 1}`}
|
|
747
|
+
aria-current={currentSlide === index ? 'step' : undefined}
|
|
748
|
+
/>
|
|
749
|
+
))}
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</section>
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function EditorialPresentation({
|
|
760
|
+
title,
|
|
761
|
+
description,
|
|
762
|
+
badge,
|
|
763
|
+
titleAccent,
|
|
764
|
+
primaryCta,
|
|
765
|
+
secondaryCta,
|
|
766
|
+
shippingInfo,
|
|
767
|
+
departments,
|
|
768
|
+
featuredDeals,
|
|
769
|
+
storeLocator,
|
|
770
|
+
currency,
|
|
771
|
+
labels,
|
|
772
|
+
onDepartmentClick,
|
|
773
|
+
onShopNow,
|
|
774
|
+
onLocateStores,
|
|
775
|
+
}: {
|
|
776
|
+
title: string
|
|
777
|
+
description: string
|
|
778
|
+
badge?: string
|
|
779
|
+
titleAccent?: string
|
|
780
|
+
primaryCta?: CTA
|
|
781
|
+
secondaryCta?: CTA
|
|
782
|
+
shippingInfo?: string
|
|
783
|
+
departments: HeroDepartment[]
|
|
784
|
+
featuredDeals: HeroFeaturedDeal[]
|
|
785
|
+
storeLocator?: HeroStoreLocator
|
|
786
|
+
currency?: string
|
|
787
|
+
labels: HeroLabels
|
|
788
|
+
onDepartmentClick?: (department: HeroDepartment) => void
|
|
789
|
+
onShopNow?: (item: HeroFeaturedDeal | HeroDeal) => void
|
|
790
|
+
onLocateStores?: () => void
|
|
791
|
+
}) {
|
|
792
|
+
const departmentChipClasses = cn(
|
|
793
|
+
'flex flex-col items-center gap-4 rounded-lg p-2 text-center outline-none',
|
|
794
|
+
'transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring',
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<div className='bg-background relative overflow-hidden'>
|
|
799
|
+
<div className='relative z-10'>
|
|
800
|
+
<div className='mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20'>
|
|
801
|
+
<div className='mx-auto max-w-4xl text-center'>
|
|
802
|
+
<EditorialHeading title={title} titleAccent={titleAccent} badge={badge} />
|
|
803
|
+
|
|
804
|
+
<p className='text-muted-foreground mx-auto mt-6 max-w-2xl text-lg'>{description}</p>
|
|
805
|
+
|
|
806
|
+
{(primaryCta || secondaryCta) && (
|
|
807
|
+
<div className='mt-8 flex flex-wrap justify-center gap-4'>
|
|
808
|
+
{primaryCta && (
|
|
809
|
+
<CtaButton cta={primaryCta} className='group h-10 cursor-pointer px-4 text-base'>
|
|
810
|
+
{primaryCta.label}
|
|
811
|
+
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
812
|
+
</CtaButton>
|
|
813
|
+
)}
|
|
814
|
+
{secondaryCta && (
|
|
815
|
+
<CtaButton cta={secondaryCta} variant='outline' className='group h-10 cursor-pointer px-4 text-base'>
|
|
816
|
+
{secondaryCta.label}
|
|
817
|
+
</CtaButton>
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
)}
|
|
821
|
+
|
|
822
|
+
{shippingInfo && (
|
|
823
|
+
<div className='mt-6'>
|
|
824
|
+
<div className='bg-background/80 text-muted-foreground inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm backdrop-blur-sm'>
|
|
825
|
+
<MapPin className='text-primary size-4' aria-hidden='true' />
|
|
826
|
+
<span>{shippingInfo}</span>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
)}
|
|
830
|
+
|
|
831
|
+
{departments.length > 0 && (
|
|
832
|
+
<div className='mt-12 grid grid-cols-3 gap-8'>
|
|
833
|
+
{departments.map(department => {
|
|
834
|
+
const chip = (
|
|
835
|
+
<>
|
|
836
|
+
{department.image && department.image.trim().length > 0 ? (
|
|
837
|
+
<img
|
|
838
|
+
src={department.image}
|
|
839
|
+
alt={department.imageAlt ?? department.name}
|
|
840
|
+
className='bg-card border-border size-16 rounded-full border object-cover'
|
|
841
|
+
loading='lazy'
|
|
842
|
+
/>
|
|
843
|
+
) : null}
|
|
844
|
+
<div>
|
|
845
|
+
<div className='text-lg font-medium'>{department.name}</div>
|
|
846
|
+
{department.count && (
|
|
847
|
+
<div className='text-muted-foreground mt-1 text-sm'>{department.count}</div>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
</>
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
if (department.href) {
|
|
854
|
+
return (
|
|
855
|
+
<a
|
|
856
|
+
key={department.id}
|
|
857
|
+
href={department.href}
|
|
858
|
+
className={cn(departmentChipClasses, 'cursor-pointer')}
|
|
859
|
+
>
|
|
860
|
+
{chip}
|
|
861
|
+
</a>
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return (
|
|
866
|
+
<button
|
|
867
|
+
key={department.id}
|
|
868
|
+
type='button'
|
|
869
|
+
onClick={onDepartmentClick ? () => onDepartmentClick(department) : undefined}
|
|
870
|
+
className={cn(departmentChipClasses, 'cursor-pointer')}
|
|
871
|
+
>
|
|
872
|
+
{chip}
|
|
873
|
+
</button>
|
|
874
|
+
)
|
|
875
|
+
})}
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
|
|
879
|
+
{featuredDeals.length > 0 && (
|
|
880
|
+
<div className='mt-16 grid gap-6 sm:grid-cols-2'>
|
|
881
|
+
{featuredDeals.map(deal => {
|
|
882
|
+
const onSale = hasRealDiscount(deal)
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<Card key={deal.id} className='mx-auto h-full w-full overflow-hidden border-0 py-0 shadow-lg'>
|
|
886
|
+
<CardContent className='p-0'>
|
|
887
|
+
<div className='relative aspect-[3/2] overflow-hidden'>
|
|
888
|
+
{deal.image && deal.image.trim().length > 0 ? (
|
|
889
|
+
<img
|
|
890
|
+
src={deal.image}
|
|
891
|
+
alt={deal.imageAlt ?? deal.name}
|
|
892
|
+
className='size-full object-cover object-center transition-transform duration-700 hover:scale-105'
|
|
893
|
+
loading='lazy'
|
|
894
|
+
/>
|
|
895
|
+
) : null}
|
|
896
|
+
<div className='from-background/90 via-background/30 absolute inset-0 bg-gradient-to-t to-transparent' />
|
|
897
|
+
<div className='absolute inset-0 flex items-end p-8'>
|
|
898
|
+
<div className='w-full'>
|
|
899
|
+
<div className='bg-background/80 flex flex-col items-center justify-between gap-6 rounded-lg p-6 backdrop-blur-sm sm:flex-row sm:gap-8'>
|
|
900
|
+
<div className='text-center sm:text-left'>
|
|
901
|
+
<h3 className='text-2xl font-bold tracking-tight sm:text-3xl'>{deal.name}</h3>
|
|
902
|
+
{deal.description && (
|
|
903
|
+
<p className='text-muted-foreground mt-1'>{deal.description}</p>
|
|
904
|
+
)}
|
|
905
|
+
<div className='mt-2 flex items-baseline justify-center gap-2 sm:justify-start'>
|
|
906
|
+
<span className='text-lg font-bold'>{formatAmount(deal.price, currency)}</span>
|
|
907
|
+
{onSale && (
|
|
908
|
+
<span className='text-muted-foreground text-sm line-through'>
|
|
909
|
+
{formatAmount(deal.originalPrice as number, currency)}
|
|
910
|
+
</span>
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
{(deal.cta?.href || onShopNow) && (
|
|
915
|
+
<div className='flex-shrink-0'>
|
|
916
|
+
{deal.cta?.href ? (
|
|
917
|
+
<Button size='lg' className='group h-10 cursor-pointer px-4' asChild>
|
|
918
|
+
<a href={deal.cta.href}>
|
|
919
|
+
{deal.cta.label}
|
|
920
|
+
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
921
|
+
</a>
|
|
922
|
+
</Button>
|
|
923
|
+
) : (
|
|
924
|
+
<Button
|
|
925
|
+
size='lg'
|
|
926
|
+
className='group h-10 cursor-pointer px-4'
|
|
927
|
+
onClick={() => onShopNow?.(deal)}
|
|
928
|
+
>
|
|
929
|
+
{deal.cta?.label ?? labels.shopNowLabel}
|
|
930
|
+
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
931
|
+
</Button>
|
|
932
|
+
)}
|
|
933
|
+
</div>
|
|
934
|
+
)}
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
</CardContent>
|
|
940
|
+
</Card>
|
|
941
|
+
)
|
|
942
|
+
})}
|
|
943
|
+
</div>
|
|
944
|
+
)}
|
|
945
|
+
|
|
946
|
+
{storeLocator && (
|
|
947
|
+
<div className='border-border/50 bg-background/50 mt-8 rounded-lg border p-6 backdrop-blur-sm'>
|
|
948
|
+
<div className='flex flex-col items-center gap-6 sm:flex-row sm:justify-between'>
|
|
949
|
+
<div className='flex items-center gap-4'>
|
|
950
|
+
<div className='bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full'>
|
|
951
|
+
<MapPin className='size-6' aria-hidden='true' />
|
|
952
|
+
</div>
|
|
953
|
+
<div className='text-start'>
|
|
954
|
+
<h4 className='text-lg font-medium'>{storeLocator.title}</h4>
|
|
955
|
+
<p className='text-muted-foreground'>{storeLocator.description}</p>
|
|
956
|
+
</div>
|
|
957
|
+
</div>
|
|
958
|
+
{(storeLocator.ctaHref || onLocateStores) && (
|
|
959
|
+
<div>
|
|
960
|
+
{storeLocator.ctaHref ? (
|
|
961
|
+
<Button variant='outline' className='h-9 cursor-pointer px-4 py-2 whitespace-nowrap' asChild>
|
|
962
|
+
<a href={storeLocator.ctaHref}>{storeLocator.ctaLabel}</a>
|
|
963
|
+
</Button>
|
|
964
|
+
) : (
|
|
965
|
+
<Button
|
|
966
|
+
variant='outline'
|
|
967
|
+
className='h-9 cursor-pointer px-4 py-2 whitespace-nowrap'
|
|
968
|
+
onClick={onLocateStores}
|
|
969
|
+
>
|
|
970
|
+
{storeLocator.ctaLabel}
|
|
971
|
+
</Button>
|
|
972
|
+
)}
|
|
973
|
+
</div>
|
|
974
|
+
)}
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
)}
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function PromotionalPresentation({
|
|
986
|
+
title,
|
|
987
|
+
description,
|
|
988
|
+
badge,
|
|
989
|
+
titleAccent,
|
|
990
|
+
deals,
|
|
991
|
+
categories,
|
|
992
|
+
rotationInterval,
|
|
993
|
+
labels,
|
|
994
|
+
onShopNow,
|
|
995
|
+
onCategoryClick,
|
|
996
|
+
}: {
|
|
997
|
+
title: string
|
|
998
|
+
description: string
|
|
999
|
+
badge?: string
|
|
1000
|
+
titleAccent?: string
|
|
1001
|
+
deals: HeroDeal[]
|
|
1002
|
+
categories: HeroCategory[]
|
|
1003
|
+
rotationInterval: number
|
|
1004
|
+
labels: HeroLabels
|
|
1005
|
+
onShopNow?: (item: HeroFeaturedDeal | HeroDeal) => void
|
|
1006
|
+
onCategoryClick?: (category: HeroCategory) => void
|
|
1007
|
+
}) {
|
|
1008
|
+
const reduceMotion = useReducedMotion()
|
|
1009
|
+
const [currentDeal, setCurrentDeal] = useState(0)
|
|
1010
|
+
|
|
1011
|
+
useEffect(() => {
|
|
1012
|
+
if (deals.length <= 1 || reduceMotion) return
|
|
1013
|
+
|
|
1014
|
+
const interval = setInterval(() => {
|
|
1015
|
+
setCurrentDeal(prev => (prev + 1) % deals.length)
|
|
1016
|
+
}, rotationInterval)
|
|
1017
|
+
|
|
1018
|
+
return () => clearInterval(interval)
|
|
1019
|
+
}, [deals.length, reduceMotion, rotationInterval])
|
|
1020
|
+
|
|
1021
|
+
const deal = deals[currentDeal]
|
|
1022
|
+
|
|
1023
|
+
return (
|
|
1024
|
+
<div className='relative'>
|
|
1025
|
+
{deal && (
|
|
1026
|
+
<header className='bg-primary text-primary-foreground sticky top-0 z-50 shadow-md'>
|
|
1027
|
+
<div className='mx-auto w-full max-w-7xl px-4 py-2 sm:px-6 lg:px-8'>
|
|
1028
|
+
<div className='flex items-center justify-center'>
|
|
1029
|
+
<div className='flex w-full max-w-3xl items-center justify-center gap-4'>
|
|
1030
|
+
<div className='flex items-center gap-2'>
|
|
1031
|
+
<Badge
|
|
1032
|
+
variant='secondary'
|
|
1033
|
+
className='bg-primary-foreground text-primary px-2.5 py-0.5 font-semibold'
|
|
1034
|
+
>
|
|
1035
|
+
{deal.title}
|
|
1036
|
+
</Badge>
|
|
1037
|
+
{deal.discount && <span className='font-semibold'>{deal.discount}</span>}
|
|
1038
|
+
</div>
|
|
1039
|
+
{deal.endTime && (
|
|
1040
|
+
<span className='hidden sm:inline'>
|
|
1041
|
+
{labels.endsInLabel} {deal.endTime}
|
|
1042
|
+
</span>
|
|
1043
|
+
)}
|
|
1044
|
+
{onShopNow && (
|
|
1045
|
+
<Button
|
|
1046
|
+
variant='link'
|
|
1047
|
+
size='sm'
|
|
1048
|
+
className='text-primary-foreground hover:text-primary-foreground/80 ml-auto h-8 cursor-pointer px-3 text-xs'
|
|
1049
|
+
onClick={() => onShopNow(deal)}
|
|
1050
|
+
>
|
|
1051
|
+
{labels.shopNowLabel} <ArrowRight />
|
|
1052
|
+
</Button>
|
|
1053
|
+
)}
|
|
1054
|
+
</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</header>
|
|
1058
|
+
)}
|
|
1059
|
+
|
|
1060
|
+
<main className='mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8'>
|
|
1061
|
+
<div className='mb-12 text-center'>
|
|
1062
|
+
{badge && (
|
|
1063
|
+
<div className='relative inline-block'>
|
|
1064
|
+
<Badge
|
|
1065
|
+
variant='outline'
|
|
1066
|
+
className='mb-4 flex h-auto items-center rounded-full px-3.5 py-1.5 text-sm font-normal'
|
|
1067
|
+
>
|
|
1068
|
+
{badge}
|
|
1069
|
+
</Badge>
|
|
1070
|
+
</div>
|
|
1071
|
+
)}
|
|
1072
|
+
<h1 className='text-4xl font-bold tracking-tight text-balance sm:text-5xl md:text-6xl'>
|
|
1073
|
+
{title}
|
|
1074
|
+
{titleAccent && (
|
|
1075
|
+
<>
|
|
1076
|
+
<br />
|
|
1077
|
+
{titleAccent}
|
|
1078
|
+
</>
|
|
1079
|
+
)}
|
|
1080
|
+
</h1>
|
|
1081
|
+
<p className='text-muted-foreground mx-auto mt-4 max-w-2xl text-lg'>{description}</p>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
{categories.length > 0 && (
|
|
1085
|
+
<div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
|
|
1086
|
+
{categories.map(category => (
|
|
1087
|
+
<Card key={category.id} className='group relative overflow-hidden py-0 transition-all duration-500'>
|
|
1088
|
+
{category.image && category.image.trim().length > 0 ? (
|
|
1089
|
+
<img
|
|
1090
|
+
src={category.image}
|
|
1091
|
+
alt={category.imageAlt ?? category.name}
|
|
1092
|
+
className='aspect-4/5 size-full object-cover grayscale transition-all duration-500 hover:grayscale-0'
|
|
1093
|
+
width={400}
|
|
1094
|
+
height={500}
|
|
1095
|
+
loading='lazy'
|
|
1096
|
+
/>
|
|
1097
|
+
) : (
|
|
1098
|
+
<div className='bg-muted aspect-4/5 size-full' />
|
|
1099
|
+
)}
|
|
1100
|
+
<CardContent className='absolute inset-x-0 bottom-0 p-4'>
|
|
1101
|
+
<div className='mt-1 flex items-center justify-between'>
|
|
1102
|
+
<div>
|
|
1103
|
+
<h2 className='text-background dark:text-foreground text-lg font-medium'>{category.name}</h2>
|
|
1104
|
+
{category.count && (
|
|
1105
|
+
<span className='text-background/80 dark:text-foreground/80 text-sm'>
|
|
1106
|
+
{category.count} {labels.productsLabel}
|
|
1107
|
+
</span>
|
|
1108
|
+
)}
|
|
1109
|
+
</div>
|
|
1110
|
+
{category.href ? (
|
|
1111
|
+
<Button
|
|
1112
|
+
variant='secondary'
|
|
1113
|
+
size='icon'
|
|
1114
|
+
className='h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs'
|
|
1115
|
+
aria-label={`${labels.viewLabel} ${category.name}`}
|
|
1116
|
+
asChild
|
|
1117
|
+
>
|
|
1118
|
+
<a href={category.href}>
|
|
1119
|
+
<ArrowRight />
|
|
1120
|
+
</a>
|
|
1121
|
+
</Button>
|
|
1122
|
+
) : onCategoryClick ? (
|
|
1123
|
+
<Button
|
|
1124
|
+
variant='secondary'
|
|
1125
|
+
size='icon'
|
|
1126
|
+
className='h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs'
|
|
1127
|
+
aria-label={`${labels.viewLabel} ${category.name}`}
|
|
1128
|
+
onClick={() => onCategoryClick(category)}
|
|
1129
|
+
>
|
|
1130
|
+
<ArrowRight />
|
|
1131
|
+
</Button>
|
|
1132
|
+
) : null}
|
|
1133
|
+
</div>
|
|
1134
|
+
</CardContent>
|
|
1135
|
+
</Card>
|
|
1136
|
+
))}
|
|
1137
|
+
</div>
|
|
1138
|
+
)}
|
|
1139
|
+
</main>
|
|
1140
|
+
</div>
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export function StorefrontHero({
|
|
1145
|
+
title,
|
|
1146
|
+
description,
|
|
1147
|
+
presentation = 'compact',
|
|
1148
|
+
currency,
|
|
1149
|
+
products,
|
|
1150
|
+
badge,
|
|
1151
|
+
titleAccent,
|
|
1152
|
+
primaryCta,
|
|
1153
|
+
secondaryCta,
|
|
1154
|
+
heroImage,
|
|
1155
|
+
onSearch,
|
|
1156
|
+
departments = [],
|
|
1157
|
+
featuredDeals = [],
|
|
1158
|
+
storeLocator,
|
|
1159
|
+
shippingInfo,
|
|
1160
|
+
deals = [],
|
|
1161
|
+
categories = [],
|
|
1162
|
+
rotationInterval = 3000,
|
|
1163
|
+
onAddToCart,
|
|
1164
|
+
onBuyNow,
|
|
1165
|
+
onSelectColor,
|
|
1166
|
+
onDepartmentClick,
|
|
1167
|
+
onShopNow,
|
|
1168
|
+
onLocateStores,
|
|
1169
|
+
onCategoryClick,
|
|
1170
|
+
addToCartLabel = 'Agregar al carrito',
|
|
1171
|
+
buyNowLabel,
|
|
1172
|
+
newLabel = 'Nuevo',
|
|
1173
|
+
outOfStockLabel = 'Agotado',
|
|
1174
|
+
colorsLabel = 'Colores:',
|
|
1175
|
+
discountSuffixLabel = '% OFF',
|
|
1176
|
+
searchPlaceholder = 'Buscar productos...',
|
|
1177
|
+
searchLabel = 'Buscar',
|
|
1178
|
+
featuredDescription,
|
|
1179
|
+
shopNowLabel = 'Comprar ahora',
|
|
1180
|
+
endsInLabel = 'Termina en',
|
|
1181
|
+
productsLabel = 'productos',
|
|
1182
|
+
viewLabel = 'Ver',
|
|
1183
|
+
}: StorefrontHeroProps) {
|
|
1184
|
+
// Presence-based, never label-language based: a host-supplied custom action
|
|
1185
|
+
// label is a purchase affordance that must be handler-gated, while the
|
|
1186
|
+
// block's own default label is a non-purchase detail affordance that may
|
|
1187
|
+
// render without a handler (preserving the accepted default detail).
|
|
1188
|
+
const resolvedBuyNowLabel = buyNowLabel ?? 'Ver producto'
|
|
1189
|
+
const hasCustomBuyNowLabel = buyNowLabel !== undefined
|
|
1190
|
+
const labels: HeroLabels = {
|
|
1191
|
+
addToCartLabel,
|
|
1192
|
+
buyNowLabel: resolvedBuyNowLabel,
|
|
1193
|
+
newLabel,
|
|
1194
|
+
outOfStockLabel,
|
|
1195
|
+
colorsLabel,
|
|
1196
|
+
discountSuffixLabel,
|
|
1197
|
+
searchPlaceholder,
|
|
1198
|
+
searchLabel,
|
|
1199
|
+
featuredDescription,
|
|
1200
|
+
shopNowLabel,
|
|
1201
|
+
endsInLabel,
|
|
1202
|
+
productsLabel,
|
|
1203
|
+
viewLabel,
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (presentation === 'showcase') {
|
|
1207
|
+
return (
|
|
1208
|
+
<ShowcasePresentation
|
|
1209
|
+
title={title}
|
|
1210
|
+
description={description}
|
|
1211
|
+
products={products}
|
|
1212
|
+
badge={badge}
|
|
1213
|
+
primaryCta={primaryCta}
|
|
1214
|
+
secondaryCta={secondaryCta}
|
|
1215
|
+
currency={currency}
|
|
1216
|
+
labels={labels}
|
|
1217
|
+
onSearch={onSearch}
|
|
1218
|
+
onBuyNow={onBuyNow}
|
|
1219
|
+
rotationInterval={rotationInterval}
|
|
1220
|
+
/>
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (presentation === 'editorial') {
|
|
1225
|
+
return (
|
|
1226
|
+
<EditorialPresentation
|
|
1227
|
+
title={title}
|
|
1228
|
+
description={description}
|
|
1229
|
+
badge={badge}
|
|
1230
|
+
titleAccent={titleAccent}
|
|
1231
|
+
primaryCta={primaryCta}
|
|
1232
|
+
secondaryCta={secondaryCta}
|
|
1233
|
+
shippingInfo={shippingInfo}
|
|
1234
|
+
departments={departments}
|
|
1235
|
+
featuredDeals={featuredDeals}
|
|
1236
|
+
storeLocator={storeLocator}
|
|
1237
|
+
currency={currency}
|
|
1238
|
+
labels={labels}
|
|
1239
|
+
onDepartmentClick={onDepartmentClick}
|
|
1240
|
+
onShopNow={onShopNow}
|
|
1241
|
+
onLocateStores={onLocateStores}
|
|
1242
|
+
/>
|
|
1243
|
+
)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (presentation === 'promotional') {
|
|
1247
|
+
return (
|
|
1248
|
+
<PromotionalPresentation
|
|
1249
|
+
title={title}
|
|
1250
|
+
description={description}
|
|
1251
|
+
badge={badge}
|
|
1252
|
+
titleAccent={titleAccent}
|
|
1253
|
+
deals={deals}
|
|
1254
|
+
categories={categories}
|
|
1255
|
+
rotationInterval={rotationInterval}
|
|
1256
|
+
labels={labels}
|
|
1257
|
+
onShopNow={onShopNow}
|
|
1258
|
+
onCategoryClick={onCategoryClick}
|
|
1259
|
+
/>
|
|
1260
|
+
)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return (
|
|
1264
|
+
<CompactPresentation
|
|
1265
|
+
title={title}
|
|
1266
|
+
description={description}
|
|
1267
|
+
products={products}
|
|
1268
|
+
badge={badge}
|
|
1269
|
+
titleAccent={titleAccent}
|
|
1270
|
+
primaryCta={primaryCta}
|
|
1271
|
+
secondaryCta={secondaryCta}
|
|
1272
|
+
heroImage={heroImage}
|
|
1273
|
+
currency={currency}
|
|
1274
|
+
labels={labels}
|
|
1275
|
+
onAddToCart={onAddToCart}
|
|
1276
|
+
onBuyNow={onBuyNow}
|
|
1277
|
+
onSelectColor={onSelectColor}
|
|
1278
|
+
hasCustomBuyNowLabel={hasCustomBuyNowLabel}
|
|
1279
|
+
/>
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
export default StorefrontHero
|