This commit is contained in:
Your Name committed 2026-06-21 07:04:03 -04:00
1 parent f7ac94ca0d
commit 335f3b4f59
41 files changed
+809 -413

No files matched your search

@@ -2,7 +2,8 @@
import Image from 'next/image'
import Link from '../LocalizedLink'
import { Clock, Mail, MapPin, MessageCircle } from 'lucide-react'
import { Clock, Mail, MapPin, MessageCircle, Send } from 'lucide-react'
import { useState } from 'react'
import type { Category, PageSection, Product } from '@homaroma/types'
import { getCategoryImage } from '@homaroma/types'
import ProductCard from '../ProductCard'
@@ -125,12 +126,12 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
<div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0">
<div className="space-y-6">
{tagline && (
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-surface-card drop-shadow-md md:text-sm">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#EAEFEA] drop-shadow-md md:text-sm">
{tagline}
</p>
)}
<h1 className="ui-hero-title">
{renderAccentTitle(title || '')}
{renderAccentTitle(title || '', 'text-[#D4AF37] font-medium italic')}
</h1>
{description && (
<p className="ui-hero-copy">
@@ -146,7 +147,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
</Link>
<a
href="#categories"
className="ui-hero-link"
className="ui-hero-link group"
>
{heroLearnMoreText(resolvedLang)}
<span className="transition-transform group-hover:translate-x-1">→</span>
@@ -208,11 +209,11 @@ function CategoriesSection({
unoptimized
className="absolute inset-0 h-full w-full object-cover transition duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-text-main/65 to-transparent" />
<div className="absolute bottom-5 left-5 right-5 text-surface-card">
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
<div className="absolute bottom-5 left-5 right-5 z-20 text-white">
<span className="block text-lg font-semibold">{name}</span>
{category._count && (
<span className="text-sm text-surface-card">
<span className="text-sm text-white/90">
{category._count.products} {productsLabel[resolvedLang] ?? productsLabel.en}
</span>
)}
@@ -665,6 +666,207 @@ function ContactInfoSection({ payload, lang }: { payload: Record<string, unknown
)
}
function ContactFormSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
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 title = localizedFromRecord(payload, 'title', resolvedLang)
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
const labels: Record<string, Record<string, string>> = {
th: {
title: 'ติดต่อเรา',
subtitle: 'ส่งข้อความถึงทีมงานของเรา เรายินดีให้บริการและพร้อมตอบทุกข้อสงสัย',
successTitle: 'ส่งข้อความสำเร็จ',
successBody: 'ขอบคุณที่ติดต่อเรา ทีมงานจะรีบตอบกลับโดยเร็วที่สุด',
name: 'ชื่อ-นามสกุล',
namePlaceholder: 'กรอกชื่อของคุณ',
email: 'อีเมล',
message: 'ข้อความ',
messagePlaceholder: 'พิมพ์ข้อความของคุณที่นี่...',
submit: 'ส่งข้อความ',
},
en: {
title: 'Contact Us',
subtitle: 'Send a message to our team and we will get back to you soon.',
successTitle: 'Message sent successfully',
successBody: 'Thanks for reaching out. Our team will reply as soon as possible.',
name: 'Full Name',
namePlaceholder: 'Enter your name',
email: 'Email',
message: 'Message',
messagePlaceholder: 'Type your message here...',
submit: 'Send Message',
},
ja: {
title: 'お問い合わせ',
subtitle: 'チームへメッセージをお送りください。できるだけ早くご返信します。',
successTitle: '送信に成功しました',
successBody: 'お問い合わせありがとうございます。担当者ができるだけ早く返信します。',
name: '氏名',
namePlaceholder: 'お名前を入力してください',
email: 'メール',
message: 'メッセージ',
messagePlaceholder: 'ここにメッセージを入力してください...',
submit: '送信',
},
zh: {
title: '联系我们',
subtitle: '给我们的团队留言,我们会尽快回复您。',
successTitle: '发送成功',
successBody: '感谢您的联系,我们会尽快回复。',
name: '姓名',
namePlaceholder: '请输入您的姓名',
email: '邮箱',
message: '留言',
messagePlaceholder: '在这里输入您的留言...',
submit: '发送消息',
},
vi: {
title: 'Liên hệ',
subtitle: 'Gửi tin nhắn cho đội ngũ của chúng tôi, chúng tôi sẽ phản hồi sớm nhất.',
successTitle: 'Gửi tin nhắn thành công',
successBody: 'Cảm ơn bạn đã liên hệ, đội ngũ của chúng tôi sẽ phản hồi sớm nhất có thể.',
name: 'Họ và tên',
namePlaceholder: 'Nhập tên của bạn',
email: 'Email',
message: 'Tin nhắn',
messagePlaceholder: 'Nhập tin nhắn của bạn tại đây...',
submit: 'Gửi tin nhắn',
},
ko: {
title: '문의하기',
subtitle: '팀에 메시지를 보내주시면 최대한 빨리 답변드리겠습니다.',
successTitle: '메시지 전송 완료',
successBody: '문의해 주셔서 감사합니다. 가능한 빨리 답변드리겠습니다.',
name: '이름',
namePlaceholder: '이름을 입력하세요',
email: '이메일',
message: '메시지',
messagePlaceholder: '여기에 메시지를 입력하세요...',
submit: '메시지 보내기',
}
}
const copy = labels[resolvedLang] ?? labels.en
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
setIsSuccess(true)
setFormData({ name: '', email: '', message: '' })
// Reset success message after 3 seconds
setTimeout(() => setIsSuccess(false), 3000)
}, 1000)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
return (
<section className="ui-section-band bg-surface-page px-4 py-20 md:px-8">
<div className="mx-auto max-w-3xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-semibold text-text-main md:text-4xl">
{title || copy.title}
</h2>
<p className="mx-auto max-w-xl text-lg text-muted-text">
{subtitle || copy.subtitle}
</p>
</div>
<div className="rounded-3xl border border-line-primary/20 bg-surface-card p-8 shadow-card md:p-12">
{isSuccess ? (
<div className="py-12 text-center">
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-brand-success/20 text-brand-success">
<Send size={32} />
</div>
<h3 className="mb-2 text-2xl font-semibold text-text-main">{copy.successTitle}</h3>
<p className="text-muted-text">{copy.successBody}</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<label htmlFor="name" className="block text-sm font-medium text-text-main">
{copy.name}
</label>
<input
type="text"
id="name"
name="name"
required
value={formData.name}
onChange={handleChange}
className="w-full rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder={copy.namePlaceholder}
/>
</div>
<div className="space-y-2">
<label htmlFor="email" className="block text-sm font-medium text-text-main">
{copy.email}
</label>
<input
type="email"
id="email"
name="email"
required
value={formData.email}
onChange={handleChange}
className="w-full rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder="example@email.com"
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="message" className="block text-sm font-medium text-text-main">
{copy.message}
</label>
<textarea
id="message"
name="message"
required
value={formData.message}
onChange={handleChange}
rows={5}
className="w-full resize-none rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder={copy.messagePlaceholder}
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-primary px-6 py-4 font-semibold text-white transition-colors hover:bg-brand-primary-hover disabled:cursor-not-allowed disabled:opacity-70"
>
{isSubmitting ? (
<span className="h-6 w-6 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : (
<>
<Send size={18} />
{copy.submit}
</>
)}
</button>
</form>
)}
</div>
</div>
</section>
)
}
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
const i18nLang = useLang()
// Use i18n.language reactively — switches immediately when user changes language
@@ -709,6 +911,8 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
return <AccordionSection key={section.id} payload={payload} lang={lang} />
case 'CONTACT_INFO':
return <ContactInfoSection key={section.id} payload={payload} lang={lang} />
case 'CONTACT_FORM':
return <ContactFormSection key={section.id} payload={payload} lang={lang} />
default:
if (process.env.NODE_ENV !== 'production') {
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)