From 64f7ecf729e5a014965622c535d9c5a7b5b24a2a Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 17 Oct 2026 09:25:16 +0700 Subject: [PATCH] feat(cms): add Interactive 360 Turntable and Macro Zoom Showcase block --- .../page-builder/AddSectionModal.tsx | 29 +- .../Interactive360ShowcaseForm.tsx | 548 ++++++++++++++++++ apps/admin/src/pages/PageBuilderPage.tsx | 13 + apps/backend/src/routes/pages.js | 7 +- apps/backend/src/routes/pages.ts | 6 + apps/spa/public/images/3d/sprocket-detail.png | Bin 0 -> 2017887 bytes apps/spa/public/images/3d/sprocket-views.png | Bin 0 -> 2099104 bytes .../public/images/3d/sprocket-detail.png | Bin 0 -> 2017887 bytes .../public/images/3d/sprocket-views.png | Bin 0 -> 2099104 bytes .../src/components/Interactive360Showcase.tsx | 521 +++++++++++++++++ .../dynamic/DynamicPageRenderer.tsx | 31 + packages/types/index.ts | 33 +- 12 files changed, 1185 insertions(+), 3 deletions(-) create mode 100644 apps/admin/src/components/page-builder/Interactive360ShowcaseForm.tsx create mode 100644 apps/spa/public/images/3d/sprocket-detail.png create mode 100644 apps/spa/public/images/3d/sprocket-views.png create mode 100644 apps/website/public/images/3d/sprocket-detail.png create mode 100644 apps/website/public/images/3d/sprocket-views.png create mode 100644 apps/website/src/components/Interactive360Showcase.tsx diff --git a/apps/admin/src/components/page-builder/AddSectionModal.tsx b/apps/admin/src/components/page-builder/AddSectionModal.tsx index e271a95..0d6ae9b 100644 --- a/apps/admin/src/components/page-builder/AddSectionModal.tsx +++ b/apps/admin/src/components/page-builder/AddSectionModal.tsx @@ -143,7 +143,34 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS ) }, - // 2.9 APPLE_SHOWCASE + // 2.95 INTERACTIVE_360_SHOWCASE + { + type: 'INTERACTIVE_360_SHOWCASE', + category: 'hero', + title: 'INTERACTIVE 360° TURNTABLE SHOWCASE', + description: 'Ultra-fast 2.5D Sprite Sheet 360° turntable, scroll-driven rotation, interactive drag-to-spin, and seamless macro detail zoom transition.', + badge: '3D Web · Hot', + renderPreview: () => ( +
+
+ + 360° TURNTABLE + MACRO ZOOM + +
+ PRECISION TO EVERY ANGLE +
+
+ + 16-Angle Turntable Scrub +
+
+
+ 360° +
+
+ ) + }, + // 2.9 APPLE_SHOWCASE { type: 'APPLE_SHOWCASE', category: 'hero', diff --git a/apps/admin/src/components/page-builder/Interactive360ShowcaseForm.tsx b/apps/admin/src/components/page-builder/Interactive360ShowcaseForm.tsx new file mode 100644 index 0000000..ec823c8 --- /dev/null +++ b/apps/admin/src/components/page-builder/Interactive360ShowcaseForm.tsx @@ -0,0 +1,548 @@ +import { useState, useEffect } from 'react' +import { + Sparkles, + Upload, + Package, + ExternalLink, + RotateCcw, + Layers, +} from 'lucide-react' +import { API_BASE } from '@homaroma/types' + +interface SpecItem { + number?: string + title: string + subtitle?: string +} + +interface Props { + payload: Record + updatePayloadValue: (field: string, value: unknown) => void + onBrowseMedia?: (onSelect: (url: string) => void) => void +} + +const DEFAULT_SPECS: SpecItem[] = [ + { + number: '01', + title: 'โครเมียมขัดเงาระดับพรีเมียม', + subtitle: 'POLISHED CHROME FINISH', + }, + { + number: '02', + title: 'เจาะรูกลึงลดน้ำหนักตามหลักอากาศพลศาสตร์', + subtitle: 'LIGHTWEIGHT PRECISION CUTOUTS', + }, + { + number: '03', + title: 'โปรไฟล์ฟันเฟือง 428H แม่นยำระดับไมครอน', + subtitle: '428H PRECISION TOOTH PROFILE', + }, +] + +export default function Interactive360ShowcaseForm({ + payload, + updatePayloadValue, + onBrowseMedia, +}: Props) { + const [productsList, setProductsList] = useState([]) + const [loadingProducts, setLoadingProducts] = useState(false) + + const spriteSheetUrl = (payload.spriteSheetUrl as string) || '/images/3d/sprocket-views.png' + const detailImageUrl = (payload.detailImageUrl as string) || '/images/3d/sprocket-detail.png' + const eyebrow = (payload.eyebrow as string) || '' + const title = (payload.title as string) || '' + const subtitle = (payload.subtitle as string) || '' + const detailKicker = (payload.detailKicker as string) || '' + const detailTitle = (payload.detailTitle as string) || '' + const detailCaption = (payload.detailCaption as string) || '' + const productName = (payload.productName as string) || '' + const productBadge = (payload.productBadge as string) || '' + const accentColor = (payload.accentColor as string) || '#d5ff63' + const theme = (payload.theme as string) || 'dark' + const endCtaTitle = (payload.endCtaTitle as string) || '' + const endCtaButtonText = (payload.endCtaButtonText as string) || '' + const endCtaLink = (payload.endCtaLink as string) || '' + const selectedProductId = payload.productId ? String(payload.productId) : '' + + const rawSpecs = Array.isArray(payload.specs) ? (payload.specs as SpecItem[]) : DEFAULT_SPECS + + useEffect(() => { + let isMounted = true + const fetchCatalog = async () => { + setLoadingProducts(true) + try { + const res = await fetch(`${API_BASE}/products?limit=100`) + if (!res.ok) return + const data = await res.json() + const items = data.data || data.products || (Array.isArray(data) ? data : []) + if (isMounted) setProductsList(items) + } catch (err) { + console.error('Failed to load products in Interactive360ShowcaseForm', err) + } finally { + if (isMounted) setLoadingProducts(false) + } + } + fetchCatalog() + return () => { isMounted = false } + }, []) + + // Auto-seed defaults if brand new block + useEffect(() => { + if (!payload.spriteSheetUrl) updatePayloadValue('spriteSheetUrl', '/images/3d/sprocket-views.png') + if (!payload.detailImageUrl) updatePayloadValue('detailImageUrl', '/images/3d/sprocket-detail.png') + if (!payload.eyebrow) updatePayloadValue('eyebrow', 'ENGINEERED TO MOVE') + if (!payload.title) updatePayloadValue('title', 'เหล็กหนึ่งชิ้น ที่เล่าได้ทุกองศา') + if (!payload.subtitle) updatePayloadValue('subtitle', 'สเตอร์จักรยานยนต์โครเมียม ดีไซน์น้ำหนักเบา ให้ทุกการหมุนมีจังหวะที่แม่นยำ พร้อมสัมผัสรายละเอียดระดับ Ultra-Macro') + if (!payload.detailKicker) updatePayloadValue('detailKicker', 'FINISHED TO THE EDGE') + if (!payload.detailTitle) updatePayloadValue('detailTitle', 'ฟันแน่น แข็งแรง ผิวเรียบทุกขอบ') + if (!payload.detailCaption) updatePayloadValue('detailCaption', 'ผิวชุบโครเมียมขัดเงา สะท้อนแสงเงางาม ทนทานต่อการเสียดสี และป้องกันสนิมในทุกสภาพการใช้งาน') + if (!payload.productName) updatePayloadValue('productName', 'Chrome Sprocket 428H') + if (!payload.productBadge) updatePayloadValue('productBadge', 'MONO / 01') + if (!payload.endCtaTitle) updatePayloadValue('endCtaTitle', 'พร้อมยกระดับสมรรถนะการขับขี่?') + if (!payload.endCtaButtonText) updatePayloadValue('endCtaButtonText', 'สั่งซื้อสินค้าตอนนี้') + if (!payload.specs) updatePayloadValue('specs', DEFAULT_SPECS) + if (!payload.theme) updatePayloadValue('theme', 'dark') + if (!payload.accentColor) updatePayloadValue('accentColor', '#d5ff63') + }, []) + + const handleSelectProduct = (productIdStr: string) => { + if (!productIdStr) { + updatePayloadValue('productId', null) + return + } + const pid = Number(productIdStr) + updatePayloadValue('productId', pid) + + const prod = productsList.find((p) => p.id === pid) + if (prod) { + const prodName = typeof prod.name === 'string' ? prod.name : prod.name?.th || prod.name?.en || '' + if (prodName) updatePayloadValue('productName', prodName) + const slug = prod.slug || `product-${pid}` + updatePayloadValue('endCtaLink', `/products/${slug}`) + } + } + + const handleApplyPreset = () => { + updatePayloadValue('spriteSheetUrl', '/images/3d/sprocket-views.png') + updatePayloadValue('detailImageUrl', '/images/3d/sprocket-detail.png') + updatePayloadValue('eyebrow', 'ENGINEERED TO MOVE') + updatePayloadValue('title', 'เหล็กหนึ่งชิ้น ที่เล่าได้ทุกองศา') + updatePayloadValue('subtitle', 'สเตอร์จักรยานยนต์โครเมียม ดีไซน์น้ำหนักเบา ให้ทุกการหมุนมีจังหวะที่แม่นยำ พร้อมสัมผัสรายละเอียดระดับ Ultra-Macro') + updatePayloadValue('detailKicker', 'FINISHED TO THE EDGE') + updatePayloadValue('detailTitle', 'ฟันแน่น แข็งแรง ผิวเรียบทุกขอบ') + updatePayloadValue('detailCaption', 'ผิวชุบโครเมียมขัดเงา สะท้อนแสงเงางาม ทนทานต่อการเสียดสี และป้องกันสนิมในทุกสภาพการใช้งาน') + updatePayloadValue('productName', 'Chrome Sprocket 428H') + updatePayloadValue('productBadge', 'MONO / 01') + updatePayloadValue('endCtaTitle', 'พร้อมยกระดับสมรรถนะการขับขี่?') + updatePayloadValue('endCtaButtonText', 'สั่งซื้อสินค้าตอนนี้') + updatePayloadValue('specs', DEFAULT_SPECS) + updatePayloadValue('theme', 'dark') + updatePayloadValue('accentColor', '#d5ff63') + } + + const handleUpdateSpec = (idx: number, field: keyof SpecItem, val: string) => { + const updated = [...rawSpecs] + updated[idx] = { ...updated[idx], [field]: val } + updatePayloadValue('specs', updated) + } + + return ( +
+ {/* ── Preset & Binding Banner ── */} +
+
+
+ + 3D Interactive Turntable & Macro Zoom Showcase +
+

+ แสดงผลสเตอร์/ชิ้นส่วน 360 องศาด้วย Sprite Sheet + Scroll Scrollytelling + ซูม Macro Detail อัตโนมัติ +

+
+ + +
+ + {/* ── 1-Click Product Linking ── */} +
+
+
+ + ผูกกับสินค้าในระบบ (1-Click Bind Product) +
+ {selectedProductId && ( + + Product ID: #{selectedProductId} + + )} +
+ +
+
+ +

+ เมื่อเลือกสินค้า ระบบจะดึงชื่อและตั้งค่าลิงก์ปุ่มสั่งซื้ออัตโนมัติ +

+
+ +
+ updatePayloadValue('endCtaLink', e.target.value)} + placeholder="ลิงก์ปลายทางเมื่อกดสั่งซื้อ เช่น /products/chain-sprocket-p1" + className="admin-input flex-1 text-xs" + /> + {endCtaLink && ( + + + + )} +
+
+
+ + {/* ── Visual Media Uploads (Sprite Sheet + Macro Detail) ── */} +
+ {/* Sprite Sheet 16-Angles */} +
+ + {spriteSheetUrl ? ( +
+ 360 Sprite Sheet +
+ {onBrowseMedia && ( + + )} +
+
+ ) : ( +
onBrowseMedia && onBrowseMedia((url) => updatePayloadValue('spriteSheetUrl', url))} + className="aspect-video rounded-xl border border-dashed border-white/20 bg-black/40 flex flex-col items-center justify-center gap-2 cursor-pointer hover:border-lime-400 transition-colors" + > + + คลิกเพื่อเลือกภาพ Sprite Sheet +
+ )} + updatePayloadValue('spriteSheetUrl', e.target.value)} + placeholder="/images/3d/sprocket-views.png" + className="admin-input w-full text-xs font-mono" + /> +
+ + {/* Macro Detail Shot */} +
+ + {detailImageUrl ? ( +
+ Macro Detail +
+ {onBrowseMedia && ( + + )} +
+
+ ) : ( +
onBrowseMedia && onBrowseMedia((url) => updatePayloadValue('detailImageUrl', url))} + className="aspect-video rounded-xl border border-dashed border-white/20 bg-black/40 flex flex-col items-center justify-center gap-2 cursor-pointer hover:border-lime-400 transition-colors" + > + + คลิกเพื่อเลือกภาพ Macro Detail +
+ )} + updatePayloadValue('detailImageUrl', e.target.value)} + placeholder="/images/3d/sprocket-detail.png" + className="admin-input w-full text-xs font-mono" + /> +
+
+ + {/* ── Headline & Intro Texts ── */} +
+
+ ข้อความหัวเรื่อง (Header & Intro) +
+ +
+
+ + updatePayloadValue('eyebrow', e.target.value)} + className="admin-input w-full text-xs font-mono" + placeholder="ENGINEERED TO MOVE" + /> +
+ +
+ + updatePayloadValue('productName', e.target.value)} + className="admin-input w-full text-xs" + placeholder="Chrome Sprocket 428H" + /> +
+ +
+ + updatePayloadValue('productBadge', e.target.value)} + className="admin-input w-full text-xs font-mono" + placeholder="MONO / 01" + /> +
+
+ +
+ + updatePayloadValue('title', e.target.value)} + className="admin-input w-full text-xs" + placeholder="เหล็กหนึ่งชิ้น ที่เล่าได้ทุกองศา" + /> +
+ +
+ +