x
This commit is contained in:
1 parent
34be2599fa
commit
3f9db48df4
8 files changed
+2037
-4
No files matched your search
@@ -0,0 +1,390 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Sparkles,
|
||||
Upload,
|
||||
Image as ImageIcon,
|
||||
X,
|
||||
Layers,
|
||||
CheckCircle,
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
|
||||
export interface StorySceneItem {
|
||||
id: string
|
||||
kicker: string
|
||||
title: string
|
||||
description: string
|
||||
metric?: string
|
||||
chips?: string[]
|
||||
theme: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
const DEFAULT_SCENES: StorySceneItem[] = [
|
||||
{
|
||||
id: 'hero',
|
||||
kicker: '01 / PRODUCT STORY',
|
||||
title: 'STOP WITH\nCONFIDENCE.',
|
||||
description: 'พัฒนาขึ้นสำหรับการแข่งขันและการขับขี่สมรรถนะสูง ให้สินค้ากลายเป็นพระเอกก่อนพาลงลึกไปทีละมิติของประสิทธิภาพ',
|
||||
theme: 'hero',
|
||||
},
|
||||
{
|
||||
id: 'material',
|
||||
kicker: '02 / MATERIAL',
|
||||
title: 'CERAMIC\nCOMPOUND.',
|
||||
description: 'สูตรผสมเนื้อเซรามิกเกรดพรีเมียม สึกหรอน้อย เสียงเบียบเงียบ ไร้แรงสั่นสะเทือน และลดฝุ่นผงจับจานเบรกอย่างเห็นได้ชัด',
|
||||
chips: ['Low Noise', 'Low Vibration', 'Cleaner Braking'],
|
||||
theme: 'material',
|
||||
},
|
||||
{
|
||||
id: 'heat',
|
||||
kicker: '03 / HEAT CONTROL',
|
||||
title: 'HIGH-HEAT\nCONTROL.',
|
||||
metric: '300°C',
|
||||
description: 'ทนต่อความร้อนสูงสะสม ป้องกันอาการเบรกเฟด (Brake Fade) ให้ระยะหยุดสม่ำเสมอแม้ใช้งานต่อเนื่องบนสนามแข่งหรือทางลาดชัน',
|
||||
theme: 'heat',
|
||||
},
|
||||
{
|
||||
id: 'friction',
|
||||
kicker: '04 / PERFORMANCE',
|
||||
title: 'STABLE\nFRICTION.',
|
||||
description: 'ค่าสัมประสิทธิ์แรงเสียดทานคงที่ในทุกช่วงอุณหภูมิ ให้การจับตัวนุ่มนวลแต่แน่นหนา มั่นใจในทุกโค้ง',
|
||||
theme: 'friction',
|
||||
},
|
||||
{
|
||||
id: 'control',
|
||||
kicker: '05 / CONTROL',
|
||||
title: 'SMOOTH\nENGAGEMENT.',
|
||||
description: 'โครงสร้างแม่นยำมาตรฐานสูง สัมผัสก้านเบรกตอบสนองเป็นเส้นตรง (Linear Modulation) คุมน้ำหนักเบรกได้ละเอียดดั่งใจ',
|
||||
theme: 'control',
|
||||
},
|
||||
{
|
||||
id: 'durability',
|
||||
kicker: '06 / DURABILITY',
|
||||
title: 'BUILT\nTO LAST.',
|
||||
description: 'เนื้อผ้าเบรกถนอมจานเบรก ไม่กินจาน ยืดอายุการใช้งานยาวนาน คุ้มค่าสำหรับการขับขี่ทั้งในชีวิตประจำวันและทริปเดินทางไกล',
|
||||
chips: ['Durable', 'Rotor Friendly', 'Everyday Use'],
|
||||
theme: 'durability',
|
||||
},
|
||||
{
|
||||
id: 'fitment',
|
||||
kicker: '07 / FITMENT',
|
||||
title: 'MADE FOR\nYOUR BIKE.',
|
||||
description: 'ออกแบบตรงรุ่นพร้อมติดตั้งแบบ Plug & Play ครอบคลุมรถจักรยานยนต์ยอดนิยมทุกค่าย',
|
||||
theme: 'fitment',
|
||||
},
|
||||
]
|
||||
|
||||
interface Props {
|
||||
payload: Record<string, unknown>
|
||||
onChange: (nextPayload: Record<string, unknown>) => void
|
||||
onBrowseMedia: (onSelect: (url: string) => void) => void
|
||||
}
|
||||
|
||||
export default function ProductStoryScrollForm({ payload, onChange, onBrowseMedia }: Props) {
|
||||
const [selectedSceneIndex, setSelectedSceneIndex] = useState(0)
|
||||
|
||||
const heroImage = (payload.storyHeroImage as string) || ''
|
||||
const endCtaTitle = (payload.endCtaTitle as string) || ''
|
||||
const endCtaButtonText = (payload.endCtaButtonText as string) || ''
|
||||
|
||||
// Auto-seed default mockup scenes on first open if empty
|
||||
useEffect(() => {
|
||||
if (!payload.storyScenes || !Array.isArray(payload.storyScenes) || payload.storyScenes.length === 0) {
|
||||
onChange({
|
||||
...payload,
|
||||
storyScenes: DEFAULT_SCENES,
|
||||
endCtaTitle: payload.endCtaTitle || 'SELECT YOUR MODEL.',
|
||||
endCtaButtonText: payload.endCtaButtonText || 'Choose Your Bike',
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const rawScenes = payload.storyScenes
|
||||
const scenes: StorySceneItem[] = Array.isArray(rawScenes) && rawScenes.length > 0
|
||||
? (rawScenes as any[]).map((s, idx) => ({
|
||||
...DEFAULT_SCENES[idx % DEFAULT_SCENES.length],
|
||||
...s,
|
||||
}))
|
||||
: DEFAULT_SCENES
|
||||
|
||||
const updateScenes = (newScenes: StorySceneItem[]) => {
|
||||
onChange({
|
||||
...payload,
|
||||
storyScenes: newScenes,
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetToMockup = () => {
|
||||
onChange({
|
||||
...payload,
|
||||
storyScenes: DEFAULT_SCENES,
|
||||
endCtaTitle: 'SELECT YOUR MODEL.',
|
||||
endCtaButtonText: 'Choose Your Bike',
|
||||
})
|
||||
}
|
||||
|
||||
const updateSelectedScene = (field: keyof StorySceneItem, val: any) => {
|
||||
const updated = [...scenes]
|
||||
updated[selectedSceneIndex] = {
|
||||
...updated[selectedSceneIndex],
|
||||
[field]: val,
|
||||
}
|
||||
updateScenes(updated)
|
||||
}
|
||||
|
||||
const currentScene = scenes[selectedSceneIndex] || scenes[0]
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pt-4 border-t border-cyan-glow/20">
|
||||
{/* Banner Intro */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 rounded-2xl bg-cyan-glow/[0.05] border border-cyan-glow/20 text-cyan-glow">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className="w-5 h-5 shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-cyan-glow">
|
||||
โหมดนำเสนอพิเศษ: Cinematic Story Scroll (Apple Style)
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
เมื่อเปิดโหมดนี้ หน้าสินค้าบนจอ PC Wide จะแสดงส่วน Scrollytelling เต็มจอ 100vh เล่าเรื่องเทคโนโลยี 7 ฉากแบบภาพยนตร์ไฮเทค พร้อมปุ่มสั่งซื้อที่ด้านล่างสุด
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetToMockup}
|
||||
className="shrink-0 flex items-center gap-2 px-3 py-2 rounded-xl bg-cyan-glow/10 border border-cyan-glow/30 hover:bg-cyan-glow/20 text-cyan-glow text-xs font-bold transition-all shadow-sm"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
โหลด Mockup Data ทั้งหมด
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 1. Core Hero Showcase Image */}
|
||||
<div className="p-4 rounded-2xl bg-night-950 border border-white/10 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<ImageIcon className="w-4 h-4 text-cyan-glow" />
|
||||
ภาพสินค้าหลักสำหรับโชว์ใน Story Stage
|
||||
</h4>
|
||||
<p className="text-xs text-slate-500">
|
||||
แนะนำใช้ภาพสินค้าไดคัทพื้นหลังโปร่งใส (Transparent PNG หรือ WebP) เพื่อให้หมุนและเรืองแสงได้เต็มตา (หากไม่ระบุ จะดึงภาพแรกของสินค้าให้อัตโนมัติ)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative w-24 h-24 rounded-2xl bg-night-900 border border-white/15 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{heroImage ? (
|
||||
<img src={heroImage} alt="Story Hero" className="w-full h-full object-contain p-2" />
|
||||
) : (
|
||||
<span className="text-xs text-slate-600 font-semibold text-center px-2">
|
||||
ใช้ภาพแรกอัตโนมัติ
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onBrowseMedia((url) => onChange({ ...payload, storyHeroImage: url }))}
|
||||
className="px-4 py-2 rounded-xl bg-cyan-glow/15 hover:bg-cyan-glow/25 text-cyan-glow text-xs font-bold flex items-center gap-2 transition-all cursor-pointer border border-cyan-glow/30"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>{heroImage ? 'เปลี่ยนรูปภาพ' : 'อัปโหลด / เลือกรูปภาพ'}</span>
|
||||
</button>
|
||||
{heroImage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...payload, storyHeroImage: '' })}
|
||||
className="px-3 py-2 rounded-xl bg-white/5 hover:bg-rose-500/10 text-slate-400 hover:text-rose-400 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
ล้างรูปภาพ
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
{heroImage ? `URL: ${heroImage.slice(0, 50)}...` : '* ดึงภาพสินค้าที่มีอยู่แล้วมาแสดงอัตโนมัติ'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Story Scenes Manager (7 Scenes Tabs) */}
|
||||
<div className="p-4 rounded-2xl bg-night-950 border border-white/10 space-y-5">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-cyan-glow" />
|
||||
ปรับแต่งฉาก Story Scenes ทั้ง 7 ตอน
|
||||
</h4>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
คลิกเลือกตอนที่ต้องการปรับเปลี่ยนข้อความ ตัวเลข หรือใส่รูปภาพเฉพาะฉาก
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scene Tabs */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2 custom-scrollbar">
|
||||
{scenes.map((s, idx) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedSceneIndex(idx)}
|
||||
className={`px-3.5 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all cursor-pointer border ${
|
||||
selectedSceneIndex === idx
|
||||
? 'bg-cyan-glow/20 text-cyan-glow border-cyan-glow/40 shadow-sm'
|
||||
: 'bg-white/5 text-slate-400 border-white/5 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{`Scene ${idx + 1}: ${s.kicker.split('/')[1]?.trim() || s.id}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Selected Scene Editor */}
|
||||
{currentScene && (
|
||||
<div className="p-4 rounded-2xl bg-night-900 border border-white/10 space-y-4">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-white/5">
|
||||
<span className="text-xs uppercase font-mono font-bold tracking-wider text-cyan-glow">
|
||||
แก้ไขฉากที่ {selectedSceneIndex + 1} ({currentScene.id.toUpperCase()})
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
ธีมแสง: {currentScene.theme}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Custom Image for this Scene */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
|
||||
รูปภาพเฉพาะของฉากนี้ (ถ้ามี เช่น รูปซูมเนื้อผิว / กราฟความร้อน)
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-16 h-16 rounded-xl bg-night-950 border border-white/10 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{currentScene.imageUrl ? (
|
||||
<img src={currentScene.imageUrl} alt={currentScene.title} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-600 text-center">ไม่มีรูป</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onBrowseMedia((url) => updateSelectedScene('imageUrl', url))}
|
||||
className="px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 text-slate-200 text-xs font-semibold flex items-center gap-1.5 cursor-pointer"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>{currentScene.imageUrl ? 'เปลี่ยนรูป' : 'อัปโหลดรูป'}</span>
|
||||
</button>
|
||||
{currentScene.imageUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSelectedScene('imageUrl', '')}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-rose-400"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kicker & Title */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Kicker</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentScene.kicker || ''}
|
||||
onChange={(e) => updateSelectedScene('kicker', e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-950 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40"
|
||||
placeholder="e.g. 02 / MATERIAL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Metric ตัวเลขเด่น (ถ้ามี)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentScene.metric || ''}
|
||||
onChange={(e) => updateSelectedScene('metric', e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-950 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40"
|
||||
placeholder="e.g. 300°C หรือ 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Headline (ขึ้นบรรทัดใหม่ได้)</label>
|
||||
<textarea
|
||||
value={currentScene.title || ''}
|
||||
onChange={(e) => updateSelectedScene('title', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-950 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40 resize-none font-bold"
|
||||
placeholder="e.g. CERAMIC\nCOMPOUND."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">คำอธิบาย</label>
|
||||
<textarea
|
||||
value={currentScene.description || ''}
|
||||
onChange={(e) => updateSelectedScene('description', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-950 border border-white/10 text-xs text-slate-300 outline-none focus:border-cyan-glow/40 resize-none leading-relaxed"
|
||||
placeholder="คำอธิบายรายละเอียดทางเทคนิคของฉากนี้..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chips */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Feature Chips (คั่นด้วยจุลภาค ,)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={(currentScene.chips || []).join(', ')}
|
||||
onChange={(e) => {
|
||||
const items = e.target.value.split(',').map(s => s.trim()).filter(Boolean)
|
||||
updateSelectedScene('chips', items)
|
||||
}}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-950 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40"
|
||||
placeholder="e.g. Low Noise, Low Vibration, Cleaner Braking"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3. End CTA Settings */}
|
||||
<div className="p-4 rounded-2xl bg-night-950 border border-white/10 space-y-4">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-400" />
|
||||
ข้อความส่วนท้าย (End CTA)
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">หัวข้อส่วนท้าย</label>
|
||||
<input
|
||||
type="text"
|
||||
value={endCtaTitle}
|
||||
onChange={(e) => onChange({ ...payload, endCtaTitle: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-900 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40"
|
||||
placeholder="e.g. SELECT YOUR MODEL."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">ข้อความบนปุ่มสั่งซื้อ</label>
|
||||
<input
|
||||
type="text"
|
||||
value={endCtaButtonText}
|
||||
onChange={(e) => onChange({ ...payload, endCtaButtonText: e.target.value })}
|
||||
className="w-full px-3 py-2 rounded-xl bg-night-900 border border-white/10 text-xs text-white outline-none focus:border-cyan-glow/40"
|
||||
placeholder="e.g. Choose Your Bike / สั่งซื้อรุ่นของคุณ"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import FocalPointPicker from '../components/page-builder/FocalPointPicker'
|
||||
import ProductPickerModal from '../components/page-builder/ProductPickerModal'
|
||||
import ProductHeroCarouselBlockForm from '../components/products/ProductHeroCarouselBlockForm'
|
||||
import ProductDividerBlockForm from '../components/products/ProductDividerBlockForm'
|
||||
import ProductStoryScrollForm from '../components/products/ProductStoryScrollForm'
|
||||
import { validateProductHeroCarouselPayload } from '../components/products/productHeroCarouselValidation'
|
||||
import { getAdminDisplayString } from '../utils/localization'
|
||||
import BlockTranslationModal, { isTranslatableBlock } from '../components/page-builder/BlockTranslationModal'
|
||||
@@ -898,11 +899,38 @@ export default function ProductCmsPage() {
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Layout Mode</label>
|
||||
<select
|
||||
value={typeof sectionPayload.layoutMode === 'string' ? sectionPayload.layoutMode : 'standard'}
|
||||
onChange={(event) => syncNativePayload('layoutMode', event.target.value)}
|
||||
onChange={(event) => {
|
||||
const val = event.target.value
|
||||
if (val === 'story_scroll') {
|
||||
const next = {
|
||||
...sectionPayload,
|
||||
layoutMode: 'story_scroll',
|
||||
storyScenes: Array.isArray(sectionPayload.storyScenes) && sectionPayload.storyScenes.length > 0
|
||||
? sectionPayload.storyScenes
|
||||
: [
|
||||
{ id: 'hero', kicker: '01 / PRODUCT STORY', title: 'STOP WITH\nCONFIDENCE.', description: 'พัฒนาขึ้นสำหรับการแข่งขันและการขับขี่สมรรถนะสูง ให้สินค้ากลายเป็นพระเอกก่อนพาลงลึกไปทีละมิติของประสิทธิภาพ', theme: 'hero' },
|
||||
{ id: 'material', kicker: '02 / MATERIAL', title: 'CERAMIC\nCOMPOUND.', description: 'สูตรผสมเนื้อเซรามิกเกรดพรีเมียม สึกหรอน้อย เสียงเบียบเงียบ ไร้แรงสั่นสะเทือน และลดฝุ่นผงจับจานเบรกอย่างเห็นได้ชัด', chips: ['Low Noise', 'Low Vibration', 'Cleaner Braking'], theme: 'material' },
|
||||
{ id: 'heat', kicker: '03 / HEAT CONTROL', title: 'HIGH-HEAT\nCONTROL.', metric: '300°C', description: 'ทนต่อความร้อนสูงสะสม ป้องกันอาการเบรกเฟด (Brake Fade) ให้ระยะหยุดสม่ำเสมอแม้ใช้งานต่อเนื่องบนสนามแข่งหรือทางลาดชัน', theme: 'heat' },
|
||||
{ id: 'friction', kicker: '04 / PERFORMANCE', title: 'STABLE\nFRICTION.', description: 'ค่าสัมประสิทธิ์แรงเสียดทานคงที่ในทุกช่วงอุณหภูมิ ให้การจับตัวนุ่มนวลแต่แน่นหนา มั่นใจในทุกโค้ง', theme: 'friction' },
|
||||
{ id: 'control', kicker: '05 / CONTROL', title: 'SMOOTH\nENGAGEMENT.', description: 'โครงสร้างแม่นยำมาตรฐานสูง สัมผัสก้านเบรกตอบสนองเป็นเส้นตรง (Linear Modulation) คุมน้ำหนักเบรกได้ละเอียดดั่งใจ', theme: 'control' },
|
||||
{ id: 'durability', kicker: '06 / DURABILITY', title: 'BUILT\nTO LAST.', description: 'เนื้อผ้าเบรกถนอมจานเบรก ไม่กินจาน ยืดอายุการใช้งานยาวนาน คุ้มค่าสำหรับการขับขี่ทั้งในชีวิตประจำวันและทริปเดินทางไกล', chips: ['Durable', 'Rotor Friendly', 'Everyday Use'], theme: 'durability' },
|
||||
{ id: 'fitment', kicker: '07 / FITMENT', title: 'MADE FOR\nYOUR BIKE.', description: 'ออกแบบตรงรุ่นพร้อมติดตั้งแบบ Plug & Play ครอบคลุมรถจักรยานยนต์ยอดนิยมทุกค่าย', theme: 'fitment' },
|
||||
],
|
||||
endCtaTitle: (sectionPayload.endCtaTitle as string) || 'SELECT YOUR MODEL.',
|
||||
endCtaButtonText: (sectionPayload.endCtaButtonText as string) || 'Choose Your Bike',
|
||||
}
|
||||
setSectionPayload(next)
|
||||
setSectionJson(JSON.stringify(next, null, 2))
|
||||
} else {
|
||||
syncNativePayload('layoutMode', val)
|
||||
}
|
||||
}}
|
||||
className="admin-input w-full bg-slate-900 border-white/10"
|
||||
>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="sticky_gallery">Sticky Gallery</option>
|
||||
<option value="story_scroll">Cinematic Story Scroll (Apple Style)</option>
|
||||
<option value="apple_watch_tour">Apple Watch Keynote & Swatch Tour</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -936,6 +964,20 @@ export default function ProductCmsPage() {
|
||||
<span className="text-xs font-bold text-slate-300">Show brand/category meta</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{(sectionPayload.layoutMode === 'story_scroll' || sectionPayload.layoutMode === 'apple_watch_tour') && (
|
||||
<ProductStoryScrollForm
|
||||
payload={sectionPayload}
|
||||
onChange={(nextPayload) => {
|
||||
setSectionPayload(nextPayload)
|
||||
setSectionJson(JSON.stringify(nextPayload, null, 2))
|
||||
}}
|
||||
onBrowseMedia={(cb) => {
|
||||
setOnSelectMedia(() => cb)
|
||||
setIsMediaModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{sectionType === 'PRODUCT_REVIEWS' && (
|
||||
|
||||
Reference in new issue
Block a user