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 ProductPickerModal from '../components/page-builder/ProductPickerModal'
|
||||||
import ProductHeroCarouselBlockForm from '../components/products/ProductHeroCarouselBlockForm'
|
import ProductHeroCarouselBlockForm from '../components/products/ProductHeroCarouselBlockForm'
|
||||||
import ProductDividerBlockForm from '../components/products/ProductDividerBlockForm'
|
import ProductDividerBlockForm from '../components/products/ProductDividerBlockForm'
|
||||||
|
import ProductStoryScrollForm from '../components/products/ProductStoryScrollForm'
|
||||||
import { validateProductHeroCarouselPayload } from '../components/products/productHeroCarouselValidation'
|
import { validateProductHeroCarouselPayload } from '../components/products/productHeroCarouselValidation'
|
||||||
import { getAdminDisplayString } from '../utils/localization'
|
import { getAdminDisplayString } from '../utils/localization'
|
||||||
import BlockTranslationModal, { isTranslatableBlock } from '../components/page-builder/BlockTranslationModal'
|
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>
|
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Layout Mode</label>
|
||||||
<select
|
<select
|
||||||
value={typeof sectionPayload.layoutMode === 'string' ? sectionPayload.layoutMode : 'standard'}
|
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"
|
className="admin-input w-full bg-slate-900 border-white/10"
|
||||||
>
|
>
|
||||||
<option value="standard">Standard</option>
|
<option value="standard">Standard</option>
|
||||||
<option value="sticky_gallery">Sticky Gallery</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<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>
|
<span className="text-xs font-bold text-slate-300">Show brand/category meta</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sectionType === 'PRODUCT_REVIEWS' && (
|
{sectionType === 'PRODUCT_REVIEWS' && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
// Trigger reload: support titleSize in HERO and HERO_VIDEO payloads
|
// Trigger reload: support story_scroll and apple_watch_tour in ProductLayoutSectionPayloadSchema
|
||||||
import {
|
import {
|
||||||
LocalizedStringSchema,
|
LocalizedStringSchema,
|
||||||
HeroSectionPayloadSchema,
|
HeroSectionPayloadSchema,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import ProductGallery from '@/components/ProductGallery'
|
|||||||
import ProductHeroCarousel from '@/components/ProductHeroCarousel'
|
import ProductHeroCarousel from '@/components/ProductHeroCarousel'
|
||||||
import ProductDivider from '@/components/ProductDivider'
|
import ProductDivider from '@/components/ProductDivider'
|
||||||
import ProductStickyPurchaseBar from '@/components/ProductStickyPurchaseBar'
|
import ProductStickyPurchaseBar from '@/components/ProductStickyPurchaseBar'
|
||||||
|
import CinematicStoryScroll from '@/components/CinematicStoryScroll'
|
||||||
|
import AppleWatchShowcase from '@/components/AppleWatchShowcase'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import ProductCard from '@/components/ProductCard'
|
import ProductCard from '@/components/ProductCard'
|
||||||
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettings, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
|
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettings, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
|
||||||
@@ -99,7 +101,14 @@ function ProductMainSection({
|
|||||||
imageUrl: string
|
imageUrl: string
|
||||||
payload: Record<string, unknown>
|
payload: Record<string, unknown>
|
||||||
}) {
|
}) {
|
||||||
const layoutMode = payload.layoutMode === 'sticky_gallery' ? 'sticky_gallery' : 'standard'
|
const rawLayout = typeof payload.layoutMode === 'string' ? payload.layoutMode : 'standard'
|
||||||
|
const layoutMode = rawLayout === 'story_scroll'
|
||||||
|
? 'story_scroll'
|
||||||
|
: rawLayout === 'apple_watch_tour' || rawLayout === 'apple_keynote'
|
||||||
|
? 'apple_watch_tour'
|
||||||
|
: rawLayout === 'sticky_gallery'
|
||||||
|
? 'sticky_gallery'
|
||||||
|
: 'standard'
|
||||||
const galleryBehavior = payload.galleryBehavior === 'stacked' ? 'stacked' : 'carousel'
|
const galleryBehavior = payload.galleryBehavior === 'stacked' ? 'stacked' : 'carousel'
|
||||||
const purchaseCardSticky = payload.purchaseCardSticky !== false
|
const purchaseCardSticky = payload.purchaseCardSticky !== false
|
||||||
const showBreadcrumbs = payload.showBreadcrumbs !== false
|
const showBreadcrumbs = payload.showBreadcrumbs !== false
|
||||||
@@ -109,6 +118,98 @@ function ProductMainSection({
|
|||||||
? images
|
? images
|
||||||
: [{ id: -1, url: imageUrl, mobileUrl: null, order: 0 }]
|
: [{ id: -1, url: imageUrl, mobileUrl: null, order: 0 }]
|
||||||
|
|
||||||
|
if (layoutMode === 'story_scroll') {
|
||||||
|
const storyHeroImage = (payload.storyHeroImage as string) || undefined
|
||||||
|
const storyScenes = Array.isArray(payload.storyScenes) ? (payload.storyScenes as any[]) : undefined
|
||||||
|
const endCtaTitle = (payload.endCtaTitle as string) || undefined
|
||||||
|
const endCtaButtonText = (payload.endCtaButtonText as string) || undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-12">
|
||||||
|
<div id="product-purchase-top" className="space-y-8 scroll-mt-24">
|
||||||
|
{showBreadcrumbs && (
|
||||||
|
<nav className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-widest text-muted-text/70">
|
||||||
|
<Link href="/" className="transition hover:text-brand-primary">Home</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<Link href="/shop" className="transition hover:text-brand-primary">Shop</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<Link href={`/category/${product.category.id}`} className="transition hover:text-brand-primary">
|
||||||
|
{categoryName}
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-text-main">{productName}</span>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="grid gap-10 md:grid-cols-2 md:items-start md:gap-16">
|
||||||
|
<ProductGallery
|
||||||
|
images={galleryImages}
|
||||||
|
productName={productName}
|
||||||
|
categoryName={categoryName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddToCartSection product={product} showBrandMeta={showBrandMeta} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cinematic Scrollytelling Story Showcase */}
|
||||||
|
<div className="-mx-4 sm:-mx-6 lg:-mx-8 xl:-mx-12">
|
||||||
|
<CinematicStoryScroll
|
||||||
|
product={product}
|
||||||
|
heroImage={storyHeroImage}
|
||||||
|
scenes={storyScenes}
|
||||||
|
lang="th"
|
||||||
|
endCtaTitle={endCtaTitle}
|
||||||
|
endCtaButtonText={endCtaButtonText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutMode === 'apple_watch_tour') {
|
||||||
|
const storyHeroImage = (payload.storyHeroImage as string) || undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-12">
|
||||||
|
<div id="product-purchase-top" className="space-y-8 scroll-mt-24">
|
||||||
|
{showBreadcrumbs && (
|
||||||
|
<nav className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-widest text-muted-text/70">
|
||||||
|
<Link href="/" className="transition hover:text-brand-primary">Home</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<Link href="/shop" className="transition hover:text-brand-primary">Shop</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<Link href={`/category/${product.category.id}`} className="transition hover:text-brand-primary">
|
||||||
|
{categoryName}
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-text-main">{productName}</span>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="grid gap-10 md:grid-cols-2 md:items-start md:gap-16">
|
||||||
|
<ProductGallery
|
||||||
|
images={galleryImages}
|
||||||
|
productName={productName}
|
||||||
|
categoryName={categoryName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddToCartSection product={product} showBrandMeta={showBrandMeta} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apple Watch Style Keynote & Finishes Tour Showcase */}
|
||||||
|
<div className="-mx-4 sm:-mx-6 lg:-mx-8 xl:-mx-12">
|
||||||
|
<AppleWatchShowcase
|
||||||
|
product={product}
|
||||||
|
heroImage={storyHeroImage}
|
||||||
|
lang="th"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (layoutMode === 'sticky_gallery') {
|
if (layoutMode === 'sticky_gallery') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -0,0 +1,642 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import type { Product } from '@homaroma/types'
|
||||||
|
import { resolveLocalized } from '../lib/localization'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
product: Product
|
||||||
|
heroImage?: string
|
||||||
|
lang?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HighlightCard {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
metric?: string
|
||||||
|
metricLabel?: string
|
||||||
|
tag: string
|
||||||
|
accentColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const HIGHLIGHTS: HighlightCard[] = [
|
||||||
|
{
|
||||||
|
id: 'sensing',
|
||||||
|
title: 'ระบบสัมผัสและเนื้อเซรามิกแบบใหม่หมด',
|
||||||
|
subtitle: 'มาพร้อมการกระจายแรงเสียดทานที่สม่ำเสมอที่สุดในระดับรถมอเตอร์ไซค์สมรรถนะสูง',
|
||||||
|
metric: '0.42 μ',
|
||||||
|
metricLabel: 'สัมประสิทธิ์แรงเสียดทานคงที่',
|
||||||
|
tag: 'Friction Material',
|
||||||
|
accentColor: '#38bdf8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heat',
|
||||||
|
title: 'ทนความร้อนสูงระดับ 300°C',
|
||||||
|
subtitle: 'ฉนวนเซรามิกช่วยสลายความร้อนสะสม ป้องกันอาการเบรกเฟด (Brake Fade) แม้ลงเขายาวนาน',
|
||||||
|
metric: '300°C',
|
||||||
|
metricLabel: 'Peak Operating Heat',
|
||||||
|
tag: 'Thermal Resistance',
|
||||||
|
accentColor: '#fb923c',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acoustics',
|
||||||
|
title: 'เงียบสนิท ไร้เสียงหวีดและแรงสั่น',
|
||||||
|
subtitle: 'แผ่นชิมกันเสียงรบกวนความถี่สูง พร้อมโครงสร้างซับแรงกระแทก ให้ทุกการกำเบรกนุ่มนวลเป็นธรรมชาติ',
|
||||||
|
metric: '< 15 dB',
|
||||||
|
metricLabel: 'Low Acoustic Noise',
|
||||||
|
tag: 'Acoustic Comfort',
|
||||||
|
accentColor: '#a78bfa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'longevity',
|
||||||
|
title: 'ถนอมจานเบรก ยืดอายุการใช้งาน 2 เท่า',
|
||||||
|
subtitle: 'ส่วนผสมไร้ใยหิน (Non-Asbestos) ไม่กินเนื้อจานเบรก ไม่ทิ้งคราบฝุ่นดำสกปรกบนล้อแม็ก',
|
||||||
|
metric: '2.4x',
|
||||||
|
metricLabel: 'Rotor Life Extension',
|
||||||
|
tag: 'Rotor Friendly',
|
||||||
|
accentColor: '#4ade80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'safety',
|
||||||
|
title: 'ความปลอดภัยสูงสุดในทุกสภาพถนน',
|
||||||
|
subtitle: 'ผ่านการทดสอบมาตรฐานความปลอดภัยระดับสากล จับตัวได้แม่นยำแม้ในสภาวะฝนตกหนักหรือถนนเปียกลื่น',
|
||||||
|
metric: '100%',
|
||||||
|
metricLabel: 'All-Weather Confidence',
|
||||||
|
tag: 'Safety Features',
|
||||||
|
accentColor: '#f43f5e',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
interface FinishOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
labelTh: string
|
||||||
|
color: string
|
||||||
|
ringBorder: string
|
||||||
|
desc: string
|
||||||
|
glow: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FINISHES: FinishOption[] = [
|
||||||
|
{
|
||||||
|
id: 'ceramic-white',
|
||||||
|
name: 'Ceramic Pearl White',
|
||||||
|
labelTh: 'เซรามิก สีขาวไข่มุก',
|
||||||
|
color: '#e8e6df',
|
||||||
|
ringBorder: '#ffffff',
|
||||||
|
desc: 'ตัวเรือนและเนื้อคอมพาวด์เซรามิกเกรดบริสุทธิ์ ลดการสะสมของคราบเขม่าดำ ให้สัมผัสเรียบหรูและทนทานระดับพรีเมียม',
|
||||||
|
glow: 'rgba(232, 230, 223, 0.22)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ceramic-blue',
|
||||||
|
name: 'Ceramic Night Blue',
|
||||||
|
labelTh: 'เซรามิก สีไนท์บลู',
|
||||||
|
color: '#1e3a5f',
|
||||||
|
ringBorder: '#3b82f6',
|
||||||
|
desc: 'เฉดสีน้ำเงินล้ำลึก สื่อถึงประสิทธิภาพการสลายความร้อนและเทคโนโลยีการระบายอุณหภูมิขั้นสูง',
|
||||||
|
glow: 'rgba(59, 130, 246, 0.28)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aluminum-bronze',
|
||||||
|
name: 'Aluminum Dark Bronze',
|
||||||
|
labelTh: 'อะลูมิเนียม สีบรอนซ์เข้ม',
|
||||||
|
color: '#453529',
|
||||||
|
ringBorder: '#d97706',
|
||||||
|
desc: 'โครงสร้างโลหะผสมบรอนซ์เข้ม แข็งแกร่ง ทนต่อการบิดงอในสภาวะการเบรกกระชากด้วยความเร็วสูง',
|
||||||
|
glow: 'rgba(217, 119, 6, 0.25)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aluminum-gold',
|
||||||
|
name: 'Aluminum Light Gold',
|
||||||
|
labelTh: 'อะลูมิเนียม สีทองอ่อน',
|
||||||
|
color: '#d4c29d',
|
||||||
|
ringBorder: '#fef08a',
|
||||||
|
desc: 'เฉดสีทองแชมเปญแบบสปอร์ต สะท้อนเอกลักษณ์ของชุดอัปเกรดระดับท็อปสำหรับมอเตอร์ไซค์คันโปรด',
|
||||||
|
glow: 'rgba(234, 179, 8, 0.22)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aluminum-black',
|
||||||
|
name: 'Stealth Racing Black',
|
||||||
|
labelTh: 'อะลูมิเนียม สีดำสเตลท์',
|
||||||
|
color: '#1a1a1e',
|
||||||
|
ringBorder: '#71717a',
|
||||||
|
desc: 'การเคลือบผิวแบบ Hard Anodized สีดำด้าน ดุดัน สไตล์สนามแข่ง ทนการกัดกร่อนจากสารเคมีและไอเกลือ',
|
||||||
|
glow: 'rgba(113, 113, 122, 0.2)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'titanium-gold',
|
||||||
|
name: 'Titanium Radiant Gold',
|
||||||
|
labelTh: 'ไทเทเนียม เรเดียนท์โกลด์',
|
||||||
|
color: '#bfa163',
|
||||||
|
ringBorder: '#fbbf24',
|
||||||
|
desc: 'ผิวสัมผัสไทเทเนียมเรเดียนท์โกลด์ ทนแรงเสียดสีและความร้อนสูงพิเศษ เหมาะสำหรับการขับขี่แบบสปอร์ตดุดัน',
|
||||||
|
glow: 'rgba(251, 191, 36, 0.3)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'titanium-natural',
|
||||||
|
name: 'Titanium Natural Raw',
|
||||||
|
labelTh: 'ไทเทเนียมธรรมชาติ',
|
||||||
|
color: '#8b8a85',
|
||||||
|
ringBorder: '#e4e4e7',
|
||||||
|
desc: 'สีเนื้อไทเทเนียมธรรมชาติแบบดิบ สะท้อนวิศวกรรมน้ำหนักเบาและโครงสร้างทนทานระดับ Superbike',
|
||||||
|
glow: 'rgba(228, 228, 231, 0.2)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
interface DeepFeature {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
badge: string
|
||||||
|
description: string
|
||||||
|
stats: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEEP_FEATURES: DeepFeature[] = [
|
||||||
|
{
|
||||||
|
id: 'compound',
|
||||||
|
title: 'Ceramic Shield Compound',
|
||||||
|
badge: 'วัสดุเซรามิกทนความร้อนสูง',
|
||||||
|
description: 'พัฒนาขึ้นด้วยโครงสร้างไมโครเซรามิกโมเลกุลละเอียด ช่วยสร้างฟิล์มสัมผัสบางเบาบนจานเบรก ทำให้เบรกจับตัวแน่นขึ้นโดยไม่ทำลายผิวหน้าจาน',
|
||||||
|
stats: 'ทนความร้อนสูงสุด 300°C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fade',
|
||||||
|
title: 'Anti-Fade Thermal Dynamics',
|
||||||
|
badge: 'ป้องกันอาการเบรกเฟด 100%',
|
||||||
|
description: 'ร่องระบายฝุ่นและแก๊สร้อนที่ออกแบบตามหลักอากาศพลศาสตร์ ช่วยลดการก่อตัวของชั้นก๊าซระหว่างผ้าเบรกกับจานเบรกขณะเบรกต่อเนื่อง',
|
||||||
|
stats: 'ความหน่วงสม่ำเสมอตลอดทริป',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'acoustic',
|
||||||
|
title: 'Acoustic Damping Shim',
|
||||||
|
badge: 'แผ่นซับเสียงและลดแรงสั่น',
|
||||||
|
description: 'ติดตั้งแผ่นชิมโลหะหลายชั้นพร้อมชั้นกาวหน่วงการสั่นสะเทือน (Viscoelastic Layer) เพื่อตัดวงจรเสียงหวีดแหลมขณะเบรกที่ความเร็วต่ำ',
|
||||||
|
stats: 'ลดเสียงดังรบกวนได้ถึง 90%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fitment',
|
||||||
|
title: '100% OEM Direct Fit',
|
||||||
|
badge: 'ติดตั้งตรงรุ่นโดยไม่ต้องดัดแปลง',
|
||||||
|
description: 'ผ่านการขึ้นรูปตามสเปกมาตรฐานของแท้ติดรถ (Original Equipment) สลักและสปริงกดแนบสนิท สามารถเปลี่ยนแทนของเดิมได้ทันที',
|
||||||
|
stats: 'รองรับ ADV, PCX, Vario, NVX',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AppleWatchShowcase({ product, heroImage, lang = 'th' }: Props) {
|
||||||
|
const productName = resolveLocalized(product.name, lang)
|
||||||
|
const mainImage = heroImage || product.images?.[0]?.url || '/placeholder.jpg'
|
||||||
|
|
||||||
|
// Highlights state
|
||||||
|
const [activeHighlight, setActiveHighlight] = useState(0)
|
||||||
|
const [isPlaying, setIsPlaying] = useState(true)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
||||||
|
// Finishes state
|
||||||
|
const [activeFinish, setActiveFinish] = useState(0)
|
||||||
|
const [activeDeepFeature, setActiveDeepFeature] = useState<string | null>('compound')
|
||||||
|
|
||||||
|
const autoPlayRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// Timer for highlights gallery auto-play
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlaying) {
|
||||||
|
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(0)
|
||||||
|
const stepTime = 50 // ms
|
||||||
|
const totalTime = 4000 // 4 seconds per slide
|
||||||
|
const increment = (stepTime / totalTime) * 100
|
||||||
|
|
||||||
|
progressIntervalRef.current = setInterval(() => {
|
||||||
|
setProgress((prev) => {
|
||||||
|
if (prev >= 100) {
|
||||||
|
setActiveHighlight((curr) => (curr + 1) % HIGHLIGHTS.length)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev + increment
|
||||||
|
})
|
||||||
|
}, stepTime)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
|
||||||
|
}
|
||||||
|
}, [activeHighlight, isPlaying])
|
||||||
|
|
||||||
|
const currentFinish = FINISHES[activeFinish]
|
||||||
|
const currentHighlight = HIGHLIGHTS[activeHighlight]
|
||||||
|
|
||||||
|
const scrollToPurchase = () => {
|
||||||
|
const el = document.getElementById('product-purchase-top')
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden bg-[#0c0f17] text-white font-sans selection:bg-sky-500/30 selection:text-white">
|
||||||
|
{/* ---------------- 1. MEDIA HERO SECTION ---------------- */}
|
||||||
|
<section className="relative min-h-[90vh] flex flex-col justify-between px-6 pt-16 pb-12 md:px-12 lg:px-20 border-b border-white/10 overflow-hidden">
|
||||||
|
{/* Ambient Top Glow */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute -top-40 left-1/2 -translate-x-1/2 h-[560px] w-[900px] rounded-full blur-[140px] opacity-40 transition-all duration-1000"
|
||||||
|
style={{ background: currentFinish.glow }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hero Header Lockup */}
|
||||||
|
<div className="relative z-10 max-w-4xl mx-auto text-center space-y-4 pt-6">
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-sky-400 backdrop-blur-md">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-sky-400 animate-pulse" />
|
||||||
|
Flagship Engineering Series
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-black tracking-tight leading-[0.95] text-white">
|
||||||
|
นวัตกรรมที่คิด<br />
|
||||||
|
<span className="bg-gradient-to-r from-white via-slate-200 to-slate-400 bg-clip-text text-transparent">
|
||||||
|
เพื่อทุกจังหวะเบรก
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="max-w-2xl mx-auto text-base sm:text-lg md:text-xl text-slate-400 font-normal leading-relaxed pt-2">
|
||||||
|
สัมผัสประสบการณ์เบรกระดับพรีเมียม ตอบสนองฉับไว ปลอดภัย ทนความร้อนสูง 300°C และเป็นมิตรต่อจานเบรกในทุกย่านความเร็ว
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Dynamic Pricing & Fast Action Platter */}
|
||||||
|
<div className="pt-4 flex flex-wrap items-center justify-center gap-4">
|
||||||
|
<div className="inline-flex items-center gap-3 rounded-2xl border border-white/10 bg-white/5 px-5 py-3 backdrop-blur-xl">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider text-slate-400">เริ่มต้นที่</span>
|
||||||
|
<span className="text-2xl font-black text-white">
|
||||||
|
฿{typeof product.price === 'number' ? product.price.toLocaleString() : '—'}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-emerald-500/20 px-2.5 py-0.5 text-[11px] font-bold text-emerald-400">
|
||||||
|
พร้อมจัดส่งทันที
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={scrollToPurchase}
|
||||||
|
className="inline-flex items-center justify-center rounded-2xl bg-white px-8 py-3.5 text-sm font-bold text-slate-950 hover:bg-slate-200 active:scale-95 transition-all shadow-xl shadow-white/10"
|
||||||
|
>
|
||||||
|
เลือกซื้อรุ่นของคุณ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Visual Stage */}
|
||||||
|
<div className="relative z-10 my-10 flex justify-center items-center">
|
||||||
|
<div className="relative w-full max-w-[540px] aspect-square flex items-center justify-center">
|
||||||
|
{/* Multi-ring metallic visual background */}
|
||||||
|
<div className="absolute inset-4 rounded-full border border-white/10 bg-gradient-to-b from-white/5 to-transparent backdrop-blur-md" />
|
||||||
|
<div className="absolute inset-16 rounded-full border border-white/5" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-28 rounded-full blur-3xl opacity-30 transition-all duration-700"
|
||||||
|
style={{ background: currentFinish.glow }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Product Center Image */}
|
||||||
|
<div className="relative w-4/5 h-4/5 transition-transform duration-700 hover:scale-105 drop-shadow-[0_35px_60px_rgba(0,0,0,0.8)]">
|
||||||
|
<Image
|
||||||
|
src={mainImage}
|
||||||
|
alt={productName}
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
sizes="(min-width: 1024px) 500px, 80vw"
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll down teaser */}
|
||||||
|
<div className="relative z-10 flex justify-center text-xs uppercase tracking-widest text-slate-500">
|
||||||
|
<span>เลื่อนลงเพื่อดูไฮไลท์และนวัตกรรม ↓</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------------- 2. HIGHLIGHTS GALLERY CAROUSEL ---------------- */}
|
||||||
|
<section className="relative px-6 py-20 md:px-12 lg:px-20 border-b border-white/10 bg-[#090c13]">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-10">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-sky-400">KEY HIGHLIGHTS</p>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-black tracking-tight text-white mt-1">
|
||||||
|
ดูไฮไลท์ต่างๆ
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timed Controls & Dotnav */}
|
||||||
|
<div className="flex items-center gap-3 bg-white/5 border border-white/10 rounded-full px-4 py-2 backdrop-blur-md">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPlaying(!isPlaying)}
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold bg-white/10 hover:bg-white/20 text-white transition-all"
|
||||||
|
title={isPlaying ? 'Pause' : 'Play'}
|
||||||
|
aria-label={isPlaying ? 'Pause Highlights' : 'Play Highlights'}
|
||||||
|
>
|
||||||
|
{isPlaying ? '⏸' : '▶'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{HIGHLIGHTS.map((item, idx) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveHighlight(idx)
|
||||||
|
setProgress(0)
|
||||||
|
}}
|
||||||
|
className="group relative h-2.5 rounded-full transition-all duration-300 overflow-hidden"
|
||||||
|
style={{ width: activeHighlight === idx ? '48px' : '10px' }}
|
||||||
|
aria-label={`Highlight ${idx + 1}`}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-white/20 rounded-full" />
|
||||||
|
{activeHighlight === idx && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 bg-white rounded-full transition-all duration-75"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Active Highlight Card */}
|
||||||
|
<div className="relative rounded-3xl border border-white/10 bg-gradient-to-br from-white/10 via-white/5 to-transparent p-8 md:p-14 backdrop-blur-2xl shadow-2xl overflow-hidden transition-all duration-500">
|
||||||
|
{/* Background Glow */}
|
||||||
|
<div
|
||||||
|
className="absolute -top-32 -right-32 w-96 h-96 rounded-full blur-[120px] opacity-25 transition-all duration-500"
|
||||||
|
style={{ background: currentHighlight.accentColor }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 grid gap-8 lg:grid-cols-[1.2fr_0.8fr] items-center">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<span
|
||||||
|
className="inline-block rounded-full px-4 py-1 text-xs font-bold uppercase tracking-wider border"
|
||||||
|
style={{
|
||||||
|
borderColor: `${currentHighlight.accentColor}40`,
|
||||||
|
backgroundColor: `${currentHighlight.accentColor}15`,
|
||||||
|
color: currentHighlight.accentColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentHighlight.tag}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h3 className="text-2xl sm:text-4xl md:text-5xl font-black tracking-tight text-white leading-tight">
|
||||||
|
{currentHighlight.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-base sm:text-lg text-slate-300 leading-relaxed max-w-xl">
|
||||||
|
{currentHighlight.subtitle}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{currentHighlight.metric && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div
|
||||||
|
className="text-5xl sm:text-7xl font-black tracking-tight"
|
||||||
|
style={{ color: currentHighlight.accentColor }}
|
||||||
|
>
|
||||||
|
{currentHighlight.metric}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-slate-400 mt-1">
|
||||||
|
{currentHighlight.metricLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlights Visual Feature Block */}
|
||||||
|
<div className="relative flex justify-center items-center min-h-[260px] rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-md">
|
||||||
|
<div className="relative w-48 h-48 md:w-60 md:h-60 transition-transform duration-500 hover:rotate-6">
|
||||||
|
<Image
|
||||||
|
src={mainImage}
|
||||||
|
alt={currentHighlight.title}
|
||||||
|
fill
|
||||||
|
className="object-contain drop-shadow-[0_20px_35px_rgba(0,0,0,0.7)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Card Nav Pill */}
|
||||||
|
<div className="mt-8 flex flex-wrap gap-2 pt-6 border-t border-white/10">
|
||||||
|
{HIGHLIGHTS.map((item, idx) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveHighlight(idx)
|
||||||
|
setProgress(0)
|
||||||
|
}}
|
||||||
|
className={`rounded-xl px-4 py-2 text-xs font-bold transition-all ${
|
||||||
|
activeHighlight === idx
|
||||||
|
? 'bg-white text-slate-950 shadow-md'
|
||||||
|
: 'bg-white/5 text-slate-400 hover:bg-white/10 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------------- 3. FINISHES & SWATCH EXPLORER ("มาดูกันใกล้ๆ") ---------------- */}
|
||||||
|
<section className="relative px-6 py-24 md:px-12 lg:px-20 border-b border-white/10 bg-[#0c0f17]">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-12">
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-amber-400">FINISHES & MATERIALS</p>
|
||||||
|
<h2 className="text-3xl sm:text-5xl md:text-6xl font-black tracking-tight text-white">
|
||||||
|
มาดูกันใกล้ๆ
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm sm:text-base text-slate-400 max-w-xl mx-auto">
|
||||||
|
เลือกชมรายละเอียดพื้นผิวและวัสดุเกรดแข่งขันที่ได้รับการออกแบบมาอย่างประณีต
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Central Swatch Viewer Canvas */}
|
||||||
|
<div className="relative rounded-3xl border border-white/10 bg-gradient-to-b from-white/5 to-transparent p-8 md:p-14 backdrop-blur-xl">
|
||||||
|
{/* Ambient Reactive Dynamic Glow */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 blur-3xl opacity-30 transition-all duration-700 rounded-3xl"
|
||||||
|
style={{ background: currentFinish.glow }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 grid gap-12 lg:grid-cols-[1.1fr_0.9fr] items-center">
|
||||||
|
{/* Product Visual Centerpiece */}
|
||||||
|
<div className="relative flex items-center justify-center min-h-[380px] sm:min-h-[460px]">
|
||||||
|
<div className="relative w-72 h-72 sm:w-96 sm:h-96 transition-all duration-700 ease-out transform">
|
||||||
|
<Image
|
||||||
|
src={mainImage}
|
||||||
|
alt={currentFinish.name}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 1024px) 450px, 80vw"
|
||||||
|
className="object-contain drop-shadow-[0_30px_50px_rgba(0,0,0,0.8)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swatch Selector Controls */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-bold uppercase tracking-widest text-slate-400">สีและวัสดุตัวเรือน</span>
|
||||||
|
<h3 className="text-2xl sm:text-3xl font-black text-white mt-1">
|
||||||
|
{currentFinish.labelTh}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs uppercase tracking-wider text-slate-500 font-mono mt-0.5">
|
||||||
|
{currentFinish.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm sm:text-base text-slate-300 leading-relaxed border-l-2 border-white/20 pl-4 py-1">
|
||||||
|
{currentFinish.desc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Apple-style Round Swatches Grid */}
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<label className="text-[11px] font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
เลือกเฉดสี ({activeFinish + 1} / {FINISHES.length}):
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{FINISHES.map((finish, idx) => (
|
||||||
|
<button
|
||||||
|
key={finish.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveFinish(idx)}
|
||||||
|
className={`relative w-10 h-10 rounded-full transition-all duration-200 flex items-center justify-center ${
|
||||||
|
activeFinish === idx
|
||||||
|
? 'scale-110 ring-2 ring-offset-2 ring-offset-[#0c0f17] shadow-lg'
|
||||||
|
: 'hover:scale-105 opacity-80 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: finish.color,
|
||||||
|
boxShadow: activeFinish === idx ? `0 0 16px ${finish.glow}` : undefined,
|
||||||
|
}}
|
||||||
|
title={finish.labelTh}
|
||||||
|
aria-label={finish.labelTh}
|
||||||
|
>
|
||||||
|
{activeFinish === idx && (
|
||||||
|
<span
|
||||||
|
className="w-3.5 h-3.5 rounded-full border-2 border-white/80"
|
||||||
|
style={{ borderColor: finish.ringBorder }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Paddle Navigation Buttons */}
|
||||||
|
<div className="flex items-center gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveFinish((prev) => (prev > 0 ? prev - 1 : FINISHES.length - 1))}
|
||||||
|
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-xs font-bold text-slate-300 hover:bg-white/10 transition-all"
|
||||||
|
>
|
||||||
|
← ก่อนหน้า
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveFinish((prev) => (prev < FINISHES.length - 1 ? prev + 1 : 0))}
|
||||||
|
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-xs font-bold text-slate-300 hover:bg-white/10 transition-all"
|
||||||
|
>
|
||||||
|
ถัดไป →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------------- 4. ENGINEERING BENTO DEEP-DIVE ---------------- */}
|
||||||
|
<section className="relative px-6 py-20 md:px-12 lg:px-20 border-b border-white/10 bg-[#090c13]">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-12">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-emerald-400">TECHNICAL INNOVATION</p>
|
||||||
|
<h2 className="text-3xl sm:text-5xl font-black tracking-tight text-white">
|
||||||
|
เจาะลึกนวัตกรรม
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm sm:text-base text-slate-400">
|
||||||
|
ทุกชิ้นส่วนถูกออกแบบมาเพื่อความแม่นยำและความปลอดภัยในชีวิตจริง
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Feature Accordion Pills */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{DEEP_FEATURES.map((feat) => {
|
||||||
|
const isOpen = activeDeepFeature === feat.id
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={feat.id}
|
||||||
|
onClick={() => setActiveDeepFeature(isOpen ? null : feat.id)}
|
||||||
|
className={`cursor-pointer rounded-2xl border p-6 transition-all duration-300 ${
|
||||||
|
isOpen
|
||||||
|
? 'border-sky-500/50 bg-sky-950/20 shadow-xl shadow-sky-500/5'
|
||||||
|
: 'border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/[0.07]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wider text-sky-400">
|
||||||
|
{feat.badge}
|
||||||
|
</span>
|
||||||
|
<h4 className="text-lg sm:text-xl font-bold text-white">
|
||||||
|
{feat.title}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<span className="text-xl text-slate-400 font-bold transition-transform duration-300">
|
||||||
|
{isOpen ? '−' : '+'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-white/10 space-y-3 animate-fadeIn">
|
||||||
|
<p className="text-sm text-slate-300 leading-relaxed">
|
||||||
|
{feat.description}
|
||||||
|
</p>
|
||||||
|
<div className="inline-block rounded-lg bg-white/10 px-3 py-1 text-xs font-semibold text-white">
|
||||||
|
⚡ {feat.stats}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------------- 5. CONVERSION DOCK & FOOTER CTA ---------------- */}
|
||||||
|
<section className="relative px-6 py-24 md:px-12 lg:px-20 bg-gradient-to-t from-[#05070c] to-[#0c0f17] text-center">
|
||||||
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-slate-500">READY TO UPGRADE</p>
|
||||||
|
<h2 className="text-4xl sm:text-6xl font-black tracking-tight text-white">
|
||||||
|
อัปเกรดระบบเบรก<br />
|
||||||
|
สู่มาตรฐานระดับโลก
|
||||||
|
</h2>
|
||||||
|
<p className="text-base sm:text-lg text-slate-400 leading-relaxed">
|
||||||
|
พร้อมรับประกันคุณภาพและความเข้ากันได้กับรถของคุณ สั่งซื้อวันนี้เพื่อสัมผัสความมั่นใจในทุกระยะหยุด
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={scrollToPurchase}
|
||||||
|
className="inline-flex items-center justify-center rounded-2xl bg-white px-10 py-4 text-base font-black text-slate-950 hover:bg-slate-200 active:scale-95 transition-all shadow-2xl shadow-white/20"
|
||||||
|
>
|
||||||
|
เลือกซื้อรุ่นของคุณ (Select Model) ↑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import type { Product } from '@homaroma/types'
|
||||||
|
import { resolveLocalized } from '../lib/localization'
|
||||||
|
|
||||||
|
export interface StoryScene {
|
||||||
|
id: string
|
||||||
|
kicker?: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
metric?: string
|
||||||
|
chips?: string[]
|
||||||
|
theme?: 'hero' | 'material' | 'heat' | 'friction' | 'control' | 'durability' | 'fitment' | string
|
||||||
|
imageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
product: Product
|
||||||
|
heroImage?: string
|
||||||
|
scenes?: StoryScene[]
|
||||||
|
lang: string
|
||||||
|
endCtaTitle?: string
|
||||||
|
endCtaButtonText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SCENES: StoryScene[] = [
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function CinematicStoryScroll({
|
||||||
|
product,
|
||||||
|
heroImage,
|
||||||
|
scenes,
|
||||||
|
lang,
|
||||||
|
endCtaTitle,
|
||||||
|
endCtaButtonText,
|
||||||
|
}: Props) {
|
||||||
|
const [activeScene, setActiveScene] = useState<string>('hero')
|
||||||
|
const [isStoryVisible, setIsStoryVisible] = useState(false)
|
||||||
|
const storyRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const activeScenes = useMemo(() => {
|
||||||
|
if (scenes && scenes.length > 0) return scenes
|
||||||
|
return DEFAULT_SCENES
|
||||||
|
}, [scenes])
|
||||||
|
|
||||||
|
const resolvedProductName = useMemo(() => {
|
||||||
|
return resolveLocalized(product.name, lang) || 'Product Story'
|
||||||
|
}, [product.name, lang])
|
||||||
|
|
||||||
|
const mainShowcaseImage = useMemo(() => {
|
||||||
|
if (heroImage) return heroImage
|
||||||
|
if (product.images && product.images.length > 0) return product.images[0].url
|
||||||
|
return ''
|
||||||
|
}, [heroImage, product.images])
|
||||||
|
|
||||||
|
const fitmentModels = useMemo(() => {
|
||||||
|
if (product.options && product.options.length > 0) {
|
||||||
|
const opt1 = product.options[0]
|
||||||
|
if (opt1 && Array.isArray(opt1.values)) {
|
||||||
|
return opt1.values.map(v => {
|
||||||
|
if (typeof v === 'string') return v
|
||||||
|
return resolveLocalized(v, lang) || Object.values(v)[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['NVX155', 'LC135', 'Vario125', 'Vario150', 'PCX150', 'Beat110', 'Wave110-I', 'R15']
|
||||||
|
}, [product.options, lang])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storyEl = storyRef.current
|
||||||
|
if (!storyEl) return
|
||||||
|
|
||||||
|
const triggers = storyEl.querySelectorAll<HTMLElement>('.story-trigger')
|
||||||
|
if (triggers.length === 0) return
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const sceneId = entry.target.getAttribute('data-scene')
|
||||||
|
if (sceneId) setActiveScene(sceneId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.55 }
|
||||||
|
)
|
||||||
|
|
||||||
|
triggers.forEach((t) => observer.observe(t))
|
||||||
|
|
||||||
|
const visibilityObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
setIsStoryVisible(entry.isIntersecting)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.05 }
|
||||||
|
)
|
||||||
|
|
||||||
|
visibilityObserver.observe(storyEl)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
visibilityObserver.disconnect()
|
||||||
|
}
|
||||||
|
}, [activeScenes])
|
||||||
|
|
||||||
|
const scrollToPurchase = () => {
|
||||||
|
const el = document.getElementById('product-purchase-top')
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
} else {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active scene index
|
||||||
|
const activeIndex = Math.max(0, activeScenes.findIndex(s => s.id === activeScene))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={storyRef} className="relative w-full bg-[#0c1320] text-white overflow-hidden transition-colors duration-700 select-none">
|
||||||
|
{/* Side Progress Dots (Desktop) */}
|
||||||
|
<div
|
||||||
|
className={`fixed right-6 top-1/2 -translate-y-1/2 z-40 hidden lg:flex flex-col gap-2.5 transition-opacity duration-300 pointer-events-none ${
|
||||||
|
isStoryVisible ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activeScenes.map((s, idx) => (
|
||||||
|
<div
|
||||||
|
key={s.id}
|
||||||
|
className={`w-2 rounded-full transition-all duration-300 ${
|
||||||
|
activeIndex === idx
|
||||||
|
? 'h-7 bg-white shadow-lg shadow-white/30'
|
||||||
|
: 'h-2 bg-white/20'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Scrollytelling Stage */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Sticky 100vh Stage */}
|
||||||
|
<div className="sticky top-0 h-screen w-full overflow-hidden flex items-center justify-center">
|
||||||
|
{/* Ambient Reactive Glow Backdrops */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
|
||||||
|
activeScene === 'heat' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle at 35% 50%, rgba(255, 102, 0, 0.45), transparent 40%), linear-gradient(110deg, #180d08 0%, #0d1422 75%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
|
||||||
|
activeScene === 'material' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle at 35% 50%, rgba(30, 110, 255, 0.35), transparent 38%), linear-gradient(180deg, #0d1728 0%, #09101a 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
|
||||||
|
activeScene === 'friction' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: 'linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px)',
|
||||||
|
backgroundSize: '40px 40px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
|
||||||
|
activeScene === 'durability' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle at 40% 60%, rgba(216, 179, 90, 0.28), transparent 35%), linear-gradient(120deg, #131c28 0%, #0a0e16 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
|
||||||
|
activeScene === 'hero' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle at 35% 50%, rgba(20, 103, 255, 0.3), transparent 35%), linear-gradient(180deg, #0c1320 0%, #0a0f18 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Desktop Layout: Left Showcase Visual, Right Copy */}
|
||||||
|
<div className="relative w-full max-w-7xl mx-auto px-6 md:px-12 h-full flex flex-col lg:flex-row items-center justify-between gap-8 z-10">
|
||||||
|
{/* Left Product Core / Showcase Visual */}
|
||||||
|
<div className="w-full lg:w-1/2 flex items-center justify-center relative mt-16 lg:mt-0">
|
||||||
|
<div
|
||||||
|
className={`relative w-64 h-64 sm:w-80 sm:h-80 md:w-96 md:h-96 lg:w-[440px] lg:h-[440px] rounded-full transition-all duration-700 flex items-center justify-center ${
|
||||||
|
activeScene === 'heat'
|
||||||
|
? 'scale-105 border-[16px] border-orange-500 shadow-[0_0_90px_rgba(255,102,0,0.6)]'
|
||||||
|
: activeScene === 'material'
|
||||||
|
? 'scale-110 border-[16px] border-blue-500 shadow-[0_0_80px_rgba(20,103,255,0.5)]'
|
||||||
|
: activeScene === 'control'
|
||||||
|
? 'scale-95 -rotate-12 border-[14px] border-cyan-400 shadow-[0_0_70px_rgba(34,211,238,0.4)]'
|
||||||
|
: activeScene === 'durability'
|
||||||
|
? 'scale-100 border-[16px] border-[#d8b35a] shadow-[0_0_80px_rgba(216,179,90,0.45)]'
|
||||||
|
: 'scale-100 border-[16px] border-[#1467ff] shadow-[0_0_70px_rgba(20,103,255,0.4)]'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle, rgba(12, 19, 32, 0.95) 45%, rgba(6, 10, 18, 0.98) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Product Image */}
|
||||||
|
{mainShowcaseImage ? (
|
||||||
|
<div className="relative w-4/5 h-4/5 p-4 flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src={mainShowcaseImage}
|
||||||
|
alt={resolvedProductName}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1024px) 320px, 440px"
|
||||||
|
className="object-contain drop-shadow-[0_20px_35px_rgba(0,0,0,0.7)] transition-transform duration-700"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center p-6 space-y-2">
|
||||||
|
<span className="text-4xl">🏎️</span>
|
||||||
|
<p className="text-xs uppercase tracking-widest text-slate-400 font-bold">Premium Racing Spec</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pulsing Core Ring */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-4 rounded-full border border-white/10 pointer-events-none animate-spin-slow"
|
||||||
|
style={{ animationDuration: '40s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Narrative Copy Block */}
|
||||||
|
<div className="w-full lg:w-1/2 flex items-center justify-start relative min-h-[320px] lg:min-h-[460px] pb-12 lg:pb-0">
|
||||||
|
{activeScenes.map((scene) => {
|
||||||
|
const isActive = scene.id === activeScene
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={scene.id}
|
||||||
|
className={`absolute inset-x-0 top-1/2 -translate-y-1/2 space-y-5 transition-all duration-500 max-w-lg ${
|
||||||
|
isActive
|
||||||
|
? 'opacity-100 translate-y-[-50%] pointer-events-auto'
|
||||||
|
: 'opacity-0 translate-y-[-40%] pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Kicker */}
|
||||||
|
<div className="text-xs md:text-sm font-black tracking-[0.2em] uppercase text-cyan-300">
|
||||||
|
{scene.kicker}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headline */}
|
||||||
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight leading-[0.95] whitespace-pre-line text-white">
|
||||||
|
{scene.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Metric Highlight (e.g. 300°C) */}
|
||||||
|
{scene.metric && (
|
||||||
|
<div className="text-5xl sm:text-6xl md:text-7xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-amber-300 to-yellow-200">
|
||||||
|
{scene.metric}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm md:text-base text-slate-300 leading-relaxed max-w-md">
|
||||||
|
{scene.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Feature Chips */}
|
||||||
|
{scene.chips && scene.chips.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
|
{scene.chips.map((chip, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-3.5 py-1.5 rounded-full text-xs font-semibold bg-white/10 border border-white/20 text-slate-200 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
{chip}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fitment Grid for Fitment Scene */}
|
||||||
|
{scene.id === 'fitment' && fitmentModels.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<p className="text-xs uppercase font-bold tracking-wider text-slate-400 mb-2">
|
||||||
|
รุ่นรถยอดนิยมที่รองรับ ({fitmentModels.length} รุ่น):
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-48 overflow-y-auto custom-scrollbar pr-1">
|
||||||
|
{fitmentModels.slice(0, 18).map((model, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-2 rounded-xl text-center text-xs font-bold bg-white/10 border border-white/15 text-white truncate"
|
||||||
|
title={model}
|
||||||
|
>
|
||||||
|
{model}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll Trigger Sections (each 90vh tall to drive IntersectionObserver) */}
|
||||||
|
{activeScenes.map((scene) => (
|
||||||
|
<div
|
||||||
|
key={scene.id}
|
||||||
|
data-scene={scene.id}
|
||||||
|
className="story-trigger h-[90vh] w-full pointer-events-none opacity-0"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End CTA Section */}
|
||||||
|
<section className="relative z-10 min-h-[50vh] flex flex-col items-center justify-center text-center px-6 py-20 bg-gradient-to-b from-[#0c1320] via-[#09101a] to-[#060a12] border-t border-white/10 space-y-5">
|
||||||
|
<div className="text-xs uppercase font-black tracking-[0.25em] text-cyan-400">
|
||||||
|
END OF PRODUCT STORY
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl sm:text-5xl md:text-6xl font-black text-white tracking-tight">
|
||||||
|
{endCtaTitle || 'SELECT YOUR MODEL.'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm md:text-base text-slate-400 max-w-md mx-auto leading-relaxed">
|
||||||
|
พร้อมยกระดับสมรรถนะการเบรกให้มอเตอร์ไซค์ของคุณ สั่งซื้อได้ทันทีตรงรุ่น
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={scrollToPurchase}
|
||||||
|
className="mt-4 px-8 py-4 rounded-2xl font-black text-sm uppercase tracking-wider bg-gradient-to-r from-cyan-400 to-blue-500 hover:from-cyan-300 hover:to-blue-400 text-night-950 shadow-xl shadow-cyan-500/25 transition-all transform hover:scale-105 active:scale-95 cursor-pointer"
|
||||||
|
>
|
||||||
|
{endCtaButtonText || 'Choose Your Bike / สั่งซื้อรุ่นของคุณ'}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+17
-1
@@ -1101,17 +1101,33 @@ export const ProductCarouselSectionPayloadSchema = z.object({
|
|||||||
}).strict();
|
}).strict();
|
||||||
export type ProductCarouselSectionPayload = z.infer<typeof ProductCarouselSectionPayloadSchema>;
|
export type ProductCarouselSectionPayload = z.infer<typeof ProductCarouselSectionPayloadSchema>;
|
||||||
|
|
||||||
|
export const StorySceneSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
kicker: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
metric: z.string().optional(),
|
||||||
|
chips: z.array(z.string()).optional(),
|
||||||
|
theme: z.string().optional(),
|
||||||
|
imageUrl: z.string().optional(),
|
||||||
|
}).passthrough();
|
||||||
|
export type StoryScene = z.infer<typeof StorySceneSchema>;
|
||||||
|
|
||||||
export const ProductLayoutSectionPayloadSchema = z.object({
|
export const ProductLayoutSectionPayloadSchema = z.object({
|
||||||
title: LocalizedStringSchema.optional(),
|
title: LocalizedStringSchema.optional(),
|
||||||
subtitle: LocalizedStringSchema.optional(),
|
subtitle: LocalizedStringSchema.optional(),
|
||||||
showHeading: z.boolean().optional(),
|
showHeading: z.boolean().optional(),
|
||||||
showViewAllLink: z.boolean().optional(),
|
showViewAllLink: z.boolean().optional(),
|
||||||
layoutMode: z.enum(['standard', 'sticky_gallery']).optional(),
|
layoutMode: z.enum(['standard', 'sticky_gallery', 'story_scroll', 'apple_watch_tour', 'apple_keynote']).optional(),
|
||||||
galleryBehavior: z.enum(['carousel', 'stacked']).optional(),
|
galleryBehavior: z.enum(['carousel', 'stacked']).optional(),
|
||||||
purchaseCardSticky: z.boolean().optional(),
|
purchaseCardSticky: z.boolean().optional(),
|
||||||
showStickyPurchaseBarAfterMain: z.boolean().optional(),
|
showStickyPurchaseBarAfterMain: z.boolean().optional(),
|
||||||
showBreadcrumbs: z.boolean().optional(),
|
showBreadcrumbs: z.boolean().optional(),
|
||||||
showBrandMeta: z.boolean().optional(),
|
showBrandMeta: z.boolean().optional(),
|
||||||
|
storyHeroImage: z.string().optional(),
|
||||||
|
storyScenes: z.array(StorySceneSchema).optional(),
|
||||||
|
endCtaTitle: z.string().optional(),
|
||||||
|
endCtaButtonText: z.string().optional(),
|
||||||
}).strict();
|
}).strict();
|
||||||
export type ProductLayoutSectionPayload = z.infer<typeof ProductLayoutSectionPayloadSchema>;
|
export type ProductLayoutSectionPayload = z.infer<typeof ProductLayoutSectionPayloadSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="th">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>TOBAKI Story Scroll Mockup</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f6f3ea;
|
||||||
|
--ink:#111;
|
||||||
|
--muted:#6d6a62;
|
||||||
|
--blue:#1267ff;
|
||||||
|
--gold:#d8b35a;
|
||||||
|
--dark:#0c1320;
|
||||||
|
--panel:#ffffff;
|
||||||
|
--line:#d8d3c8;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color:var(--ink);
|
||||||
|
background:var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- top product block ---------- */
|
||||||
|
.product-top{
|
||||||
|
min-height:92vh;
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:1.05fr 1fr;
|
||||||
|
gap:4vw;
|
||||||
|
align-items:center;
|
||||||
|
padding:8vh 8vw;
|
||||||
|
background:#f7f4ec;
|
||||||
|
border-bottom:1px solid #e5e0d6;
|
||||||
|
}
|
||||||
|
.gallery{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:72px 1fr;
|
||||||
|
gap:20px;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
.thumbs{display:flex;flex-direction:column;gap:12px}
|
||||||
|
.thumb{
|
||||||
|
aspect-ratio:1;
|
||||||
|
border:1px solid #ddd5c9;
|
||||||
|
background:#fff;
|
||||||
|
border-radius:14px;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
color:#8c877e;
|
||||||
|
font-size:11px;
|
||||||
|
}
|
||||||
|
.thumb.active{border:2px solid #d8b35a}
|
||||||
|
.product-stage{
|
||||||
|
min-height:520px;
|
||||||
|
border-radius:28px;
|
||||||
|
background:#fff;
|
||||||
|
border:1px solid #e5dfd4;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
box-shadow:0 24px 60px rgba(0,0,0,.05);
|
||||||
|
position:relative;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
.fake-pack{
|
||||||
|
width:44%;
|
||||||
|
height:64%;
|
||||||
|
background:linear-gradient(180deg,#151515 0 18%,#d9b35a 18% 100%);
|
||||||
|
border-radius:18px;
|
||||||
|
box-shadow:0 30px 50px rgba(0,0,0,.14);
|
||||||
|
position:absolute;
|
||||||
|
left:18%;
|
||||||
|
}
|
||||||
|
.fake-pad{
|
||||||
|
width:28%;
|
||||||
|
height:14%;
|
||||||
|
background:#c79b43;
|
||||||
|
border-radius:50px 26px 26px 50px;
|
||||||
|
position:absolute;
|
||||||
|
right:14%;
|
||||||
|
top:44%;
|
||||||
|
transform:rotate(-18deg);
|
||||||
|
box-shadow:0 16px 24px rgba(0,0,0,.12);
|
||||||
|
}
|
||||||
|
.product-info h1{font-size:clamp(36px,4vw,64px);line-height:.98;margin:0 0 18px}
|
||||||
|
.eyebrow{font-size:12px;letter-spacing:.18em;font-weight:800}
|
||||||
|
.desc{color:var(--muted);font-size:17px;line-height:1.7;max-width:620px}
|
||||||
|
.price{font-size:52px;font-weight:900;margin:34px 0 10px}
|
||||||
|
.selector,.buyrow{
|
||||||
|
display:flex;gap:14px;align-items:center;margin-top:18px
|
||||||
|
}
|
||||||
|
.selectbox{
|
||||||
|
flex:1;padding:18px 20px;border-radius:16px;background:#fff;border:1px solid #e0dbd1
|
||||||
|
}
|
||||||
|
.qty{padding:18px 22px;border-radius:16px;background:#fff;border:1px solid #e0dbd1}
|
||||||
|
.cart{
|
||||||
|
flex:1;padding:19px 26px;border-radius:16px;background:#171a18;color:#fff;text-align:center;font-weight:800
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- story ---------- */
|
||||||
|
.story-wrap{
|
||||||
|
position:relative;
|
||||||
|
background:var(--dark);
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
.story-progress{
|
||||||
|
position:fixed;
|
||||||
|
right:24px;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
z-index:50;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:10px;
|
||||||
|
opacity:0;
|
||||||
|
pointer-events:none;
|
||||||
|
transition:.25s ease;
|
||||||
|
}
|
||||||
|
.story-progress.show{opacity:1}
|
||||||
|
.dot{width:8px;height:8px;border-radius:99px;background:rgba(255,255,255,.28)}
|
||||||
|
.dot.active{height:26px;background:#fff}
|
||||||
|
|
||||||
|
.story{
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.sticky-stage{
|
||||||
|
position:sticky;
|
||||||
|
top:0;
|
||||||
|
height:100vh;
|
||||||
|
overflow:hidden;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 34% 50%, rgba(20,103,255,.23), transparent 32%),
|
||||||
|
linear-gradient(180deg,#0c1320 0%, #0a0f18 100%);
|
||||||
|
}
|
||||||
|
.product-core{
|
||||||
|
position:absolute;
|
||||||
|
left:11vw;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
width:min(42vw,640px);
|
||||||
|
aspect-ratio:1;
|
||||||
|
border-radius:50%;
|
||||||
|
border:28px solid #1467ff;
|
||||||
|
box-shadow:
|
||||||
|
inset -30px -15px 0 #050a10,
|
||||||
|
0 0 60px rgba(18,103,255,.32);
|
||||||
|
transition:all .5s ease;
|
||||||
|
}
|
||||||
|
.product-core:after{
|
||||||
|
content:"";
|
||||||
|
position:absolute;
|
||||||
|
inset:12%;
|
||||||
|
border-radius:50%;
|
||||||
|
border:22px solid #16181b;
|
||||||
|
}
|
||||||
|
.product-core:before{
|
||||||
|
content:"";
|
||||||
|
position:absolute;
|
||||||
|
left:-14%; right:-14%; bottom:-12%;
|
||||||
|
height:18%;
|
||||||
|
filter:blur(18px);
|
||||||
|
background:rgba(0,0,0,.45);
|
||||||
|
border-radius:50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-copy{
|
||||||
|
position:absolute;
|
||||||
|
left:56vw;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
width:35vw;
|
||||||
|
max-width:650px;
|
||||||
|
opacity:0;
|
||||||
|
transition:opacity .35s ease, transform .35s ease;
|
||||||
|
}
|
||||||
|
.scene-copy.active{opacity:1;transform:translateY(-50%)}
|
||||||
|
.scene-copy .kicker{
|
||||||
|
color:#9fbfff;font-size:13px;letter-spacing:.18em;font-weight:800;margin-bottom:16px
|
||||||
|
}
|
||||||
|
.scene-copy h2{
|
||||||
|
font-size:clamp(44px,5vw,88px);
|
||||||
|
line-height:.92;
|
||||||
|
margin:0 0 18px;
|
||||||
|
letter-spacing:-.03em;
|
||||||
|
}
|
||||||
|
.scene-copy p{font-size:18px;line-height:1.65;color:#cdd4df;max-width:560px}
|
||||||
|
.metric{
|
||||||
|
font-size:clamp(64px,8vw,120px);
|
||||||
|
font-weight:900;
|
||||||
|
color:#fff;
|
||||||
|
letter-spacing:-.05em;
|
||||||
|
margin-top:20px;
|
||||||
|
}
|
||||||
|
.feature-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px}
|
||||||
|
.chip{
|
||||||
|
padding:10px 14px;border:1px solid rgba(255,255,255,.18);
|
||||||
|
border-radius:999px;background:rgba(255,255,255,.05);
|
||||||
|
color:#e9edf4;font-size:13px
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger{
|
||||||
|
height:100vh;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual states */
|
||||||
|
.story-wrap[data-scene="material"] .sticky-stage{
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 34% 50%, rgba(44,119,255,.26), transparent 30%),
|
||||||
|
linear-gradient(180deg,#0e1728 0%,#09101a 100%);
|
||||||
|
}
|
||||||
|
.story-wrap[data-scene="material"] .product-core{
|
||||||
|
transform:translateY(-50%) scale(1.18);
|
||||||
|
border-width:34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-wrap[data-scene="heat"] .sticky-stage{
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 50%, rgba(255,94,0,.42), transparent 35%),
|
||||||
|
linear-gradient(110deg,#160c08,#111522 70%);
|
||||||
|
}
|
||||||
|
.story-wrap[data-scene="heat"] .product-core{
|
||||||
|
border-color:#ff7a00;
|
||||||
|
box-shadow:inset -30px -15px 0 #050a10,0 0 80px rgba(255,122,0,.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-wrap[data-scene="friction"] .sticky-stage{
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(180deg,#0b1018,#080c13);
|
||||||
|
background-size:40px 40px,40px 40px,auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-wrap[data-scene="control"] .product-core{
|
||||||
|
transform:translateY(-50%) rotate(-18deg) scale(.93);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-wrap[data-scene="durability"] .sticky-stage{
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 75% 70%, rgba(216,179,90,.2), transparent 28%),
|
||||||
|
linear-gradient(120deg,#101722,#0a0e15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-wrap[data-scene="fitment"] .sticky-stage{
|
||||||
|
background:linear-gradient(180deg,#f6f3ea,#ece7dc);
|
||||||
|
color:#111;
|
||||||
|
}
|
||||||
|
.story-wrap[data-scene="fitment"] .scene-copy p{color:#5d5a54}
|
||||||
|
.story-wrap[data-scene="fitment"] .product-core{
|
||||||
|
left:8vw;
|
||||||
|
transform:translateY(-50%) scale(.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fit-grid{
|
||||||
|
display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:26px
|
||||||
|
}
|
||||||
|
.fit-grid span{
|
||||||
|
padding:13px 14px;border-radius:12px;background:#fff;color:#111;text-align:center;
|
||||||
|
border:1px solid #d7d1c6;font-size:13px;font-weight:700
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-cta{
|
||||||
|
min-height:65vh;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
text-align:center;
|
||||||
|
padding:10vh 8vw;
|
||||||
|
background:#f6f3ea;
|
||||||
|
}
|
||||||
|
.end-cta h3{font-size:clamp(44px,5vw,80px);margin:0}
|
||||||
|
.end-cta p{color:#6f6a61;font-size:18px}
|
||||||
|
.end-cta .btn{
|
||||||
|
display:inline-block;margin-top:20px;background:#171a18;color:#fff;
|
||||||
|
padding:16px 26px;border-radius:14px;font-weight:800
|
||||||
|
}
|
||||||
|
|
||||||
|
.note{
|
||||||
|
position:fixed;left:20px;bottom:20px;z-index:60;
|
||||||
|
background:rgba(255,255,255,.9);backdrop-filter:blur(10px);
|
||||||
|
color:#111;padding:10px 13px;border-radius:12px;font-size:12px;
|
||||||
|
box-shadow:0 12px 30px rgba(0,0,0,.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width:900px){
|
||||||
|
.product-top{grid-template-columns:1fr;padding:6vh 5vw}
|
||||||
|
.gallery{grid-template-columns:1fr}
|
||||||
|
.thumbs{flex-direction:row}
|
||||||
|
.thumb{width:64px}
|
||||||
|
.product-stage{min-height:390px}
|
||||||
|
.product-core{left:50%;top:31%;transform:translate(-50%,-50%);width:70vw}
|
||||||
|
.scene-copy{left:8vw;top:auto;bottom:8vh;transform:none;width:84vw}
|
||||||
|
.scene-copy.active{transform:none}
|
||||||
|
.scene-copy h2{font-size:48px}
|
||||||
|
.story-wrap[data-scene="material"] .product-core,
|
||||||
|
.story-wrap[data-scene="control"] .product-core,
|
||||||
|
.story-wrap[data-scene="fitment"] .product-core{left:50%;transform:translate(-50%,-50%) scale(.9)}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<section class="product-top">
|
||||||
|
<div class="gallery">
|
||||||
|
<div class="thumbs">
|
||||||
|
<div class="thumb active">IMG 01</div>
|
||||||
|
<div class="thumb">IMG 02</div>
|
||||||
|
<div class="thumb">IMG 03</div>
|
||||||
|
<div class="thumb">DETAIL</div>
|
||||||
|
<div class="thumb">TEXTURE</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-stage">
|
||||||
|
<div class="fake-pack"></div>
|
||||||
|
<div class="fake-pad"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="eyebrow">TOBAKI / BRAKING SYSTEM</div>
|
||||||
|
<h1>X Ceramic Brake Pad</h1>
|
||||||
|
<p class="desc">ช่วงแรกของหน้าเป็น Product Detail แบบปกติ — ดูภาพ, เลือกรุ่น, ราคา, สต๊อก และสั่งซื้อได้ทันที</p>
|
||||||
|
<div class="price">฿3</div>
|
||||||
|
<div class="selector">
|
||||||
|
<div class="selectbox">SELECT MODEL: <b>ADV150 / ADV160 (F)</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="buyrow">
|
||||||
|
<div class="qty">− 1 +</div>
|
||||||
|
<div class="cart">Add to Cart</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="story-wrap" id="story" data-scene="hero">
|
||||||
|
<div class="story-progress" id="progress">
|
||||||
|
<div class="dot active"></div><div class="dot"></div><div class="dot"></div>
|
||||||
|
<div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="story">
|
||||||
|
<div class="sticky-stage">
|
||||||
|
<div class="product-core" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="scene-copy active" data-copy="hero">
|
||||||
|
<div class="kicker">01 / PRODUCT STORY</div>
|
||||||
|
<h2>STOP WITH<br>CONFIDENCE.</h2>
|
||||||
|
<p>เริ่ม Story แบบภาพใหญ่เต็มจอ ให้สินค้ากลายเป็นพระเอก ก่อนจะพาคนดูลงลึกไปทีละประเด็น</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="material">
|
||||||
|
<div class="kicker">02 / MATERIAL</div>
|
||||||
|
<h2>CERAMIC<br>COMPOUND.</h2>
|
||||||
|
<p>ซูมเข้าเนื้อผ้าเบรก แล้วอธิบายเรื่อง Low Noise, Low Vibration และ Brake Dust ต่ำลง</p>
|
||||||
|
<div class="feature-row">
|
||||||
|
<span class="chip">Low Noise</span><span class="chip">Low Vibration</span><span class="chip">Cleaner Braking</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="heat">
|
||||||
|
<div class="kicker">03 / HEAT CONTROL</div>
|
||||||
|
<h2>HIGH-HEAT<br>CONTROL.</h2>
|
||||||
|
<div class="metric">300°C</div>
|
||||||
|
<p>ฉากเปลี่ยนจาก Blue → Orange/Heat เพื่อสื่อ Brake Fade Resistance แบบเห็นความต่างทันที</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="friction">
|
||||||
|
<div class="kicker">04 / PERFORMANCE</div>
|
||||||
|
<h2>STABLE<br>FRICTION.</h2>
|
||||||
|
<p>พื้นหลังเปลี่ยนเป็น Technical Grid / Data Scene เพื่อเล่าเรื่องแรงเสียดทานคงที่ โดยไม่ต้องทำ Dashboard หนัก ๆ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="control">
|
||||||
|
<div class="kicker">05 / CONTROL</div>
|
||||||
|
<h2>SMOOTH<br>ENGAGEMENT.</h2>
|
||||||
|
<p>สินค้าเริ่มหมุน/เปลี่ยนมุม มองเห็นโครงสร้างและสปริง สื่อถึง Predictable Modulation และการควบคุมที่นุ่มนวล</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="durability">
|
||||||
|
<div class="kicker">06 / DURABILITY</div>
|
||||||
|
<h2>BUILT<br>TO LAST.</h2>
|
||||||
|
<p>เปลี่ยน mood เป็นการใช้งานระยะยาว และเล่าจุดขายเรื่อง Durable Lifespan, Rotor Friendly และ Everyday Confidence</p>
|
||||||
|
<div class="feature-row">
|
||||||
|
<span class="chip">Durable</span><span class="chip">Rotor Friendly</span><span class="chip">Everyday Use</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scene-copy" data-copy="fitment">
|
||||||
|
<div class="kicker">07 / FITMENT</div>
|
||||||
|
<h2>MADE FOR<br>YOUR BIKE.</h2>
|
||||||
|
<p>Story ปิดท้ายด้วยรุ่นรถที่รองรับ ก่อนส่งผู้ใช้กลับเข้าสู่ flow การเลือก Model และซื้อสินค้า</p>
|
||||||
|
<div class="fit-grid">
|
||||||
|
<span>NVX155</span><span>LC135</span><span>Vario125</span>
|
||||||
|
<span>Vario150</span><span>PCX150</span><span>Beat110 FI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trigger" data-scene="hero"></div>
|
||||||
|
<div class="trigger" data-scene="material"></div>
|
||||||
|
<div class="trigger" data-scene="heat"></div>
|
||||||
|
<div class="trigger" data-scene="friction"></div>
|
||||||
|
<div class="trigger" data-scene="control"></div>
|
||||||
|
<div class="trigger" data-scene="durability"></div>
|
||||||
|
<div class="trigger" data-scene="fitment"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="end-cta">
|
||||||
|
<div>
|
||||||
|
<div class="eyebrow">END OF PRODUCT STORY</div>
|
||||||
|
<h3>SELECT YOUR MODEL.</h3>
|
||||||
|
<p>หลังจบ Story ค่อยพากลับมาที่ Conversion อีกครั้ง ไม่ต้องให้ animation แย่งกับการซื้อสินค้า</p>
|
||||||
|
<div class="btn">Choose Your Bike</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="note">Scroll ลงเพื่อดู Story UX Mockup</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const story = document.getElementById('story');
|
||||||
|
const progress = document.getElementById('progress');
|
||||||
|
const triggers = [...document.querySelectorAll('.trigger')];
|
||||||
|
const copies = [...document.querySelectorAll('.scene-copy')];
|
||||||
|
const dots = [...document.querySelectorAll('.dot')];
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries)=>{
|
||||||
|
entries.forEach(entry=>{
|
||||||
|
if(entry.isIntersecting){
|
||||||
|
const scene = entry.target.dataset.scene;
|
||||||
|
story.dataset.scene = scene;
|
||||||
|
copies.forEach(c=>c.classList.toggle('active', c.dataset.copy === scene));
|
||||||
|
const i = triggers.indexOf(entry.target);
|
||||||
|
dots.forEach((d,idx)=>d.classList.toggle('active',idx===i));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},{threshold:.55});
|
||||||
|
|
||||||
|
triggers.forEach(t=>observer.observe(t));
|
||||||
|
|
||||||
|
const visibilityObserver = new IntersectionObserver((entries)=>{
|
||||||
|
entries.forEach(e=>progress.classList.toggle('show', e.isIntersecting));
|
||||||
|
},{threshold:.1});
|
||||||
|
visibilityObserver.observe(story);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user