x
This commit is contained in:
1 parent
fc252e46cc
commit
ad637fd8b4
27 files changed
+764
-25
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Kanit, Sarabun } from 'next/font/google';
|
||||
import { Kanit, Sarabun, Playfair_Display } from 'next/font/google';
|
||||
import { notFound } from 'next/navigation';
|
||||
import '../globals.css';
|
||||
import Navbar from '@/components/Navbar';
|
||||
@@ -25,6 +25,13 @@ const kanit = Kanit({
|
||||
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<Metadata> {
|
||||
const { lang } = await params;
|
||||
const settings = await getStoreSettings(lang);
|
||||
@@ -127,6 +134,11 @@ function buildThemeStyle(settings: StoreSettings | null): CSSProperties | undefi
|
||||
['--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
|
||||
@@ -171,7 +183,7 @@ export default async function RootLayout({
|
||||
const themeStyle = buildThemeStyle(settings)
|
||||
|
||||
return (
|
||||
<html lang={lang} className={`${sarabun.variable} ${kanit.variable} scroll-smooth`}>
|
||||
<html lang={lang} className={`${sarabun.variable} ${kanit.variable} ${playfairDisplay.variable} scroll-smooth`}>
|
||||
<body className="min-h-screen bg-surface-page text-text-main font-sans antialiased" style={themeStyle}>
|
||||
<ClientProviders>
|
||||
<Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand-primary: #FF6B6B;
|
||||
--color-brand-primary-hover: #FF4500;
|
||||
--color-brand-accent: #FFF200;
|
||||
--color-brand-warm: #FFA07A;
|
||||
--color-brand-cool: #E0FFFF;
|
||||
--color-brand-success: #98FB98;
|
||||
--color-brand-soft: #D8BFD8;
|
||||
--color-surface-page: #FFF0F5;
|
||||
--color-surface-nav-rest: #F9F8F5;
|
||||
/* Quiet Luxury Palette — HOM AROMA Brand Reference */
|
||||
--color-brand-primary: #B8956A;
|
||||
--color-brand-primary-hover: #A07D3A;
|
||||
--color-brand-accent: #D4B97A;
|
||||
--color-brand-warm: #D4785A;
|
||||
--color-brand-cool: #EDE6DD;
|
||||
--color-brand-success: #5B9A4D;
|
||||
--color-brand-soft: #DABE7E;
|
||||
--color-surface-page: #F5F0EB;
|
||||
--color-surface-nav-rest: #FDFAF7;
|
||||
--color-surface-card: #FFFFFF;
|
||||
--color-text-main: #333333;
|
||||
--color-muted-text: #666666;
|
||||
--color-text-main: #3A2F28;
|
||||
--color-muted-text: #7A6E63;
|
||||
--color-line-primary: #00B900;
|
||||
--color-line-primary-hover: #009900;
|
||||
|
||||
/* New tokens — extended Quiet Luxury system */
|
||||
--color-caption-text: #B0A396;
|
||||
--color-border-color: #E8E0D8;
|
||||
--color-surface-muted: #EDE6DD;
|
||||
--radius-base: 8px;
|
||||
--radius-card: 16px;
|
||||
|
||||
/* pop-* aliases — used by apps/spa ThemeInjector */
|
||||
--color-pop-pink: var(--color-brand-primary);
|
||||
--color-pop-yellow: var(--color-brand-accent);
|
||||
--color-pop-orange: var(--color-brand-warm);
|
||||
@@ -25,12 +34,16 @@
|
||||
--color-pop-purple: var(--color-brand-soft);
|
||||
--color-pop-bg: var(--color-surface-page);
|
||||
--color-pop-black: var(--color-text-main);
|
||||
--color-pop-caption: var(--color-caption-text);
|
||||
--color-pop-border: var(--color-border-color);
|
||||
--color-pop-muted-bg: var(--color-surface-muted);
|
||||
|
||||
/* Serif font — mirrors apps/spa font-serif token */
|
||||
/* Serif font — Playfair Display for Quiet Luxury headings */
|
||||
--font-playfair: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;
|
||||
--font-serif: var(--font-serif-locale, ui-serif), Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
|
||||
--shadow-card: 0 10px 25px rgba(0,0,0,0.05);
|
||||
--shadow-card-hover: 0 20px 40px rgba(0,0,0,0.1);
|
||||
--shadow-card: 0 1px 4px rgba(0,0,0,0.04);
|
||||
--shadow-card-hover: 0 4px 12px rgba(0,0,0,0.06);
|
||||
--shadow-brutal: var(--shadow-card);
|
||||
--shadow-brutal-hover: var(--shadow-card-hover);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from '../LocalizedLink'
|
||||
import { Clock, Mail, MapPin, MessageCircle, Send, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
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'
|
||||
@@ -152,6 +152,140 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
||||
)
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<string, React.ElementType> = {
|
||||
leaf: Leaf,
|
||||
droplet: Droplet,
|
||||
sparkles: Sparkles,
|
||||
gift: Gift,
|
||||
check: CheckCircle,
|
||||
shield: ShieldCheck,
|
||||
heart: Heart,
|
||||
}
|
||||
|
||||
function UspBarSection({ payload, lang }: { payload: Record<string, unknown>, 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 (
|
||||
<section className="w-full py-8 border-t border-b border-line-primary/10" style={{ backgroundColor }}>
|
||||
<div className="ui-section-shell">
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-${Math.min(items.length, 4)} gap-6 divide-y sm:divide-y-0 sm:divide-x divide-line-primary/20`}>
|
||||
{items.slice(0, 4).map((item: any, idx: number) => {
|
||||
const Icon = ICON_MAP[item.icon] || CheckCircle
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-4 px-4 pt-4 sm:pt-0 first:pt-0">
|
||||
<div className="flex-shrink-0 text-brand-primary">
|
||||
<Icon className="w-8 h-8" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm md:text-base font-semibold text-text-main">
|
||||
{localizedFromRecord(item, 'title', resolvedLang)}
|
||||
</h3>
|
||||
{item.subtitle && (
|
||||
<p className="text-xs md:text-sm text-muted-text mt-1">
|
||||
{localizedFromRecord(item, 'subtitle', resolvedLang)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function HeroV2Section({ 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 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
|
||||
|
||||
// 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 (
|
||||
<section className="relative w-full h-[80vh] min-h-[600px] bg-surface-page flex items-center overflow-hidden">
|
||||
<div className="absolute inset-0 w-full h-full">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title || 'Hero'}
|
||||
fill
|
||||
unoptimized
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-surface-page/90 via-surface-page/50 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-7xl mx-auto px-6 md:px-12">
|
||||
<div className={`max-w-xl flex flex-col gap-6 ${alignClass}`}>
|
||||
{tagline && (
|
||||
<div className="flex items-center gap-4">
|
||||
{showOrnament && align !== 'right' && <div className="w-8 h-[1px] bg-brand-primary" />}
|
||||
<span className="text-sm font-semibold tracking-[0.2em] uppercase text-brand-primary">
|
||||
{tagline}
|
||||
</span>
|
||||
{showOrnament && align !== 'left' && <div className="w-8 h-[1px] bg-brand-primary" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-serif text-text-main leading-tight">
|
||||
{renderAccentTitle(title || '', 'text-brand-primary')}
|
||||
</h1>
|
||||
|
||||
{showOrnament && (
|
||||
<div className="flex justify-center items-center text-brand-primary my-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<p className="text-base md:text-lg text-text-main/80 leading-relaxed max-w-md">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={`flex flex-col sm:flex-row items-center gap-4 mt-4 w-full sm:w-auto ${align === 'center' ? 'justify-center' : align === 'right' ? 'justify-end' : 'justify-start'}`}>
|
||||
{primaryBtnText && primaryBtnLink && (
|
||||
<Link
|
||||
href={primaryBtnLink}
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 bg-[#A18868] text-white font-medium rounded-md hover:bg-[#8B7355] transition-colors w-full sm:w-auto"
|
||||
>
|
||||
{primaryBtnText}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
{secondaryBtnText && secondaryBtnLink && (
|
||||
<Link
|
||||
href={secondaryBtnLink}
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 bg-transparent border border-[#A18868] text-[#A18868] font-medium rounded-md hover:bg-[#A18868]/5 transition-colors w-full sm:w-auto"
|
||||
>
|
||||
{secondaryBtnText}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CategoriesSection({
|
||||
payload,
|
||||
categories,
|
||||
@@ -888,6 +1022,18 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
|
||||
<HeroSection payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'HERO_V2':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
<HeroV2Section payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'USP_BAR':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
<UspBarSection payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'CATEGORIES':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
|
||||
Reference in new issue
Block a user