From 5a512349e5e1faa1bda467ae277809c8724a2ed4 Mon Sep 17 00:00:00 2001 From: nicholai Date: Sat, 31 Jan 2026 19:40:37 -0700 Subject: [PATCH] Refactor: DemoSection with split sticky scrolling - Desktop: Phone mockup stays fixed while feature cards scroll - Intersection Observer triggers phone screen transitions - Crossfade animations between phone screens - Mobile: Preserves original timer-based rotation - Fixed overflow-hidden breaking sticky positioning Co-Authored-By: Claude Opus 4.5 --- src/components/landing/DemoSection.tsx | 310 ++++++++++++++++++------- 1 file changed, 227 insertions(+), 83 deletions(-) diff --git a/src/components/landing/DemoSection.tsx b/src/components/landing/DemoSection.tsx index e55f7de..be24214 100644 --- a/src/components/landing/DemoSection.tsx +++ b/src/components/landing/DemoSection.tsx @@ -1,13 +1,29 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { Cigarette, Leaf, Heart, Trophy, DollarSign, TrendingDown, CheckCircle } from 'lucide-react'; +import { useState, useEffect, useRef, type ReactNode } from 'react'; +import { Cigarette, Leaf, Heart, Trophy, DollarSign, TrendingDown, CheckCircle, type LucideIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; -const DEMO_SCREENS = [ +interface DemoScreen { + id: string; + title: string; + subtitle: string; + description: string; + Icon: LucideIcon; + iconBg: string; + iconColor: string; + content: ReactNode; +} + +const DEMO_SCREENS: DemoScreen[] = [ { id: 'logging', title: 'Log Your Usage', subtitle: 'Track daily consumption with ease', + description: 'Quick tap or scroll to log your nicotine and marijuana use. See real-time trends and weekly comparisons to stay motivated on your journey.', + Icon: Cigarette, + iconBg: 'bg-gradient-to-br from-red-500/20 to-orange-500/20', + iconColor: 'text-red-400', content: (
@@ -39,6 +55,10 @@ const DEMO_SCREENS = [ id: 'health', title: 'Health Recovery', subtitle: 'Watch your body heal', + description: 'Track your health milestones from the first 20 minutes to years of recovery. See real-time progress as your body repairs itself.', + Icon: Heart, + iconBg: 'bg-gradient-to-br from-teal-500/20 to-cyan-500/20', + iconColor: 'text-teal-400', content: (
@@ -84,6 +104,10 @@ const DEMO_SCREENS = [ id: 'achievements', title: 'Achievements', subtitle: 'Celebrate every milestone', + description: 'Unlock badges as you reach new goals. From your First Step to becoming a Monthly Master, every achievement is worth celebrating.', + Icon: Trophy, + iconBg: 'bg-gradient-to-br from-yellow-500/20 to-amber-500/20', + iconColor: 'text-yellow-400', content: (
{[ @@ -115,6 +139,10 @@ const DEMO_SCREENS = [ id: 'savings', title: 'Money Saved', subtitle: 'Track your financial wins', + description: 'See exactly how much money you save by cutting back. Watch your savings grow daily and set goals for what to do with your extra cash.', + Icon: DollarSign, + iconBg: 'bg-gradient-to-br from-emerald-500/20 to-green-500/20', + iconColor: 'text-emerald-400', content: (
@@ -139,13 +167,92 @@ const DEMO_SCREENS = [ }, ]; +// Phone mockup component +function PhoneMockup({ activeScreen }: { activeScreen: number }) { + return ( +
+ {/* Phone frame glow */} +
+ + {/* Phone frame */} +
+ {/* Notch */} +
+ + {/* Screen content with crossfade */} +
+ {/* Screen header */} +
+

{DEMO_SCREENS[activeScreen].title}

+

+ {DEMO_SCREENS[activeScreen].subtitle} +

+
+ + {/* Crossfade content */} +
+ {DEMO_SCREENS.map((screen, index) => ( +
+ {screen.content} +
+ ))} +
+
+ + {/* Bottom gradient overlay */} +
+
+
+ ); +} + +// Feature description card for scroll sections +function FeatureCard({ screen, isActive }: { screen: DemoScreen; isActive: boolean }) { + const Icon = screen.Icon; + + return ( +
+
+
+ +
+
+

{screen.title}

+

+ {screen.description} +

+
+
+
+ ); +} + export function DemoSection() { const [activeScreen, setActiveScreen] = useState(0); const [isPaused, setIsPaused] = useState(false); + const sectionRefs = useRef<(HTMLDivElement | null)[]>([]); + // Timer-based rotation for mobile useEffect(() => { if (isPaused) return; + // Only run timer on mobile (will be hidden on lg+) const interval = setInterval(() => { setActiveScreen((prev) => (prev + 1) % DEMO_SCREENS.length); }, 4000); @@ -153,19 +260,51 @@ export function DemoSection() { return () => clearInterval(interval); }, [isPaused]); + // Intersection Observer for desktop scroll spy + useEffect(() => { + const observers: IntersectionObserver[] = []; + + sectionRefs.current.forEach((ref, index) => { + if (!ref) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveScreen(index); + } + }); + }, + { + threshold: 0.5, + rootMargin: '-20% 0px -30% 0px', + } + ); + + observer.observe(ref); + observers.push(observer); + }); + + return () => { + observers.forEach((observer) => observer.disconnect()); + }; + }, []); + return (
- {/* Background accents */} -
-
+ {/* Background accents - contained to avoid overflow issues */} +
+
+
+
-
+
{/* Section Header */} -
+

-
- {/* Phone Mockup */} -
setIsPaused(true)} - onMouseLeave={() => setIsPaused(false)} - onTouchStart={() => setIsPaused(true)} - onTouchEnd={() => setIsPaused(false)} - > - {/* Phone frame glow */} -
- - {/* Phone frame */} -
- {/* Notch */} -
- - {/* Screen content */} -
- {/* Screen header */} -
-

{DEMO_SCREENS[activeScreen].title}

-

- {DEMO_SCREENS[activeScreen].subtitle} -

-
- - {/* Screen content with transition */} -
- {DEMO_SCREENS[activeScreen].content} -
-
- - {/* Bottom gradient overlay */} -
-
+ {/* Desktop: Split Sticky Scroll Layout */} +
+ {/* Sticky Phone Column */} +
+
- {/* Description and indicators */} -
-
- {/* Screen descriptions */} - {DEMO_SCREENS.map((screen, index) => ( - - ))} + {/* Scrolling Content Column */} +
+ {DEMO_SCREENS.map((screen, index) => ( +
{ sectionRefs.current[index] = el; }} + className={cn( + "min-h-[70vh] flex items-center py-8", + index === 0 && "pt-0", + index === DEMO_SCREENS.length - 1 && "min-h-[50vh]" + )} + > + +
+ ))} +
+
+ + {/* Mobile: Original Stacked Layout with Timer */} +
+
+ {/* Phone Mockup */} +
setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + onTouchStart={() => setIsPaused(true)} + onTouchEnd={() => setIsPaused(false)} + > +
- {/* Dot indicators (mobile) */} -
- {DEMO_SCREENS.map((_, index) => ( - + ))} +
+ + {/* Dot indicators */} +
+ {DEMO_SCREENS.map((_, index) => ( +