"use client" import Link from 'next/link' import { Clock, Mail, MapPin, MessageCircle } from 'lucide-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 { Swiper, SwiperSlide } from 'swiper/react' import { Autoplay } from 'swiper/modules' import 'swiper/css' 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.filter(Boolean).join(', ') return localizedValue(value, lang) } function renderAccentTitle(text: string, accentClassName = 'text-pop-pink italic') { 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 heroLearnMoreText(lang: string) { const labels: Record = { th: 'ดูรายละเอียดเพิ่มเติม', en: 'Learn More', ja: '詳しく見る', zh: '了解更多', vi: 'Tìm hiểu thêm', ko: '자세히 보기', } return labels[lang] ?? labels.en } 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 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' return (
{title
{tagline && (

{tagline}

)}

{renderAccentTitle(title || 'HomAroma', 'font-medium italic text-[#E4BC32]')}

{description && (

{description}

)}
{buttonText || 'Explore Collection'} {heroLearnMoreText(resolvedLang)} →
) } function CategoriesSection({ payload, categories, lang, }: { payload: Record categories: Category[] lang: string }) { const i18nLang = useLang() 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 const productsLabel: Record = { th: 'สินค้า', en: 'Products', ja: '商品', zh: '商品', vi: 'Sản phẩm', ko: '상품', } return (

{title}

{subtitle &&

{subtitle}

}
{displayed.map((category) => { const name = categoryName(category, resolvedLang) return ( {name}
{name} {category._count && ( {category._count.products} {productsLabel[resolvedLang] ?? productsLabel.en} )}
) })}
) } function ProductsSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Featured Picks' const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) const products = getProducts(payload) const viewAllLabel: Record = { th: 'ดูทั้งหมด →', en: 'View All Collection →', ja: 'すべて見る →', zh: '查看全部 →', vi: 'Xem tất cả →', ko: '전체 보기 →', } const emptyLabel: Record = { th: 'ไม่พบสินค้าในหมวดนี้', en: 'No products found in this section.', ja: 'このセクションには商品がありません。', zh: '此区域中没有找到产品。', vi: 'Không tìm thấy sản phẩm nào trong mục này.', ko: '이 섹션에서 찾을 수 있는 상품이 없습니다.', } return (

{title}

{subtitle &&

{subtitle}

}
{viewAllLabel[resolvedLang] ?? viewAllLabel.en}
{products.length ? (
{products.map((product) => )}
) : (

{emptyLabel[resolvedLang] ?? emptyLabel.en}

)}
) } 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 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 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) : [] if (!slides.length) return null return (
{title && (

{renderAccentTitle(title, 'text-pop-yellow italic')}

)}
{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 (
{imageUrl && (
{slideTitle
)}
{category &&

{category}

} {slideTitle &&

{renderAccentTitle(slideTitle, 'text-pop-yellow italic')}

} {description &&

{description}

} {buttonText && buttonLink && ( {buttonText} → )}
) })}
) } function HeroArticleSection({ payload, lang }: { payload: Record, lang: string }) { 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 authorPrefix: Record = { th: 'โดย', en: 'By', ja: '著者', zh: '作者', vi: 'Bởi', ko: '작성자', } return (
{imageUrl && ( <> {title}
)}
{category &&

{category}

}

{title}

{subtitle &&

{subtitle}

} {(author || date) && (

{[author && `${authorPrefix[resolvedLang] ?? authorPrefix.en} ${author}`, date].filter(Boolean).join(' / ')}

)}
) } function ScentNotesBreakdownSection({ payload, lang }: { payload: Record, lang: string }) { const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Scent Notes' const description = localizedFromRecord(payload, 'description', resolvedLang) const noteLabels: Record> = { th: { top: 'กลิ่นเปิด', heart: 'กลิ่นกลาง', base: 'กลิ่นฐาน' }, en: { top: 'Top Notes', heart: 'Heart Notes', base: 'Base Notes' }, ja: { top: 'トップノート', heart: 'ミドルノート', base: 'ベースノート' }, zh: { top: '前调', heart: '中调', base: '后调' }, vi: { top: 'Hương đầu', heart: 'Hương giữa', base: 'Hương cuối' }, ko: { top: '탑 노트', heart: '미들 노트', base: '베이스 노트' }, } const labels = noteLabels[resolvedLang] ?? noteLabels.en const notes = [ [labels.top, optionalText(payload.topNotes, resolvedLang) || 'The initial, lighter smells of the fragrance.'], [labels.heart, optionalText(payload.heartNotes ?? payload.middleNotes, resolvedLang) || 'The core of the fragrance, emerging just after the top notes.'], [labels.base, 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) 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 i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const labels: Record> = { th: { address: 'ที่อยู่', email: 'อีเมล', line: 'Line', hours: 'เวลาทำการ' }, en: { address: 'Address', email: 'Email', line: 'Line', hours: 'Business Hours' }, ja: { address: '住所', email: 'メール', line: 'Line', hours: '営業時間' }, zh: { address: '地址', email: '电子邮件', line: 'Line', hours: '营业时间' }, vi: { address: 'Địa chỉ', email: 'Email', line: 'Line', hours: 'Giờ làm việc' }, ko: { address: '주소', email: '이메일', line: 'Line', hours: '운영 시간' }, } const resolvedLabels = labels[resolvedLang] ?? labels.en 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: resolvedLabels.address, value: address, Icon: MapPin }, { key: 'email', label: resolvedLabels.email, value: email, Icon: Mail }, { key: 'line', label: resolvedLabels.line, value: line, Icon: MessageCircle }, { key: 'hours', label: resolvedLabels.hours, 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}

)}
) })}
) } 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 return (
{sections.map((section) => { if (!section.isActive) return null const payload = toRecord(section.payload) const type = section.type.toUpperCase() switch (type) { case 'ANNOUNCEMENT_BAR': return case 'HERO': return case 'CATEGORIES': return case 'PRODUCTS': case 'PRODUCT_CAROUSEL': return case 'BANNER': return case 'TEXT': return case 'NEWSLETTER': return case 'EDITORIAL_TILE': return case 'EDITORIAL_CAROUSEL': return case 'HERO_ARTICLE': return case 'RICH_TEXT': return case 'SCENT_NOTES_BREAKDOWN': return case 'QUOTE_CALLOUT': return case 'ACCORDION': return case 'CONTACT_INFO': return default: if (process.env.NODE_ENV !== 'production') { console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`) } return null } })}
) }