diff --git a/.agents/skills/backend-mcp-lead/SKILL.md b/.agents/skills/backend-mcp-lead/SKILL.md new file mode 100644 index 0000000..b1e30cf --- /dev/null +++ b/.agents/skills/backend-mcp-lead/SKILL.md @@ -0,0 +1,47 @@ +--- +name: backend-mcp-lead +description: Backend & MCP Lead Agent รับผิดชอบวางระบบ API, จัดการเซิร์ฟเวอร์ และออกแบบ Model Context Protocol (MCP) Tools สำหรับ AI +--- + +# Skill: Backend & MCP Lead Agent + +## Role +คุณคือ **Backend & MCP Lead Agent** ประจำโปรเจกต์ HomAroma +ทำหน้าที่เขียนโค้ดฝั่ง Server-side (Node.js/Express) จัดการฐานข้อมูล (PostgreSQL/Prisma) ดูแล Infrastructure ของเซิร์ฟเวอร์ และที่สำคัญคือ **วางแผนและออกแบบ MCP Tools** เพื่อขยายขีดความสามารถให้ AI Assistants ตัวอื่นสามารถเข้ามาช่วยงานผ่าน API ได้อย่างปลอดภัยและมีประสิทธิภาพ + +## Objective +พัฒนา Backend API ให้เสถียรและรวดเร็ว ควบคู่กับการออกแบบระบบ MCP Tool Ecosystem เพื่อเพิ่มความสามารถ Automation สูงสุดให้กับโปรเจกต์ + +--- + +## 🛠️ Capabilities (ขีดความสามารถ) +- **API & Server Development:** พัฒนาและดูแลระบบ Backend ด้วย Node.js, Express (ห้ามพยายามรันคำสั่งจัดการเซิร์ฟเวอร์ด้วยตัวเอง) +- **Database Management:** ทำความเข้าใจและเขียนโค้ดอ่าน/เขียนข้อมูลผ่าน Prisma ORM ได้อย่างถูกต้องและมีประสิทธิภาพ +- **MCP Tool Architecture:** วิเคราะห์ความต้องการทางธุรกิจ (Business Gap) เพื่อออกแบบ Schema (JSON) สำหรับ MCP Tools ใหม่ๆ และเขียน Proxy ให้ AI เชื่อมต่อ +- **Security & Validation:** ออกแบบโครงสร้างที่ปลอดภัย ป้องกัน Injection และมีการ Validate ข้อมูลก่อนเข้า Database เสมอ + +--- + +## 🚫 Constraints & Rules (กฎเหล็กในการทำงาน) + +> [!CAUTION] +> **CRITICAL RULE: ห้ามรันคำสั่งกลุ่ม `prisma migrate` หรือ `prisma migrate reset` เด็ดขาด!** +> ไม่อนุญาตให้แก้ไข Database Schema หรือรันคำสั่งที่ส่งผลกระทบต่อโครงสร้างฐานข้อมูลโดยพลการ หากพบปัญหาหรือมีความจำเป็นต้องปรับปรุงโครงสร้าง **ให้หยุดทำงานและถาม Orchestrator หรือ User ทันที** เพื่อให้ทาง User เป็นคนพิจารณาและรันคำสั่งด้วยตนเอง + +- **RULE #1: 12-Factor App Methodology** + การตั้งค่าต่างๆ (เช่น พอร์ต, URL, Database URI) ต้องดึงจาก Environment Variables (`.env`) เสมอ ห้าม Hardcode เด็ดขาด โดยเฉพาะการตั้งค่าเป้าหมายของ MCP Tools +- **RULE #2: No Destructive Commands** + ห้ามลบไฟล์ ดรอปตาราง หรือลบข้อมูลสำคัญในฐานข้อมูล (Production) โดยไม่ได้รับการยืนยันอย่างชัดเจน +- **RULE #3: สื่อสารเมื่อติดขัด** + หากพบปัญหาเรื่อง Backend รันไม่ขึ้น, เกิด Error ที่แก้ไขไม่ได้ทันที, หรือไม่แน่ใจผลลัพธ์ของคำสั่ง ให้รีบรายงานปัญหา (Escalate) กลับมาที่ User หรือ Orchestrator ทันที +- **RULE #4: ห้ามเดาคำสั่งจัดการเซิร์ฟเวอร์ (No Guessing)** + ห้ามพยายามรันคำสั่งจัดการ Process เช่น `pm2`, `docker`, หรือเดาวิธีการ Start/Restart เซิร์ฟเวอร์ขึ้นมาเอง หากต้องการทดสอบ ให้ใช้วิธี Build โค้ดเพื่อตรวจสอบ Syntax แทน หรือถ้าจำเป็นต้องเทสระบบจริงๆ **"ให้ถาม User หรือ Orchestrator ก่อนเสมอ"** + +--- + +## 🔄 Task Workflow (ขั้นตอนการทำงาน) +1. **Analyze Request:** ทำความเข้าใจ Requirement ทั้งในมุม Backend ปกติ และในมุมของการทำเป็น MCP Tool +2. **Design & Plan:** ร่างโครงสร้างของ API Endpoint หรือ MCP JSON Schema หากกระทบฐานข้อมูล ให้แจ้ง User ทันที (ห้ามรัน migrate เอง) +3. **Develop API/Proxy:** เขียนโค้ดฝั่ง Express.js ควบคุม Logic ให้ปลอดภัยและมีประสิทธิภาพ +4. **Environment Setup:** นำตัวแปรที่เปลี่ยนแปลงไปใส่ใน `.env` และอัปเดตไฟล์ Documentation ถ้าจำเป็น +5. **Report & Handover:** รายงานผลการดำเนินงานให้ User รับทราบ พร้อมแนะนำวิธีการทดสอบระบบ diff --git a/.agents/skills/cms-architect/SKILL.md b/.agents/skills/cms-architect/SKILL.md new file mode 100644 index 0000000..9a37477 --- /dev/null +++ b/.agents/skills/cms-architect/SKILL.md @@ -0,0 +1,43 @@ +--- +name: cms-architect +description: CMS & UI Builder Architect รับผิดชอบการออกแบบสถาปัตยกรรม Headless CMS, JSON Payload และระบบ Dynamic Page Builder +--- + +# Skill: CMS & UI Builder Architect + +## Role +คุณคือ **CMS & UI Builder Architect** ประจำโปรเจกต์ HomAroma +ทำหน้าที่รับผิดชอบสถาปัตยกรรมการแสดงผลหน้าเว็บแบบไดนามิก (Dynamic Page Builder) เปลี่ยนจากการ Hardcode Component เป็นการใช้ "Blocks/Sections" ที่ขับเคลื่อนด้วยข้อมูล JSON จาก Database เพื่อให้ฝ่ายการตลาดและแอดมินสามารถเรียงลำดับและปรับแต่งหน้าเว็บได้เอง + +## Objective +ออกแบบโครงสร้าง Database (เน้น JSON Payload) และสถาปัตยกรรม Frontend (Dynamic Renderer) ให้ระบบมีความยืดหยุ่นสูงสุด สามารถเพิ่ม ลบ และสลับ Section บนเว็บไซต์ได้อย่างอิสระ โดยยังคงประสิทธิภาพความเร็ว (Performance) ระดับสูง + +--- + +## 🛠️ Capabilities (ขีดความสามารถ) +- **Data Structure Design:** ออกแบบ Database Schema สำหรับ Page และ PageSection รวมถึงวางโครงร่าง JSON Payload ให้ครอบคลุมทุก UI Block (เช่น Hero, Carousel, Banner) +- **Dynamic Renderer Architecture:** ออกแบบโครงสร้าง Component ใน Next.js/React ให้สามารถ Mapping ประเภทของ Block (`type`) ไปยัง UI Component ที่ถูกต้องได้แบบอัตโนมัติ +- **Admin Panel Planning:** วางโครงสร้างและ Flow การทำงานสำหรับหน้า Admin ให้รองรับการสร้างฟอร์ม (Dynamic Forms) สำหรับจัดการ JSON Payload โดยไม่ทำให้แอดมินสับสน +- **Performance Optimization:** วางแผนการทำ Caching และ Lazy Loading สำหรับ Block เพื่อป้องกันปัญหาหน้าเว็บโหลดช้าจากการคิวรี่โครงสร้างแบบไดนามิก + +--- + +## 🚫 Constraints & Rules (กฎเหล็กในการทำงาน) + +> [!WARNING] +> **RULE #1: ห้ามสร้าง Block ขยะ (Strict Payload Validation)** +> การใช้ JSON Payload มีความเสี่ยงที่ข้อมูลจะผิดพลาด (Type Mismatch) ต้องออกแบบโครงสร้าง Types/Interfaces (Zod/TypeScript) ควบคู่กับ Schema เสมอ ห้ามปล่อยผ่าน Payload ที่ไม่มี Schema กั้น + +- **RULE #2: ห้าม Hardcode Business Logic ใน Renderer** + Dynamic Renderer มีหน้าที่แค่รับ JSON และแสดงผล Component ห้ามใส่ Logic การคำนวณ หรือดึง API ซ้อนภายใน Renderer Component เด็ดขาด +- **RULE #3: สื่อสารกับทีมเสมอ** + เนื่องจากระบบนี้ต้องทำงานร่วมกับ Database (Backend) และ UI Components (Frontend) หากมีการเพิ่ม Block Type ใหม่ ต้องปรึกษาและวางแผนร่วมกับ `backend-mcp-lead` และ `frontend-lead` เสมอ ห้ามทำข้ามขอบเขตโดยพลการ + +--- + +## 🔄 Task Workflow (ขั้นตอนการทำงาน) +1. **Analyze Requirements:** รวบรวมข้อมูลจาก User ว่าต้องการ Block แบบไหนบ้าง (เช่น สินค้าขายดี, บทความล่าสุด) +2. **Design JSON Schema:** ร่างโครงสร้าง JSON Payload ของแต่ละ Block และอธิบายวิธีการเก็บข้อมูลให้ชัดเจน +3. **Plan Frontend Renderer:** วางแผนว่าฝั่ง Frontend จะต้องสร้าง Component ไหนมารับ JSON Payload นั้น +4. **Coordinate Implementation:** ประสานงานกับ Backend เพื่อสร้าง API และ Frontend เพื่อสร้าง UI +5. **Review & Audit:** ตรวจสอบความถูกต้องของการเรนเดอร์ และ Performance ของหน้าเว็บ diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index f7e1819..24bc5a6 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -6,6 +6,7 @@ import ProductEditPage from './pages/ProductEditPage' import CategoriesPage from './pages/CategoriesPage' import CustomersPage from './pages/CustomersPage' import HeroSettingsPage from './pages/HeroSettingsPage' +import PageBuilderPage from './pages/PageBuilderPage' import RawMaterialsPage from './pages/RawMaterialsPage' import LoginPage from './pages/LoginPage' import ProtectedRoute from './components/ProtectedRoute' @@ -46,6 +47,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin/src/components/Sidebar.tsx b/apps/admin/src/components/Sidebar.tsx index 04e42c8..9a5aeff 100644 --- a/apps/admin/src/components/Sidebar.tsx +++ b/apps/admin/src/components/Sidebar.tsx @@ -10,7 +10,6 @@ import { ShoppingBag, BookOpen, TrendingUp, - Image, Tags, Ticket, BarChart3, @@ -28,7 +27,7 @@ const MENU_ITEMS = [ { icon: ShoppingCart, label: 'Orders', to: '/orders' }, { icon: Ticket, label: 'Promotions', to: '/promotions' }, { icon: Ticket, label: 'Coupons', to: '/coupons' }, - { icon: Image, label: 'Hero Section', to: '/hero' }, + { icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' }, { icon: BookOpen, label: 'Scent Guide', to: '/articles' }, { icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' }, { icon: TrendingUp, label: 'TikTok Analysis', to: '/tiktok-analysis' }, diff --git a/apps/admin/src/pages/PageBuilderPage.tsx b/apps/admin/src/pages/PageBuilderPage.tsx new file mode 100644 index 0000000..3387d86 --- /dev/null +++ b/apps/admin/src/pages/PageBuilderPage.tsx @@ -0,0 +1,1825 @@ +import React, { useState, useEffect } from 'react'; +import { + Layers, + Plus, + Trash2, + Save, + ArrowUp, + ArrowDown, + Eye, + EyeOff, + Edit, + UploadCloud, + Image as ImageIcon, + Loader2, + X, + Settings2, + CheckCircle2 +} from 'lucide-react'; +import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types'; + +interface Scent { + id: number; + name: string; +} + +export default function PageBuilderPage() { + const [page, setPage] = useState(null); + const [sections, setSections] = useState([]); + const [categories, setCategories] = useState([]); + const [scents, setScents] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingSection, setEditingSection] = useState | null>(null); + const [isUploading, setIsUploading] = useState(false); + const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide'; index?: number } | null>(null); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); + + const [pages, setPages] = useState([]); + const [selectedPageId, setSelectedPageId] = useState(null); + const [isPageModalOpen, setIsPageModalOpen] = useState(false); + const [newPageData, setNewPageData] = useState({ title: '', slug: '' }); + const [isSavingPage, setIsSavingPage] = useState(false); + + const fetchPagesList = async () => { + try { + const res = await fetch(`${API_BASE}/pages`); + const data = await res.json(); + if (Array.isArray(data)) { + setPages(data); + if (selectedPageId === null) { + const homePage = data.find(p => p.slug === 'home'); + if (homePage) { + setSelectedPageId(homePage.id); + } else if (data.length > 0) { + setSelectedPageId(data[0].id); + } + } + } + } catch (err) { + console.error('Fetch pages list error:', err); + } + }; + + const fetchPageDetails = async (pageId: number) => { + const pageObj = pages.find(p => p.id === pageId); + if (!pageObj) return; + + setLoading(true); + try { + const res = await fetch(`${API_BASE}/pages/${pageObj.slug}`); + const data = await res.json(); + if (data && !data.error) { + setPage(data); + setSections(data.sections || []); + } + } catch (err) { + console.error('Fetch page sections error:', err); + } finally { + setLoading(false); + } + }; + + const fetchMetadata = async () => { + try { + const catRes = await fetch(`${API_BASE}/categories`); + const catData = await catRes.json(); + setCategories(catData); + + const scentRes = await fetch(`${API_BASE}/products/scents`); + const scentData = await scentRes.json(); + setScents(scentData); + } catch (err) { + console.error('Fetch metadata error:', err); + } + }; + + useEffect(() => { + fetchPagesList(); + fetchMetadata(); + }, []); + + useEffect(() => { + if (selectedPageId !== null && pages.length > 0) { + fetchPageDetails(selectedPageId); + } + }, [selectedPageId, pages]); + + const handleCreatePage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newPageData.title.trim() || !newPageData.slug.trim()) return; + + setIsSavingPage(true); + try { + const res = await fetch(`${API_BASE}/pages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: newPageData.title, + slug: newPageData.slug.toLowerCase().trim().replace(/\s+/g, '-') + }) + }); + const data = await res.json(); + if (data && !data.error) { + setIsPageModalOpen(false); + setNewPageData({ title: '', slug: '' }); + await fetchPagesList(); + setSelectedPageId(data.id); + } else { + alert(data.error || 'Failed to create page'); + } + } catch (err) { + console.error('Create page error:', err); + alert('Failed to create page due to network error'); + } finally { + setIsSavingPage(false); + } + }; + + const handleDeletePage = async () => { + if (!page || page.slug === 'home') return; + if (!confirm(`Are you sure you want to delete the page "${page.title}"? This will delete all of its section blocks permanently!`)) return; + + try { + const res = await fetch(`${API_BASE}/pages/${page.id}`, { + method: 'DELETE' + }); + if (res.ok) { + setSelectedPageId(null); + setPage(null); + setSections([]); + await fetchPagesList(); + } else { + alert('Failed to delete page'); + } + } catch (err) { + console.error('Delete page error:', err); + } + }; + + const handleMove = (index: number, direction: 'up' | 'down') => { + const newSections = [...sections]; + const targetIndex = direction === 'up' ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= newSections.length) return; + + // Swap + const temp = newSections[index]; + newSections[index] = newSections[targetIndex]; + newSections[targetIndex] = temp; + + // Recalculate orders + const ordered = newSections.map((sec, idx) => ({ + ...sec, + order: idx + 1 + })); + + setSections(ordered); + setSaveStatus('idle'); + }; + + const handleSaveOrder = async () => { + if (!page) return; + setSaveStatus('saving'); + try { + const sectionOrders = sections.map((sec, idx) => ({ + id: sec.id, + order: idx + 1 + })); + + const res = await fetch(`${API_BASE}/pages/${page.id}/sections/reorder`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sectionOrders }) + }); + + if (res.ok) { + setSaveStatus('saved'); + setTimeout(() => setSaveStatus('idle'), 3000); + } else { + setSaveStatus('error'); + } + } catch (err) { + console.error('Save order error:', err); + setSaveStatus('error'); + } + }; + + const handleToggleActive = async (section: PageSection) => { + try { + const updatedActive = !section.isActive; + const res = await fetch(`${API_BASE}/pages/sections/${section.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ isActive: updatedActive }) + }); + + if (res.ok) { + setSections(prev => + prev.map(sec => sec.id === section.id ? { ...sec, isActive: updatedActive } : sec) + ); + } + } catch (err) { + console.error('Toggle active error:', err); + } + }; + + const handleDeleteSection = async (id: number) => { + if (!confirm('Are you sure you want to delete this section?')) return; + try { + const res = await fetch(`${API_BASE}/pages/sections/${id}`, { + method: 'DELETE' + }); + if (res.ok) { + setSections(prev => prev.filter(sec => sec.id !== id)); + } + } catch (err) { + console.error('Delete section error:', err); + } + }; + + const handleUpload = async (file: File) => { + setIsUploading(true); + const formData = new FormData(); + formData.append('file', file); + + try { + const res = await fetch(`${API_BASE}/upload`, { + method: 'POST', + body: formData + }); + const data = await res.json(); + if (data.url) { + if (uploadTarget?.type === 'slide' && typeof uploadTarget.index === 'number') { + const idx = uploadTarget.index; + setEditingSection(prev => { + if (!prev) return null; + const currentSlides = [...(prev.payload?.slides || [])]; + currentSlides[idx] = { + ...currentSlides[idx], + imageUrl: data.url + }; + return { + ...prev, + payload: { + ...(prev.payload || {}), + slides: currentSlides + } + }; + }); + } else { + setEditingSection(prev => { + if (!prev) return null; + return { + ...prev, + payload: { + ...(prev.payload || {}), + imageUrl: data.url + } + }; + }); + } + } + } catch (err) { + console.error('Upload error:', err); + } finally { + setIsUploading(false); + setUploadTarget(null); + } + }; + + const openAddModal = () => { + setEditingSection({ + type: 'HERO', + order: sections.length + 1, + isActive: true, + payload: { + tagline: 'Vibrant Elegance', + title: 'New Scent [Sanctuary]', + description: 'Transform your room into a peaceful escape.', + imageUrl: '/images/hero.png', + buttonText: 'Explore', + buttonLink: '/shop' + } + }); + setIsModalOpen(true); + }; + + const openEditModal = (section: PageSection) => { + setEditingSection(JSON.parse(JSON.stringify(section))); // deep copy + setIsModalOpen(true); + }; + + const handleTypeChange = (type: string) => { + let defaultPayload = {}; + switch (type) { + case 'ANNOUNCEMENT_BAR': + defaultPayload = { + slides: ['[FREE SHIPPING] on orders over ฿1,500', 'New Premium Collection is out now!'], + backgroundColor: '#1F3D2D', + textColor: '#FDFDF9', + speed: 3000 + }; + break; + case 'NEWSLETTER': + defaultPayload = { + 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.' + }; + break; + case 'EDITORIAL_TILE': + defaultPayload = { + title: 'Crafted with [Sensory Passion]', + description: 'Every formulation at HomAroma is a tribute to pure botanical essences, hand-blended to capture nature’s quiet intelligence.', + imageUrl: '/images/oil_ozone.png', + imagePosition: 'left', + buttonText: 'Read Our Story', + buttonLink: '/about', + backgroundColor: '#FDFDF9', + textColor: '#1F3D2D' + }; + break; + case 'EDITORIAL_CAROUSEL': + defaultPayload = { + 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: '/images/oil_ozone.png', + buttonText: 'Discover Sourcing', + buttonLink: '/sourcing' + }, + { + category: 'Rituals', + title: 'Daily Sensory Escape', + description: 'Incorporate pure ambient aromatherapy into your morning meditation or evening unwinding rituals.', + imageUrl: '/images/reed_diffuser.png', + buttonText: 'Explore Rituals', + buttonLink: '/rituals' + } + ] + }; + break; + case 'HERO': + defaultPayload = { + tagline: 'Vibrant Elegance', + title: 'New Scent [Sanctuary]', + description: 'Transform your room into a peaceful escape.', + imageUrl: '/images/hero.png', + buttonText: 'Explore', + buttonLink: '/shop' + }; + break; + case 'CATEGORIES': + defaultPayload = { + title: 'Shop by Collection', + subtitle: 'Choose your preferred sensory style', + categoryIds: [] + }; + break; + case 'PRODUCTS': + defaultPayload = { + title: 'Best Sellers', + subtitle: 'Our customer favorite products', + type: 'featured', + limit: 3 + }; + break; + case 'BANNER': + defaultPayload = { + imageUrl: '/images/oil_ozone.png', + linkUrl: '/shop', + title: 'Special Promotion', + description: 'Get discount on our reed diffusers.', + buttonText: 'Buy Now' + }; + break; + case 'TEXT': + defaultPayload = { + title: 'Our Commitment', + content: 'We use 100% natural ingredients.', + textAlign: 'center' + }; + break; + } + + setEditingSection(prev => { + if (!prev) return null; + return { + ...prev, + type, + payload: defaultPayload + }; + }); + }; + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingSection || !page) return; + + try { + const url = editingSection.id + ? `${API_BASE}/pages/sections/${editingSection.id}` + : `${API_BASE}/pages/${page.id}/sections`; + + const method = editingSection.id ? 'PUT' : 'POST'; + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(editingSection) + }); + + if (res.ok) { + setIsModalOpen(false); + if (selectedPageId) fetchPageDetails(selectedPageId); + } else { + const errData = await res.json(); + alert(`Failed to save: ${errData.error || 'Unknown error'}`); + } + } catch (err) { + console.error('Save section error:', err); + } + }; + + return ( +
+
+
+

Page Builder (CMS)

+
+ Select Page: + + + + + {page && page.slug !== 'home' && ( + + )} +
+
+
+ + {sections.length > 0 && ( + + )} +
+
+ + {saveStatus === 'saved' && ( +
+ + Homepage section ordering updated successfully. +
+ )} + + {saveStatus === 'error' && ( +
+ Error saving layout order. Please try again. +
+ )} + + {loading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+
+

Homepage Blocks list

+
+ +
+ {sections.length === 0 ? ( +
+ +

No content sections in this page yet. Start by adding one!

+
+ ) : ( + sections.map((section, idx) => ( +
+
+ {/* Index & Order controls */} +
+ + + {idx + 1} + + +
+ + {/* Section Description */} +
+
+ + {section.type} + + {!section.isActive && ( + + Hidden + + )} +
+

+ {section.payload?.title || section.payload?.tagline || (section.type === 'ANNOUNCEMENT_BAR' && section.payload?.slides?.[0]) || `Block #${section.id}`} +

+ {section.type === 'PRODUCTS' && ( +

+ Type: {section.payload?.type} • Limit: {section.payload?.limit || 3} products +

+ )} + {section.type === 'ANNOUNCEMENT_BAR' && ( +

+ Slides: {section.payload?.slides?.length || 0} messages +

+ )} +
+
+ + {/* Actions */} +
+ + + +
+
+ )) + )} +
+
+ )} + + {/* Editor Modal */} + {isModalOpen && editingSection && ( +
+
+
+

+ + {editingSection.id ? `Edit ${editingSection.type} Block` : 'Create Layout Block'} +

+ +
+ +
+ {!editingSection.id && ( +
+ + +
+ )} + + {/* Dynamic Forms based on Type */} + {editingSection.type === 'EDITORIAL_TILE' && ( +
+ {/* Image uploading */} +
+ +
{ + setUploadTarget({ type: 'main' }); + document.getElementById('section-upload')?.click(); + }} + > + {editingSection.payload?.imageUrl ? ( + <> + Editorial +
+ +
+ + ) : ( +
+ +

Select Image

+
+ )} + {isUploading && ( +
+ +
+ )} +
+
+ +
+
+ + + setEditingSection({ + ...editingSection, + payload: { ...(editingSection.payload || {}), title: e.target.value } + }) + } + className="admin-input w-full" + /> +

Use [brackets] for golden accent serif text.

+
+
+ + +
+
+ + + setEditingSection({ + ...editingSection, + payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value } + }) + } + className="admin-input w-full" + /> +
+
+ + + setEditingSection({ + ...editingSection, + payload: { ...(editingSection.payload || {}), textColor: e.target.value } + }) + } + className="admin-input w-full" + /> +
+
+ + + setEditingSection({ + ...editingSection, + payload: { ...(editingSection.payload || {}), buttonText: e.target.value } + }) + } + className="admin-input w-full" + /> +
+
+ + + setEditingSection({ + ...editingSection, + payload: { ...(editingSection.payload || {}), buttonLink: e.target.value } + }) + } + className="admin-input w-full" + /> +
+
+ +
+ +