From a5c5ec033aeb73e394d17959c54f88f9180dd481 Mon Sep 17 00:00:00 2001 From: R-Bharathraj Date: Tue, 7 Jul 2026 18:00:09 +0530 Subject: [PATCH] feat: add LytsUpSection and update home page --- src/app/globals.css | 1 + src/app/layout.tsx | 3 + src/app/page.tsx | 2 + src/components/home/LytsUpSection.tsx | 359 ++++++++++++++++++++++++++ 4 files changed, 365 insertions(+) create mode 100644 src/components/home/LytsUpSection.tsx diff --git a/src/app/globals.css b/src/app/globals.css index ac7f61d..aeba7b8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -15,6 +15,7 @@ html, body { margin: 0; padding: 0; background: #ffffff; + color-scheme: light; } html.lenis, html.lenis body { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a14ce4a..6eb3d20 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,6 +37,9 @@ export default function RootLayout({ return ( + {/* Opt out of browser/OS auto "force dark" theming — the site is + light-themed only and has no dark variant to invert into. */} + {/* Fonts */} diff --git a/src/app/page.tsx b/src/app/page.tsx index caf0659..9ad412f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,6 +11,7 @@ import ProblemSection from '@/components/home/ProblemSection'; import ShiftSection from '@/components/home/ShiftSection'; import WhatWeDo from '@/components/home/WhatWeDo'; import ProductsSection from '@/components/home/ProductsSection'; +import LytsUpSection from '@/components/home/LytsUpSection'; import TheLoop from '@/components/home/TheLoop'; import initGlobalReveal from '@/utils/scrollReveal'; @@ -473,6 +474,7 @@ export default function IndexPage() { + ); diff --git a/src/components/home/LytsUpSection.tsx b/src/components/home/LytsUpSection.tsx new file mode 100644 index 0000000..aae0eb4 --- /dev/null +++ b/src/components/home/LytsUpSection.tsx @@ -0,0 +1,359 @@ +'use client'; + +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import Link from 'next/link'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') { + gsap.registerPlugin(ScrollTrigger); +} + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +/* value: the numeric target the counter animates toward; display formats it once reached */ +const PROOF_STATS: { value: number; decimals?: number; suffix: string; label: string }[] = [ + { value: 50, suffix: 'K+', label: 'Active users' }, + { value: 4.8, decimals: 1, suffix: '★', label: 'App rating' }, + { value: 1, decimals: 0, suffix: 'M+', label: 'Lyts earned' }, +]; + +/* Headline split into masked words so each line can slide up from behind a hidden overflow band */ +const HEADLINE_WORDS = ['Get', '__STICKER__', 'Before', 'You', 'Shop.']; + +interface LytsUpSectionProps { + isLoaded?: boolean; +} + +export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { + const sectionRef = useRef(null); + const statRefs = useRef<(HTMLSpanElement | null)[]>([]); + const ctaRef = useRef(null); + const flowRef = useRef(null); + const flowFillRefs = useRef<(HTMLDivElement | null)[]>([]); + const flowNodeRefs = useRef<(HTMLSpanElement | null)[]>([]); + const flowActiveRef = useRef(0); + + useIsomorphicLayoutEffect(() => { + if (!isLoaded) return; + if (typeof window === 'undefined') return; + + const ctx = gsap.context(() => { + // Headline: masked words slide up into place, sticker pops in with a rotate-back + const tl = gsap.timeline({ + scrollTrigger: { trigger: '.lytsup-header', start: 'top 85%', toggleActions: 'play none none none' }, + }); + tl.fromTo('.lytsup-word-inner', { yPercent: 115, rotate: 4 }, { yPercent: 0, rotate: 0, duration: 0.85, stagger: 0.08, ease: 'expo.out' }, 0) + .fromTo('.lytsup-sticker-wrap', { opacity: 0, scale: 0.4, rotate: 16 }, { opacity: 1, scale: 1, rotate: -4, duration: 0.7, ease: 'back.out(2.2)' }, 0.18) + .fromTo('.lytsup-header-sub', { opacity: 0, y: 24, filter: 'blur(6px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.8, ease: 'expo.out' }, 0.45) + .fromTo('.lytsup-stat-chip', { opacity: 0, y: 18, scale: 0.85 }, { opacity: 1, y: 0, scale: 1, duration: 0.55, stagger: 0.1, ease: 'back.out(1.8)' }, 0.6); + + // Stat numbers count up once the chips have landed + PROOF_STATS.forEach((stat, i) => { + const el = statRefs.current[i]; + if (!el) return; + const counter = { val: 0 }; + tl.to(counter, { + val: stat.value, duration: 1.1, ease: 'power2.out', + onUpdate: () => { el.textContent = counter.val.toFixed(stat.decimals ?? 0) + stat.suffix; }, + }, 0.7 + i * 0.12); + }); + + // Flow stepper: nodes pop in on entry, then the active step and the + // connecting fills are driven entirely by scroll position (scrubbed). + gsap.fromTo( + '.lytsup-flow-node', + { opacity: 0, scale: 0.5 }, + { + opacity: 1, scale: 1, duration: 0.6, stagger: 0.18, ease: 'back.out(2)', + scrollTrigger: { trigger: '.lytsup-flow', start: 'top 88%', toggleActions: 'play none none none' }, + } + ); + + const setNodeActive = (index: number, active: boolean) => { + const el = flowNodeRefs.current[index]; + if (!el) return; + gsap.to(el, { + background: active ? 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)' : 'rgba(244,190,40,0.1)', + borderColor: active ? '#171512' : 'rgba(244,190,40,0.35)', + borderWidth: active ? '3px' : '1px', + boxShadow: active ? '0 12px 26px -10px rgba(23,21,18,0.4)' : '0 0px 0px rgba(0,0,0,0)', + duration: 0.35, ease: 'power2.out', + }); + if (active) gsap.fromTo(el, { scale: 1.22 }, { scale: 1, duration: 0.45, ease: 'back.out(2.4)' }); + }; + + const applyActiveStep = (index: number) => { + if (flowActiveRef.current === index) return; + setNodeActive(flowActiveRef.current, false); + setNodeActive(index, true); + flowActiveRef.current = index; + }; + + if (flowRef.current) { + ScrollTrigger.create({ + trigger: flowRef.current, + start: 'top 80%', + end: 'bottom 40%', + scrub: 0.5, + onUpdate: (self) => { + const p = self.progress; + const fill1 = gsap.utils.clamp(0, 1, p * 2); + const fill2 = gsap.utils.clamp(0, 1, (p - 0.5) * 2); + if (flowFillRefs.current[0]) gsap.set(flowFillRefs.current[0], { scaleX: fill1 }); + if (flowFillRefs.current[1]) gsap.set(flowFillRefs.current[1], { scaleX: fill2 }); + applyActiveStep(p >= 0.95 ? 2 : p >= 0.45 ? 1 : 0); + }, + }); + } + + // Closing callout + gsap.fromTo( + '.lytsup-closing', + { opacity: 0, y: 24, scale: 0.98 }, + { + opacity: 1, y: 0, scale: 1, duration: 0.8, ease: 'power3.out', + scrollTrigger: { trigger: '.lytsup-closing', start: 'top 88%', toggleActions: 'play none none none' }, + } + ); + + ScrollTrigger.refresh(); + }, sectionRef); + + // Pinned sections above (e.g. the horizontal Products scroll) can resize + // the document after mount and shift trigger positions; re-check once + // things settle so this section's reveals can't get stuck off-position. + const t = setTimeout(() => ScrollTrigger.refresh(), 500); + + return () => { clearTimeout(t); ctx.revert(); }; + }, [isLoaded]); + + // Magnetic CTA — button drifts slightly toward the cursor, snaps back on leave + useEffect(() => { + const btn = ctaRef.current; + if (!btn || typeof window === 'undefined') return; + const onMove = (e: MouseEvent) => { + const r = btn.getBoundingClientRect(); + const x = (e.clientX - r.left - r.width / 2) * 0.35; + const y = (e.clientY - r.top - r.height / 2) * 0.35; + gsap.to(btn, { x, y, duration: 0.4, ease: 'power2.out' }); + }; + const onLeave = () => gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' }); + btn.addEventListener('mousemove', onMove); + btn.addEventListener('mouseleave', onLeave); + return () => { + btn.removeEventListener('mousemove', onMove); + btn.removeEventListener('mouseleave', onLeave); + }; + }, []); + + return ( +
+ {/* Faint dot-grid texture for depth, matching the light sections elsewhere */} +
+
+ + {/* Oversized watermark type — big-type Gen-Z background flex */} +
+ LYTSUP LYTSUP +
+ + {/* Header */} +
+

+ {HEADLINE_WORDS.map((word, i) => + word === '__STICKER__' ? ( + + + Lyts + + + + ) : ( + + {word} + + ) + )} +

+ +

+ LytsUp transforms your everyday activities into real shopping rewards. Walk, play, explore, visit your favorite places, complete fun challenges, and earn Lyts that you can spend instantly at participating brands. +

+ + {/* Social-proof stat chips — numbers count up on reveal */} +
+ {PROOF_STATS.map((stat, i) => ( +
+ { statRefs.current[i] = el; }} + style={{ fontSize: '0.92rem', fontWeight: 800, color: '#171512', fontFamily: 'Sora, sans-serif', minWidth: '2.6em', display: 'inline-block' }} + > + 0{stat.suffix} + + {stat.label} +
+ ))} +
+
+ + {/* Visual flow — numbered stepper; active step + connectors are driven by scroll position */} +
+ {['Earn', 'Spend', 'Live More'].map((step, i) => ( + +
+ { flowNodeRefs.current[i] = el; }} + style={{ + width: 46, height: 46, borderRadius: '50%', + display: 'flex', alignItems: 'center', justifyContent: 'center', + fontSize: '1rem', fontWeight: 800, fontFamily: 'Sora, sans-serif', + color: '#171512', boxSizing: 'border-box', + background: i === 0 ? 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)' : 'rgba(244,190,40,0.1)', + border: i === 0 ? '3px solid #171512' : '1px solid rgba(244,190,40,0.35)', + boxShadow: i === 0 ? '0 12px 26px -10px rgba(23,21,18,0.4)' : 'none', + }} + > + {i + 1} + + + {step} + +
+ {i < 2 && ( +
+
{ flowFillRefs.current[i] = el; }} + style={{ position: 'absolute', inset: 0, borderRadius: 2, background: 'linear-gradient(90deg,#f4be28,#FF9800)', transformOrigin: 'left center', transform: 'scaleX(0)' }} + /> +
+ )} + + ))} +
+ + {/* Closing statement + CTA — dark contrast callout */} +
+ 🔥 +
+ +

+ The more you live, the more you earn. Every step, every visit, and every adventure brings you closer to your next reward. +

+ + + Download LytsUp + + + + +
+ + +
+ ); +}