fix update Section Block
This commit is contained in:
1 parent
996dfdabd0
commit
6f67ee6077
47 files changed
+1792
-197
No files matched your search
@@ -49,7 +49,13 @@ export default function DynamicPageRenderer({
|
||||
/>
|
||||
);
|
||||
case 'PRODUCTS':
|
||||
return <DynamicProducts key={section.id} payload={section.payload} />;
|
||||
return (
|
||||
<DynamicProducts
|
||||
key={section.id}
|
||||
payload={section.payload}
|
||||
categories={categories}
|
||||
/>
|
||||
);
|
||||
case 'BANNER':
|
||||
return <DynamicBanner key={section.id} payload={section.payload} />;
|
||||
case 'TEXT':
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Link } from 'react-router-dom';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Pagination } from 'swiper/modules';
|
||||
import ProductCard from './ProductCard';
|
||||
import type { Product, ProductsSectionPayload } from '@homaroma/types';
|
||||
import type { Category, Product, ProductsSectionPayload } from '@homaroma/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../utils/localization';
|
||||
|
||||
@@ -12,14 +12,16 @@ import 'swiper/css/pagination';
|
||||
|
||||
interface DynamicProductsProps {
|
||||
payload: ProductsSectionPayload & { products?: Product[] };
|
||||
categories?: Category[];
|
||||
}
|
||||
|
||||
export default function DynamicProducts({ payload }: DynamicProductsProps) {
|
||||
const { i18n } = useTranslation();
|
||||
export default function DynamicProducts({ payload, categories = [] }: DynamicProductsProps) {
|
||||
const { i18n, t } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const { products = [], layout = 'grid', enableQuickShop = false } = payload;
|
||||
const title = getLocalizedField(payload.title, lang);
|
||||
const subtitle = getLocalizedField(payload.subtitle, lang);
|
||||
const categoryLookup = new Map(categories.map((category) => [category.id, category]));
|
||||
|
||||
return (
|
||||
<section className="bg-pop-bg py-24 px-6 relative">
|
||||
@@ -37,7 +39,8 @@ export default function DynamicProducts({ payload }: DynamicProductsProps) {
|
||||
to="/shop"
|
||||
className="text-pop-pink font-medium hover:text-pop-red transition-colors flex items-center gap-2"
|
||||
>
|
||||
View All Collection →
|
||||
{t('common.viewAllCollection')}
|
||||
→
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +64,11 @@ export default function DynamicProducts({ payload }: DynamicProductsProps) {
|
||||
>
|
||||
{products.map((p) => (
|
||||
<SwiperSlide key={p.id} className="h-auto">
|
||||
<ProductCard product={p} enableQuickShop={enableQuickShop} />
|
||||
<ProductCard
|
||||
product={p}
|
||||
enableQuickShop={enableQuickShop}
|
||||
categoryNameOverride={getLocalizedField(categoryLookup.get(p.categoryId)?.name, lang)}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
@@ -70,7 +77,11 @@ export default function DynamicProducts({ payload }: DynamicProductsProps) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{products.map((p) => (
|
||||
<div key={p.id}>
|
||||
<ProductCard product={p} enableQuickShop={enableQuickShop} />
|
||||
<ProductCard
|
||||
product={p}
|
||||
enableQuickShop={enableQuickShop}
|
||||
categoryNameOverride={getLocalizedField(categoryLookup.get(p.categoryId)?.name, lang)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,21 @@ export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNote
|
||||
const p = payload || {};
|
||||
const title = getLocalizedField(p.title, lang);
|
||||
const description = getLocalizedField(p.description, lang);
|
||||
const topNotes = getLocalizedField(p.topNotes, lang);
|
||||
const heartNotes = getLocalizedField(p.heartNotes, lang);
|
||||
const baseNotes = getLocalizedField(p.baseNotes, lang);
|
||||
const notes = (value: any) => Array.isArray(value)
|
||||
? value.map((item) => getLocalizedField(item, lang)).filter(Boolean).join(', ')
|
||||
: getLocalizedField(value, lang);
|
||||
const labels = {
|
||||
th: { top: 'Top Notes', heart: 'Heart Notes', base: 'Base Notes' },
|
||||
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: '베이스 노트' }
|
||||
} as const;
|
||||
const labelSet = labels[lang as keyof typeof labels] ?? labels.en;
|
||||
const topNotes = notes(p.topNotes);
|
||||
const heartNotes = notes(p.heartNotes ?? p.middleNotes);
|
||||
const baseNotes = notes(p.baseNotes);
|
||||
|
||||
return (
|
||||
<section className="py-20 px-6 bg-spa-light">
|
||||
@@ -29,21 +41,21 @@ export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNote
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Top Notes</h3>
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">{labelSet.top}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
{topNotes || 'The initial, lighter smells of the fragrance.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Heart Notes</h3>
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">{labelSet.heart}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
{heartNotes || 'The core of the fragrance, emerging just after the top notes.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Base Notes</h3>
|
||||
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">{labelSet.base}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
{baseNotes || 'The deepest, richest scents that linger the longest.'}
|
||||
</p>
|
||||
|
||||
@@ -16,9 +16,10 @@ function resolveStr(v: TranslatableString | null | undefined, lang: string): str
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
enableQuickShop?: boolean;
|
||||
categoryNameOverride?: string;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, enableQuickShop = false }: ProductCardProps) {
|
||||
export default function ProductCard({ product, enableQuickShop = false, categoryNameOverride }: ProductCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { addItem, removeItem, isInWishlist } = useWishlistStore();
|
||||
const { addToCart } = useCartStore();
|
||||
@@ -26,6 +27,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
||||
const isWished = isInWishlist(product.id);
|
||||
const [selectedVariant, setSelectedVariant] = useState(product.variants?.[0] || null);
|
||||
const displayName = resolveStr(product.name, i18n.language) || product.scent.name;
|
||||
const displayCategoryName = categoryNameOverride || resolveStr(product.category.name, i18n.language);
|
||||
|
||||
const handleWishlistClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -49,7 +51,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
||||
{/* Image Area */}
|
||||
<div className="h-48 md:h-64 overflow-hidden rounded-xl relative bg-spa-sage/20">
|
||||
<img
|
||||
src={product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')}
|
||||
src={product.images?.[0]?.url || getCategoryImage(displayCategoryName)}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
alt={product.scent.name}
|
||||
onError={(e) => {
|
||||
@@ -58,7 +60,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
||||
/>
|
||||
{/* Category badge */}
|
||||
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
|
||||
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||
{displayCategoryName}
|
||||
</div>
|
||||
{/* Wishlist Button */}
|
||||
<button
|
||||
@@ -79,7 +81,7 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
||||
{displayName}
|
||||
</h3>
|
||||
<p className="text-[10px] md:text-xs text-spa-stone/60 mt-1 uppercase tracking-widest line-clamp-1 font-semibold">
|
||||
{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
|
||||
{displayCategoryName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -136,13 +138,13 @@ export default function ProductCard({ product, enableQuickShop = false }: Produc
|
||||
price: selectedVariant.price || 0,
|
||||
productName: displayName,
|
||||
variantSize: selectedVariant.size,
|
||||
productImage: product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '')
|
||||
productImage: product.images?.[0]?.url || getCategoryImage(displayCategoryName)
|
||||
});
|
||||
}}
|
||||
className="w-full py-2.5 bg-spa-forest text-white hover:bg-spa-forest/90 rounded-xl font-bold text-xs uppercase tracking-widest transition-all shadow-sm hover:shadow flex items-center justify-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
<ShoppingBag className="w-3.5 h-3.5" />
|
||||
Add to Cart
|
||||
{t('cart.addToCart')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import type { AccordionSectionPayload } from '@homaroma/types';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../../utils/localization';
|
||||
|
||||
interface AccordionSectionProps {
|
||||
payload: AccordionSectionPayload;
|
||||
}
|
||||
|
||||
export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const [openIndex, setOpenIndex] = useState<string | null>(null);
|
||||
const title = getLocalizedField((payload as any)?.title, lang);
|
||||
|
||||
const toggleAccordion = (index: string) => {
|
||||
setOpenIndex(openIndex === index ? null : index);
|
||||
@@ -16,11 +21,16 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
return (
|
||||
<section className="w-full py-16 px-4 md:px-8 bg-spa-sand/20">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{title && (
|
||||
<h2 className="text-3xl md:text-4xl font-serif text-spa-charcoal mb-10 text-center">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{payload.categories.map((category, catIndex) => (
|
||||
<div key={catIndex} className="mb-12 last:mb-0">
|
||||
<h3 className="text-2xl font-serif text-spa-charcoal mb-6 flex items-center gap-3">
|
||||
{category.icon && <span className="text-spa-sage">{category.icon}</span>}
|
||||
{category.name}
|
||||
{getLocalizedField(category.name, lang)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{category.questions.map((item, qIndex) => {
|
||||
@@ -37,7 +47,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
onClick={() => toggleAccordion(indexKey)}
|
||||
>
|
||||
<span className="text-lg font-medium text-spa-charcoal pr-4">
|
||||
{item.q}
|
||||
{getLocalizedField(item.q, lang)}
|
||||
</span>
|
||||
<span className="text-spa-sage flex-shrink-0">
|
||||
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
||||
@@ -49,7 +59,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
}`}
|
||||
>
|
||||
<p className="text-spa-charcoal/80 leading-relaxed whitespace-pre-wrap">
|
||||
{item.a}
|
||||
{getLocalizedField(item.a, lang)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { ContactFormSectionPayload } from '@homaroma/types';
|
||||
import { Send } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../../utils/localization';
|
||||
|
||||
interface ContactFormSectionProps {
|
||||
payload: ContactFormSectionPayload;
|
||||
}
|
||||
|
||||
export default function ContactFormSection({ payload }: ContactFormSectionProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
@@ -14,6 +18,84 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const p = payload || {};
|
||||
const title = getLocalizedField(p.title, lang);
|
||||
const subtitle = getLocalizedField(p.subtitle, lang);
|
||||
const labels = {
|
||||
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: '메시지 보내기',
|
||||
}
|
||||
} as const;
|
||||
const copy = labels[lang as keyof typeof labels] ?? labels.en;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -40,10 +122,10 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-serif text-spa-charcoal mb-4">
|
||||
{payload.title || "ติดต่อเรา"}
|
||||
{title || copy.title}
|
||||
</h2>
|
||||
<p className="text-lg text-spa-charcoal/70 max-w-xl mx-auto">
|
||||
{payload.subtitle || "ส่งข้อความถึงทีมงานของเรา เรายินดีให้บริการและพร้อมตอบทุกข้อสงสัย"}
|
||||
{subtitle || copy.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,15 +135,15 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Send size={32} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-serif text-spa-charcoal mb-2">ส่งข้อความสำเร็จ</h3>
|
||||
<p className="text-spa-charcoal/70">ขอบคุณที่ติดต่อเรา ทีมงานจะรีบตอบกลับโดยเร็วที่สุด</p>
|
||||
<h3 className="text-2xl font-serif text-spa-charcoal mb-2">{copy.successTitle}</h3>
|
||||
<p className="text-spa-charcoal/70">{copy.successBody}</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
ชื่อ-นามสกุล
|
||||
{copy.name}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -71,12 +153,12 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border border-spa-sage/30 focus:outline-none focus:ring-2 focus:ring-spa-sage/50 bg-spa-sand/5 transition-all"
|
||||
placeholder="กรอกชื่อของคุณ"
|
||||
placeholder={copy.namePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
อีเมล
|
||||
{copy.email}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -93,7 +175,7 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
ข้อความ
|
||||
{copy.message}
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
@@ -103,7 +185,7 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
onChange={handleChange}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 rounded-xl border border-spa-sage/30 focus:outline-none focus:ring-2 focus:ring-spa-sage/50 bg-spa-sand/5 transition-all resize-none"
|
||||
placeholder="พิมพ์ข้อความของคุณที่นี่..."
|
||||
placeholder={copy.messagePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +199,7 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
) : (
|
||||
<>
|
||||
<Send size={18} />
|
||||
ส่งข้อความ
|
||||
{copy.submit}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -1,57 +1,67 @@
|
||||
|
||||
import type { ContactInfoSectionPayload } from '@homaroma/types';
|
||||
import { MapPin, Mail, MessageCircle, Clock } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../../utils/localization';
|
||||
|
||||
interface ContactInfoSectionProps {
|
||||
payload: ContactInfoSectionPayload;
|
||||
}
|
||||
|
||||
export default function ContactInfoSection({ payload }: ContactInfoSectionProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const p = payload || {};
|
||||
const title = getLocalizedField((p as any).title, lang);
|
||||
const labels = {
|
||||
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: '영업 시간' }
|
||||
} as const;
|
||||
const labelSet = labels[lang as keyof typeof labels] ?? labels.en;
|
||||
|
||||
const items = [
|
||||
[labelSet.address, getLocalizedField((p as any).address, lang), MapPin],
|
||||
[labelSet.email, getLocalizedField((p as any).email, lang), Mail],
|
||||
[labelSet.line, getLocalizedField((p as any).line, lang), MessageCircle],
|
||||
[labelSet.hours, getLocalizedField((p as any).hours, lang), Clock],
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<section className="w-full py-16 px-4 md:px-8 bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{title && (
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl md:text-4xl font-serif text-spa-charcoal">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
|
||||
<div className="flex flex-col items-center text-center p-6 bg-spa-sand/10 rounded-2xl border border-spa-sage/20 hover:border-spa-sage transition-colors duration-300">
|
||||
<div className="w-12 h-12 bg-spa-sage/10 rounded-full flex items-center justify-center text-spa-sage mb-4">
|
||||
<MapPin size={24} />
|
||||
</div>
|
||||
<h4 className="text-lg font-serif text-spa-charcoal mb-2">ที่อยู่</h4>
|
||||
<p className="text-spa-charcoal/70 leading-relaxed whitespace-pre-wrap">
|
||||
{payload.address}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center text-center p-6 bg-spa-sand/10 rounded-2xl border border-spa-sage/20 hover:border-spa-sage transition-colors duration-300">
|
||||
<div className="w-12 h-12 bg-spa-sage/10 rounded-full flex items-center justify-center text-spa-sage mb-4">
|
||||
<Mail size={24} />
|
||||
</div>
|
||||
<h4 className="text-lg font-serif text-spa-charcoal mb-2">อีเมล</h4>
|
||||
<a href={`mailto:${payload.email}`} className="text-spa-charcoal/70 hover:text-spa-sage transition-colors">
|
||||
{payload.email}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center text-center p-6 bg-spa-sand/10 rounded-2xl border border-spa-sage/20 hover:border-spa-sage transition-colors duration-300">
|
||||
<div className="w-12 h-12 bg-spa-sage/10 rounded-full flex items-center justify-center text-spa-sage mb-4">
|
||||
<MessageCircle size={24} />
|
||||
</div>
|
||||
<h4 className="text-lg font-serif text-spa-charcoal mb-2">Line</h4>
|
||||
<p className="text-spa-charcoal/70">
|
||||
{payload.line}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center text-center p-6 bg-spa-sand/10 rounded-2xl border border-spa-sage/20 hover:border-spa-sage transition-colors duration-300">
|
||||
<div className="w-12 h-12 bg-spa-sage/10 rounded-full flex items-center justify-center text-spa-sage mb-4">
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
<h4 className="text-lg font-serif text-spa-charcoal mb-2">เวลาทำการ</h4>
|
||||
<p className="text-spa-charcoal/70 whitespace-pre-wrap">
|
||||
{payload.hours}
|
||||
</p>
|
||||
</div>
|
||||
{items.map(([label, value, Icon]) => {
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<div key={label} className="flex flex-col items-center text-center p-6 bg-spa-sand/10 rounded-2xl border border-spa-sage/20 hover:border-spa-sage transition-colors duration-300">
|
||||
<div className="w-12 h-12 bg-spa-sage/10 rounded-full flex items-center justify-center text-spa-sage mb-4">
|
||||
<Icon size={24} />
|
||||
</div>
|
||||
<h4 className="text-lg font-serif text-spa-charcoal mb-2">{label}</h4>
|
||||
{label === labelSet.email ? (
|
||||
<a href={`mailto:${value}`} className="text-spa-charcoal/70 hover:text-spa-sage transition-colors">
|
||||
{value}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-spa-charcoal/70 leading-relaxed whitespace-pre-wrap">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user