"use client" import Image from 'next/image' import Link from '../LocalizedLink' import { Clock, Mail, MapPin, MessageCircle, Send, ChevronLeft, ChevronRight, Leaf, Droplet, Sparkles, Gift, CheckCircle, ShieldCheck, Heart, Download, FileText, ArrowRight } from 'lucide-react' import { useState, useRef, useEffect, useMemo } from 'react' import type { Category, DividerSectionPayload, PageSection, Product } from '@homaroma/types' import { getCategoryImage, resolveCategoryMediaUrl } from '@homaroma/types' import ProductCard from '../ProductCard' import ProductSwatchCard from '../ProductSwatchCard' import { localizedFromRecord, resolveCategoryName, resolveLocalized, toRecord } from '../../lib/localization' import { useLang } from '../../hooks/useLang' import { useTranslation } from 'react-i18next' import { customRegistry } from '../../config/customRegistry' import { Swiper, SwiperSlide } from 'swiper/react' import { Autoplay, Pagination, Navigation } from 'swiper/modules' import ScrollReveal from '../ScrollReveal' import PageDivider from '../PageDivider' import 'swiper/css' import 'swiper/css/pagination' import 'swiper/css/navigation' type DynamicPageRendererProps = { sections: PageSection[] categories: Category[] lang?: string // optional override; defaults to i18n.language } function categoryName(category: Category, lang: string) { return resolveCategoryName(category, lang) } function getProducts(payload: Record): Product[] { return Array.isArray(payload.products) ? payload.products as Product[] : [] } function getCategoriesFromPayload(payload: Record): Category[] { return Array.isArray(payload.categories) ? payload.categories as Category[] : [] } function localizedValue(value: unknown, lang: string) { return resolveLocalized(value as never, lang) } function optionalText(value: unknown, lang: string) { if (Array.isArray(value)) return value.map(v => localizedValue(v, lang)).filter(Boolean).join(', ') return localizedValue(value, lang) } function renderAccentTitle(text: string, accentClassName = 'ui-accent-text') { if (!text.includes('[') || !text.includes(']')) return text return text.split('[').map((part, index) => { if (!part.includes(']')) return {part} const [accent, rest] = part.split(']') return ( {accent} {rest} ) }) } export function getSectionPaddingClasses( payload?: Record, fallback: 'normal' | 'none' | 'compact' | 'spacious' = 'normal', ) { const padTop = (typeof payload?.paddingTop === 'string' ? payload.paddingTop : (typeof payload?.sectionPadding === 'string' ? payload.sectionPadding : fallback)) as string const padBottom = (typeof payload?.paddingBottom === 'string' ? payload.paddingBottom : (typeof payload?.sectionPadding === 'string' ? payload.sectionPadding : fallback)) as string const getPt = (p: string) => { switch (p) { case 'none': return 'pt-0' case 'compact': return 'pt-6 md:pt-10' case 'spacious': return 'pt-20 md:pt-28' case 'normal': default: return 'pt-10 md:pt-16' } } const getPb = (p: string) => { switch (p) { case 'none': return 'pb-0' case 'compact': return 'pb-6 md:pb-10' case 'spacious': return 'pb-20 md:pb-28' case 'normal': default: return 'pb-10 md:pb-16' } } return `px-6 ${getPt(padTop)} ${getPb(padBottom)}` } function AnnouncementBar({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const slides = Array.isArray(payload.slides) ? payload.slides : [] const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined const speed = typeof payload.speed === 'number' ? payload.speed : 3000 if (!slides.length) return null return (
1} className="w-full select-none" > {slides.map((slide, index) => ( {localizedFromRecord({ slide }, 'slide', resolvedLang)} ))}
) } const getMobilePositionClasses = (pos: string) => { switch (pos) { case 'top-left': return { wrapper: 'justify-start items-start text-left pt-20 pb-12 pl-6 pr-6', content: 'items-start text-left', actions: 'items-start justify-start' } case 'top-center': return { wrapper: 'justify-start items-center text-center pt-20 pb-12 px-6', content: 'items-center text-center', actions: 'items-center justify-center' } case 'top-right': return { wrapper: 'justify-start items-end text-right pt-20 pb-12 pr-6 pl-6', content: 'items-end text-right', actions: 'items-end justify-end' } case 'center-left': return { wrapper: 'justify-center items-start text-left py-16 pl-6 pr-6', content: 'items-start text-left', actions: 'items-start justify-start' } case 'center-right': return { wrapper: 'justify-center items-end text-right py-16 pr-6 pl-6', content: 'items-end text-right', actions: 'items-end justify-end' } case 'bottom-left': return { wrapper: 'justify-end items-start text-left pt-12 pb-16 pl-6 pr-6', content: 'items-start text-left', actions: 'items-start justify-start' } case 'bottom-center': return { wrapper: 'justify-end items-center text-center pt-12 pb-16 px-6', content: 'items-center text-center', actions: 'items-center justify-center' } case 'bottom-right': return { wrapper: 'justify-end items-end text-right pt-12 pb-16 pr-6 pl-6', content: 'items-end text-right', actions: 'items-end justify-end' } case 'center': default: return { wrapper: 'justify-center items-center text-center py-16 px-6', content: 'items-center text-center', actions: 'items-center justify-center' } } } const getDesktopPositionClasses = (pos: string) => { switch (pos) { case 'top-left': return { wrapper: 'md:justify-start md:items-start md:text-left md:pt-32 md:pb-12 md:pl-20 md:pr-8', content: 'md:items-start md:text-left', actions: 'md:items-start md:justify-start' } case 'top-center': return { wrapper: 'md:justify-start md:items-center md:text-center md:pt-32 md:pb-12 md:px-8', content: 'md:items-center md:text-center', actions: 'md:items-center md:justify-center' } case 'top-right': return { wrapper: 'md:justify-start md:items-end md:text-right md:pt-32 md:pb-12 md:pr-20 md:pl-8', content: 'md:items-end md:text-right', actions: 'md:items-end md:justify-end' } case 'center-left': return { wrapper: 'md:justify-center md:items-start md:text-left md:py-16 md:pl-20 md:pr-8', content: 'md:items-start md:text-left', actions: 'md:items-start md:justify-start' } case 'center-right': return { wrapper: 'md:justify-center md:items-end md:text-right md:py-16 md:pr-20 md:pl-8', content: 'md:items-end md:text-right', actions: 'md:items-end md:justify-end' } case 'bottom-left': return { wrapper: 'md:justify-end md:items-start md:text-left md:pt-12 md:pb-24 md:pl-20 md:pr-8', content: 'md:items-start md:text-left', actions: 'md:items-start md:justify-start' } case 'bottom-center': return { wrapper: 'md:justify-end md:items-center md:text-center md:pt-12 md:pb-24 md:px-8', content: 'md:items-center md:text-center', actions: 'md:items-center md:justify-center' } case 'bottom-right': return { wrapper: 'md:justify-end md:items-end md:text-right md:pt-12 md:pb-24 md:pr-20 md:pl-8', content: 'md:items-end md:text-right', actions: 'md:items-end md:justify-end' } case 'center': default: return { wrapper: 'md:justify-center md:items-center md:text-center md:py-16 md:px-8', content: 'md:items-center md:text-center', actions: 'md:items-center md:justify-center' } } } function getHeroPositionClasses(mobilePos: string, desktopPos: string) { const mClasses = getMobilePositionClasses(mobilePos) const dClasses = getDesktopPositionClasses(desktopPos) return { wrapperClasses: `${mClasses.wrapper} ${dClasses.wrapper}`, contentClasses: `${mClasses.content} ${dClasses.content}`, actionsClasses: `${mClasses.actions} ${dClasses.actions}`, } } function getHeroContainerWidthClasses(contentWidth?: string) { switch (contentWidth) { case 'full': return 'w-full max-w-none px-6 md:px-12 lg:px-16' case 'compact': return 'w-full max-w-4xl mx-auto px-6' case 'standard': default: return 'w-full max-w-7xl mx-auto px-6 md:px-10 lg:px-12' } } export function getHeroTitleSizeClass(titleSize?: unknown): string { switch (titleSize) { case 'sm': return 'ui-hero-title-sm' case 'md': return 'ui-hero-title-md' case 'lg': return 'ui-hero-title-lg' case 'xl': default: return 'ui-hero-title-xl' } } export function getTailwindHeroTitleClass(titleSize?: unknown, defaultSize = 'xl'): string { const size = (titleSize || defaultSize) as string switch (size) { case 'sm': return 'text-2xl sm:text-3xl md:text-4xl' case 'md': return 'text-3xl sm:text-4xl md:text-5xl' case 'lg': return 'text-4xl sm:text-5xl md:text-6xl' case 'xl': default: return 'text-4xl sm:text-5xl md:text-6xl lg:text-7xl' } } function HeroSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const rawTagline = localizedFromRecord(payload, 'tagline', resolvedLang) const rawTitle = localizedFromRecord(payload, 'title', resolvedLang) const rawDescription = localizedFromRecord(payload, 'description', resolvedLang) const rawButtonText = localizedFromRecord(payload, 'buttonText', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png' const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop' const desktopPosition = typeof payload.buttonPosition === 'string' ? payload.buttonPosition : 'center' const mobilePosition = typeof payload.mobileButtonPosition === 'string' ? payload.mobileButtonPosition : desktopPosition const contentWidth = typeof payload.contentWidth === 'string' ? payload.contentWidth : 'standard' const textColorScheme = typeof payload.textColorScheme === 'string' ? payload.textColorScheme : 'light' const isDarkText = textColorScheme === 'dark' const titleSize = typeof payload.titleSize === 'string' ? payload.titleSize : 'xl' const titleSizeClass = getHeroTitleSizeClass(titleSize) const textWrapperWidthClass = titleSize === 'sm' || contentWidth === 'compact' ? 'max-w-xl' : titleSize === 'md' ? 'max-w-2xl' : 'max-w-3xl' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined // If fields are explicitly defined as empty strings, respect them as empty const hasTagline = payload.tagline !== undefined && payload.tagline !== null ? !!rawTagline?.trim() : true const tagline = hasTagline ? (rawTagline || 'VIBRANT ELEGANCE') : '' const hasTitle = payload.title !== undefined && payload.title !== null ? !!rawTitle?.trim() : true const title = hasTitle ? (rawTitle || 'New Scent [Sanctuary]') : '' const hasDescription = payload.description !== undefined && payload.description !== null ? !!rawDescription?.trim() : false const description = hasDescription ? rawDescription : '' const buttonText = rawButtonText !== undefined && rawButtonText !== null ? rawButtonText.trim() : 'Explore Collection' const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition) const containerClasses = getHeroContainerWidthClasses(contentWidth) const overlayStyle = typeof payload.overlayStyle === 'string' ? payload.overlayStyle : 'gradient' const overlayOpacity = typeof payload.overlayOpacity === 'number' ? payload.overlayOpacity : 45 const renderOverlay = () => { if (overlayStyle === 'none' || overlayOpacity === 0) { return null } const opacityRatio = overlayOpacity / 100 const rgb = isDarkText ? '255, 255, 255' : '0, 0, 0' if (overlayStyle === 'solid') { return (
) } // Gradient style (default): smooth vertical gradient scaled by opacity const bottomAlpha = Math.min(0.95, opacityRatio * 1.85) const midAlpha = opacityRatio const topAlpha = Math.max(0, opacityRatio * 0.65) return (
) } const aspectRatio = typeof payload.aspectRatio === 'string' ? payload.aspectRatio : undefined const mobileAspectRatio = typeof payload.mobileAspectRatio === 'string' ? payload.mobileAspectRatio : undefined const mobileImageUrl = typeof payload.mobileImageUrl === 'string' && payload.mobileImageUrl ? payload.mobileImageUrl : imageUrl const isFullScreen = !aspectRatio || aspectRatio === 'full_screen' const getAspectClass = () => { switch (aspectRatio) { case '21/6': return 'md:aspect-[21/6]' case '21/7': return 'md:aspect-[21/7]' case '21/9': return 'md:aspect-[21/9]' case '16/5': return 'md:aspect-[16/5]' case '16/6': return 'md:aspect-[16/6]' case '4/1': return 'md:aspect-[4/1]' case '5/1': return 'md:aspect-[5/1]' case 'full_screen': return 'md:h-[90vh] md:aspect-[21/9]' default: return 'md:aspect-[21/7]' } } const getMobileAspectClass = () => { switch (mobileAspectRatio) { case '16/9': return 'aspect-[16/9]' case '4/3': return 'aspect-[4/3]' case '1/1': return 'aspect-[1/1]' case '21/9': return 'aspect-[21/9]' case 'full_screen': return 'min-h-[85vh]' default: return 'aspect-[16/9]' } } const sectionHeightClass = isFullScreen ? 'min-h-[85vh] sm:min-h-[90vh] md:h-[90vh] md:aspect-[21/9]' : `${getMobileAspectClass()} ${getAspectClass()}` const innerHeightClass = isFullScreen ? 'min-h-[85vh] sm:min-h-[90vh] md:min-h-full' : 'h-full justify-center' return (
{/* Desktop Image */}
{title
{/* Mobile Image (if provided) */} {mobileImageUrl !== imageUrl && (
{title
)} {/* Dynamic Dark / Light Gradient / Solid Overlay */} {renderOverlay()}
{tagline && (

{tagline}

)} {title && (

{renderAccentTitle(title, isDarkText ? 'text-brand-primary font-medium' : 'ui-accent-text font-medium')}

)} {description && (

{description}

)}
) } function HeroCarouselSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const slides = Array.isArray(payload.slides) ? payload.slides : [] const autoplay = typeof payload.autoplay === 'boolean' ? payload.autoplay : true const interval = typeof payload.interval === 'number' ? payload.interval : 4500 const showArrows = typeof payload.showArrows === 'boolean' ? payload.showArrows : true const showDots = typeof payload.showDots === 'boolean' ? payload.showDots : true const aspectRatio = typeof payload.aspectRatio === 'string' ? payload.aspectRatio : '21/7' const mobileAspectRatio = typeof payload.mobileAspectRatio === 'string' ? payload.mobileAspectRatio : '16/9' const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined if (!slides.length) return null const getAspectClass = () => { switch (aspectRatio) { case '21/6': return 'md:aspect-[21/6]' case '21/7': return 'md:aspect-[21/7]' case '21/9': return 'md:aspect-[21/9]' case '16/5': return 'md:aspect-[16/5]' case '16/6': return 'md:aspect-[16/6]' case '4/1': return 'md:aspect-[4/1]' case '5/1': return 'md:aspect-[5/1]' default: return 'md:aspect-[21/7]' } } const getMobileAspectClass = () => { switch (mobileAspectRatio) { case '16/9': return 'aspect-[16/9]' case '4/3': return 'aspect-[4/3]' case '1/1': return 'aspect-[1/1]' case '21/9': return 'aspect-[21/9]' default: return 'aspect-[16/9]' } } return (
1} autoplay={autoplay ? { delay: interval, disableOnInteraction: false } : false} pagination={showDots ? { clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-brand-primary' } : false} navigation={showArrows} grabCursor={true} className="w-full select-none [&_.swiper-button-next]:text-white [&_.swiper-button-prev]:text-white [&_.swiper-button-next]:after:text-sm md:[&_.swiper-button-next]:after:text-base [&_.swiper-button-prev]:after:text-sm md:[&_.swiper-button-prev]:after:text-base [&_.swiper-button-next]:w-8 md:[&_.swiper-button-next]:w-10 [&_.swiper-button-next]:h-8 md:[&_.swiper-button-next]:h-10 [&_.swiper-button-prev]:w-8 md:[&_.swiper-button-prev]:w-10 [&_.swiper-button-prev]:h-8 md:[&_.swiper-button-prev]:h-10 [&_.swiper-button-next]:bg-black/35 [&_.swiper-button-prev]:bg-black/35 [&_.swiper-button-next]:backdrop-blur-sm [&_.swiper-button-prev]:backdrop-blur-sm [&_.swiper-button-next]:rounded-full [&_.swiper-button-prev]:rounded-full [&_.swiper-button-next]:hover:bg-black/60 [&_.swiper-button-prev]:hover:bg-black/60 [&_.swiper-pagination-bullet]:bg-white/60 [&_.swiper-pagination-bullet-active]:!bg-brand-primary [&_.swiper-pagination]:!bottom-2.5 md:[&_.swiper-pagination]:!bottom-4" > {slides.map((slide, index) => { const title = localizedFromRecord(slide, 'title', resolvedLang) const subtitle = localizedFromRecord(slide, 'subtitle', resolvedLang) const alt = localizedFromRecord(slide, 'alt', resolvedLang) || title || `Slide ${index + 1}` const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang) const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : '' const mobileImageUrl = typeof slide.mobileImageUrl === 'string' && slide.mobileImageUrl ? slide.mobileImageUrl : imageUrl const linkUrl = typeof slide.linkUrl === 'string' ? slide.linkUrl : '' const focalPoint = typeof slide.focalPoint === 'object' && slide.focalPoint ? (slide.focalPoint as { x: string, y: string }) : undefined if (!imageUrl) return null const slideContent = (
{/* Desktop Image */}
{alt}
{/* Mobile Image */}
{alt}
{/* Optional Text Overlay */} {(title || subtitle || buttonText) && (() => { const slideDesktopPos = typeof slide.buttonPosition === 'string' ? slide.buttonPosition : 'bottom-left' const slideMobilePos = typeof slide.mobileButtonPosition === 'string' ? slide.mobileButtonPosition : slideDesktopPos const slideWidth = typeof slide.contentWidth === 'string' ? slide.contentWidth : 'standard' const slideColor = typeof slide.textColorScheme === 'string' ? slide.textColorScheme : 'light' const isDarkSlide = slideColor === 'dark' const { wrapperClasses: sWrap, contentClasses: sContent, actionsClasses: sActions } = getHeroPositionClasses(slideMobilePos, slideDesktopPos) const sContainer = getHeroContainerWidthClasses(slideWidth) return (
{subtitle &&

{subtitle}

} {title &&

{renderAccentTitle(title)}

} {buttonText && (
{buttonText}
)}
) })()}
) return ( {linkUrl ? ( {slideContent} ) : ( slideContent )} ) })}
) } function HeroVideoSection({ payload, lang }: { payload: Record, 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 focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined 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]' } } const desktopPosition = typeof payload.buttonPosition === 'string' ? payload.buttonPosition : 'center' const mobilePosition = typeof payload.mobileButtonPosition === 'string' ? payload.mobileButtonPosition : desktopPosition const contentWidth = typeof payload.contentWidth === 'string' ? payload.contentWidth : 'standard' const textColorScheme = typeof payload.textColorScheme === 'string' ? payload.textColorScheme : 'light' const isDarkText = textColorScheme === 'dark' const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition) const containerClasses = getHeroContainerWidthClasses(contentWidth) return (
{videoUrl ? ( ) : posterImageUrl ? ( Hero Poster ) : null}
{tagline && (

{tagline}

)} {title && (

{renderAccentTitle(title, isDarkText ? 'text-brand-primary font-medium' : undefined)}

)} {description && (

{description}

)} {(buttonText || secondaryButtonText) && (
{buttonText && buttonLink && ( {buttonText} )} {secondaryButtonText && secondaryButtonLink && ( {secondaryButtonText} )}
)}
) } const ICON_MAP: Record = { leaf: Leaf, droplet: Droplet, sparkles: Sparkles, gift: Gift, check: CheckCircle, shield: ShieldCheck, heart: Heart, } function UspBarSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const items = Array.isArray(payload.items) ? payload.items : [] const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : 'var(--surface-page)' if (items.length === 0) return null return (
{items.slice(0, 4).map((item: any, idx: number) => { const Icon = ICON_MAP[item.icon] || CheckCircle return (

{localizedFromRecord(item, 'title', resolvedLang)}

{item.subtitle && (

{localizedFromRecord(item, 'subtitle', resolvedLang)}

)}
) })}
) } function AnnouncementBarMinimal({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const slides = Array.isArray(payload.slides) ? payload.slides : [] const backgroundColor = typeof payload.backgroundColor === 'string' && payload.backgroundColor ? payload.backgroundColor : '#0F1713' const textColor = typeof payload.textColor === 'string' && payload.textColor ? payload.textColor : '#DCE6E0' const speed = typeof payload.speed === 'number' ? payload.speed : 4000 if (!slides.length) return null return (
1} className="w-full" > {slides.map((slide, index) => ( {localizedFromRecord({ slide }, 'slide', resolvedLang)} ))}
) } function HeroMinimalSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const tagline = localizedFromRecord(payload, 'tagline', resolvedLang) const title = localizedFromRecord(payload, 'title', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png' const overlayOpacity = typeof payload.overlayOpacity === 'number' ? payload.overlayOpacity : 20 const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'center' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined const alignClass = align === 'left' ? 'text-left items-start' : align === 'right' ? 'text-right items-end ml-auto' : 'text-center items-center mx-auto' return (
{title
{tagline && ( {tagline} )} {title && (

{renderAccentTitle(title, 'text-brand-accent font-medium')}

)}
) } function HeroSplitMinimalSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/oil_ozone.png' const imagePosition = payload.imagePosition === 'left' ? 'left' : 'right' const backgroundColor = typeof payload.backgroundColor === 'string' && payload.backgroundColor ? payload.backgroundColor : undefined const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined return (
{title && (

{renderAccentTitle(title, 'text-brand-primary font-normal')}

)} {subtitle && (

{subtitle}

)}
{title
) } function HeroVideoMinimalSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const tagline = localizedFromRecord(payload, 'tagline', resolvedLang) const title = localizedFromRecord(payload, 'title', resolvedLang) 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 : 25 const height = typeof payload.height === 'string' ? payload.height : 'large' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined const getHeightClass = (h: string) => { switch (h) { case 'full': return 'h-screen' case 'medium': return 'h-[55vh] min-h-[400px]' case 'large': default: return 'h-[80vh] min-h-[500px]' } } return (
{videoUrl ? ( ) : posterImageUrl ? ( Hero Poster ) : null}
{tagline && (

{tagline}

)} {title && (

{renderAccentTitle(title)}

)}
) } function BannerMinimalSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' && payload.imageUrl ? payload.imageUrl : undefined const linkUrl = typeof payload.linkUrl === 'string' ? payload.linkUrl : '' const backgroundColor = typeof payload.backgroundColor === 'string' && payload.backgroundColor ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' && payload.textColor ? payload.textColor : undefined const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined const inner = (
{imageUrl && ( <> {title
)}
{title && (

{renderAccentTitle(title)}

)} {subtitle && (

{subtitle}

)}
) return (
{linkUrl ? {inner} : inner}
) } function UspMinimalSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const items = Array.isArray(payload.items) ? payload.items : [] const separator = typeof payload.separator === 'string' ? payload.separator : 'dot' const backgroundColor = typeof payload.backgroundColor === 'string' && payload.backgroundColor ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' && payload.textColor ? payload.textColor : undefined if (items.length === 0) return null const sepSymbol = separator === 'slash' ? '/' : separator === 'line' ? '|' : '·' return (
{items.map((item: any, idx: number) => ( {idx > 0 && {sepSymbol}} {localizedFromRecord({ item }, 'item', resolvedLang)} ))}
) } function HeroV2Section({ payload, lang }: { payload: Record, 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 primaryBtnText = localizedFromRecord(payload, 'primaryButtonText', resolvedLang) const primaryBtnLink = typeof payload.primaryButtonLink === 'string' ? payload.primaryButtonLink : undefined const secondaryBtnText = localizedFromRecord(payload, 'secondaryButtonText', resolvedLang) const secondaryBtnLink = typeof payload.secondaryButtonLink === 'string' ? payload.secondaryButtonLink : undefined const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png' const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left' const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined // Alignment classes const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start' return (
{title
{tagline && (
{showOrnament && align !== 'right' &&
} {tagline} {showOrnament && align !== 'left' &&
}
)}

{renderAccentTitle(title || '', 'text-brand-primary')}

{showOrnament && (
)} {description && (

{description}

)}
{primaryBtnText && primaryBtnLink && ( {primaryBtnText} )} {secondaryBtnText && secondaryBtnLink && ( {secondaryBtnText} )}
) } function CategoriesSection({ payload, categories, lang, }: { payload: Record categories: Category[] lang: string }) { const i18nLang = useLang() const { t } = useTranslation() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Our Collections' const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const categoryIds = Array.isArray(payload.categoryIds) ? payload.categoryIds.map(Number) : [] const payloadCategories = getCategoriesFromPayload(payload) const sourceCategories = payloadCategories.length ? payloadCategories : categories const displayed = categoryIds.length ? sourceCategories.filter((category) => categoryIds.includes(category.id)) : sourceCategories return (

{title}

{subtitle &&

{subtitle}

}
{displayed.map((category) => { const name = categoryName(category, resolvedLang) return ( {name}
{name} {category._count && ( {category._count.products} {t('dynamic.products', { lng: resolvedLang })} )}
) })}
) } function ProductsSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Featured Picks' const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const layout = typeof payload.layout === 'string' ? payload.layout : 'grid' const cardStyle = typeof payload.cardStyle === 'string' ? payload.cardStyle : 'standard' const viewAllLink = typeof payload.viewAllLink === 'string' && payload.viewAllLink.trim() ? payload.viewAllLink.trim() : '/shop' const viewAllText = localizedFromRecord(payload, 'viewAllText', resolvedLang) || t('dynamic.viewAll', { lng: resolvedLang }) || 'View All' const badgeText = localizedFromRecord(payload, 'badgeText', resolvedLang) const products = getProducts(payload) const carouselRef = useRef(null) const scrollLeft = () => carouselRef.current?.scrollBy({ left: -300, behavior: 'smooth' }) const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' }) const isMinimal = cardStyle === 'minimal' || cardStyle === 'swatch' || cardStyle === 'minimal_swatch' const containerWidth = typeof payload.containerWidth === 'string' ? payload.containerWidth : 'standard' const cardSize = typeof payload.cardSize === 'string' ? payload.cardSize : 'medium' const getContainerWidthClass = (width: string) => { switch (width) { case 'full': return 'w-full max-w-none' case 'wide': return 'w-full max-w-[1600px] mx-auto' case 'standard': default: return 'w-full max-w-7xl mx-auto' } } const getCarouselCardWidth = () => { switch (cardSize) { case 'compact': return 'w-[46vw] sm:w-[calc((100%-32px)/3)] md:w-[calc((100%-60px)/4)] lg:w-[calc((100%-100px)/6)] flex-none snap-start' case 'large': return 'w-[78vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-20px)/2)] lg:w-[calc((100%-40px)/3)] flex-none snap-start' case 'medium': default: return 'w-[68vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-40px)/3)] lg:w-[calc((100%-60px)/4)] flex-none snap-start' } } const getGridColsClass = () => { switch (cardSize) { case 'compact': return 'grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6' case 'large': return 'grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3' case 'medium': default: return 'grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4' } } return (

{title}

{subtitle && (

{subtitle}

)}
{viewAllText}
{products.length ? ( layout === 'carousel' ? (
{products.map((product) => (
))}
) : (
{products.map((product) => ( ))}
) ) : (

{t('dynamic.noProducts', { lng: resolvedLang })}

)}
) } function BannerSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const description = localizedFromRecord(payload, 'description', resolvedLang) const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png' const linkUrl = typeof payload.linkUrl === 'string' ? payload.linkUrl : '' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined const desktopPosition = typeof payload.buttonPosition === 'string' ? payload.buttonPosition : 'center-left' const mobilePosition = typeof payload.mobileButtonPosition === 'string' ? payload.mobileButtonPosition : desktopPosition const contentWidth = typeof payload.contentWidth === 'string' ? payload.contentWidth : 'standard' const textColorScheme = typeof payload.textColorScheme === 'string' ? payload.textColorScheme : 'light' const isDarkText = textColorScheme === 'dark' const { wrapperClasses: bWrap, contentClasses: bContent, actionsClasses: bActions } = getHeroPositionClasses(mobilePosition, desktopPosition) const shellWidthClass = contentWidth === 'full' ? '!max-w-none !rounded-none md:!rounded-3xl mx-4 md:mx-8' : contentWidth === 'compact' ? '!max-w-4xl' : '!max-w-7xl' const content = (
{title
{title && (

{title}

)} {description &&

{description}

} {buttonText && (
{buttonText}
)}
) return
{linkUrl ? {content} : content}
} function TextSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const content = localizedFromRecord(payload, 'content', resolvedLang) const alignClass = payload.textAlign === 'center' ? 'text-center' : payload.textAlign === 'right' ? 'text-right' : 'text-left' return (
{title &&

{title}

} {content &&
}
) } function ArticleHeaderSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const category = localizedFromRecord(payload, 'category', resolvedLang) const author = localizedFromRecord(payload, 'author', resolvedLang) const imageAlt = localizedFromRecord(payload, 'imageAlt', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '' const publishDate = typeof payload.publishDate === 'string' ? payload.publishDate : '' if (!title) return null return (
{category &&

{category}

}

{title}

{subtitle &&

{subtitle}

} {(author || publishDate) && (

{[author, publishDate].filter(Boolean).join(' · ')}

)}
{imageUrl && (
{imageAlt
)}
) } function ArticleMediaSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const heading = localizedFromRecord(payload, 'heading', resolvedLang) const body = localizedFromRecord(payload, 'body', resolvedLang) const imageAlt = localizedFromRecord(payload, 'imageAlt', resolvedLang) const imageCaption = localizedFromRecord(payload, 'imageCaption', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '' const imagePosition = payload.imagePosition === 'left' || payload.imagePosition === 'top' ? payload.imagePosition : 'right' const widthClass = payload.width === 'narrow' ? 'max-w-3xl' : payload.width === 'wide' ? 'max-w-7xl' : 'max-w-6xl' const backgroundClass = payload.background === 'muted' ? 'bg-surface-card' : 'bg-surface-page' if (!body) return null const copy = (
{heading && (

{heading}

)}

{body}

) const media = imageUrl ? (
{imageAlt
{imageCaption && (
{imageCaption}
)}
) : null return (
{!media ? copy : imagePosition === 'top' ? (
{media} {copy}
) : (
{media}
{copy}
)}
) } function ArticleGallerySection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const images = Array.isArray(payload.images) ? payload.images : [] const featured = payload.layout === 'featured' if (images.length < 2) return null return (
{title &&

{title}

}
{images.map((image, index) => { const item = toRecord(image) const imageUrl = typeof item.imageUrl === 'string' ? item.imageUrl : '' const alt = localizedFromRecord(item, 'alt', resolvedLang) const caption = localizedFromRecord(item, 'caption', resolvedLang) if (!imageUrl) return null return (
{alt}
{caption &&
{caption}
}
) })}
) } function ArticleFactsSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const items = Array.isArray(payload.items) ? payload.items : [] if (items.length === 0) return null return (
{title &&

{title}

}
{items.map((item, index) => { const record = toRecord(item) const itemTitle = localizedFromRecord(record, 'title', resolvedLang) const description = localizedFromRecord(record, 'description', resolvedLang) return (
{itemTitle}
{description &&
{description}
}
) })}
) } function getVideoEmbedUrl(videoUrl: string) { try { const url = new URL(videoUrl) if (url.hostname === 'youtu.be') return `https://www.youtube.com/embed/${url.pathname.slice(1)}` if (url.hostname === 'youtube.com' || url.hostname.endsWith('.youtube.com')) { const id = url.searchParams.get('v') return id ? `https://www.youtube.com/embed/${id}` : '' } if (url.hostname === 'vimeo.com' || url.hostname.endsWith('.vimeo.com')) { const id = url.pathname.split('/').filter(Boolean).pop() return id ? `https://player.vimeo.com/video/${id}` : '' } } catch { return '' } return '' } function ArticleVideoSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const caption = localizedFromRecord(payload, 'caption', resolvedLang) const videoUrl = typeof payload.videoUrl === 'string' ? payload.videoUrl : '' const posterUrl = typeof payload.posterUrl === 'string' ? payload.posterUrl : undefined const embedUrl = getVideoEmbedUrl(videoUrl) const aspectClass = payload.aspectRatio === '4:3' ? 'aspect-[4/3]' : payload.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-video' if (!videoUrl) return null return (
{title &&

{title}

}
{embedUrl ? (