@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.
@@ -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
- }