import type { Metadata } from 'next'; import { Kanit, Sarabun, Playfair_Display } from 'next/font/google'; import { notFound } from 'next/navigation'; import '../globals.css'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import ClientProviders from '@/components/ClientProviders'; import { API_BASE } from '@/lib/api'; import type { MegaMenuPayload } from '@/components/navigation/menuTypes'; import type { MenuItem, StoreSettings } from '@homaroma/types'; import { isSupportedLocale } from '@/lib/locales'; import type { CSSProperties } from 'react'; const sarabun = Sarabun({ subsets: ['latin', 'thai'], display: 'swap', weight: ['300', '400', '500', '600', '700'], variable: '--font-sarabun', }); const kanit = Kanit({ subsets: ['latin', 'thai'], display: 'swap', weight: ['300', '400', '500', '600', '700'], variable: '--font-kanit', }); const playfairDisplay = Playfair_Display({ subsets: ['latin'], display: 'swap', weight: ['400', '500', '600', '700'], variable: '--font-playfair', }); export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { const { lang } = await params; const settings = await getStoreSettings(lang); const brandName = settings?.brandName || ''; return { title: { template: `%s | ${brandName}`, default: brandName, }, description: 'Pure Origins, Artisan Craft, Zero Compromise', metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), }; } // Cache configuration: 0 for dev (no cache), strict numbers for production const getCacheTime = (prodSeconds: number) => 0; // Temporary bypass for 5-day testing // Fetch categories for Navbar and Footer on the server async function getCategories(lang: string) { try { const res = await fetch(`${API_BASE}/categories?lang=${lang}`, { next: { revalidate: getCacheTime(3600) } }); if (!res.ok) return []; return await res.json(); } catch (error) { console.error('Failed to fetch categories in layout', error); return []; } } // Fetch mega menu on the server — eliminates client-side flash async function getMegaMenu(lang: string): Promise { try { const res = await fetch(`${API_BASE}/menus/main?lang=${lang}`, { next: { revalidate: getCacheTime(300) } }); if (!res.ok) return []; const data = await res.json() as { payload?: MegaMenuPayload[] }; return Array.isArray(data.payload) ? data.payload : []; } catch (error) { console.error('Failed to fetch mega menu in layout', error); return []; } } // Fetch footer menus (explore + support) on the server async function getFooterMenus(lang: string): Promise<{ exploreMenu: MenuItem[]; supportMenu: MenuItem[] }> { const fetchMenu = async (slug: string): Promise => { try { const res = await fetch(`${API_BASE}/menus/${slug}?lang=${lang}`, { next: { revalidate: getCacheTime(300) } }); if (!res.ok) return []; const data = await res.json() as { items?: MenuItem[] }; return Array.isArray(data.items) ? data.items.sort((a, b) => a.order - b.order) : []; } catch { return []; } }; const [exploreMenu, supportMenu] = await Promise.all([ fetchMenu('footer-explore'), fetchMenu('footer-support'), ]); return { exploreMenu, supportMenu }; } // Fetch store settings (social links, footer texts) on the server async function getStoreSettings(lang: string): Promise { try { const res = await fetch(`${API_BASE}/settings?lang=${lang}`, { next: { revalidate: getCacheTime(600) } }); if (!res.ok) return null; return await res.json() as StoreSettings; } catch (error) { console.error('Failed to fetch store settings in layout', error); return null; } } function buildThemeStyle(settings: StoreSettings | null): CSSProperties | undefined { const tokens = settings?.themeTokens if (!tokens || typeof tokens !== 'object') return undefined const style: Record = {} const vars: Array<[string, string | undefined]> = [ ['--color-brand-primary', tokens.brandPrimary], ['--color-brand-primary-hover', tokens.brandPrimaryHover], ['--color-brand-accent', tokens.brandAccent], ['--color-brand-warm', tokens.brandWarm], ['--color-brand-cool', tokens.brandCool], ['--color-brand-success', tokens.brandSuccess], ['--color-brand-soft', tokens.brandSoft], ['--color-surface-page', tokens.surfacePage], ['--color-surface-nav-rest', tokens.surfaceNavRest], ['--color-surface-card', tokens.surfaceCard], ['--color-text-main', tokens.textMain], ['--color-muted-text', tokens.mutedText], ['--color-line-primary', tokens.linePrimary], ['--color-line-primary-hover', tokens.linePrimaryHover], ['--font-body-active', tokens.fontBody], ['--font-display-active', tokens.fontDisplay], ['--shadow-card', tokens.shadowCard], ['--shadow-card-hover', tokens.shadowCardHover], ['--color-caption-text', tokens.captionText], ['--color-border-color', tokens.borderColor], ['--color-surface-muted', tokens.surfaceMuted], ['--radius-base', tokens.radiusBase], ['--radius-card', tokens.radiusCard], ] // Add Scroll Reveal Settings const scrollEnabled = settings?.scrollRevealEnabled !== false ? '1' : '0' const scrollStyle = settings?.scrollRevealStyle || 'fade-up' const scrollDuration = settings?.scrollRevealDuration ? `${settings.scrollRevealDuration}ms` : '700ms' style['--scroll-reveal-enabled'] = scrollEnabled style['--scroll-reveal-style'] = scrollStyle style['--scroll-reveal-duration'] = scrollDuration for (const [key, value] of vars) { if (typeof value === 'string' && value.trim()) { style[key] = value } } return Object.keys(style).length > 0 ? (style as CSSProperties) : undefined } export default async function RootLayout({ children, params, }: { children: React.ReactNode; params: Promise<{ lang: string }>; }) { const { lang } = await params; if (!isSupportedLocale(lang)) { notFound(); } const [categories, initialMenu, footerMenus, settings] = await Promise.all([ getCategories(lang), getMegaMenu(lang), getFooterMenus(lang), getStoreSettings(lang), ]); const themeStyle = buildThemeStyle(settings) return (
{children}