diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
index 1f357a0..b902569 100644
--- a/apps/admin/src/App.tsx
+++ b/apps/admin/src/App.tsx
@@ -8,6 +8,7 @@ import ProductCmsPage from './pages/ProductCmsPage'
import CategoriesPage from './pages/CategoriesPage'
import CustomersPage from './pages/CustomersPage'
import PageBuilderPage from './pages/PageBuilderPage'
+import CmsMockupPage from './pages/CmsMockupPage'
import RawMaterialsPage from './pages/RawMaterialsPage'
import LoginPage from './pages/LoginPage'
import ProtectedRoute from './components/ProtectedRoute'
@@ -64,6 +65,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/admin/src/components/Sidebar.tsx b/apps/admin/src/components/Sidebar.tsx
index 1783b1e..fc999f9 100644
--- a/apps/admin/src/components/Sidebar.tsx
+++ b/apps/admin/src/components/Sidebar.tsx
@@ -26,6 +26,7 @@ import {
FileText,
Wrench,
ChevronDown,
+ Sparkles,
} from 'lucide-react'
const MAIN_MENU = [
@@ -39,6 +40,7 @@ const MAIN_MENU = [
{ icon: Ticket, label: 'Promotions', to: '/promotions' },
{ icon: Ticket, label: 'Coupons', to: '/coupons' },
{ icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' },
+ { icon: Sparkles, label: 'CMS Block Showcase', to: '/cms-mockup' },
{ icon: ImageIcon, label: 'Media Library', to: '/media' },
{ icon: BookOpen, label: 'Scent Guide', to: '/articles' },
{ icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' },
diff --git a/apps/admin/src/components/page-builder/EditorialTileForm.tsx b/apps/admin/src/components/page-builder/EditorialTileForm.tsx
index 72d4f5e..c3b798e 100644
--- a/apps/admin/src/components/page-builder/EditorialTileForm.tsx
+++ b/apps/admin/src/components/page-builder/EditorialTileForm.tsx
@@ -78,6 +78,24 @@ export default function EditorialTileForm({
Right
+
+ Image Ratio (สัดส่วนรูปภาพ)
+
+ setEditingSection({
+ ...editingSection,
+ payload: { ...(editingSection.payload || {}), imageAspectRatio: e.target.value }
+ })
+ }
+ className="admin-input w-full bg-slate-900 border-white/10"
+ >
+ Square 1:1 (จัตุรัส)
+ Portrait 3:4 (แนวสูง 3:4)
+ Tall Fashion 2:3 (แนวสูง 2:3)
+ Natural / Original (รูปต้นฉบับ)
+
+
Background Color
React.ReactNode;
+}
+
+export default function CmsMockupPage() {
+ const [activeCategory, setActiveCategory] = useState('all');
+ const [copiedType, setCopiedType] = useState
(null);
+ const [showJsonFor, setShowJsonFor] = useState(null);
+
+ const copyPayload = (type: string, payload: any) => {
+ navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
+ setCopiedType(type);
+ setTimeout(() => setCopiedType(null), 2000);
+ };
+
+ const mockBlocks: MockBlock[] = [
+ // 1. ANNOUNCEMENT_BAR
+ {
+ type: 'ANNOUNCEMENT_BAR',
+ category: 'hero',
+ description: 'Top notice bar with custom styling and scrolling announcements.',
+ payload: {
+ slides: ['[FREE SHIPPING] on orders over ฿1,500', 'New Premium Collection is out now!'],
+ backgroundColor: '#1F3D2D',
+ textColor: '#FDFDF9',
+ speed: 3000
+ },
+ renderPreview: () => (
+
+ ✨ [FREE SHIPPING] on orders over ฿1,500 | New Premium Collection is out now!
+
+ )
+ },
+ // 2. HERO
+ {
+ type: 'HERO',
+ category: 'hero',
+ description: 'Premium full-screen cover with headline, background image, and CTA.',
+ payload: {
+ tagline: 'Vibrant Elegance',
+ title: 'New Scent [Sanctuary]',
+ description: 'Transform your room into a peaceful escape.',
+ imageUrl: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=800&q=80',
+ buttonText: 'Explore Collection',
+ buttonLink: '/shop'
+ },
+ renderPreview: () => (
+
+
+
Vibrant Elegance
+
New Scent [Sanctuary]
+
Transform your room into a peaceful escape.
+
+ Explore Collection
+
+
+
+ )
+ },
+ // 3. HERO_V2
+ {
+ type: 'HERO_V2',
+ category: 'hero',
+ description: 'Split Layout Hero featuring text on one side and a premium image card on the other.',
+ payload: {
+ tagline: 'Pure Wellness',
+ title: 'The Organic [Reed Diffuser]',
+ description: 'Crafted with hand-harvested lavender and sweet orange essential oils.',
+ imageUrl: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=800&q=80',
+ buttonText: 'Shop Diffusers',
+ buttonLink: '/shop/diffusers',
+ imagePosition: 'right'
+ },
+ renderPreview: () => (
+
+
+
Pure Wellness
+
The Organic [Reed Diffuser]
+
Crafted with hand-harvested lavender and sweet orange essential oils.
+
+ Shop Diffusers
+
+
+
+
+
+
+ )
+ },
+ // 4. USP_BAR
+ {
+ type: 'USP_BAR',
+ category: 'hero',
+ description: 'Unique Selling Proposition Bar highlighting product perks (Icons & Labels).',
+ payload: {
+ items: [
+ { icon: 'ShieldCheck', title: '100% Organic', desc: 'Certificated natural oils' },
+ { icon: 'Truck', title: 'Free Shipping', desc: 'On orders above ฿1,500' },
+ { icon: 'RotateCcw', title: 'Easy Returns', desc: '30-day money back guarantee' }
+ ]
+ },
+ renderPreview: () => (
+
+
+
+
100% Organic
+
Certificated natural oils
+
+
+
+
Free Shipping
+
On orders above ฿1,500
+
+
+
+
Easy Returns
+
30-day money back guarantee
+
+
+ )
+ },
+ // 5. BANNER
+ {
+ type: 'BANNER',
+ category: 'hero',
+ description: 'Clickable promotional block with headline overlay, ideal for campaigns.',
+ payload: {
+ imageUrl: 'https://images.unsplash.com/photo-1540555700478-4be289fbecef?auto=format&fit=crop&w=800&q=80',
+ linkUrl: '/promotions',
+ title: 'Mid-Year Secret Sale',
+ description: 'Get up to 30% off on premium scent reed diffusers.',
+ buttonText: 'Shop Sale'
+ },
+ renderPreview: () => (
+
+
+
Mid-Year Secret Sale
+
Get up to 30% off on premium scent reed diffusers.
+
+ Shop Sale
+
+
+
+ )
+ },
+ // 6. CATEGORIES
+ {
+ type: 'CATEGORIES',
+ category: 'products',
+ description: 'Visual category grid showcase to guide users to specific collections.',
+ payload: {
+ title: 'Shop by Collection',
+ subtitle: 'Choose your preferred sensory style',
+ categoryIds: [1, 2, 3]
+ },
+ renderPreview: () => (
+
+
+
Shop by Collection
+
Choose your preferred sensory style
+
+
+ {[
+ { name: 'Essential Oils', img: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=200&q=80' },
+ { name: 'Reed Diffusers', img: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=200&q=80' },
+ { name: 'Room Sprays', img: 'https://images.unsplash.com/photo-1612817288484-6f916006741a?auto=format&fit=crop&w=200&q=80' }
+ ].map((cat, i) => (
+
+
+
+ {cat.name}
+
+
+ ))}
+
+
+ )
+ },
+ // 7. PRODUCTS
+ {
+ type: 'PRODUCTS',
+ category: 'products',
+ description: 'Showcase grid or carousel for featured products, brand items, or a customized list with Quick Shop option.',
+ payload: {
+ title: 'Our Best Sellers',
+ subtitle: 'Hand-picked organic favorites',
+ type: 'featured',
+ limit: 3,
+ layout: 'grid',
+ enableQuickShop: true
+ },
+ renderPreview: () => (
+
+
+
Our Best Sellers
+
Hand-picked organic favorites
+
+
+ {[
+ { name: 'Ozone Ambient Blend', price: '฿850', img: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=200&q=80' },
+ { name: 'Lavender Sanctuary Diffuser', price: '฿1,290', img: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=200&q=80' },
+ { name: 'Citrus Zest Room Mist', price: '฿450', img: 'https://images.unsplash.com/photo-1612817288484-6f916006741a?auto=format&fit=crop&w=200&q=80' }
+ ].map((prod, i) => (
+
+
+
+
+
+
{prod.name}
+
{prod.price}
+
+
+ Quick Add
+
+
+ ))}
+
+
+ )
+ },
+ // 8. PRODUCT_CAROUSEL
+ {
+ type: 'PRODUCT_CAROUSEL',
+ category: 'products',
+ description: 'Swipeable horizontal card carousel displaying specific product collections.',
+ payload: {
+ title: 'Featured Collection',
+ description: 'Selected scents for wellness journeys.',
+ collectionId: 'wellness-campaign',
+ limit: 5,
+ showPrice: true,
+ showAddToCart: true
+ },
+ renderPreview: () => (
+
+
+
+
Featured Collection
+
Selected scents for wellness journeys
+
+
Slide next →
+
+
+ {[
+ { name: 'Ozone Ambient', price: '฿850', img: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=150&q=80' },
+ { name: 'Lavender Sanctuary', price: '฿1,290', img: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=150&q=80' },
+ { name: 'Citrus Zest Mist', price: '฿450', img: 'https://images.unsplash.com/photo-1612817288484-6f916006741a?auto=format&fit=crop&w=150&q=80' },
+ { name: 'Eucalyptus Mint', price: '฿890', img: 'https://images.unsplash.com/photo-1540555700478-4be289fbecef?auto=format&fit=crop&w=150&q=80' }
+ ].map((prod, i) => (
+
+
+
+
+
+
{prod.name}
+
{prod.price}
+
+
+ ))}
+
+
+ )
+ },
+ // 9. RELATED_PRODUCTS
+ {
+ type: 'RELATED_PRODUCTS',
+ category: 'products',
+ description: 'Dynamic block recommending items from the same category for the product page layout.',
+ payload: {
+ title: 'You May Also Like',
+ showHeading: true,
+ showViewAllLink: true
+ },
+ renderPreview: () => (
+
+
+
You May Also Like
+ View All
+
+
+
+
+
+
+
+
Ozone Ambient Blend
+
฿850
+
+
+
+
+
+
+
+
Lavender sanctuary
+
฿1,290
+
+
+
+
+ )
+ },
+ // 10. EDITORIAL_TILE
+ {
+ type: 'EDITORIAL_TILE',
+ category: 'editorial',
+ description: 'Stunning editorial grid block presenting storytelling text alongside a curated campaign photo.',
+ payload: {
+ title: 'Moodboard [IRL]',
+ description: 'An expressive mix of texture, shape and movement. Deconstructed details, utilitarian elements and fluid tailoring come together in looks that feel personal.',
+ imageUrl: 'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?auto=format&fit=crop&w=800&q=80',
+ imagePosition: 'left',
+ imageAspectRatio: 'tall',
+ buttonText: 'Shop the looks',
+ buttonLink: '/shop',
+ backgroundColor: '#1E293B',
+ textColor: '#FFFFFF'
+ },
+ renderPreview: () => (
+
+
+
+
+
+
Crafted with [Sensory Passion]
+
+ Every formulation at HomAroma is a tribute to pure botanical essences, hand-blended to capture nature’s quiet intelligence.
+
+
+ Read Our Story
+
+
+
+ )
+ },
+ // 11. EDITORIAL_CAROUSEL
+ {
+ type: 'EDITORIAL_CAROUSEL',
+ category: 'editorial',
+ description: 'Stories and values slider displaying image slideshows side-by-side with localized editorial statements.',
+ payload: {
+ title: 'The Wellness Philosophy',
+ slides: [
+ {
+ category: 'Heritage',
+ title: 'Rooted in Nature',
+ description: 'Our ingredients are sustainably sourced from organic growers who share our respect for ecological balance.',
+ imageUrl: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=400&q=80',
+ buttonText: 'Discover Sourcing',
+ buttonLink: '/sourcing'
+ }
+ ]
+ },
+ renderPreview: () => (
+
+
The Wellness Philosophy
+
+
+
+
+
+
Heritage
+
Rooted in Nature
+
Our ingredients are sustainably sourced from organic growers who share our respect for ecological balance.
+
Discover Sourcing →
+
+
+
+ )
+ },
+ // 12. SCENT_NOTES_BREAKDOWN
+ {
+ type: 'SCENT_NOTES_BREAKDOWN',
+ category: 'editorial',
+ description: 'Fragrance profile breakdowns containing Top, Heart/Middle, and Base notes lists.',
+ payload: {
+ title: 'Scent Signature Breakdown',
+ description: 'Detailed analysis of fragrance volatility levels.',
+ topNotes: ['Italian Bergamot', 'Citrus Peel'],
+ heartNotes: ['Jasmine Absolute', 'French Lavender'],
+ baseNotes: ['Sandalwood', 'Clean Musk']
+ },
+ renderPreview: () => (
+
+
+
Scent Signature Breakdown
+
Detailed analysis of fragrance volatility levels
+
+
+
+
Top
+
Italian Bergamot, Citrus Peel
+
+
+
Heart
+
Jasmine Absolute, French Lavender
+
+
+
Base
+
Sandalwood, Clean Musk
+
+
+
+ )
+ },
+ // 13. QUOTE_CALLOUT
+ {
+ type: 'QUOTE_CALLOUT',
+ category: 'editorial',
+ description: 'Elegant text banner presenting callout quotations, ideal for customer testimonials.',
+ payload: {
+ quote: 'Fragrance is the invisible, unforgettable fashion accessory.',
+ author: 'Coco Chanel',
+ role: 'Fashion Designer'
+ },
+ renderPreview: () => (
+
+
“
+
+ "Fragrance is the invisible, unforgettable fashion accessory."
+
+
+ — Coco Chanel | Fashion Designer
+
+
+ )
+ },
+ // 14. TEXT
+ {
+ type: 'TEXT',
+ category: 'editorial',
+ description: 'Plain text component for headers, centered paragraphs, or brand catchphrases.',
+ payload: {
+ title: 'Natural & Ecological Policy',
+ content: 'We use 100% natural, botanically derived ingredients in all formulations. Cruelty-free.',
+ textAlign: 'center'
+ },
+ renderPreview: () => (
+
+
Natural & Ecological Policy
+
+ We use 100% natural, botanically derived ingredients in all formulations. Cruelty-free.
+
+
+ )
+ },
+ // 15. RICH_TEXT
+ {
+ type: 'RICH_TEXT',
+ category: 'editorial',
+ description: 'Styled markdown block designed for longer stories or HTML embedding.',
+ payload: {
+ title: 'Corporate Social Responsibility',
+ content: 'Our commitments extend beyond scent creation. HomAroma proudly partners with rural farming families in Northern Thailand to source organic lemongrass oil.
Sustainable development goals Fair-trade wages '
+ },
+ renderPreview: () => (
+
+
Corporate Social Responsibility
+
+
Our commitments extend beyond scent creation. HomAroma proudly partners with rural farming families in Northern Thailand to source organic lemongrass oil.
+
+ Sustainable development goals
+ Fair-trade wages
+
+
+
+ )
+ },
+ // 16. HERO_ARTICLE
+ {
+ type: 'HERO_ARTICLE',
+ category: 'articles',
+ description: 'Editorial article header with background blur, date, author name, and title overlays.',
+ payload: {
+ title: 'Fragrance & Mindfulness Journey',
+ subtitle: 'How ambient scent transforms focus and breathing cycles.',
+ author: 'Dr. Aromatherapist',
+ category: 'Insights',
+ publishDate: '2026-07-27',
+ imageUrl: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=600&q=80'
+ },
+ renderPreview: () => (
+
+
+
+ Insights
+ •
+ 2026-07-27
+
+
Fragrance & Mindfulness Journey
+
How ambient scent transforms focus and breathing cycles.
+
Written by Dr. Aromatherapist
+
+
+ )
+ },
+ // 17. ARTICLE_HEADER
+ {
+ type: 'ARTICLE_HEADER',
+ category: 'articles',
+ description: 'Introductory page header for standard text articles.',
+ payload: {
+ title: 'The Chemistry of Volatility',
+ subtitle: 'Understanding high, middle, and low evaporation rates of pure aromatherapy elements.'
+ },
+ renderPreview: () => (
+
+
The Chemistry of Volatility
+
Understanding high, middle, and low evaporation rates of pure aromatherapy elements.
+
+ )
+ },
+ // 18. ARTICLE_MEDIA
+ {
+ type: 'ARTICLE_MEDIA',
+ category: 'articles',
+ description: 'Article paragraph paired with a side image (left/right alignment).',
+ payload: {
+ heading: 'Sourcing Lavender Extract',
+ body: 'Our lavender is harvested at dawn in Provence when oil concentrations are highest, then steam-distilled locally within 4 hours.',
+ imagePosition: 'right',
+ imageUrl: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=100&q=80'
+ },
+ renderPreview: () => (
+
+
+
Sourcing Lavender Extract
+
+ Our lavender is harvested at dawn in Provence when oil concentrations are highest, then steam-distilled locally within 4 hours.
+
+
+
+
+
+
+ )
+ },
+ // 19. ARTICLE_GALLERY
+ {
+ type: 'ARTICLE_GALLERY',
+ category: 'articles',
+ description: 'Photo collection grids to show off multiple illustrations or steps.',
+ payload: {
+ layout: 'grid',
+ images: [
+ { imageUrl: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=200&q=80' },
+ { imageUrl: 'https://images.unsplash.com/photo-1547887537-6158d64c35b3?auto=format&fit=crop&w=200&q=80' }
+ ]
+ },
+ renderPreview: () => (
+
+
+
+
+
+
+
+
+ )
+ },
+ // 20. ARTICLE_FACTS
+ {
+ type: 'ARTICLE_FACTS',
+ category: 'articles',
+ description: 'Quick summaries or key takeaway bullets for editorial pieces.',
+ payload: {
+ title: 'Key Scent Benefits',
+ items: [
+ { title: 'Relaxation', description: 'Proven scent to lower blood pressure and cortisol levels.' },
+ { title: 'Focus Boost', description: 'Peppermint elements trigger sensory wakefulness.' }
+ ]
+ },
+ renderPreview: () => (
+
+
+ Key Scent Benefits
+
+
+
+ Relaxation: Proven scent to lower blood pressure and cortisol levels.
+
+
+ Focus Boost: Peppermint elements trigger sensory wakefulness.
+
+
+
+ )
+ },
+ // 21. ARTICLE_VIDEO
+ {
+ type: 'ARTICLE_VIDEO',
+ category: 'articles',
+ description: 'Embedded video player wrapper for articles.',
+ payload: {
+ videoUrl: 'https://www.youtube.com/watch?v=mock',
+ aspectRatio: '16:9'
+ },
+ renderPreview: () => (
+
+
+
+
Watch Youtube Video Preview
+
+ )
+ },
+ // 22. ARTICLE_REFERENCES
+ {
+ type: 'ARTICLE_REFERENCES',
+ category: 'articles',
+ description: 'Citation list or scientific reference cards.',
+ payload: {
+ title: 'Study Sources',
+ items: [
+ { title: 'Journal of Aromatherapy Research (Vol. 12)', link: 'https://example.com' },
+ { title: 'Clinical Scent Testing & Sleep Induction (2024)', link: 'https://example.com' }
+ ]
+ },
+ renderPreview: () => (
+
+ )
+ },
+ // 23. ARTICLE_TABLE
+ {
+ type: 'ARTICLE_TABLE',
+ category: 'articles',
+ description: 'Comparison table displaying technical parameters or specs.',
+ payload: {
+ title: 'Volatility Comparison',
+ columns: ['Fragrance Type', 'Top Note Evaporation', 'Average Life'],
+ rows: [
+ { label: 'Essential Oil', cells: ['High (Fast)', '4-6 hours'] },
+ { label: 'Carrier Blend', cells: ['Low (Slow)', '24-48 hours'] }
+ ]
+ },
+ renderPreview: () => (
+
+
+
+
+ Fragrance Type
+ Top Note Evaporation
+ Average Life
+
+
+
+
+ Essential Oil
+ High (Fast)
+ 4-6 hours
+
+
+ Carrier Blend
+ Low (Slow)
+ 24-48 hours
+
+
+
+
+ )
+ },
+ // 24. ARTICLE_DOWNLOAD
+ {
+ type: 'ARTICLE_DOWNLOAD',
+ category: 'articles',
+ description: 'File attachments download cards (PDF guides, catalogues).',
+ payload: {
+ title: 'Available Resources',
+ files: [
+ { title: 'Organic Certification PDF', fileType: 'PDF', url: '#' },
+ { title: 'Aromatherapy Scent Catalogue', fileType: 'PDF', url: '#' }
+ ]
+ },
+ renderPreview: () => (
+
+
Available Resources
+
+ {[
+ { title: 'Organic Certification PDF', size: '2.4 MB' },
+ { title: 'Scent Catalogue', size: '4.8 MB' }
+ ].map((f, i) => (
+
+
+
{f.title}
+
{f.size} • PDF
+
+
+
+
+
+ ))}
+
+
+ )
+ },
+ // 25. ARTICLE_CTA
+ {
+ type: 'ARTICLE_CTA',
+ category: 'articles',
+ description: 'Call-to-action blocks prompting next-step user clicks.',
+ payload: {
+ title: 'Ready to start your wellness journey?',
+ primaryText: 'Explore Collection',
+ primaryLink: '/shop',
+ alignment: 'center'
+ },
+ renderPreview: () => (
+
+
Ready to start your wellness journey?
+
+ Explore Collection
+
+
+ )
+ },
+ // 26. NEWSLETTER
+ {
+ type: 'NEWSLETTER',
+ category: 'forms',
+ description: 'Email subscription block to attract newsletter sign-ups.',
+ payload: {
+ title: 'Begin Your Wellness Journey',
+ description: 'Subscribe to receive 10% off your first purchase and exclusive updates.',
+ buttonText: 'Subscribe',
+ placeholderText: 'Enter your email address',
+ showPrivacyCheckbox: true,
+ privacyText: 'I agree to the privacy policy.'
+ },
+ renderPreview: () => (
+
+
+
Begin Your Wellness Journey
+
Subscribe to receive 10% off your first purchase and exclusive updates.
+
+
+
+
+ Subscribe
+
+
+
+
+ I agree to the privacy policy.
+
+
+ )
+ },
+ // 27. ACCORDION
+ {
+ type: 'ACCORDION',
+ category: 'forms',
+ description: 'Question-answer accordions for FAQs, divided by custom categories.',
+ payload: {
+ title: 'Frequently Asked Questions',
+ categories: [
+ {
+ name: 'General Policy',
+ icon: 'HelpCircle',
+ questions: [
+ { q: 'How long do Reed Diffusers last?', a: 'Normally, they evaporate gradually over 4 to 6 weeks depending on temperature levels.' },
+ { q: 'Are essential oils safe for pets?', a: 'Some oils are toxic to dogs/cats. Please consult a veterinarian before diffusing.' }
+ ]
+ }
+ ]
+ },
+ renderPreview: () => (
+
+
+ Frequently Asked Questions (General Policy)
+
+
+ {[
+ { q: 'How long do Reed Diffusers last?', a: 'Normally, they evaporate gradually over 4 to 6 weeks depending on temperature levels.' },
+ { q: 'Are essential oils safe for pets?', a: 'Some oils are toxic to dogs/cats. Please consult a veterinarian before diffusing.' }
+ ].map((faq, i) => (
+
+
+ {faq.q}
+
+
+
{faq.a}
+
+ ))}
+
+
+ )
+ },
+ // 28. CONTACT_INFO
+ {
+ type: 'CONTACT_INFO',
+ category: 'forms',
+ description: 'Beautiful information box showing address, telephone, and social details.',
+ payload: {
+ title: 'Contact Details',
+ address: '123 Sukhumvit Rd, Bangkok, Thailand',
+ email: 'hello@homaroma.com',
+ line: '@homaroma',
+ hours: 'Mon-Fri: 9AM - 6PM'
+ },
+ renderPreview: () => (
+
+
Contact Details
+
+
+
+ 123 Sukhumvit Rd, Bangkok
+
+
+
+ hello@homaroma.com
+
+
+
+
+ Mon-Fri: 9AM - 6PM
+
+
+
+ )
+ },
+ // 29. CONTACT_FORM
+ {
+ type: 'CONTACT_FORM',
+ category: 'forms',
+ description: 'General support email inquiry contact form.',
+ payload: {
+ title: 'Get In Touch',
+ subtitle: 'We would love to hear from you.'
+ },
+ renderPreview: () => (
+
+
+
Get In Touch
+
We would love to hear from you.
+
+
+
+
+
+
+ Send Message
+
+
+
+ )
+ }
+ ];
+
+ const filteredBlocks = activeCategory === 'all'
+ ? mockBlocks
+ : mockBlocks.filter(b => b.category === activeCategory);
+
+ return (
+
+ {/* Page Header */}
+
+
+
+
+ CMS Blocks Showcase
+
+
+ Visual preview and template schema payloads for all available Page Builder blocks.
+
+
+
+
+ {/* Categories Filter Tabs */}
+
+ {CATEGORIES.map((cat) => (
+ setActiveCategory(cat.id)}
+ className={`px-5 py-2 text-xs font-bold transition-all rounded-xl whitespace-nowrap ${
+ activeCategory === cat.id
+ ? 'bg-cyan-glow text-night-900 shadow-lg shadow-cyan-glow/20'
+ : 'text-slate-400 hover:text-white hover:bg-white/5'
+ }`}
+ >
+ {cat.name}
+
+ ))}
+
+
+ {/* Blocks Listing Grid */}
+
+ {filteredBlocks.map((block) => (
+
+ {/* Block Header Info */}
+
+
+
+
+ {block.type}
+
+
+ {block.category}
+
+
+
{block.description}
+
+
+ {/* Action buttons */}
+
+ setShowJsonFor(showJsonFor === block.type ? null : block.type)}
+ className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 ${
+ showJsonFor === block.type ? 'bg-cyan-glow text-night-900 border-cyan-glow/30' : 'bg-white/5 hover:bg-white/10 text-slate-300'
+ }`}
+ >
+
+ {showJsonFor === block.type ? 'Hide Payload' : 'View Payload'}
+
+ copyPayload(block.type, block.payload)}
+ className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 ${
+ copiedType === block.type ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10 text-slate-300'
+ }`}
+ >
+ {copiedType === block.type ? : }
+ {copiedType === block.type ? 'Copied' : 'Copy Payload'}
+
+
+
+
+ {/* JSON Code view */}
+ {showJsonFor === block.type && (
+
+
{JSON.stringify(block.payload, null, 2)}
+
+ )}
+
+ {/* Live Visual Preview Frame */}
+
+
+ {block.renderPreview()}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/admin/src/pages/PageBuilderPage.tsx b/apps/admin/src/pages/PageBuilderPage.tsx
index 3283a6a..fce1033 100644
--- a/apps/admin/src/pages/PageBuilderPage.tsx
+++ b/apps/admin/src/pages/PageBuilderPage.tsx
@@ -13,7 +13,9 @@ import {
X,
Settings2,
CheckCircle2,
- ExternalLink
+ ExternalLink,
+ Copy,
+ Check
} from 'lucide-react';
import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types';
import { getAdminDisplayString } from '../utils/localization';
@@ -84,6 +86,7 @@ export default function PageBuilderPage() {
const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide' | 'gallery'; index?: number } | null>(null);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
const [isSavingPageSettings, setIsSavingPageSettings] = useState(false);
+ const [isCopied, setIsCopied] = useState(false);
const [isReorderModalOpen, setIsReorderModalOpen] = useState(false);
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
@@ -943,6 +946,35 @@ export default function PageBuilderPage() {
/>
+
+
+ Page URL
+
+
+ (e.target as HTMLInputElement).select()}
+ />
+ {
+ navigator.clipboard.writeText(getPageUrl(page.slug));
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 2000);
+ }}
+ className={`p-2.5 rounded-xl border border-white/10 text-white transition-all flex items-center justify-center ${
+ isCopied ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10'
+ }`}
+ title="Copy page URL"
+ >
+ {isCopied ? : }
+
+
+
+
= {
EDITORIAL_TILE: {
type: "EDITORIAL_TILE",
requiredFields: ["title", "description", "imageUrl", "imagePosition"],
- optionalFields: ["buttonText", "buttonLink", "backgroundColor", "textColor"],
- renderedFields: ["title", "description", "imageUrl", "imagePosition", "buttonText", "buttonLink", "backgroundColor", "textColor"],
+ optionalFields: ["buttonText", "buttonLink", "backgroundColor", "textColor", "imageAspectRatio"],
+ renderedFields: ["title", "description", "imageUrl", "imagePosition", "imageAspectRatio", "buttonText", "buttonLink", "backgroundColor", "textColor"],
storedOnlyFields: [],
discoveryToolsNeeded: [],
- warnings: ["imagePosition must be left or right."],
+ warnings: ["imagePosition must be left or right.", "imageAspectRatio can be square, portrait, tall, or auto."],
minimalPayload: {
title: localizedObjectExample,
description: localizedObjectExample,
@@ -970,6 +970,7 @@ const SECTION_TOOL_CONFIGS = [
sectionId: { type: "number", description: "The numeric ID of the section to update" },
imageUrl: { type: "string", description: "Plain string main image URL" },
imagePosition: { type: "string", description: "Plain string image position ('left' or 'right')" },
+ imageAspectRatio: { type: "string", description: "Plain string image aspect ratio ('square', 'portrait', 'tall', or 'auto')" },
title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` },
description: { ...localizedStringSchema, description: `Localized description, for example ${localizedStringExample}` },
buttonText: { ...localizedStringSchema, description: `Localized button text, for example ${localizedStringExample}` },
diff --git a/apps/spa/src/components/DynamicEditorialTile.tsx b/apps/spa/src/components/DynamicEditorialTile.tsx
index a084057..3a8cc22 100644
--- a/apps/spa/src/components/DynamicEditorialTile.tsx
+++ b/apps/spa/src/components/DynamicEditorialTile.tsx
@@ -13,12 +13,27 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
const {
imageUrl,
imagePosition = 'left',
+ imageAspectRatio = 'square',
buttonLink
} = payload;
const title = getLocalizedField(payload.title, lang);
const description = getLocalizedField(payload.description, lang);
const buttonText = getLocalizedField(payload.buttonText, lang);
+ const getAspectClass = (ratio?: string) => {
+ switch (ratio) {
+ case 'portrait':
+ return 'aspect-[3/4]';
+ case 'tall':
+ return 'aspect-[2/3]';
+ case 'auto':
+ return 'aspect-auto';
+ case 'square':
+ default:
+ return 'aspect-square';
+ }
+ };
+
const renderTitle = (text: string) => {
if (!text) return null;
return text.split('[').map((part, i) => {
@@ -43,7 +58,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
>
-
{
+ switch (ratio) {
+ case 'portrait':
+ return 'aspect-[3/4]'
+ case 'tall':
+ return 'aspect-[2/3]'
+ case 'auto':
+ return 'aspect-auto'
+ case 'square':
+ default:
+ return 'aspect-square'
+ }
+ }
+
return (