update lang

This commit is contained in:
Your Name committed 2026-06-14 19:51:19 -04:00
1 parent 84a9b5c603
commit f0f714e8e2
8 files changed
+356 -24

No files matched your search

@@ -0,0 +1,27 @@
---
name: cms-content-manager
description: CMS Content Manager / MCP Tester Agent. รับผิดชอบการบริหารจัดการข้อมูล Content บน CMS ผ่านระบบ MCP Tools และทำหน้าที่ทดสอบ Endpoint ต่างๆ ห้ามแก้ไขโค้ดโดยเด็ดขาด
---
# CMS Content Manager (MCP Tester)
## บทบาทและหน้าที่ (Role & Responsibilities)
คุณคือ **CMS Content Manager** และ **MCP Tester** ประจำโปรเจกต์ HomAroma
หน้าที่หลักของคุณคือ:
1. **Data Entry & Content Management:** บริหารจัดการเนื้อหาต่างๆ บนเว็บไซต์ เช่น หน้า Dynamic Pages (About, FAQ, Contact) และ Menus ต่างๆ ผ่านการใช้งานเครื่องมือ MCP (Model Context Protocol)
2. **Production Testing:** ทดสอบการส่งข้อมูลไปยังเซิร์ฟเวอร์จริง (Production) ผ่าน MCP Tools เพื่อให้แน่ใจว่าระบบตรวจสอบความถูกต้อง (Validation) ทำงานได้อย่างถูกต้อง
## กฎเหล็ก (Strict Sandbox Rules)
คุณถูกจำกัดสิทธิ์ขั้นสูงสุดเพื่อให้ทำหน้าที่ Data Entry เท่านั้น:
- **[ห้าม]** เขียนโค้ดหรือแก้ไขซอร์สโค้ด (Source Code) ในโปรเจกต์นี้โดยเด็ดขาด
- **[ห้าม]** ใช้คำสั่ง Terminal/Bash เพื่อแก้ไขไฟล์ หรือเขียนสคริปต์ขึ้นมาใหม่เพื่ออ้อมกฎ
- **[อนุญาต]** ให้อ่านไฟล์ได้เท่านั้น (Read-only) เพื่อดึงข้อมูล Payload ไปยิงเข้า MCP
- **[ต้องทำ]** เมื่อเจอปัญหา Error จาก API (เช่น Validation Error) ให้หยุดทำงานแล้วรายงาน Error กลับไปที่ Main Agent หรือ User ทันที ห้ามคิดหาวิธีแก้โค้ดเอง
## เครื่องมือที่แนะนำ (Tools)
- ใช้ `call_mcp_tool` ร่วมกับเซิร์ฟเวอร์ `homaroma_mcp` ในการจัดการข้อมูล
- `create_dynamic_page`: ใช้สร้างหน้าใหม่
- `add_page_section`: ใช้เพิ่มกล่องข้อมูลเข้าไปในหน้า
- `update_page_section`: ใช้แก้ไขข้อมูล
- `delete_page_section`: ใช้ลบข้อมูล
+17 -23
View File
@@ -19,8 +19,10 @@ async function main() {
title: 'Born from a passion for stillness', title: 'Born from a passion for stillness',
description: 'HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.', description: 'HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.',
align: 'center', align: 'center',
bgImage: '', imageUrl: '/images/hero.png',
overlay: true overlay: true,
buttonText: 'Discover More',
buttonLink: '/shop'
} }
}, },
{ {
@@ -39,8 +41,8 @@ async function main() {
payload: { payload: {
tagline: 'Our Mission', tagline: 'Our Mission',
title: 'Transforming spaces into sanctuaries of peace', title: 'Transforming spaces into sanctuaries of peace',
content: 'We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.\n\nEvery HomAroma product is designed with intention: the bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.', description: 'We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.\n\nEvery HomAroma product is designed with intention: the bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.',
imageSrc: '', imageUrl: '/images/hero.png',
imageAlt: 'HomAroma Studio, Bangkok', imageAlt: 'HomAroma Studio, Bangkok',
reverse: false reverse: false
} }
@@ -62,7 +64,8 @@ async function main() {
title: 'Ready to find your scent?', title: 'Ready to find your scent?',
description: 'Let our Scent Guide help you discover the perfect blend for your moment.', description: 'Let our Scent Guide help you discover the perfect blend for your moment.',
buttonText: 'Take the Scent Guide', buttonText: 'Take the Scent Guide',
buttonLink: '/scent-guide' buttonLink: '/scent-guide',
imageUrl: '/images/hero.png'
} }
} }
] ]
@@ -98,7 +101,7 @@ async function main() {
{ {
name: 'Products', name: 'Products',
icon: '🌿', icon: '🌿',
items: [ questions: [
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' }, { q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' }, { q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' }, { q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' },
@@ -108,7 +111,7 @@ async function main() {
{ {
name: 'Usage', name: 'Usage',
icon: '💡', icon: '💡',
items: [ questions: [
{ q: 'How do I use a Reed Diffuser?', a: 'Remove the stopper, place the reeds into the bottle, and let them absorb the oil for 1 hour before flipping. For optimal fragrance, flip the reeds every 2–3 days. Keep away from direct sunlight and air conditioning vents for longest lasting performance.' }, { q: 'How do I use a Reed Diffuser?', a: 'Remove the stopper, place the reeds into the bottle, and let them absorb the oil for 1 hour before flipping. For optimal fragrance, flip the reeds every 2–3 days. Keep away from direct sunlight and air conditioning vents for longest lasting performance.' },
{ q: 'Can I mix different scents?', a: 'Yes! Blending scents is an art form we encourage. A general rule: combine no more than 3 scents at a time, using one dominant (60%), one supporting (30%), and one accent (10%). Our Scent Guide can help you find complementary combinations.' }, { q: 'Can I mix different scents?', a: 'Yes! Blending scents is an art form we encourage. A general rule: combine no more than 3 scents at a time, using one dominant (60%), one supporting (30%), and one accent (10%). Our Scent Guide can help you find complementary combinations.' },
{ q: 'How long should I run my aroma diffuser?', a: 'For best results and to avoid olfactory fatigue, we recommend 30–60 minute sessions with 30-minute breaks in between. This also extends product life and prevents overwhelming fragrance build-up in small spaces.' }, { q: 'How long should I run my aroma diffuser?', a: 'For best results and to avoid olfactory fatigue, we recommend 30–60 minute sessions with 30-minute breaks in between. This also extends product life and prevents overwhelming fragrance build-up in small spaces.' },
@@ -117,7 +120,7 @@ async function main() {
{ {
name: 'Shipping & Orders', name: 'Shipping & Orders',
icon: '📦', icon: '📦',
items: [ questions: [
{ q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' }, { q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' },
{ q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' }, { q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' },
{ q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@homaroma.shop with your order number.' }, { q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@homaroma.shop with your order number.' },
@@ -126,7 +129,7 @@ async function main() {
{ {
name: 'Scent Guide', name: 'Scent Guide',
icon: '🧪', icon: '🧪',
items: [ questions: [
{ q: 'How do I choose the right scent for my space?', a: 'Consider the function of the space first. Bedrooms benefit from calming scents like lavender and chamomile. Living rooms work well with warm, welcoming notes like sandalwood and vanilla. Home offices pair best with focus-enhancing eucalyptus and citrus. Use our interactive Scent Guide for personalized recommendations.' }, { q: 'How do I choose the right scent for my space?', a: 'Consider the function of the space first. Bedrooms benefit from calming scents like lavender and chamomile. Living rooms work well with warm, welcoming notes like sandalwood and vanilla. Home offices pair best with focus-enhancing eucalyptus and citrus. Use our interactive Scent Guide for personalized recommendations.' },
{ q: 'Can I request custom scent blending?', a: 'Yes! Our bespoke blending service is available for orders of 6 units or more. Contact us at least 10 business days before your desired delivery date. Custom blending fees start at ฿500 per unique formula.' }, { q: 'Can I request custom scent blending?', a: 'Yes! Our bespoke blending service is available for orders of 6 units or more. Contact us at least 10 business days before your desired delivery date. Custom blending fees start at ฿500 per unique formula.' },
] ]
@@ -141,7 +144,8 @@ async function main() {
title: 'Still have questions?', title: 'Still have questions?',
description: 'Our team is here to help — usually responds within 2 hours.', description: 'Our team is here to help — usually responds within 2 hours.',
buttonText: 'Contact Us', buttonText: 'Contact Us',
buttonLink: '/contact' buttonLink: '/contact',
imageUrl: '/images/hero.png'
} }
} }
] ]
@@ -173,21 +177,11 @@ async function main() {
type: 'CONTACT_INFO', type: 'CONTACT_INFO',
order: 2, order: 2,
payload: { payload: {
title: 'Reach Us Directly', address: 'Bangkok, Thailand',
items: [ email: 'hello@homaroma.shop',
{ icon: '📧', label: 'Email', value: 'hello@homaroma.shop', href: 'mailto:hello@homaroma.shop' }, line: '@homaroma',
{ icon: '📱', label: 'LINE Official', value: '@homaroma', href: '#' },
{ icon: '📸', label: 'Instagram', value: '@homaroma', href: '#' },
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
],
responseTime: {
title: 'Response Time',
email: '~2 hours',
line: '~30 minutes',
instagram: '~4 hours',
hours: 'Mon–Sat, 9:00–18:00 (ICT)' hours: 'Mon–Sat, 9:00–18:00 (ICT)'
} }
}
}, },
{ {
type: 'CONTACT_FORM', type: 'CONTACT_FORM',
+19 -1
View File
@@ -5,7 +5,10 @@ import {
RichTextPayloadSchema, RichTextPayloadSchema,
ScentNotesBreakdownPayloadSchema, ScentNotesBreakdownPayloadSchema,
QuoteCalloutPayloadSchema, QuoteCalloutPayloadSchema,
ProductCarouselPayloadSchema ProductCarouselPayloadSchema,
AccordionSectionPayloadSchema,
ContactInfoSectionPayloadSchema,
ContactFormSectionPayloadSchema
} from "@homaroma/types"; } from "@homaroma/types";
import { adminOnly } from "../middleware/auth"; import { adminOnly } from "../middleware/auth";
@@ -501,6 +504,21 @@ function validateSectionPayload(type: string, payload: any): string | null {
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break; break;
} }
case 'ACCORDION': {
const res = AccordionSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'CONTACT_INFO': {
const res = ContactInfoSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'CONTACT_FORM': {
const res = ContactFormSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
default: default:
return `Unknown section type: ${type}`; return `Unknown section type: ${type}`;
} }
@@ -12,6 +12,9 @@ import DynamicHeroArticle from './DynamicHeroArticle';
import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown'; import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown';
import DynamicQuoteCallout from './DynamicQuoteCallout'; import DynamicQuoteCallout from './DynamicQuoteCallout';
import DynamicProductCarousel from './DynamicProductCarousel'; import DynamicProductCarousel from './DynamicProductCarousel';
import AccordionSection from './sections/AccordionSection';
import ContactInfoSection from './sections/ContactInfoSection';
import ContactFormSection from './sections/ContactFormSection';
interface DynamicPageRendererProps { interface DynamicPageRendererProps {
sections: PageSection[]; sections: PageSection[];
@@ -63,6 +66,12 @@ export default function DynamicPageRenderer({
return <DynamicQuoteCallout key={section.id} payload={section.payload} />; return <DynamicQuoteCallout key={section.id} payload={section.payload} />;
case 'PRODUCT_CAROUSEL': case 'PRODUCT_CAROUSEL':
return <DynamicProductCarousel key={section.id} payload={section.payload} />; return <DynamicProductCarousel key={section.id} payload={section.payload} />;
case 'ACCORDION':
return <AccordionSection key={section.id} payload={section.payload as any} />;
case 'CONTACT_INFO':
return <ContactInfoSection key={section.id} payload={section.payload as any} />;
case 'CONTACT_FORM':
return <ContactFormSection key={section.id} payload={section.payload as any} />;
default: default:
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`); console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
return null; return null;
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import type { AccordionSectionPayload } from '@homaroma/types';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface AccordionSectionProps {
payload: AccordionSectionPayload;
}
export default function AccordionSection({ payload }: AccordionSectionProps) {
const [openIndex, setOpenIndex] = useState<string | null>(null);
const toggleAccordion = (index: string) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<section className="w-full py-16 px-4 md:px-8 bg-spa-sand/20">
<div className="max-w-4xl mx-auto">
{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}
</h3>
<div className="flex flex-col gap-4">
{category.questions.map((item, qIndex) => {
const indexKey = `${catIndex}-${qIndex}`;
const isOpen = openIndex === indexKey;
return (
<div
key={qIndex}
className="border border-spa-sage/30 rounded-xl bg-white overflow-hidden transition-all duration-300"
>
<button
type="button"
className="w-full flex items-center justify-between p-5 text-left focus:outline-none focus:ring-2 focus:ring-spa-sage/50"
onClick={() => toggleAccordion(indexKey)}
>
<span className="text-lg font-medium text-spa-charcoal pr-4">
{item.q}
</span>
<span className="text-spa-sage flex-shrink-0">
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</span>
</button>
<div
className={`px-5 overflow-hidden transition-all duration-300 ${
isOpen ? 'max-h-96 pb-5 opacity-100' : 'max-h-0 opacity-0'
}`}
>
<p className="text-spa-charcoal/80 leading-relaxed whitespace-pre-wrap">
{item.a}
</p>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</section>
);
}
@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import type { ContactFormSectionPayload } from '@homaroma/types';
import { Send } from 'lucide-react';
interface ContactFormSectionProps {
payload: ContactFormSectionPayload;
}
export default function ContactFormSection({ payload }: ContactFormSectionProps) {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
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="w-full py-20 px-4 md:px-8 bg-spa-sand/30">
<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 || "ติดต่อเรา"}
</h2>
<p className="text-lg text-spa-charcoal/70 max-w-xl mx-auto">
{payload.subtitle || "ส่งข้อความถึงทีมงานของเรา เรายินดีให้บริการและพร้อมตอบทุกข้อสงสัย"}
</p>
</div>
<div className="bg-white rounded-3xl shadow-xl shadow-spa-sand/50 p-8 md:p-12 border border-spa-sage/10">
{isSuccess ? (
<div className="text-center py-12">
<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>
</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">
ชื่อ-นามสกุล
</label>
<input
type="text"
id="name"
name="name"
required
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="กรอกชื่อของคุณ"
/>
</div>
<div className="space-y-2">
<label htmlFor="email" className="block text-sm font-medium text-spa-charcoal/80">
อีเมล
</label>
<input
type="email"
id="email"
name="email"
required
value={formData.email}
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="example@email.com"
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="message" className="block text-sm font-medium text-spa-charcoal/80">
ข้อความ
</label>
<textarea
id="message"
name="message"
required
value={formData.message}
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="พิมพ์ข้อความของคุณที่นี่..."
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full py-4 px-6 bg-spa-sage hover:bg-spa-sage-dark text-white rounded-xl font-medium flex items-center justify-center gap-2 transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<span className="w-6 h-6 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
<Send size={18} />
ส่งข้อความ
</>
)}
</button>
</form>
)}
</div>
</div>
</section>
);
}
@@ -0,0 +1,59 @@
import React from 'react';
import type { ContactInfoSectionPayload } from '@homaroma/types';
import { MapPin, Mail, MessageCircle, Clock } from 'lucide-react';
interface ContactInfoSectionProps {
payload: ContactInfoSectionPayload;
}
export default function ContactInfoSection({ payload }: ContactInfoSectionProps) {
return (
<section className="w-full py-16 px-4 md:px-8 bg-white">
<div className="max-w-6xl mx-auto">
<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>
</div>
</div>
</section>
);
}
+31
View File
@@ -390,3 +390,34 @@ export const ScentGuidePageSchema = z.object({
sections: z.array(ScentGuideSectionSchema), sections: z.array(ScentGuideSectionSchema),
}); });
export type ScentGuidePage = z.infer<typeof ScentGuidePageSchema>; export type ScentGuidePage = z.infer<typeof ScentGuidePageSchema>;
export const AccordionSectionPayloadSchema = z.object({
categories: z.array(
z.object({
name: z.string().min(1, "Category name is required"),
icon: z.string().optional(),
questions: z.array(
z.object({
q: z.string().min(1, "Question is required"),
a: z.string().min(1, "Answer is required"),
})
),
})
),
});
export type AccordionSectionPayload = z.infer<typeof AccordionSectionPayloadSchema>;
export const ContactInfoSectionPayloadSchema = z.object({
address: z.string().min(1, "Address is required"),
email: z.string().min(1, "Email is required"),
line: z.string().min(1, "Line ID is required"),
hours: z.string().min(1, "Business hours are required"),
});
export type ContactInfoSectionPayload = z.infer<typeof ContactInfoSectionPayloadSchema>;
export const ContactFormSectionPayloadSchema = z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
});
export type ContactFormSectionPayload = z.infer<typeof ContactFormSectionPayloadSchema>;