@ecomiq/storefront 2.0.22 → 2.1.4
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/README.md +8 -28
- package/package.json +2 -5
- package/src/cart-session.ts +13 -15
- package/src/client.ts +1 -1
- package/src/codegen.mjs +12 -1
- package/src/components/checkout/checkout-session-view.tsx +4 -4
- package/src/components/checkout/format.ts +2 -2
- package/src/components/checkout/use-checkout-customer-prefill.ts +4 -4
- package/src/components/checkout/use-checkout-session.ts +37 -38
- package/src/config.mjs +1 -0
- package/src/hooks/recently-viewed.ts +2 -2
- package/src/index.ts +2 -9
- package/src/pages/checkout.$checkoutId.tsx +1 -1
- package/src/pages/pages.$pageSlug.tsx +2 -2
- package/src/pages/thank-you.tsx +1 -1
- package/src/providers/cart.tsx +17 -17
- package/src/server-config-store.ts +1 -0
- package/src/server.ts +6 -12
- package/src/styles.css +0 -13
- package/src/cloud-runtime-client.ts +0 -218
- package/src/storefront-definition/contract.ts +0 -1
- package/src/storefront-definition/index.ts +0 -2
- package/src/storefront-definition/renderer.tsx +0 -905
- package/src/storefront-layout.ts +0 -37
- package/src/storefront-runtime.ts +0 -277
|
@@ -1,905 +0,0 @@
|
|
|
1
|
-
import { Fragment, type CSSProperties, type ReactNode } from 'react'
|
|
2
|
-
import type { Collection, ProductSummary } from '@ecomiq/core'
|
|
3
|
-
import { buttonVariants } from '../ui/button'
|
|
4
|
-
import { cn } from '../ui/cn'
|
|
5
|
-
import type {
|
|
6
|
-
CollectionGridStorefrontSection,
|
|
7
|
-
HeroStorefrontSection,
|
|
8
|
-
ImageTextStorefrontSection,
|
|
9
|
-
NewsletterStorefrontSection,
|
|
10
|
-
ProductGridStorefrontSection,
|
|
11
|
-
PromoBannerStorefrontSection,
|
|
12
|
-
RichTextStorefrontSection,
|
|
13
|
-
StorefrontAction,
|
|
14
|
-
StorefrontDefinition,
|
|
15
|
-
StorefrontFontFamily,
|
|
16
|
-
StorefrontImage,
|
|
17
|
-
StorefrontNavigationItem,
|
|
18
|
-
StorefrontSection,
|
|
19
|
-
StorefrontThemeTokens,
|
|
20
|
-
TestimonialsStorefrontSection,
|
|
21
|
-
} from '@ecomiq/storefront-definition'
|
|
22
|
-
|
|
23
|
-
export interface DefinitionData {
|
|
24
|
-
productsBySectionId?: Readonly<
|
|
25
|
-
Record<string, readonly ProductSummary[] | undefined>
|
|
26
|
-
>
|
|
27
|
-
collectionsBySectionId?: Readonly<
|
|
28
|
-
Record<string, readonly Collection[] | undefined>
|
|
29
|
-
>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface SectionContext {
|
|
33
|
-
data: DefinitionData
|
|
34
|
-
locale: string
|
|
35
|
-
currency: string
|
|
36
|
-
assets: AssetManifest
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type AssetManifest = Readonly<Record<string, string>>
|
|
40
|
-
|
|
41
|
-
export function storefrontImageSrc(
|
|
42
|
-
image: StorefrontImage | undefined,
|
|
43
|
-
assets: AssetManifest,
|
|
44
|
-
): string | undefined {
|
|
45
|
-
if (image === undefined) return undefined
|
|
46
|
-
if (typeof image === 'string') return image
|
|
47
|
-
return assets[image.assetId]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function storefrontImageAlt(
|
|
51
|
-
image: StorefrontImage | undefined,
|
|
52
|
-
fallback: string,
|
|
53
|
-
): string {
|
|
54
|
-
return typeof image === 'object' && image.alt ? image.alt : fallback
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SectionProps<
|
|
58
|
-
TSection extends StorefrontSection,
|
|
59
|
-
> {
|
|
60
|
-
section: TSection
|
|
61
|
-
context: SectionContext
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface SectionRegistry {
|
|
65
|
-
hero: (
|
|
66
|
-
props: SectionProps<HeroStorefrontSection>,
|
|
67
|
-
) => ReactNode
|
|
68
|
-
'product-grid': (
|
|
69
|
-
props: SectionProps<ProductGridStorefrontSection>,
|
|
70
|
-
) => ReactNode
|
|
71
|
-
'collection-grid': (
|
|
72
|
-
props: SectionProps<CollectionGridStorefrontSection>,
|
|
73
|
-
) => ReactNode
|
|
74
|
-
'promo-banner': (
|
|
75
|
-
props: SectionProps<PromoBannerStorefrontSection>,
|
|
76
|
-
) => ReactNode
|
|
77
|
-
'image-text': (
|
|
78
|
-
props: SectionProps<ImageTextStorefrontSection>,
|
|
79
|
-
) => ReactNode
|
|
80
|
-
'rich-text': (
|
|
81
|
-
props: SectionProps<RichTextStorefrontSection>,
|
|
82
|
-
) => ReactNode
|
|
83
|
-
testimonials: (
|
|
84
|
-
props: SectionProps<TestimonialsStorefrontSection>,
|
|
85
|
-
) => ReactNode
|
|
86
|
-
newsletter: (
|
|
87
|
-
props: SectionProps<NewsletterStorefrontSection>,
|
|
88
|
-
) => ReactNode
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface DefinitionSectionsProps {
|
|
92
|
-
definition: StorefrontDefinition
|
|
93
|
-
data?: DefinitionData
|
|
94
|
-
locale?: string
|
|
95
|
-
currency?: string
|
|
96
|
-
registry?: SectionRegistry
|
|
97
|
-
assets?: AssetManifest
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface DefinitionShellProps {
|
|
101
|
-
definition: StorefrontDefinition
|
|
102
|
-
children: ReactNode
|
|
103
|
-
showChrome?: boolean
|
|
104
|
-
className?: string
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface DefinitionRendererProps extends DefinitionSectionsProps {
|
|
108
|
-
showChrome?: boolean
|
|
109
|
-
className?: string
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
type StorefrontThemeStyle = CSSProperties & {
|
|
113
|
-
'--background': string
|
|
114
|
-
'--foreground': string
|
|
115
|
-
'--card': string
|
|
116
|
-
'--card-foreground': string
|
|
117
|
-
'--primary': string
|
|
118
|
-
'--primary-foreground': string
|
|
119
|
-
'--secondary': string
|
|
120
|
-
'--secondary-foreground': string
|
|
121
|
-
'--muted': string
|
|
122
|
-
'--muted-foreground': string
|
|
123
|
-
'--accent': string
|
|
124
|
-
'--accent-foreground': string
|
|
125
|
-
'--border': string
|
|
126
|
-
'--input': string
|
|
127
|
-
'--ring': string
|
|
128
|
-
'--radius': string
|
|
129
|
-
'--storefront-font-body': string
|
|
130
|
-
'--storefront-font-heading': string
|
|
131
|
-
'--storefront-section-padding': string
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function fontStack(font: StorefrontFontFamily): string {
|
|
135
|
-
if (font === 'editorial-serif') {
|
|
136
|
-
return 'Iowan Old Style, Baskerville, Times New Roman, serif'
|
|
137
|
-
}
|
|
138
|
-
if (font === 'humanist-sans') {
|
|
139
|
-
return 'Avenir Next, Avenir, Segoe UI, sans-serif'
|
|
140
|
-
}
|
|
141
|
-
return 'ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif'
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function radiusValue(radius: StorefrontThemeTokens['radius']): string {
|
|
145
|
-
if (radius === 'none') return '0rem'
|
|
146
|
-
if (radius === 'small') return '0.375rem'
|
|
147
|
-
if (radius === 'large') return '1rem'
|
|
148
|
-
return '0.625rem'
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function sectionPaddingValue(
|
|
152
|
-
density: StorefrontThemeTokens['density'],
|
|
153
|
-
): string {
|
|
154
|
-
if (density === 'compact') return '2.5rem'
|
|
155
|
-
if (density === 'airy') return '5rem'
|
|
156
|
-
return '4rem'
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function themeStyle(theme: StorefrontThemeTokens): StorefrontThemeStyle {
|
|
160
|
-
return {
|
|
161
|
-
'--background': theme.colors.background,
|
|
162
|
-
'--foreground': theme.colors.foreground,
|
|
163
|
-
'--card': theme.colors.card,
|
|
164
|
-
'--card-foreground': theme.colors.cardForeground,
|
|
165
|
-
'--primary': theme.colors.primary,
|
|
166
|
-
'--primary-foreground': theme.colors.primaryForeground,
|
|
167
|
-
'--secondary': theme.colors.secondary,
|
|
168
|
-
'--secondary-foreground': theme.colors.secondaryForeground,
|
|
169
|
-
'--muted': theme.colors.muted,
|
|
170
|
-
'--muted-foreground': theme.colors.mutedForeground,
|
|
171
|
-
'--accent': theme.colors.accent,
|
|
172
|
-
'--accent-foreground': theme.colors.accentForeground,
|
|
173
|
-
'--border': theme.colors.border,
|
|
174
|
-
'--input': theme.colors.border,
|
|
175
|
-
'--ring': theme.colors.primary,
|
|
176
|
-
'--radius': radiusValue(theme.radius),
|
|
177
|
-
'--storefront-font-body': fontStack(theme.typography.body),
|
|
178
|
-
'--storefront-font-heading': fontStack(theme.typography.heading),
|
|
179
|
-
'--storefront-section-padding': sectionPaddingValue(theme.density),
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function sectionSurfaceClass(section: StorefrontSection): string {
|
|
184
|
-
if (section.surface === 'muted') return 'bg-muted'
|
|
185
|
-
if (section.surface === 'accent') {
|
|
186
|
-
return 'bg-accent text-accent-foreground'
|
|
187
|
-
}
|
|
188
|
-
return 'bg-background'
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function StorefrontActionLink({
|
|
192
|
-
action,
|
|
193
|
-
variant = 'default',
|
|
194
|
-
}: {
|
|
195
|
-
action: StorefrontAction
|
|
196
|
-
variant?: 'default' | 'secondary' | 'outline'
|
|
197
|
-
}) {
|
|
198
|
-
const className = buttonVariants({ variant, size: 'lg' })
|
|
199
|
-
const isExternal =
|
|
200
|
-
action.href.startsWith('https://') ||
|
|
201
|
-
action.href.startsWith('mailto:') ||
|
|
202
|
-
action.href.startsWith('tel:')
|
|
203
|
-
|
|
204
|
-
const opensNewWindow = action.href.startsWith('https://')
|
|
205
|
-
return (
|
|
206
|
-
<a
|
|
207
|
-
href={action.href}
|
|
208
|
-
className={className}
|
|
209
|
-
rel={isExternal && opensNewWindow ? 'noreferrer' : undefined}
|
|
210
|
-
target={isExternal && opensNewWindow ? '_blank' : undefined}
|
|
211
|
-
>
|
|
212
|
-
{action.label}
|
|
213
|
-
</a>
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function StorefrontNavigationLink({
|
|
218
|
-
item,
|
|
219
|
-
}: {
|
|
220
|
-
item: StorefrontNavigationItem
|
|
221
|
-
}) {
|
|
222
|
-
const className =
|
|
223
|
-
'flex min-h-10 items-center px-2 text-sm text-muted-foreground transition-colors [@media(hover:hover)]:hover:text-foreground'
|
|
224
|
-
const opensNewWindow = item.external || item.href.startsWith('https://')
|
|
225
|
-
const isExternal =
|
|
226
|
-
opensNewWindow ||
|
|
227
|
-
item.href.startsWith('mailto:') ||
|
|
228
|
-
item.href.startsWith('tel:')
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<a
|
|
232
|
-
href={item.href}
|
|
233
|
-
className={className}
|
|
234
|
-
rel={isExternal && opensNewWindow ? 'noreferrer' : undefined}
|
|
235
|
-
target={isExternal && opensNewWindow ? '_blank' : undefined}
|
|
236
|
-
>
|
|
237
|
-
{item.label}
|
|
238
|
-
</a>
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function HeroSectionRenderer({
|
|
243
|
-
section,
|
|
244
|
-
context,
|
|
245
|
-
}: SectionProps<HeroStorefrontSection>) {
|
|
246
|
-
const isCentered = section.align === 'center'
|
|
247
|
-
const heroImageSrc = storefrontImageSrc(section.image, context.assets)
|
|
248
|
-
return (
|
|
249
|
-
<section
|
|
250
|
-
className={cn(
|
|
251
|
-
'storefront-definition-section relative isolate min-h-[min(72vh,680px)] overflow-hidden',
|
|
252
|
-
sectionSurfaceClass(section),
|
|
253
|
-
)}
|
|
254
|
-
aria-labelledby={`${section.id}-title`}
|
|
255
|
-
data-storefront-section={section.id}
|
|
256
|
-
>
|
|
257
|
-
{heroImageSrc ? (
|
|
258
|
-
<>
|
|
259
|
-
<img
|
|
260
|
-
src={heroImageSrc}
|
|
261
|
-
alt={storefrontImageAlt(section.image, section.imageAlt)}
|
|
262
|
-
width={1600}
|
|
263
|
-
height={900}
|
|
264
|
-
fetchPriority="high"
|
|
265
|
-
className="absolute inset-0 -z-20 h-full w-full object-cover"
|
|
266
|
-
/>
|
|
267
|
-
<div className="absolute inset-0 -z-10 bg-black/55" aria-hidden />
|
|
268
|
-
</>
|
|
269
|
-
) : null}
|
|
270
|
-
<div
|
|
271
|
-
className={cn(
|
|
272
|
-
'mx-auto flex min-h-[inherit] w-full max-w-6xl items-center px-4',
|
|
273
|
-
isCentered && 'justify-center text-center',
|
|
274
|
-
)}
|
|
275
|
-
>
|
|
276
|
-
<div
|
|
277
|
-
className={cn(
|
|
278
|
-
'max-w-2xl',
|
|
279
|
-
section.image && 'text-white',
|
|
280
|
-
isCentered && 'flex flex-col items-center',
|
|
281
|
-
)}
|
|
282
|
-
>
|
|
283
|
-
{section.eyebrow ? (
|
|
284
|
-
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.16em] opacity-75">
|
|
285
|
-
{section.eyebrow}
|
|
286
|
-
</p>
|
|
287
|
-
) : null}
|
|
288
|
-
<h1
|
|
289
|
-
id={`${section.id}-title`}
|
|
290
|
-
className="text-balance text-4xl font-semibold leading-[1.02] tracking-[-0.022em] sm:text-5xl lg:text-6xl"
|
|
291
|
-
>
|
|
292
|
-
{section.title}
|
|
293
|
-
</h1>
|
|
294
|
-
{section.description ? (
|
|
295
|
-
<p className="mt-5 max-w-[65ch] text-pretty text-base leading-relaxed opacity-80 sm:text-lg">
|
|
296
|
-
{section.description}
|
|
297
|
-
</p>
|
|
298
|
-
) : null}
|
|
299
|
-
{section.primaryAction || section.secondaryAction ? (
|
|
300
|
-
<div className="mt-8 flex flex-wrap gap-3">
|
|
301
|
-
{section.primaryAction ? (
|
|
302
|
-
<StorefrontActionLink action={section.primaryAction} />
|
|
303
|
-
) : null}
|
|
304
|
-
{section.secondaryAction ? (
|
|
305
|
-
<StorefrontActionLink
|
|
306
|
-
action={section.secondaryAction}
|
|
307
|
-
variant="outline"
|
|
308
|
-
/>
|
|
309
|
-
) : null}
|
|
310
|
-
</div>
|
|
311
|
-
) : null}
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
</section>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function gridColumnsClass(columns: 2 | 3 | 4): string {
|
|
319
|
-
if (columns === 2) return 'sm:grid-cols-2'
|
|
320
|
-
if (columns === 3) return 'sm:grid-cols-2 lg:grid-cols-3'
|
|
321
|
-
return 'sm:grid-cols-2 lg:grid-cols-4'
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function SectionHeading({
|
|
325
|
-
id,
|
|
326
|
-
title,
|
|
327
|
-
description,
|
|
328
|
-
}: {
|
|
329
|
-
id: string
|
|
330
|
-
title: string
|
|
331
|
-
description?: string
|
|
332
|
-
}) {
|
|
333
|
-
return (
|
|
334
|
-
<header className="mb-8 max-w-2xl">
|
|
335
|
-
<h2
|
|
336
|
-
id={id}
|
|
337
|
-
className="text-balance text-3xl font-semibold tracking-[-0.022em]"
|
|
338
|
-
>
|
|
339
|
-
{title}
|
|
340
|
-
</h2>
|
|
341
|
-
{description ? (
|
|
342
|
-
<p className="mt-3 text-pretty leading-relaxed text-muted-foreground">
|
|
343
|
-
{description}
|
|
344
|
-
</p>
|
|
345
|
-
) : null}
|
|
346
|
-
</header>
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function ProductGridSectionRenderer({
|
|
351
|
-
section,
|
|
352
|
-
context,
|
|
353
|
-
}: SectionProps<ProductGridStorefrontSection>) {
|
|
354
|
-
const products = (context.data.productsBySectionId?.[section.id] ?? []).slice(
|
|
355
|
-
0,
|
|
356
|
-
section.limit,
|
|
357
|
-
)
|
|
358
|
-
const formatter = new Intl.NumberFormat(context.locale, {
|
|
359
|
-
style: 'currency',
|
|
360
|
-
currency: context.currency,
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
return (
|
|
364
|
-
<section
|
|
365
|
-
className={cn(
|
|
366
|
-
'storefront-definition-section',
|
|
367
|
-
sectionSurfaceClass(section),
|
|
368
|
-
)}
|
|
369
|
-
aria-labelledby={`${section.id}-title`}
|
|
370
|
-
data-storefront-section={section.id}
|
|
371
|
-
>
|
|
372
|
-
<div className="mx-auto w-full max-w-6xl px-4">
|
|
373
|
-
<SectionHeading
|
|
374
|
-
id={`${section.id}-title`}
|
|
375
|
-
title={section.title}
|
|
376
|
-
description={section.description}
|
|
377
|
-
/>
|
|
378
|
-
{products.length > 0 ? (
|
|
379
|
-
<div
|
|
380
|
-
className={cn(
|
|
381
|
-
'grid grid-cols-2 gap-x-4 gap-y-8',
|
|
382
|
-
gridColumnsClass(section.columns),
|
|
383
|
-
)}
|
|
384
|
-
>
|
|
385
|
-
{products.map((product) => (
|
|
386
|
-
<a
|
|
387
|
-
key={product.id}
|
|
388
|
-
href={`/products/${product.slug}`}
|
|
389
|
-
className="group rounded-lg focus-visible:ring-2 focus-visible:ring-ring"
|
|
390
|
-
>
|
|
391
|
-
<div className="aspect-square overflow-hidden rounded-lg bg-muted outline outline-1 -outline-offset-1 outline-black/10">
|
|
392
|
-
{product.imageUrl ? (
|
|
393
|
-
<img
|
|
394
|
-
src={product.imageUrl}
|
|
395
|
-
alt={product.name}
|
|
396
|
-
width={640}
|
|
397
|
-
height={640}
|
|
398
|
-
loading="lazy"
|
|
399
|
-
className="h-full w-full object-cover transition-transform duration-300 ease-out [@media(hover:hover)]:group-hover:scale-[1.025] motion-reduce:transition-none"
|
|
400
|
-
/>
|
|
401
|
-
) : (
|
|
402
|
-
<div className="flex h-full items-center justify-center px-4 text-center text-xs text-muted-foreground">
|
|
403
|
-
Imagen no disponible
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
406
|
-
</div>
|
|
407
|
-
<div className="pt-3">
|
|
408
|
-
{product.brand ? (
|
|
409
|
-
<p className="text-xs text-muted-foreground">
|
|
410
|
-
{product.brand}
|
|
411
|
-
</p>
|
|
412
|
-
) : null}
|
|
413
|
-
<h3 className="mt-1 line-clamp-2 text-pretty text-sm font-medium leading-snug">
|
|
414
|
-
{product.name}
|
|
415
|
-
</h3>
|
|
416
|
-
<p className="mt-1 text-sm font-semibold tabular-nums">
|
|
417
|
-
{formatter.format(product.price)}
|
|
418
|
-
</p>
|
|
419
|
-
</div>
|
|
420
|
-
</a>
|
|
421
|
-
))}
|
|
422
|
-
</div>
|
|
423
|
-
) : (
|
|
424
|
-
<p className="border-t py-6 text-sm text-muted-foreground">
|
|
425
|
-
Aún no hay productos para mostrar.
|
|
426
|
-
</p>
|
|
427
|
-
)}
|
|
428
|
-
</div>
|
|
429
|
-
</section>
|
|
430
|
-
)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function CollectionGridSectionRenderer({
|
|
434
|
-
section,
|
|
435
|
-
context,
|
|
436
|
-
}: SectionProps<CollectionGridStorefrontSection>) {
|
|
437
|
-
const collections = (
|
|
438
|
-
context.data.collectionsBySectionId?.[section.id] ?? []
|
|
439
|
-
).slice(0, section.limit)
|
|
440
|
-
|
|
441
|
-
return (
|
|
442
|
-
<section
|
|
443
|
-
className={cn(
|
|
444
|
-
'storefront-definition-section',
|
|
445
|
-
sectionSurfaceClass(section),
|
|
446
|
-
)}
|
|
447
|
-
aria-labelledby={`${section.id}-title`}
|
|
448
|
-
data-storefront-section={section.id}
|
|
449
|
-
>
|
|
450
|
-
<div className="mx-auto w-full max-w-6xl px-4">
|
|
451
|
-
<SectionHeading
|
|
452
|
-
id={`${section.id}-title`}
|
|
453
|
-
title={section.title}
|
|
454
|
-
description={section.description}
|
|
455
|
-
/>
|
|
456
|
-
{collections.length > 0 ? (
|
|
457
|
-
<div className={cn('grid gap-4', gridColumnsClass(section.columns))}>
|
|
458
|
-
{collections.map((collection) => (
|
|
459
|
-
<a
|
|
460
|
-
key={collection.id}
|
|
461
|
-
href={`/collections/${collection.slug}`}
|
|
462
|
-
className="group relative min-h-72 overflow-hidden rounded-lg bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
463
|
-
>
|
|
464
|
-
{collection.imageUrl ? (
|
|
465
|
-
<img
|
|
466
|
-
src={collection.imageUrl}
|
|
467
|
-
alt={collection.name}
|
|
468
|
-
width={960}
|
|
469
|
-
height={720}
|
|
470
|
-
loading="lazy"
|
|
471
|
-
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 ease-out [@media(hover:hover)]:group-hover:scale-105 motion-reduce:transition-none"
|
|
472
|
-
/>
|
|
473
|
-
) : null}
|
|
474
|
-
<div className="absolute inset-0 bg-black/55" aria-hidden />
|
|
475
|
-
<div className="absolute inset-x-0 bottom-0 p-5 text-white">
|
|
476
|
-
<h3 className="text-balance text-xl font-semibold tracking-[-0.012em]">
|
|
477
|
-
{collection.name}
|
|
478
|
-
</h3>
|
|
479
|
-
{collection.description ? (
|
|
480
|
-
<p className="mt-1 line-clamp-2 text-pretty text-sm text-white/80">
|
|
481
|
-
{collection.description}
|
|
482
|
-
</p>
|
|
483
|
-
) : null}
|
|
484
|
-
</div>
|
|
485
|
-
</a>
|
|
486
|
-
))}
|
|
487
|
-
</div>
|
|
488
|
-
) : (
|
|
489
|
-
<p className="border-t py-6 text-sm text-muted-foreground">
|
|
490
|
-
Aún no hay colecciones para mostrar.
|
|
491
|
-
</p>
|
|
492
|
-
)}
|
|
493
|
-
</div>
|
|
494
|
-
</section>
|
|
495
|
-
)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function PromoBannerSectionRenderer({
|
|
499
|
-
section,
|
|
500
|
-
}: SectionProps<PromoBannerStorefrontSection>) {
|
|
501
|
-
const isPrimary = section.tone === 'primary'
|
|
502
|
-
return (
|
|
503
|
-
<section
|
|
504
|
-
className={cn(
|
|
505
|
-
'storefront-definition-section',
|
|
506
|
-
isPrimary
|
|
507
|
-
? 'bg-primary text-primary-foreground'
|
|
508
|
-
: sectionSurfaceClass(section),
|
|
509
|
-
)}
|
|
510
|
-
aria-labelledby={`${section.id}-title`}
|
|
511
|
-
data-storefront-section={section.id}
|
|
512
|
-
>
|
|
513
|
-
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-4 sm:flex-row sm:items-end sm:justify-between">
|
|
514
|
-
<div className="max-w-3xl">
|
|
515
|
-
{section.eyebrow ? (
|
|
516
|
-
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] opacity-75">
|
|
517
|
-
{section.eyebrow}
|
|
518
|
-
</p>
|
|
519
|
-
) : null}
|
|
520
|
-
<h2
|
|
521
|
-
id={`${section.id}-title`}
|
|
522
|
-
className="text-balance text-3xl font-semibold tracking-[-0.022em]"
|
|
523
|
-
>
|
|
524
|
-
{section.title}
|
|
525
|
-
</h2>
|
|
526
|
-
{section.description ? (
|
|
527
|
-
<p className="mt-3 max-w-[65ch] text-pretty leading-relaxed opacity-80">
|
|
528
|
-
{section.description}
|
|
529
|
-
</p>
|
|
530
|
-
) : null}
|
|
531
|
-
</div>
|
|
532
|
-
{section.action ? (
|
|
533
|
-
<StorefrontActionLink
|
|
534
|
-
action={section.action}
|
|
535
|
-
variant={isPrimary ? 'secondary' : 'default'}
|
|
536
|
-
/>
|
|
537
|
-
) : null}
|
|
538
|
-
</div>
|
|
539
|
-
</section>
|
|
540
|
-
)
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function ImageTextSectionRenderer({
|
|
544
|
-
section,
|
|
545
|
-
context,
|
|
546
|
-
}: SectionProps<ImageTextStorefrontSection>) {
|
|
547
|
-
const imageFirst = section.imagePosition === 'left'
|
|
548
|
-
const imageSrc = storefrontImageSrc(section.image, context.assets)
|
|
549
|
-
return (
|
|
550
|
-
<section
|
|
551
|
-
className={cn(
|
|
552
|
-
'storefront-definition-section',
|
|
553
|
-
sectionSurfaceClass(section),
|
|
554
|
-
)}
|
|
555
|
-
aria-labelledby={`${section.id}-title`}
|
|
556
|
-
data-storefront-section={section.id}
|
|
557
|
-
>
|
|
558
|
-
<div className="mx-auto grid w-full max-w-6xl items-center gap-8 px-4 md:grid-cols-2 md:gap-12">
|
|
559
|
-
<img
|
|
560
|
-
src={imageSrc}
|
|
561
|
-
alt={storefrontImageAlt(section.image, section.imageAlt)}
|
|
562
|
-
width={960}
|
|
563
|
-
height={720}
|
|
564
|
-
loading="lazy"
|
|
565
|
-
className={cn(
|
|
566
|
-
'aspect-[4/3] h-full w-full rounded-lg object-cover outline outline-1 -outline-offset-1 outline-black/10',
|
|
567
|
-
!imageFirst && 'md:order-2',
|
|
568
|
-
)}
|
|
569
|
-
/>
|
|
570
|
-
<div className={cn(!imageFirst && 'md:order-1')}>
|
|
571
|
-
{section.eyebrow ? (
|
|
572
|
-
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
573
|
-
{section.eyebrow}
|
|
574
|
-
</p>
|
|
575
|
-
) : null}
|
|
576
|
-
<h2
|
|
577
|
-
id={`${section.id}-title`}
|
|
578
|
-
className="text-balance text-3xl font-semibold tracking-[-0.022em]"
|
|
579
|
-
>
|
|
580
|
-
{section.title}
|
|
581
|
-
</h2>
|
|
582
|
-
<p className="mt-4 max-w-[65ch] whitespace-pre-line text-pretty leading-relaxed text-muted-foreground">
|
|
583
|
-
{section.body}
|
|
584
|
-
</p>
|
|
585
|
-
{section.action ? (
|
|
586
|
-
<div className="mt-7">
|
|
587
|
-
<StorefrontActionLink action={section.action} />
|
|
588
|
-
</div>
|
|
589
|
-
) : null}
|
|
590
|
-
</div>
|
|
591
|
-
</div>
|
|
592
|
-
</section>
|
|
593
|
-
)
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function RichTextSectionRenderer({
|
|
597
|
-
section,
|
|
598
|
-
}: SectionProps<RichTextStorefrontSection>) {
|
|
599
|
-
const isCentered = section.align === 'center'
|
|
600
|
-
return (
|
|
601
|
-
<section
|
|
602
|
-
className={cn(
|
|
603
|
-
'storefront-definition-section',
|
|
604
|
-
sectionSurfaceClass(section),
|
|
605
|
-
)}
|
|
606
|
-
aria-labelledby={section.title ? `${section.id}-title` : undefined}
|
|
607
|
-
data-storefront-section={section.id}
|
|
608
|
-
>
|
|
609
|
-
<div
|
|
610
|
-
className={cn(
|
|
611
|
-
'mx-auto w-full max-w-3xl px-4',
|
|
612
|
-
isCentered && 'text-center',
|
|
613
|
-
)}
|
|
614
|
-
>
|
|
615
|
-
{section.title ? (
|
|
616
|
-
<h2
|
|
617
|
-
id={`${section.id}-title`}
|
|
618
|
-
className="text-balance text-3xl font-semibold tracking-[-0.022em]"
|
|
619
|
-
>
|
|
620
|
-
{section.title}
|
|
621
|
-
</h2>
|
|
622
|
-
) : null}
|
|
623
|
-
<div className={cn('space-y-4', section.title && 'mt-5')}>
|
|
624
|
-
{section.paragraphs.map((paragraph, index) => (
|
|
625
|
-
<p
|
|
626
|
-
key={`${section.id}-paragraph-${index}`}
|
|
627
|
-
className="whitespace-pre-line text-pretty leading-7 text-muted-foreground"
|
|
628
|
-
>
|
|
629
|
-
{paragraph}
|
|
630
|
-
</p>
|
|
631
|
-
))}
|
|
632
|
-
</div>
|
|
633
|
-
</div>
|
|
634
|
-
</section>
|
|
635
|
-
)
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function testimonialColumnsClass(columns: 1 | 2 | 3): string {
|
|
639
|
-
if (columns === 1) return 'grid-cols-1'
|
|
640
|
-
if (columns === 2) return 'md:grid-cols-2'
|
|
641
|
-
return 'md:grid-cols-2 lg:grid-cols-3'
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function TestimonialsSectionRenderer({
|
|
645
|
-
section,
|
|
646
|
-
}: SectionProps<TestimonialsStorefrontSection>) {
|
|
647
|
-
return (
|
|
648
|
-
<section
|
|
649
|
-
className={cn(
|
|
650
|
-
'storefront-definition-section',
|
|
651
|
-
sectionSurfaceClass(section),
|
|
652
|
-
)}
|
|
653
|
-
aria-labelledby={`${section.id}-title`}
|
|
654
|
-
data-storefront-section={section.id}
|
|
655
|
-
>
|
|
656
|
-
<div className="mx-auto w-full max-w-6xl px-4">
|
|
657
|
-
<SectionHeading
|
|
658
|
-
id={`${section.id}-title`}
|
|
659
|
-
title={section.title}
|
|
660
|
-
description={section.description}
|
|
661
|
-
/>
|
|
662
|
-
<div
|
|
663
|
-
className={cn('grid gap-5', testimonialColumnsClass(section.columns))}
|
|
664
|
-
>
|
|
665
|
-
{section.items.map((item) => (
|
|
666
|
-
<figure
|
|
667
|
-
key={item.id}
|
|
668
|
-
className="flex h-full flex-col justify-between border-t pt-6 text-foreground"
|
|
669
|
-
>
|
|
670
|
-
<blockquote className="text-pretty text-lg leading-relaxed">
|
|
671
|
-
“{item.quote}”
|
|
672
|
-
</blockquote>
|
|
673
|
-
<figcaption className="mt-8 text-sm">
|
|
674
|
-
<span className="font-semibold">{item.author}</span>
|
|
675
|
-
{item.role ? (
|
|
676
|
-
<span className="mt-1 block text-muted-foreground">
|
|
677
|
-
{item.role}
|
|
678
|
-
</span>
|
|
679
|
-
) : null}
|
|
680
|
-
</figcaption>
|
|
681
|
-
</figure>
|
|
682
|
-
))}
|
|
683
|
-
</div>
|
|
684
|
-
</div>
|
|
685
|
-
</section>
|
|
686
|
-
)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function NewsletterSectionRenderer({
|
|
690
|
-
section,
|
|
691
|
-
}: SectionProps<NewsletterStorefrontSection>) {
|
|
692
|
-
return (
|
|
693
|
-
<section
|
|
694
|
-
className={cn(
|
|
695
|
-
'storefront-definition-section',
|
|
696
|
-
sectionSurfaceClass(section),
|
|
697
|
-
)}
|
|
698
|
-
aria-labelledby={`${section.id}-title`}
|
|
699
|
-
data-storefront-section={section.id}
|
|
700
|
-
>
|
|
701
|
-
<div className="mx-auto grid w-full max-w-6xl gap-7 px-4 md:grid-cols-[1fr_minmax(280px,440px)] md:items-end">
|
|
702
|
-
<div className="max-w-2xl">
|
|
703
|
-
{section.eyebrow ? (
|
|
704
|
-
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
705
|
-
{section.eyebrow}
|
|
706
|
-
</p>
|
|
707
|
-
) : null}
|
|
708
|
-
<h2
|
|
709
|
-
id={`${section.id}-title`}
|
|
710
|
-
className="text-balance text-3xl font-semibold tracking-[-0.022em]"
|
|
711
|
-
>
|
|
712
|
-
{section.title}
|
|
713
|
-
</h2>
|
|
714
|
-
{section.description ? (
|
|
715
|
-
<p className="mt-3 text-pretty leading-relaxed text-muted-foreground">
|
|
716
|
-
{section.description}
|
|
717
|
-
</p>
|
|
718
|
-
) : null}
|
|
719
|
-
</div>
|
|
720
|
-
<form action={section.formAction} method="post" className="flex gap-2">
|
|
721
|
-
<label htmlFor={`${section.id}-email`} className="sr-only">
|
|
722
|
-
Correo electrónico
|
|
723
|
-
</label>
|
|
724
|
-
<input
|
|
725
|
-
id={`${section.id}-email`}
|
|
726
|
-
name="email"
|
|
727
|
-
type="email"
|
|
728
|
-
autoComplete="email"
|
|
729
|
-
required
|
|
730
|
-
placeholder={section.emailPlaceholder}
|
|
731
|
-
className="min-h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-3 text-sm text-foreground shadow-sm placeholder:text-muted-foreground"
|
|
732
|
-
/>
|
|
733
|
-
<button
|
|
734
|
-
type="submit"
|
|
735
|
-
className={buttonVariants({ variant: 'default', size: 'lg' })}
|
|
736
|
-
>
|
|
737
|
-
{section.buttonLabel}
|
|
738
|
-
</button>
|
|
739
|
-
</form>
|
|
740
|
-
</div>
|
|
741
|
-
</section>
|
|
742
|
-
)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
export const defaultStorefrontSectionRegistry: SectionRegistry = {
|
|
746
|
-
hero: HeroSectionRenderer,
|
|
747
|
-
'product-grid': ProductGridSectionRenderer,
|
|
748
|
-
'collection-grid': CollectionGridSectionRenderer,
|
|
749
|
-
'promo-banner': PromoBannerSectionRenderer,
|
|
750
|
-
'image-text': ImageTextSectionRenderer,
|
|
751
|
-
'rich-text': RichTextSectionRenderer,
|
|
752
|
-
testimonials: TestimonialsSectionRenderer,
|
|
753
|
-
newsletter: NewsletterSectionRenderer,
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
export function renderStorefrontSection(
|
|
757
|
-
section: StorefrontSection,
|
|
758
|
-
registry: SectionRegistry,
|
|
759
|
-
context: SectionContext,
|
|
760
|
-
): ReactNode {
|
|
761
|
-
if (section.type === 'hero') return registry.hero({ section, context })
|
|
762
|
-
if (section.type === 'product-grid') {
|
|
763
|
-
return registry['product-grid']({ section, context })
|
|
764
|
-
}
|
|
765
|
-
if (section.type === 'collection-grid') {
|
|
766
|
-
return registry['collection-grid']({ section, context })
|
|
767
|
-
}
|
|
768
|
-
if (section.type === 'promo-banner') {
|
|
769
|
-
return registry['promo-banner']({ section, context })
|
|
770
|
-
}
|
|
771
|
-
if (section.type === 'image-text') {
|
|
772
|
-
return registry['image-text']({ section, context })
|
|
773
|
-
}
|
|
774
|
-
if (section.type === 'rich-text') {
|
|
775
|
-
return registry['rich-text']({ section, context })
|
|
776
|
-
}
|
|
777
|
-
if (section.type === 'testimonials') {
|
|
778
|
-
return registry.testimonials({ section, context })
|
|
779
|
-
}
|
|
780
|
-
return registry.newsletter({ section, context })
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function StorefrontChrome({
|
|
784
|
-
definition,
|
|
785
|
-
children,
|
|
786
|
-
}: {
|
|
787
|
-
definition: StorefrontDefinition
|
|
788
|
-
children: ReactNode
|
|
789
|
-
}) {
|
|
790
|
-
return (
|
|
791
|
-
<>
|
|
792
|
-
<a
|
|
793
|
-
href="#storefront-definition-main"
|
|
794
|
-
className="sr-only z-100 rounded-md bg-background px-3 py-2 text-sm focus:not-sr-only focus:fixed focus:left-3 focus:top-3"
|
|
795
|
-
>
|
|
796
|
-
Ir al contenido
|
|
797
|
-
</a>
|
|
798
|
-
{definition.navigation.items.length > 0 ? (
|
|
799
|
-
<header className="border-b bg-background">
|
|
800
|
-
<nav
|
|
801
|
-
className="mx-auto flex min-h-14 w-full max-w-6xl flex-wrap items-center gap-x-4 px-4"
|
|
802
|
-
aria-label="Navegación principal"
|
|
803
|
-
>
|
|
804
|
-
{definition.navigation.items.map((item) => (
|
|
805
|
-
<StorefrontNavigationLink key={item.id} item={item} />
|
|
806
|
-
))}
|
|
807
|
-
</nav>
|
|
808
|
-
</header>
|
|
809
|
-
) : null}
|
|
810
|
-
<main id="storefront-definition-main">{children}</main>
|
|
811
|
-
<footer className="border-t bg-muted/40">
|
|
812
|
-
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-8 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
|
813
|
-
{definition.footer.text ? <p>{definition.footer.text}</p> : null}
|
|
814
|
-
{definition.footer.items.length > 0 ? (
|
|
815
|
-
<nav
|
|
816
|
-
className="flex flex-wrap gap-x-3"
|
|
817
|
-
aria-label="Navegación del pie"
|
|
818
|
-
>
|
|
819
|
-
{definition.footer.items.map((item) => (
|
|
820
|
-
<StorefrontNavigationLink key={item.id} item={item} />
|
|
821
|
-
))}
|
|
822
|
-
</nav>
|
|
823
|
-
) : null}
|
|
824
|
-
</div>
|
|
825
|
-
</footer>
|
|
826
|
-
</>
|
|
827
|
-
)
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
export function DefinitionSections({
|
|
831
|
-
definition,
|
|
832
|
-
data = {},
|
|
833
|
-
locale = 'es-PE',
|
|
834
|
-
currency = 'PEN',
|
|
835
|
-
registry = defaultStorefrontSectionRegistry,
|
|
836
|
-
assets = {},
|
|
837
|
-
}: DefinitionSectionsProps) {
|
|
838
|
-
const context: SectionContext = {
|
|
839
|
-
data,
|
|
840
|
-
locale,
|
|
841
|
-
currency,
|
|
842
|
-
assets,
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
return definition.sections
|
|
846
|
-
.filter((section) => section.enabled)
|
|
847
|
-
.map((section) => (
|
|
848
|
-
<Fragment key={section.id}>
|
|
849
|
-
{renderStorefrontSection(section, registry, context)}
|
|
850
|
-
</Fragment>
|
|
851
|
-
))
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
export function DefinitionShell({
|
|
855
|
-
definition,
|
|
856
|
-
children,
|
|
857
|
-
showChrome = true,
|
|
858
|
-
className,
|
|
859
|
-
}: DefinitionShellProps) {
|
|
860
|
-
const content = showChrome ? (
|
|
861
|
-
<StorefrontChrome definition={definition}>{children}</StorefrontChrome>
|
|
862
|
-
) : (
|
|
863
|
-
children
|
|
864
|
-
)
|
|
865
|
-
|
|
866
|
-
return (
|
|
867
|
-
<div
|
|
868
|
-
className={cn(
|
|
869
|
-
'storefront-definition min-h-full bg-background text-foreground antialiased',
|
|
870
|
-
className,
|
|
871
|
-
)}
|
|
872
|
-
style={themeStyle(definition.theme)}
|
|
873
|
-
data-storefront-definition-version={definition.version}
|
|
874
|
-
data-storefront-definition-revision={definition.revision}
|
|
875
|
-
>
|
|
876
|
-
{content}
|
|
877
|
-
</div>
|
|
878
|
-
)
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
export function DefinitionRenderer({
|
|
882
|
-
definition,
|
|
883
|
-
data,
|
|
884
|
-
locale,
|
|
885
|
-
currency,
|
|
886
|
-
registry,
|
|
887
|
-
showChrome = true,
|
|
888
|
-
className,
|
|
889
|
-
}: DefinitionRendererProps) {
|
|
890
|
-
return (
|
|
891
|
-
<DefinitionShell
|
|
892
|
-
definition={definition}
|
|
893
|
-
showChrome={showChrome}
|
|
894
|
-
className={className}
|
|
895
|
-
>
|
|
896
|
-
<DefinitionSections
|
|
897
|
-
definition={definition}
|
|
898
|
-
data={data}
|
|
899
|
-
locale={locale}
|
|
900
|
-
currency={currency}
|
|
901
|
-
registry={registry}
|
|
902
|
-
/>
|
|
903
|
-
</DefinitionShell>
|
|
904
|
-
)
|
|
905
|
-
}
|