feat(cms): add Interactive 360 Turntable and Macro Zoom Showcase block

This commit is contained in:
Your Name committed 2026-10-17 09:25:16 +07:00
1 parent 8d9f2fc587
commit 64f7ecf729
12 files changed
+1185 -3

No files matched your search

@@ -143,7 +143,34 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
</div>
)
},
// 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: () => (
<div className="relative w-full aspect-[21/9] rounded-xl overflow-hidden bg-[#0f0f0e] flex items-center justify-between p-3 border border-[#d5ff63]/30">
<div className="space-y-1 max-w-[60%] z-10">
<span className="text-[6px] uppercase font-mono font-bold text-[#d5ff63] tracking-widest">
360° TURNTABLE + MACRO ZOOM
</span>
<h5 className="text-[10px] font-black text-white leading-tight">
PRECISION TO EVERY ANGLE
</h5>
<div className="flex items-center gap-1 text-[6px] text-slate-400 font-mono">
<span className="w-1.5 h-1.5 rounded-full bg-[#d5ff63]" />
<span>16-Angle Turntable Scrub</span>
</div>
</div>
<div className="w-14 h-14 rounded-full bg-black/60 border border-[#d5ff63]/40 flex items-center justify-center p-1 shadow-2xl shrink-0">
<span className="text-[9px] font-bold font-mono text-[#d5ff63]">360°</span>
</div>
</div>
)
},
// 2.9 APPLE_SHOWCASE
{
type: 'APPLE_SHOWCASE',
category: 'hero',
@@ -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<string, unknown>
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<any[]>([])
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 (
<div className="space-y-6 text-slate-200">
{/* ── Preset & Binding Banner ── */}
<div className="p-4 rounded-2xl bg-gradient-to-r from-lime-500/10 via-slate-900/40 to-transparent border border-lime-500/30 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-lime-400 font-bold text-xs uppercase tracking-wider">
<Sparkles className="w-4 h-4 text-lime-400" />
<span>3D Interactive Turntable & Macro Zoom Showcase</span>
</div>
<p className="text-xs text-slate-400">
แสดงผลสเตอร์/ชิ้นส่วน 360 องศาด้วย Sprite Sheet + Scroll Scrollytelling + ซูม Macro Detail อัตโนมัติ
</p>
</div>
<button
type="button"
onClick={handleApplyPreset}
className="px-4 py-2 rounded-xl bg-lime-400 text-slate-950 font-bold text-xs hover:bg-lime-300 transition-all flex items-center gap-1.5 shrink-0 shadow-lg cursor-pointer"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>รีเซ็ต / โหลด Mockup สเตอร์โครเมียม</span>
</button>
</div>
{/* ── 1-Click Product Linking ── */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-bold text-slate-200 uppercase tracking-wider">
<Package className="w-4 h-4 text-lime-400" />
<span>ผูกกับสินค้าในระบบ (1-Click Bind Product)</span>
</div>
{selectedProductId && (
<span className="text-[10px] text-lime-400 font-mono">
Product ID: #{selectedProductId}
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<select
value={selectedProductId}
onChange={(e) => handleSelectProduct(e.target.value)}
disabled={loadingProducts}
className="admin-input w-full text-xs"
>
<option value="">-- ไม่ระบุ (ใช้เป็นหน้า Brand/Product Story อิสระ) --</option>
{productsList.map((p) => {
const nameStr = typeof p.name === 'string' ? p.name : p.name?.th || p.name?.en || 'Unnamed'
return (
<option key={p.id} value={p.id}>
#{p.id} - {nameStr}
</option>
)
})}
</select>
<p className="text-[10px] text-slate-500 mt-1">
เมื่อเลือกสินค้า ระบบจะดึงชื่อและตั้งค่าลิงก์ปุ่มสั่งซื้ออัตโนมัติ
</p>
</div>
<div className="flex items-center gap-2">
<input
type="text"
value={endCtaLink}
onChange={(e) => updatePayloadValue('endCtaLink', e.target.value)}
placeholder="ลิงก์ปลายทางเมื่อกดสั่งซื้อ เช่น /products/chain-sprocket-p1"
className="admin-input flex-1 text-xs"
/>
{endCtaLink && (
<a
href={endCtaLink}
target="_blank"
rel="noreferrer"
className="p-2 rounded-lg bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white"
>
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
</div>
</div>
{/* ── Visual Media Uploads (Sprite Sheet + Macro Detail) ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Sprite Sheet 16-Angles */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-3">
<label className="text-xs font-bold text-slate-200 uppercase tracking-wider block">
1. Sprite Sheet 360° (ภาพรวม 16 มุมมอง 4x4 Grid)
</label>
{spriteSheetUrl ? (
<div className="relative aspect-video rounded-xl bg-black border border-white/10 overflow-hidden group">
<img
src={spriteSheetUrl}
alt="360 Sprite Sheet"
className="w-full h-full object-contain p-2"
/>
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 flex items-center justify-center gap-2 transition-opacity">
{onBrowseMedia && (
<button
type="button"
onClick={() => onBrowseMedia((url) => updatePayloadValue('spriteSheetUrl', url))}
className="px-3 py-1.5 rounded-lg bg-lime-400 text-black text-xs font-bold cursor-pointer"
>
เปลี่ยนรูป
</button>
)}
</div>
</div>
) : (
<div
onClick={() => 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"
>
<Upload className="w-6 h-6 text-slate-500" />
<span className="text-xs text-slate-400">คลิกเพื่อเลือกภาพ Sprite Sheet</span>
</div>
)}
<input
type="text"
value={spriteSheetUrl}
onChange={(e) => updatePayloadValue('spriteSheetUrl', e.target.value)}
placeholder="/images/3d/sprocket-views.png"
className="admin-input w-full text-xs font-mono"
/>
</div>
{/* Macro Detail Shot */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-3">
<label className="text-xs font-bold text-slate-200 uppercase tracking-wider block">
2. Macro Detail Shot (ภาพซูมระยะประชิดระดับ 4K)
</label>
{detailImageUrl ? (
<div className="relative aspect-video rounded-xl bg-black border border-white/10 overflow-hidden group">
<img
src={detailImageUrl}
alt="Macro Detail"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 flex items-center justify-center gap-2 transition-opacity">
{onBrowseMedia && (
<button
type="button"
onClick={() => onBrowseMedia((url) => updatePayloadValue('detailImageUrl', url))}
className="px-3 py-1.5 rounded-lg bg-lime-400 text-black text-xs font-bold cursor-pointer"
>
เปลี่ยนรูป
</button>
)}
</div>
</div>
) : (
<div
onClick={() => 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"
>
<Upload className="w-6 h-6 text-slate-500" />
<span className="text-xs text-slate-400">คลิกเพื่อเลือกภาพ Macro Detail</span>
</div>
)}
<input
type="text"
value={detailImageUrl}
onChange={(e) => updatePayloadValue('detailImageUrl', e.target.value)}
placeholder="/images/3d/sprocket-detail.png"
className="admin-input w-full text-xs font-mono"
/>
</div>
</div>
{/* ── Headline & Intro Texts ── */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-4">
<div className="text-xs font-bold text-slate-200 uppercase tracking-wider">
ข้อความหัวเรื่อง (Header & Intro)
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
หมวด/ป้ายกำกับ (Eyebrow Tag)
</label>
<input
type="text"
value={eyebrow}
onChange={(e) => updatePayloadValue('eyebrow', e.target.value)}
className="admin-input w-full text-xs font-mono"
placeholder="ENGINEERED TO MOVE"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
ชื่อสินค้า (Product Name)
</label>
<input
type="text"
value={productName}
onChange={(e) => updatePayloadValue('productName', e.target.value)}
className="admin-input w-full text-xs"
placeholder="Chrome Sprocket 428H"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
ซีรีส์สินค้า (Badge / Series)
</label>
<input
type="text"
value={productBadge}
onChange={(e) => updatePayloadValue('productBadge', e.target.value)}
className="admin-input w-full text-xs font-mono"
placeholder="MONO / 01"
/>
</div>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
หัวเรื่องหลัก (Main Title)
</label>
<input
type="text"
value={title}
onChange={(e) => updatePayloadValue('title', e.target.value)}
className="admin-input w-full text-xs"
placeholder="เหล็กหนึ่งชิ้น ที่เล่าได้ทุกองศา"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
คำบรรยายเกริ่นนำ (Intro Subtitle)
</label>
<textarea
value={subtitle}
onChange={(e) => updatePayloadValue('subtitle', e.target.value)}
rows={2}
className="admin-input w-full text-xs"
placeholder="สเตอร์จักรยานยนต์โครเมียม ดีไซน์น้ำหนักเบา ให้ทุกการหมุนมีจังหวะที่แม่นยำ"
/>
</div>
</div>
{/* ── Macro Zoom Detail Overlay Texts ── */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-4">
<div className="text-xs font-bold text-lime-400 uppercase tracking-wider flex items-center gap-2">
<Layers className="w-4 h-4" />
<span>ข้อความส่วนซูม Macro Detail (แสดงเมื่อเลื่อนจอลงถึงช่วงท้าย)</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
Kicker Tag
</label>
<input
type="text"
value={detailKicker}
onChange={(e) => updatePayloadValue('detailKicker', e.target.value)}
className="admin-input w-full text-xs font-mono"
placeholder="FINISHED TO THE EDGE"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
Headline ตอนซูม
</label>
<input
type="text"
value={detailTitle}
onChange={(e) => updatePayloadValue('detailTitle', e.target.value)}
className="admin-input w-full text-xs font-bold"
placeholder="ฟันแน่น แข็งแรง ผิวเรียบทุกขอบ"
/>
</div>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
คำอธิบายรายละเอียด
</label>
<textarea
value={detailCaption}
onChange={(e) => updatePayloadValue('detailCaption', e.target.value)}
rows={2}
className="admin-input w-full text-xs"
placeholder="ผิวชุบโครเมียมขัดเงา สะท้อนแสงเงางาม ทนทานต่อการเสียดสี และป้องกันสนิมในทุกสภาพการใช้งาน"
/>
</div>
</div>
{/* ── 3-Spec Highlights Grid ── */}
<div className="p-4 rounded-2xl bg-slate-900/60 border border-white/5 space-y-4">
<div className="text-xs font-bold text-slate-200 uppercase tracking-wider">
จุดเด่นด้านสมรรถนะ 3 หัวข้อ (Technical Pillars)
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{rawSpecs.map((spec, idx) => (
<div key={idx} className="p-3 rounded-xl bg-black/40 border border-white/10 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-mono font-bold text-lime-400">
#{idx + 1}
</span>
<input
type="text"
value={spec.number || `0${idx + 1}`}
onChange={(e) => handleUpdateSpec(idx, 'number', e.target.value)}
className="admin-input w-16 text-center text-xs font-mono"
placeholder={`0${idx + 1}`}
/>
</div>
<input
type="text"
value={spec.title}
onChange={(e) => handleUpdateSpec(idx, 'title', e.target.value)}
className="admin-input w-full text-xs font-bold"
placeholder="หัวข้อสเปก"
/>
<input
type="text"
value={spec.subtitle || ''}
onChange={(e) => handleUpdateSpec(idx, 'subtitle', e.target.value)}
className="admin-input w-full text-xs font-mono text-slate-400"
placeholder="SUBTITLE EN"
/>
</div>
))}
</div>
</div>
{/* ── Theme & CTA Config ── */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 rounded-2xl bg-slate-900/60 border border-white/5">
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
สี Accent Highlight
</label>
<div className="flex items-center gap-2">
<input
type="color"
value={accentColor}
onChange={(e) => updatePayloadValue('accentColor', e.target.value)}
className="w-8 h-8 rounded border-0 cursor-pointer bg-transparent"
/>
<input
type="text"
value={accentColor}
onChange={(e) => updatePayloadValue('accentColor', e.target.value)}
className="admin-input flex-1 text-xs font-mono"
/>
</div>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
หัวข้อแบนเนอร์สั่งซื้อ (CTA Headline)
</label>
<input
type="text"
value={endCtaTitle}
onChange={(e) => updatePayloadValue('endCtaTitle', e.target.value)}
className="admin-input w-full text-xs"
placeholder="พร้อมยกระดับสมรรถนะการขับขี่?"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
ข้อความปุ่มสั่งซื้อ (CTA Button)
</label>
<input
type="text"
value={endCtaButtonText}
onChange={(e) => updatePayloadValue('endCtaButtonText', e.target.value)}
className="admin-input w-full text-xs"
placeholder="สั่งซื้อสินค้าตอนนี้"
/>
</div>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">
โทนพื้นหลัง (Theme)
</label>
<select
value={theme}
onChange={(e) => updatePayloadValue('theme', e.target.value)}
className="admin-input w-full text-xs"
>
<option value="dark">Dark Theater (#0f0f0e - แนะนำ)</option>
<option value="light">Light Studio (#f8f9fa)</option>
</select>
</div>
</div>
</div>
)
}
+13
View File
@@ -33,6 +33,7 @@ import { getAuthHeaders } from '../utils/auth';
import CinematicStoryScrollForm from '../components/page-builder/CinematicStoryScrollForm';
import AppleShowcaseForm from '../components/page-builder/AppleShowcaseForm';
import Interactive360ShowcaseForm from '../components/page-builder/Interactive360ShowcaseForm';
import HeroForm from '../components/page-builder/HeroForm';
import AnnouncementBarForm from '../components/page-builder/AnnouncementBarForm';
import NewsletterForm from '../components/page-builder/NewsletterForm';
@@ -1469,6 +1470,7 @@ export default function PageBuilderPage() {
onChange={(e) => handleTypeChange(e.target.value)}
className="admin-input w-full bg-slate-900 border-white/10"
>
<option value="INTERACTIVE_360_SHOWCASE">⚡ INTERACTIVE 360° TURNTABLE (3D Web Sprocket)</option>
<option value="CINEMATIC_STORY_SCROLL">✨ CINEMATIC STORY SCROLL (Tobaki Scrollytelling)</option>
<option value="APPLE_SHOWCASE">✨ APPLE KEYNOTE SHOWCASE (Apple Style Tour)</option>
<option value="HERO">HERO (Campaign Intro)</option>
@@ -1593,6 +1595,17 @@ export default function PageBuilderPage() {
/>
)}
{editingSection.type?.toUpperCase() === 'INTERACTIVE_360_SHOWCASE' && (
<Interactive360ShowcaseForm
payload={editingSection.payload || {}}
updatePayloadValue={updatePayloadValue}
onBrowseMedia={(cb) => {
setCustomMediaCallback(() => cb);
setIsMediaModalOpen(true);
}}
/>
)}
{editingSection.type?.toUpperCase() === 'CINEMATIC_STORY_SCROLL' && (
<CinematicStoryScrollForm
payload={editingSection.payload || {}}