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