This commit is contained in:
Your Name committed 2026-10-17 08:11:33 +07:00
1 parent 34be2599fa
commit 3f9db48df4
8 files changed
+2037 -4

No files matched your search

@@ -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>
)
}