"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 } from 'lucide-react' import { useState, useRef } from 'react' import type { Category, PageSection, Product } from '@homaroma/types' import { getCategoryImage } from '@homaroma/types' import ProductCard from '../ProductCard' 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 '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} ) }) } 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)} ))}
) } function HeroSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() 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 imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/images/hero.png' const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined return (
{title
{tagline && (

{tagline}

)}

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

{description && (

{description}

)}
) } 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 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 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' }) return (

{title}

{subtitle &&

{subtitle}

}
{t('dynamic.viewAll', { lng: resolvedLang })}
{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 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 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 NewsletterSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Stay in touch' const description = localizedFromRecord(payload, 'description', resolvedLang) const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang) || 'Subscribe' const placeholder = typeof payload.placeholderText === 'string' ? payload.placeholderText : 'you@example.com' return (

{title}

{description &&

{description}

}
) } function EditorialTileSection({ 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 buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '' const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '' const imagePosition = payload.imagePosition === 'right' ? 'right' : 'left' const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined return (
{imageUrl && (
{title
)}
{title && (

{renderAccentTitle(title)}

)}
{description &&

{description}

} {buttonText && buttonLink && ( {buttonText} )}
) } function EditorialCarouselSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : [] const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined const bgClass = backgroundColor ? `bg-pop-${backgroundColor}` : 'bg-surface-page' const textClass = textColor ? `text-pop-${textColor}` : 'text-text-main' const isSplitScreen = payload.layout === 'split_screen' const imagePosition = typeof payload.imagePosition === 'string' ? payload.imagePosition : 'left' if (!slides.length) return null return (
{title && (

{renderAccentTitle(title)}

)}
{slides.map((slide, index) => { const slideTitle = localizedFromRecord(slide, 'title', resolvedLang) const description = localizedFromRecord(slide, 'description', resolvedLang) const category = localizedFromRecord(slide, 'category', resolvedLang) const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang) const buttonLink = typeof slide.buttonLink === 'string' ? slide.buttonLink : '' const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : '' return ( {isSplitScreen ? (
{imageUrl ? (
{slideTitle
) : null}
{category && {category}} {slideTitle &&

{renderAccentTitle(slideTitle)}

} {description &&

{description}

}
{buttonText && buttonLink && (
{buttonText} →
)}
) : (
{imageUrl ? (
{slideTitle
) : null}
{category && {category}} {slideTitle &&

{renderAccentTitle(slideTitle)}

} {description &&

{description}

}
{buttonText && buttonLink && (
{buttonText} →
)}
)}
) })}
) } function HeroArticleSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Scent Guide Article' const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const author = localizedFromRecord(payload, 'author', resolvedLang) const category = localizedFromRecord(payload, 'category', resolvedLang) const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : typeof payload.backgroundImageUrl === 'string' ? payload.backgroundImageUrl : '' const date = typeof payload.date === 'string' ? payload.date : typeof payload.publishDate === 'string' ? payload.publishDate : '' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined return (
{imageUrl && ( <> {title} )}
{category &&

{category}

}

{title}

{subtitle &&

{subtitle}

} {(author || date) && (

{[author && `${t('dynamic.authorBy', { lng: resolvedLang })} ${author}`, date].filter(Boolean).join(' / ')}

)}
) } function ScentNotesBreakdownSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Scent Notes' const description = localizedFromRecord(payload, 'description', resolvedLang) const notes = [ [t('dynamic.notes.top', { lng: resolvedLang }), optionalText(payload.topNotes, resolvedLang) || 'The initial, lighter smells of the fragrance.'], [t('dynamic.notes.heart', { lng: resolvedLang }), optionalText(payload.heartNotes ?? payload.middleNotes, resolvedLang) || 'The core of the fragrance, emerging just after the top notes.'], [t('dynamic.notes.base', { lng: resolvedLang }), optionalText(payload.baseNotes, resolvedLang) || 'The deepest, richest scents that linger the longest.'], ] return (

{title}

{description &&

{description}

}
{notes.map(([label, value]) => (

{label}

{value}

))}
) } function QuoteCalloutSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const quote = localizedFromRecord(payload, 'quote', resolvedLang) const author = localizedFromRecord(payload, 'author', resolvedLang) const role = localizedFromRecord(payload, 'role', resolvedLang) if (!quote) return null return (

"

{quote}
{(author || role) && (

{[author, role].filter(Boolean).join(' / ')}

)}
) } function AccordionSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const categories = Array.isArray(payload.categories) ? payload.categories.map(toRecord) : [] if (!categories.length) return null const sectionTitle = localizedFromRecord(payload, 'title', resolvedLang) const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined const iconColor = typeof payload.iconColor === 'string' ? payload.iconColor : undefined return (
{sectionTitle &&

{sectionTitle}

} {categories.map((category, categoryIndex) => { const questions = Array.isArray(category.questions) ? category.questions.map(toRecord) : [] const name = localizedFromRecord(category, 'name', resolvedLang) const icon = typeof category.icon === 'string' ? category.icon : '' return (
{name && (

{icon && {icon}} {name}

)}
{questions.map((item, itemIndex) => { const question = localizedFromRecord(item, 'q', resolvedLang) const answer = localizedFromRecord(item, 'a', resolvedLang) if (!question || !answer) return null return (
{question}

{answer}

) })}
) })}
) } function ContactInfoSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) const address = localizedFromRecord(payload, 'address', resolvedLang) const email = typeof payload.email === 'string' ? payload.email : '' const line = typeof payload.line === 'string' ? payload.line : '' const hours = localizedFromRecord(payload, 'hours', resolvedLang) const items = [ { key: 'address', label: t('dynamic.contactInfo.address', { lng: resolvedLang }), value: address, Icon: MapPin }, { key: 'email', label: t('dynamic.contactInfo.email', { lng: resolvedLang }), value: email, Icon: Mail }, { key: 'line', label: t('dynamic.contactInfo.line', { lng: resolvedLang }), value: line, Icon: MessageCircle }, { key: 'hours', label: t('dynamic.contactInfo.hours', { lng: resolvedLang }), value: hours, Icon: Clock }, ] as const return (
{title && (

{title}

)} {items.map(({ key, label, value, Icon }) => { if (!value) return null return (

{label}

{key === 'email' ? ( {value} ) : (

{value}

)}
) })}
) } function ContactFormSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const [formData, setFormData] = useState({ name: '', email: '', message: '' }) const [isSubmitting, setIsSubmitting] = useState(false) const [isSuccess, setIsSuccess] = useState(false) const [error, setError] = useState(null) const title = localizedFromRecord(payload, 'title', resolvedLang) const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setIsSubmitting(true) setError(null) setIsSuccess(false) try { const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3051/api'; const res = await fetch(`${apiUrl}/contacts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const data = await res.json().catch(() => ({})); if (!res.ok || data.error) { throw new Error(data.error || 'Failed to send message'); } setIsSuccess(true) setFormData({ name: '', email: '', message: '' }) setTimeout(() => setIsSuccess(false), 3000) } catch (err: any) { console.error(err); setError(err.message || 'An unexpected error occurred. Please try again.'); } finally { setIsSubmitting(false); } } const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target setFormData(prev => ({ ...prev, [name]: value })) } return (

{title || t('contactForm.title', { lng: resolvedLang })}

{subtitle || t('contactForm.subtitle', { lng: resolvedLang })}

{error && (
{error}
)} {isSuccess ? (

{t('contactForm.successTitle', { lng: resolvedLang })}

{t('contactForm.successBody', { lng: resolvedLang })}

) : (