This commit is contained in:
Your Name committed 2026-10-17 08:25:18 +07:00
1 parent 3f9db48df4
commit 9a5a30778e
5 files changed
+268 -71

No files matched your search

@@ -160,6 +160,53 @@ export default function ProductStoryScrollForm({ payload, onChange, onBrowseMedi
</button>
</div>
{/* Visual Theme Selector (Dark Theater vs Light Luxury) */}
<div className="p-4 rounded-2xl bg-night-950 border border-white/10 space-y-3">
<div className="flex items-center justify-between">
<label className="text-xs font-bold uppercase tracking-widest text-slate-300 flex items-center gap-2">
<Layers className="w-4 h-4 text-cyan-glow" />
ธีมการแสดงผลของ Story (Visual Theme)
</label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
type="button"
onClick={() => onChange({ ...payload, storyTheme: 'dark' })}
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
(payload.storyTheme || 'dark') === 'dark'
? 'bg-cyan-glow/10 border-cyan-glow/50 text-white ring-1 ring-cyan-glow/30'
: 'bg-white/5 border-white/10 text-slate-400 hover:text-white'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-black uppercase tracking-wider">Dark Theater (Apple Pro)</span>
{(payload.storyTheme || 'dark') === 'dark' && <CheckCircle className="w-4 h-4 text-cyan-glow" />}
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
ฉากมืดสไตล์โรงภาพยนตร์ แสงนีออนและ Radial Glow เรืองแสงรอบสินค้า
</p>
</button>
<button
type="button"
onClick={() => onChange({ ...payload, storyTheme: 'light' })}
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
payload.storyTheme === 'light'
? 'bg-cyan-glow/10 border-cyan-glow/50 text-white ring-1 ring-cyan-glow/30'
: 'bg-white/5 border-white/10 text-slate-400 hover:text-white'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-black uppercase tracking-wider">Light Studio (Clean Luxury)</span>
{payload.storyTheme === 'light' && <CheckCircle className="w-4 h-4 text-cyan-glow" />}
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
ฉากสว่าง หรูหรา คลีนตา กลมกลืนกับธีมสีครีม/ขาวของเว็บไซต์ ไม่เป็นกล่องดำ
</p>
</button>
</div>
</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">
+1 -1
View File
@@ -1,6 +1,6 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
// Trigger reload: support story_scroll and apple_watch_tour in ProductLayoutSectionPayloadSchema
// Trigger reload: support storyTheme in ProductLayoutSectionPayloadSchema
import {
LocalizedStringSchema,
HeroSectionPayloadSchema,
@@ -123,6 +123,7 @@ function ProductMainSection({
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
const storyTheme = (payload.storyTheme as string) || 'dark'
return (
<div className="space-y-12">
@@ -161,6 +162,7 @@ function ProductMainSection({
lang="th"
endCtaTitle={endCtaTitle}
endCtaButtonText={endCtaButtonText}
storyTheme={storyTheme}
/>
</div>
</div>
@@ -23,6 +23,7 @@ interface Props {
lang: string
endCtaTitle?: string
endCtaButtonText?: string
storyTheme?: 'dark' | 'light' | string
}
const DEFAULT_SCENES: StoryScene[] = [
@@ -37,7 +38,7 @@ const DEFAULT_SCENES: StoryScene[] = [
id: 'material',
kicker: '02 / MATERIAL',
title: 'CERAMIC\nCOMPOUND.',
description: 'สูตรผสมเนื้อเซรามิกเกรดพรีเมียม สึกหรอน้อย เสียงเบียบเงียบ ไร้แรงสั่นสะเทือน และลดฝุ่นผงจับจานเบรกอย่างเห็นได้ชัด',
description: 'สูตรผสมเนื้อเซรามิกเกรดพรีเมียม สึกหรอน้อย เสียงเงียบ ไร้แรงสั่นสะเทือน และลดฝุ่นผงจับจานเบรกอย่างเห็นได้ชัด',
chips: ['Low Noise', 'Low Vibration', 'Cleaner Braking'],
theme: 'material',
},
@@ -87,11 +88,14 @@ export default function CinematicStoryScroll({
lang,
endCtaTitle,
endCtaButtonText,
storyTheme = 'dark',
}: Props) {
const [activeScene, setActiveScene] = useState<string>('hero')
const [isStoryVisible, setIsStoryVisible] = useState(false)
const storyRef = useRef<HTMLDivElement | null>(null)
const isLight = storyTheme === 'light'
const activeScenes = useMemo(() => {
if (scenes && scenes.length > 0) return scenes
return DEFAULT_SCENES
@@ -136,7 +140,10 @@ export default function CinematicStoryScroll({
}
})
},
{ threshold: 0.55 }
{
rootMargin: '-20% 0px -20% 0px',
threshold: [0.1, 0.4],
}
)
triggers.forEach((t) => observer.observe(t))
@@ -167,25 +174,53 @@ export default function CinematicStoryScroll({
}
}
// Active scene index
const scrollToScene = (index: number) => {
const storyEl = storyRef.current
if (!storyEl) return
const triggers = storyEl.querySelectorAll<HTMLElement>('.story-trigger')
if (triggers[index]) {
triggers[index].scrollIntoView({ behavior: 'smooth' })
}
}
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">
<div
ref={storyRef}
className={`relative w-full overflow-hidden transition-colors duration-700 select-none ${
isLight
? 'bg-[#f7f5ed] text-slate-900 border-y border-line-primary/20'
: 'bg-[#0b101c] text-white'
}`}
>
{/* Top Atmosphere Gradient Blend (prevents harsh abrupt cut into dark) */}
{!isLight && (
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-surface-page via-[#0b101c]/60 to-[#0b101c] pointer-events-none z-20" />
)}
{/* 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 ${
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-auto ${
isStoryVisible ? 'opacity-100' : 'opacity-0'
}`}
>
{activeScenes.map((s, idx) => (
<div
<button
key={s.id}
className={`w-2 rounded-full transition-all duration-300 ${
type="button"
onClick={() => scrollToScene(idx)}
className={`rounded-full transition-all duration-300 cursor-pointer ${
activeIndex === idx
? 'h-7 bg-white shadow-lg shadow-white/30'
: 'h-2 bg-white/20'
? isLight
? 'w-2.5 h-7 bg-brand-primary shadow-md'
: 'w-2.5 h-7 bg-white shadow-lg shadow-white/40'
: isLight
? 'w-2.5 h-2.5 bg-slate-300 hover:bg-slate-400'
: 'w-2.5 h-2.5 bg-white/20 hover:bg-white/50'
}`}
title={`Scene 0${idx + 1}`}
aria-label={`Jump to scene 0${idx + 1}`}
/>
))}
</div>
@@ -195,47 +230,97 @@ export default function CinematicStoryScroll({
{/* 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%)',
}}
/>
{isLight ? (
<>
{/* Light Theme Reactive Glows */}
<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, 120, 30, 0.16), transparent 45%), linear-gradient(180deg, #fef7ee 0%, #faeee0 100%)',
}}
/>
<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, 120, 255, 0.14), transparent 45%), linear-gradient(180deg, #f4f8fe 0%, #edf3fd 100%)',
}}
/>
<div
className={`absolute inset-0 transition-opacity duration-700 pointer-events-none ${
activeScene === 'friction' ? 'opacity-100' : 'opacity-0'
}`}
style={{
backgroundImage: 'linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.04) 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.18), transparent 45%), linear-gradient(120deg, #faf6ed 0%, #f3ece0 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.12), transparent 42%), linear-gradient(180deg, #f7f5ed 0%, #f0ede3 100%)',
}}
/>
</>
) : (
<>
{/* Dark Theater Reactive Glows */}
<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">
@@ -243,7 +328,17 @@ export default function CinematicStoryScroll({
<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'
isLight
? activeScene === 'heat'
? 'scale-105 border-[14px] border-orange-400 shadow-[0_20px_60px_rgba(255,102,0,0.25)]'
: activeScene === 'material'
? 'scale-110 border-[14px] border-blue-500 shadow-[0_20px_60px_rgba(20,103,255,0.22)]'
: activeScene === 'control'
? 'scale-95 -rotate-12 border-[14px] border-cyan-500 shadow-[0_20px_50px_rgba(6,182,212,0.2)]'
: activeScene === 'durability'
? 'scale-100 border-[14px] border-[#d8b35a] shadow-[0_20px_50px_rgba(216,179,90,0.25)]'
: 'scale-100 border-[14px] border-slate-300 shadow-[0_20px_60px_rgba(0,0,0,0.08)]'
: 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)]'
@@ -254,10 +349,12 @@ export default function CinematicStoryScroll({
: '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%)',
background: isLight
? 'radial-gradient(circle, #ffffff 50%, #f4f0e6 100%)'
: 'radial-gradient(circle, rgba(16, 24, 40, 0.95) 45%, rgba(8, 14, 24, 0.98) 100%)',
}}
>
{/* Product Image */}
{/* Product Image Stage */}
{mainShowcaseImage ? (
<div className="relative w-4/5 h-4/5 p-4 flex items-center justify-center">
<Image
@@ -265,20 +362,24 @@ export default function CinematicStoryScroll({
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"
className="object-contain drop-shadow-[0_20px_35px_rgba(0,0,0,0.4)] 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>
<p className={`text-xs uppercase tracking-widest font-bold ${isLight ? 'text-slate-500' : 'text-slate-400'}`}>
Premium Spec
</p>
</div>
)}
{/* Pulsing Core Ring */}
<div
className="absolute inset-4 rounded-full border border-white/10 pointer-events-none animate-spin-slow"
className={`absolute inset-4 rounded-full border pointer-events-none animate-spin-slow ${
isLight ? 'border-slate-300/60' : 'border-white/10'
}`}
style={{ animationDuration: '40s' }}
/>
</div>
@@ -298,24 +399,36 @@ export default function CinematicStoryScroll({
}`}
>
{/* Kicker */}
<div className="text-xs md:text-sm font-black tracking-[0.2em] uppercase text-cyan-300">
<div
className={`text-xs md:text-sm font-black tracking-[0.2em] uppercase ${
isLight ? 'text-brand-primary' : '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">
<h2
className={`text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight leading-[0.95] whitespace-pre-line ${
isLight ? 'text-slate-900' : '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">
<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-500 via-amber-500 to-yellow-500">
{scene.metric}
</div>
)}
{/* Description */}
<p className="text-sm md:text-base text-slate-300 leading-relaxed max-w-md">
<p
className={`text-sm md:text-base leading-relaxed max-w-md ${
isLight ? 'text-slate-600' : 'text-slate-300'
}`}
>
{scene.description}
</p>
@@ -325,7 +438,11 @@ export default function CinematicStoryScroll({
{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"
className={`px-3.5 py-1.5 rounded-full text-xs font-semibold backdrop-blur-sm ${
isLight
? 'bg-slate-900/5 border border-slate-300 text-slate-800'
: 'bg-white/10 border border-white/20 text-slate-200'
}`}
>
{chip}
</span>
@@ -336,14 +453,22 @@ export default function CinematicStoryScroll({
{/* 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
className={`text-xs uppercase font-bold tracking-wider mb-2 ${
isLight ? 'text-slate-500' : 'text-slate-400'
}`}
>
รุ่นรถที่รองรับ ({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"
className={`px-3 py-2 rounded-xl text-center text-xs font-bold truncate ${
isLight
? 'bg-white border border-slate-200 text-slate-900 shadow-sm'
: 'bg-white/10 border border-white/15 text-white'
}`}
title={model}
>
{model}
@@ -370,20 +495,42 @@ export default function CinematicStoryScroll({
</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">
<section
className={`relative z-10 min-h-[50vh] flex flex-col items-center justify-center text-center px-6 py-20 border-t space-y-5 ${
isLight
? 'bg-[#f0ede3] border-slate-200 text-slate-900'
: 'bg-gradient-to-b from-[#0b101c] via-[#09101a] to-surface-page border-white/10 text-white'
}`}
>
<div
className={`text-xs uppercase font-black tracking-[0.25em] ${
isLight ? 'text-brand-primary' : 'text-cyan-400'
}`}
>
END OF PRODUCT STORY
</div>
<h3 className="text-3xl sm:text-5xl md:text-6xl font-black text-white tracking-tight">
<h3
className={`text-3xl sm:text-5xl md:text-6xl font-black tracking-tight ${
isLight ? 'text-slate-900' : 'text-white'
}`}
>
{endCtaTitle || 'SELECT YOUR MODEL.'}
</h3>
<p className="text-sm md:text-base text-slate-400 max-w-md mx-auto leading-relaxed">
พร้อมยกระดับสมรรถนะการเบรกให้มอเตอร์ไซค์ของคุณ สั่งซื้อได้ทันทีตรงรุ่น
<p
className={`text-sm md:text-base max-w-md mx-auto leading-relaxed ${
isLight ? 'text-slate-600' : 'text-slate-400'
}`}
>
พร้อมยกระดับสมรรถนะให้รถของคุณ สั่งซื้อได้ทันทีตรงรุ่น
</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"
className={`mt-4 px-8 py-4 rounded-2xl font-black text-sm uppercase tracking-wider transition-all transform hover:scale-105 active:scale-95 cursor-pointer shadow-xl ${
isLight
? 'bg-brand-primary hover:bg-brand-primary-hover text-white shadow-brand-primary/25'
: 'bg-gradient-to-r from-cyan-400 to-blue-500 hover:from-cyan-300 hover:to-blue-400 text-slate-950 shadow-cyan-500/25'
}`}
>
{endCtaButtonText || 'Choose Your Bike / สั่งซื้อรุ่นของคุณ'}
</button>
+1
View File
@@ -1125,6 +1125,7 @@ export const ProductLayoutSectionPayloadSchema = z.object({
showBreadcrumbs: z.boolean().optional(),
showBrandMeta: z.boolean().optional(),
storyHeroImage: z.string().optional(),
storyTheme: z.enum(['dark', 'light']).optional(),
storyScenes: z.array(StorySceneSchema).optional(),
endCtaTitle: z.string().optional(),
endCtaButtonText: z.string().optional(),