This commit is contained in:
Your Name committed 2026-07-28 00:13:57 -04:00
1 parent 406cad801b
commit a2da091b15
21 files changed
+2454 -63

No files matched your search

@@ -155,6 +155,95 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
)
}
function HeroVideoSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const tagline = localizedFromRecord(payload, 'tagline', resolvedLang)
const title = localizedFromRecord(payload, 'title', resolvedLang)
const description = localizedFromRecord(payload, 'description', resolvedLang)
const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang)
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : ''
const secondaryButtonText = localizedFromRecord(payload, 'secondaryButtonText', resolvedLang)
const secondaryButtonLink = typeof payload.secondaryButtonLink === 'string' ? payload.secondaryButtonLink : ''
const videoUrl = typeof payload.videoUrl === 'string' ? payload.videoUrl : ''
const posterImageUrl = typeof payload.posterImageUrl === 'string' ? payload.posterImageUrl : undefined
const overlayOpacity = typeof payload.overlayOpacity === 'number' ? payload.overlayOpacity : 40
const height = typeof payload.height === 'string' ? payload.height : 'large'
const getHeightClass = (h: string) => {
switch (h) {
case 'full':
return 'h-screen'
case 'medium':
return 'h-[60vh] min-h-[400px]'
case 'large':
default:
return 'h-[85vh] min-h-[550px]'
}
}
return (
<section className={`relative w-full ${getHeightClass(height)} overflow-hidden flex items-center justify-center bg-[#0B0F17] text-white`}>
{videoUrl ? (
<video
autoPlay={payload.autoplay !== false}
muted={payload.muted !== false}
loop={payload.loop !== false}
playsInline
poster={posterImageUrl}
className="absolute inset-0 w-full h-full object-cover z-0"
>
<source src={videoUrl} type="video/mp4" />
<source src={videoUrl} type="video/webm" />
{posterImageUrl && <Image src={posterImageUrl} alt="Hero fallback" fill unoptimized className="object-cover" />}
</video>
) : posterImageUrl ? (
<Image src={posterImageUrl} alt="Hero Poster" fill unoptimized className="object-cover z-0" />
) : null}
<div className="absolute inset-0 z-10 bg-black transition-opacity" style={{ opacity: overlayOpacity / 100 }}></div>
<div className="relative z-20 mx-auto px-6 py-16 w-full max-w-5xl text-center space-y-6 flex flex-col items-center justify-center">
{tagline && (
<p className="text-xs md:text-sm font-semibold uppercase tracking-[0.35em] text-[#EAEFEA] drop-shadow-md">
{tagline}
</p>
)}
{title && (
<h1 className="font-serif text-4xl md:text-6xl lg:text-7xl font-light leading-tight tracking-wide drop-shadow-lg text-white">
{renderAccentTitle(title)}
</h1>
)}
{description && (
<p className="max-w-2xl text-sm md:text-lg font-light leading-relaxed opacity-90 drop-shadow">
{description}
</p>
)}
{(buttonText || secondaryButtonText) && (
<div className="flex flex-wrap items-center justify-center gap-4 pt-4">
{buttonText && buttonLink && (
<Link
href={buttonLink}
className="px-8 py-3.5 bg-white text-neutral-900 font-medium text-xs uppercase tracking-widest hover:bg-brand-primary hover:text-white transition-all shadow-lg rounded-sm"
>
{buttonText}
</Link>
)}
{secondaryButtonText && secondaryButtonLink && (
<Link
href={secondaryButtonLink}
className="px-8 py-3.5 bg-transparent border border-white/40 text-white font-medium text-xs uppercase tracking-widest hover:bg-white/10 hover:border-white transition-all rounded-sm backdrop-blur-sm"
>
{secondaryButtonText}
</Link>
)}
</div>
)}
</div>
</section>
)
}
const ICON_MAP: Record<string, React.ElementType> = {
leaf: Leaf,
droplet: Droplet,
@@ -944,6 +1033,110 @@ function EditorialTileSection({ payload, lang }: { payload: Record<string, unkno
)
}
function FeaturedTilesSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
const columns = typeof payload.columns === 'number' ? payload.columns : 2
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
const tiles = Array.isArray(payload.tiles) ? payload.tiles.map(toRecord) : []
const getColsClass = (cols: number) => {
switch (cols) {
case 3:
return 'grid-cols-1 md:grid-cols-3'
case 4:
return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
case 2:
default:
return 'grid-cols-1 md:grid-cols-2'
}
}
const getAspectClass = (ratio?: string) => {
switch (ratio) {
case 'portrait':
return 'aspect-[3/4]'
case 'tall':
return 'aspect-[2/3]'
case 'auto':
return 'aspect-auto'
case 'square':
default:
return 'aspect-square'
}
}
return (
<section className="overflow-hidden bg-surface-page ui-section-band" style={{ backgroundColor, color: textColor }}>
<div className="mx-auto max-w-7xl space-y-12">
{(title || subtitle) && (
<div className="mx-auto max-w-2xl text-center space-y-3">
{title && (
<h2 className="text-3xl font-semibold leading-tight text-text-main md:text-5xl" style={{ color: textColor }}>
{renderAccentTitle(title)}
</h2>
)}
{subtitle && <p className="text-base leading-relaxed text-muted-text md:text-lg" style={{ color: textColor }}>{subtitle}</p>}
<div className="mx-auto h-px w-14 bg-brand-primary" />
</div>
)}
<div className={`grid gap-8 md:gap-12 ${getColsClass(columns)}`}>
{tiles.map((tile, idx) => {
const tileTitle = localizedFromRecord(tile, 'title', resolvedLang)
const tileDesc = localizedFromRecord(tile, 'description', resolvedLang)
const buttonText = localizedFromRecord(tile, 'buttonText', resolvedLang)
const buttonLink = typeof tile.buttonLink === 'string' ? tile.buttonLink : ''
const imageUrl = typeof tile.imageUrl === 'string' ? tile.imageUrl : ''
const imageAspectRatio = typeof tile.imageAspectRatio === 'string' ? tile.imageAspectRatio : 'square'
return (
<div key={idx} className="group flex flex-col items-center text-center space-y-5">
{imageUrl && (
<div className={`relative ${getAspectClass(imageAspectRatio)} w-full overflow-hidden rounded-3xl shadow-card`}>
<Image
src={imageUrl}
alt={tileTitle || 'Campaign Card'}
fill
unoptimized
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
/>
</div>
)}
<div className="space-y-3">
{tileTitle && (
<h3 className="text-xl font-semibold leading-snug text-text-main md:text-2xl" style={{ color: textColor }}>
{tileTitle}
</h3>
)}
{tileDesc && (
<p className="text-sm leading-relaxed text-muted-text md:text-base" style={{ color: textColor }}>
{tileDesc}
</p>
)}
{buttonText && buttonLink && (
<div className="pt-2">
<Link
href={buttonLink}
className="inline-flex rounded-full border border-current px-8 py-3 text-xs font-bold uppercase tracking-[0.22em] transition hover:bg-brand-primary hover:text-surface-card"
>
{buttonText}
</Link>
</div>
)}
</div>
</div>
)
})}
</div>
</div>
</section>
)
}
function EditorialCarouselSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
@@ -1415,12 +1608,30 @@ function ContactFormSection({ payload, lang }: { payload: Record<string, unknown
)
}
import { useEffect } from 'react'
import ScrollReveal from '../ScrollReveal'
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
const i18nLang = useLang()
// Use i18n.language reactively — switches immediately when user changes language
const lang = langProp ?? i18nLang
useEffect(() => {
const HERO_TYPES = new Set(['HERO', 'HERO_V2', 'HERO_VIDEO'])
const firstActiveSection = sections.find(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR')
const isHero = Boolean(firstActiveSection && HERO_TYPES.has(firstActiveSection.type.toUpperCase()))
if (typeof document !== 'undefined') {
document.documentElement.dataset.heroFirst = isHero ? 'true' : 'false'
}
return () => {
if (typeof document !== 'undefined') {
document.documentElement.dataset.heroFirst = 'false'
}
}
}, [sections])
return (
<div className="flex w-full flex-col">
{sections.map((section) => {
@@ -1444,6 +1655,8 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
<HeroV2Section payload={payload} lang={lang} />
</ScrollReveal>
)
case 'HERO_VIDEO':
return <HeroVideoSection key={section.id} payload={payload} lang={lang} />
case 'USP_BAR':
return (
<ScrollReveal key={section.id}>
@@ -1492,6 +1705,12 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
<EditorialTileSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'FEATURED_TILES':
return (
<ScrollReveal key={section.id}>
<FeaturedTilesSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'EDITORIAL_CAROUSEL':
return (
<ScrollReveal key={section.id}>