feat: add LytsUpSection and update home page

This commit is contained in:
R-Bharathraj
2026-07-07 18:00:09 +05:30
parent e261cde8f5
commit a5c5ec033a
4 changed files with 365 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ html, body {
margin: 0;
padding: 0;
background: #ffffff;
color-scheme: light;
}
html.lenis, html.lenis body {

View File

@@ -37,6 +37,9 @@ export default function RootLayout({
return (
<html lang="en">
<head>
{/* Opt out of browser/OS auto "force dark" theming — the site is
light-themed only and has no dark variant to invert into. */}
<meta name="color-scheme" content="light" />
{/* Fonts */}
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />

View File

@@ -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() {
<ShiftSection isLoaded={imagesLoaded} />
<WhatWeDo isLoaded={imagesLoaded} />
<ProductsSection isLoaded={imagesLoaded} />
<LytsUpSection />
<TheLoop />
</>
);

View File

@@ -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<HTMLDivElement>(null);
const statRefs = useRef<(HTMLSpanElement | null)[]>([]);
const ctaRef = useRef<HTMLAnchorElement>(null);
const flowRef = useRef<HTMLDivElement>(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 (
<section
ref={sectionRef}
id="lytsup"
className="relative overflow-hidden"
style={{
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: 32,
padding: 'clamp(64px, 8vw, 110px) clamp(20px, 4vw, 64px) clamp(64px, 8vw, 96px)',
zIndex: 2,
background: '#ffffff',
boxShadow: 'inset 0 0 0 1px rgba(23,21,18,0.05)',
}}
>
{/* Faint dot-grid texture for depth, matching the light sections elsewhere */}
<div
aria-hidden
style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
backgroundImage: 'radial-gradient(rgba(23,21,18,0.06) 1px, transparent 1px)',
backgroundSize: '28px 28px',
maskImage: 'radial-gradient(ellipse 75% 60% at 50% 25%, #000 0%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse 75% 60% at 50% 25%, #000 0%, transparent 75%)',
}}
/>
<div style={{ position: 'absolute', top: '-10%', right: '-6%', width: 480, height: 480, borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.14) 0%, transparent 70%)', pointerEvents: 'none' }} />
{/* Oversized watermark type — big-type Gen-Z background flex */}
<div
aria-hidden
style={{
position: 'absolute', top: '2%', left: '50%', transform: 'translateX(-50%)',
fontSize: 'clamp(6rem, 16vw, 13rem)', fontWeight: 900, letterSpacing: '-0.05em',
color: 'rgba(23,21,18,0.035)', fontFamily: 'Sora, sans-serif', whiteSpace: 'nowrap',
pointerEvents: 'none', userSelect: 'none', zIndex: 0,
}}
>
LYTSUP LYTSUP
</div>
{/* Header */}
<div className="lytsup-header relative z-10 text-center max-w-3xl mx-auto mb-12 flex flex-col items-center">
<h2
style={{
margin: 0,
padding: 'clamp(8px, 2vw, 20px) 0',
fontSize: 'clamp(2.4rem, 5.4vw, 4.4rem)',
fontWeight: 800, color: '#171512',
lineHeight: 1, letterSpacing: '-0.04em',
fontFamily: 'Sora, sans-serif',
}}
>
{HEADLINE_WORDS.map((word, i) =>
word === '__STICKER__' ? (
<span key={i} className="lytsup-sticker-wrap" style={{ display: 'inline-block', position: 'relative', margin: '0 0.18em' }}>
<span
style={{
display: 'inline-block',
color: '#ffffff',
background: 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)',
padding: '0.02em 0.28em 0.06em',
borderRadius: '0.3em',
border: '4px solid #ffffff',
outline: '3px solid #171512',
boxShadow: '0 10px 22px -8px rgba(23,21,18,0.35)',
}}
>
Lyts
</span>
<span className="lytsup-sticker-spark" aria-hidden style={{ position: 'absolute', top: '-0.4em', right: '-0.5em', fontSize: '0.42em' }}></span>
</span>
) : (
<span key={i} className="lytsup-word" style={{ display: 'inline-block', overflow: 'hidden', verticalAlign: 'top', marginRight: '0.22em' }}>
<span className="lytsup-word-inner" style={{ display: 'inline-block' }}>{word}</span>
</span>
)
)}
</h2>
<p
className="lytsup-header-sub"
style={{
margin: '22px 0 0', maxWidth: 620,
fontSize: 'clamp(0.95rem, 1.15vw, 1.1rem)', lineHeight: 1.7,
color: 'rgba(23,21,18,0.55)', fontFamily: 'Sora, sans-serif',
}}
>
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.
</p>
{/* Social-proof stat chips — numbers count up on reveal */}
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 10, marginTop: 26 }}>
{PROOF_STATS.map((stat, i) => (
<div
key={stat.label}
className="lytsup-stat-chip"
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 16px', borderRadius: 100,
background: 'rgba(244,190,40,0.08)', border: '1px solid rgba(244,190,40,0.28)',
}}
>
<span
ref={(el) => { 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}
</span>
<span style={{ fontSize: '0.72rem', fontWeight: 600, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif' }}>{stat.label}</span>
</div>
))}
</div>
</div>
{/* Visual flow — numbered stepper; active step + connectors are driven by scroll position */}
<div ref={flowRef} className="lytsup-flow relative z-10 flex items-start justify-center gap-2 sm:gap-4 mb-16 sm:mb-20 max-w-xl mx-auto">
{['Earn', 'Spend', 'Live More'].map((step, i) => (
<React.Fragment key={step}>
<div className="lytsup-flow-node" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, width: 108 }}>
<span
ref={(el) => { 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}
</span>
<span style={{ fontSize: '0.9rem', fontWeight: 800, fontFamily: 'Sora, sans-serif', color: '#171512', letterSpacing: '-0.01em' }}>
{step}
</span>
</div>
{i < 2 && (
<div style={{ position: 'relative', flex: 1, height: 2, marginTop: 23, borderRadius: 2, background: 'rgba(23,21,18,0.12)', overflow: 'hidden' }}>
<div
ref={(el) => { flowFillRefs.current[i] = el; }}
style={{ position: 'absolute', inset: 0, borderRadius: 2, background: 'linear-gradient(90deg,#f4be28,#FF9800)', transformOrigin: 'left center', transform: 'scaleX(0)' }}
/>
</div>
)}
</React.Fragment>
))}
</div>
{/* Closing statement + CTA — dark contrast callout */}
<div
className="lytsup-closing relative z-10 mx-auto"
style={{
maxWidth: 720, textAlign: 'center',
padding: 'clamp(36px, 4vw, 52px) clamp(24px, 4vw, 48px)',
borderRadius: 26,
background: '#171512',
position: 'relative', overflow: 'hidden',
}}
>
<span aria-hidden style={{ position: 'absolute', top: -18, right: 26, fontSize: 34, transform: 'rotate(12deg)' }}>🔥</span>
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 70% 60% at 30% 0%, rgba(244,190,40,0.14) 0%, transparent 70%)', pointerEvents: 'none' }} />
<p
style={{
margin: '0 0 28px', position: 'relative',
fontSize: 'clamp(1.05rem, 1.6vw, 1.4rem)', fontWeight: 600,
lineHeight: 1.6, color: 'rgba(255,255,255,0.85)',
fontFamily: 'Sora, sans-serif', letterSpacing: '-0.01em',
}}
>
The more you live, the more you earn. Every step, every visit, and every adventure brings you closer to your next reward.
</p>
<Link
ref={ctaRef}
href="/contacts"
style={{
display: 'inline-flex', alignItems: 'center', gap: 10, position: 'relative',
padding: '15px 34px', borderRadius: 100,
fontSize: '1rem', fontWeight: 800, fontFamily: 'Sora, sans-serif',
color: '#171512', textDecoration: 'none',
background: 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)',
boxShadow: '0 16px 40px -12px rgba(244,190,40,0.5)',
}}
>
Download LytsUp
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
</div>
<style>{`
.lytsup-sticker-spark { display: inline-block; animation: lytsupSpark 2.2s ease-in-out infinite; }
@keyframes lytsupSpark {
0%, 100% { transform: scale(1) rotate(0deg); }
50% { transform: scale(1.25) rotate(-12deg); }
}
@media (prefers-reduced-motion: reduce) {
.lytsup-sticker-spark { animation: none; }
}
@media (max-width: 900px) {
.lytsup-flow { max-width: 380px !important; }
}
`}</style>
</section>
);
}