diff --git a/public/For_people/01_earn-cropped.png b/public/For_people/01_earn-cropped.png new file mode 100644 index 0000000..10beaae Binary files /dev/null and b/public/For_people/01_earn-cropped.png differ diff --git a/public/For_people/01_earn.png b/public/For_people/01_earn.png new file mode 100644 index 0000000..d0de6f3 Binary files /dev/null and b/public/For_people/01_earn.png differ diff --git a/public/For_people/02_spend-cropped.png b/public/For_people/02_spend-cropped.png new file mode 100644 index 0000000..2eaa382 Binary files /dev/null and b/public/For_people/02_spend-cropped.png differ diff --git a/public/For_people/02_spend.png b/public/For_people/02_spend.png new file mode 100644 index 0000000..14f75a8 Binary files /dev/null and b/public/For_people/02_spend.png differ diff --git a/public/For_people/03_live-cropped.png b/public/For_people/03_live-cropped.png new file mode 100644 index 0000000..92f8f0a Binary files /dev/null and b/public/For_people/03_live-cropped.png differ diff --git a/public/For_people/03_live.png b/public/For_people/03_live.png new file mode 100644 index 0000000..f2dddf0 Binary files /dev/null and b/public/For_people/03_live.png differ diff --git a/public/For_people/04_Lyts Universe.png b/public/For_people/04_Lyts Universe.png new file mode 100644 index 0000000..cb35f46 Binary files /dev/null and b/public/For_people/04_Lyts Universe.png differ diff --git a/public/For_people/04_Lyts_Universe-cropped.png b/public/For_people/04_Lyts_Universe-cropped.png new file mode 100644 index 0000000..e872307 Binary files /dev/null and b/public/For_people/04_Lyts_Universe-cropped.png differ diff --git a/public/home_hero/home_hero_01.png b/public/home_hero/home_hero_01.png new file mode 100644 index 0000000..2b7201d Binary files /dev/null and b/public/home_hero/home_hero_01.png differ diff --git a/public/home_hero/home_hero_02.png b/public/home_hero/home_hero_02.png new file mode 100644 index 0000000..bad1205 Binary files /dev/null and b/public/home_hero/home_hero_02.png differ diff --git a/public/home_hero/home_hero_03.png b/public/home_hero/home_hero_03.png new file mode 100644 index 0000000..d684bb6 Binary files /dev/null and b/public/home_hero/home_hero_03.png differ diff --git a/public/images/lyts.png b/public/images/lyts.png new file mode 100644 index 0000000..983627e Binary files /dev/null and b/public/images/lyts.png differ diff --git a/public/images/stock/discover.gif b/public/images/stock/discover.gif new file mode 100644 index 0000000..9c072e1 Binary files /dev/null and b/public/images/stock/discover.gif differ diff --git a/public/images/stock/discover.png b/public/images/stock/discover.png new file mode 100644 index 0000000..0d2f857 Binary files /dev/null and b/public/images/stock/discover.png differ diff --git a/public/images/stock/one_platform_bg.png b/public/images/stock/one_platform_bg.png new file mode 100644 index 0000000..bd7ab36 Binary files /dev/null and b/public/images/stock/one_platform_bg.png differ diff --git a/public/shift/1.png b/public/shift/1.png index 6a339f5..65bd977 100644 Binary files a/public/shift/1.png and b/public/shift/1.png differ diff --git a/public/shift/2.png b/public/shift/2.png index f8abec5..61fd5a9 100644 Binary files a/public/shift/2.png and b/public/shift/2.png differ diff --git a/public/shift/3.png b/public/shift/3.png index 878efc9..33a199b 100644 Binary files a/public/shift/3.png and b/public/shift/3.png differ diff --git a/public/shift/4.png b/public/shift/4.png index 075076c..eb7e204 100644 Binary files a/public/shift/4.png and b/public/shift/4.png differ diff --git a/public/shift/5.png b/public/shift/5.png index 00764dc..4c5f2ac 100644 Binary files a/public/shift/5.png and b/public/shift/5.png differ diff --git a/public/shift/6.png b/public/shift/6.png index 56a7534..7917396 100644 Binary files a/public/shift/6.png and b/public/shift/6.png differ diff --git a/src/app/for_people/page.tsx b/src/app/for_people/page.tsx index 26442f2..5ef2796 100644 --- a/src/app/for_people/page.tsx +++ b/src/app/for_people/page.tsx @@ -3,23 +3,21 @@ import React from 'react'; import VerticalScrollWrapper from '@/components/for-people/VerticalScrollWrapper'; import HeroSection from '@/components/for-people/HeroSection'; -import PillarsSection from '@/components/for-people/PillarsSection'; -import WhyLoyalySection from '@/components/for-people/WhyLoyalySection'; import DiscoverDealsSection from '@/components/for-people/DiscoverDealsSection'; import EarnRewardsSection from '@/components/for-people/EarnRewardsSection'; import SpendAnywhereSection from '@/components/for-people/SpendAnywhereSection'; -import AIRecommendationsSection from '@/components/for-people/AIRecommendationsSection'; +import LiveMoreSection from '@/components/for-people/LiveMoreSection'; +import LytsUniverseSection from '@/components/for-people/LytsUniverseSection'; import ExperienceSection from '@/components/for-people/ExperienceSection'; import FinalCTASection from '@/components/for-people/FinalCTASection'; const SECTIONS = [ 'Intro', - 'The Gap', - 'Save More', 'Discover', 'Earn', 'Spend', - 'Intelligence', + 'Live', + 'Lyts Universe', 'Experience', 'Get Started', ]; @@ -29,15 +27,15 @@ export default function ForPeoplePage() {
- - - + +
); } + diff --git a/src/app/globals.css b/src/app/globals.css index aeba7b8..c0865d9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -260,3 +260,7 @@ body.is-home-page.scroll-up .sticky-active { animation-play-state: paused !important; } } + +/* Hero carousel layout lives in HeroCarousel.tsx as a scoped ); } diff --git a/src/components/for-people/EarnRewardsSection.tsx b/src/components/for-people/EarnRewardsSection.tsx index 8fe7dab..291ceb6 100644 --- a/src/components/for-people/EarnRewardsSection.tsx +++ b/src/components/for-people/EarnRewardsSection.tsx @@ -1,29 +1,22 @@ 'use client'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; -import Image from 'next/image'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; -import { PhoneFrame, PhoneBottomNav, BellIcon, FlameIcon } from './PhoneMockup'; +import { PhoneMockupImage } from './PhoneMockup'; +import { FpTagCard, FpStatCard } from './FpCard'; +import { useCountUp, useStaggerReveal } from './fpMotion'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; -const feed = [ - { store: 'Daily Login', pts: +50, time: '2m ago', type: 'earn' }, - { store: 'Walking 5,000 steps', pts: +80, time: '1h ago', type: 'earn' }, - { store: 'Spin Wheel Game', pts: +120, time: '3h ago', type: 'earn' }, - { store: 'Selfie Booth', pts: +200, time: 'Yesterday', type: 'earn' }, -]; - export default function EarnRewardsSection() { - const [pts, setPts] = useState(0); const panelRef = useRef(null); const phoneRef = useRef(null); - const rafRef = useRef(0); - const rowRefs = useRef<(HTMLDivElement | null)[]>([]); - const activeRowRef = useRef(0); + const tagsRef = useStaggerReveal(); + const statsRef = useStaggerReveal(); + const waysCountRef = useCountUp(10); // Phone mockup rises in from the bottom as the section scrolls into view useIsomorphicLayoutEffect(() => { @@ -52,216 +45,88 @@ export default function EarnRewardsSection() { return () => ctx.revert(); }, []); - // Move the highlighted feed card as the section scrolls through the viewport - useEffect(() => { - if (typeof window === 'undefined') return; - const el = panelRef.current; - if (!el) return; - - const applyRow = (idx: number) => { - if (idx === activeRowRef.current) return; - activeRowRef.current = idx; - rowRefs.current.forEach((row, i) => { - if (!row) return; - const active = i === idx; - row.style.background = active ? '#f0fdf4' : 'transparent'; - row.style.borderColor = active ? '#16a34a' : 'transparent'; - row.style.transform = active ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)'; - row.style.boxShadow = active ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none'; - - const icon = row.querySelector('.feed-icon'); - if (icon) { - icon.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)'; - icon.style.borderColor = active ? '#16a34a' : 'rgba(23,21,18,0.08)'; - icon.style.color = active ? '#16a34a' : '#9ca3af'; - } - const points = row.querySelector('.feed-pts'); - if (points) { - points.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)'; - } - }); - }; - - const st = ScrollTrigger.create({ - trigger: el, - start: 'top 65%', - end: 'bottom 60%', - onUpdate: (self) => { - applyRow(Math.min(feed.length - 1, Math.floor(self.progress * feed.length))); - }, - }); - - return () => st.kill(); - }, []); - - useEffect(() => { - const el = panelRef.current; - if (!el) return; - const obs = new MutationObserver(() => { - if (el.hasAttribute('data-active')) { - const start = performance.now(); - const tick = (now: number) => { - const t = Math.min((now - start) / 1800, 1); - setPts(Math.round((1 - Math.pow(1 - t, 3)) * 284)); - if (t < 1) rafRef.current = requestAnimationFrame(tick); - }; - rafRef.current = requestAnimationFrame(tick); - } - }); - obs.observe(el, { attributes: true, attributeFilter: ['data-active'] }); - return () => { obs.disconnect(); cancelAnimationFrame(rafRef.current); }; - }, []); + const earnTags = [ + { label: 'Walk', icon: '🚶', bg: '#eef2ff', fg: '#4c5fd5' }, + { label: 'Visit', icon: '📍', bg: '#fdeeee', fg: '#d5514c' }, + { label: 'Selfie', icon: '🤳', bg: '#fef1e8', fg: '#d5824c' }, + { label: 'Spin', icon: '🎡', bg: '#f1eefd', fg: '#7a4cd5' }, + { label: 'Scratch', icon: '🎟️', bg: '#fdeef7', fg: '#d54ca3' }, + { label: 'Challenge', icon: '🏆', bg: '#fff8e1', fg: '#b8860b' }, + { label: 'Friend', icon: '🤝', bg: '#eef9f1', fg: '#3f9d5c' }, + { label: 'Shop', icon: '🛍️', bg: '#eef7fd', fg: '#2f8fc4' }, + { label: 'Event', icon: '🎉', bg: '#fdf1ee', fg: '#c45f2f' }, + { label: 'Brand', icon: '🏷️', bg: '#fef7ea', fg: '#c49a2f' }, + ]; return ( -
-
-
+
- {/* Left — points + headline */} -
- {/* Live label */} -
- - Live LYTs balance -
+ {/* Ambient decor — fills the white space behind the copy */} +
+ 02 +
+
- {/* Giant gold number */} -
- {pts.toLocaleString()} -
+
+
- {/* Redeemable line */} -
- ₹{pts.toLocaleString()} - redeemable value -
- -
Earn04
- -

- Every step. - Every game. - Every day. -

- -

- Earn LYTs (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYTs to your balance. -

+ {/* Left — phone mockup, matches the in-app Earn / Home experience */} +
+
- {/* Right — phone mockup, matches the in-app Earn / Home experience */} -
- + {/* Right — headline */} +
+
Earn02
- {/* Amber header */} -
-
- Loyaly.AI -
- -
-
-
- - 6-day streak · keep it going! -
-
+

+ Every Action + Earns Lyts. +

- {/* Balance card */} -
-
-
Total LYTs
-
284
-
≈ ₹284 redeemable
-
+

+ No app-opening rituals, no forced check-ins — just live your day and watch the balance move. +

- {/* Feed header */} -
+ {earnTags.map((tag) => ( + + {tag.label} Lyts + + ))} +
+ + {/* Stat strip — same card system as the tags above, distinguished by a filled dark surface */} +
+ 10} label="ways to earn" /> + + +
+ +
+ +

- Activity feed - - - LIVE - -

- - {/* Feed rows */} -
- {feed.map((tx, i) => ( -
{ rowRefs.current[i] = el; }} style={{ - display: 'grid', gridTemplateColumns: '32px minmax(0,1fr) auto', alignItems: 'center', gap: 10, - padding: '8px 8px', borderRadius: 12, - background: i === 0 ? '#f0fdf4' : 'transparent', - border: `1px solid ${i === 0 ? '#16a34a' : 'transparent'}`, - transform: i === 0 ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)', - boxShadow: i === 0 ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none', - transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease', - }}> -
- -
-
-
{tx.store}
-
{tx.time}
-
- - +{tx.pts} - -
- ))} -
- - {/* Summary strip */} -
- {[ - { label: 'Earned this month', val: '660 LYTs', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' }, - { label: 'Redeemable value', val: '₹660', color: '#bc7122', bg: '#fff7e0', border: '#f4be28' }, - ].map((s, i) => ( -
-
{s.label}
-
{s.val}
-
- ))} -
- - - + The more you live, the more you earn. +

+
diff --git a/src/components/for-people/ExperienceSection.tsx b/src/components/for-people/ExperienceSection.tsx index 21fd38a..83fe4a4 100644 --- a/src/components/for-people/ExperienceSection.tsx +++ b/src/components/for-people/ExperienceSection.tsx @@ -54,7 +54,7 @@ export default function ExperienceSection() {
- The Experience07 + The Experience06
diff --git a/src/components/for-people/FinalCTASection.tsx b/src/components/for-people/FinalCTASection.tsx index 2389f1e..6f06cf6 100644 --- a/src/components/for-people/FinalCTASection.tsx +++ b/src/components/for-people/FinalCTASection.tsx @@ -4,6 +4,7 @@ import React, { useRef, useEffect, useLayoutEffect } from 'react'; import Link from 'next/link'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { useMagnetic as useMagneticShared } from './fpMotion'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); @@ -27,19 +28,61 @@ function useMagnetic() { } const PROOF = [ - { val: '50K+', label: 'Active users', color: '#f4be28', bg: '#fffbeb', border: 'rgba(244,190,40,0.3)' }, - { val: '10K+', label: 'Partner stores', color: '#16a34a', bg: '#f0fdf4', border: 'rgba(22,163,74,0.25)' }, - { val: '₹28L+', label: 'Rewards given', color: '#2563eb', bg: '#eff6ff', border: 'rgba(37,99,235,0.2)' }, - { val: '4.9★', label: 'App rating', color: '#9333ea', bg: '#faf5ff', border: 'rgba(147,51,234,0.2)' }, + { val: '50K+', label: 'Active users', color: '#f4be28', icon: '👥', iconBg: '#fffbeb' }, + { val: '10K+', label: 'Partner stores', color: '#16a34a', icon: '🏬', iconBg: '#f0fdf4' }, + { val: '₹28L+', label: 'Rewards given', color: '#2563eb', icon: '🎁', iconBg: '#eff6ff' }, + { val: '4.9★', label: 'App rating', color: '#9333ea', icon: '⭐', iconBg: '#faf5ff' }, ]; export default function FinalCTASection() { const sectionRef = useRef(null); const mag1 = useMagnetic(); const mag2 = useMagnetic(); + const coinLayerRef = useRef(null); + const badgeMag1 = useMagneticShared(10); + const badgeMag2 = useMagneticShared(10); useIsomorphicLayoutEffect(() => { if (typeof window === 'undefined') return; + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const desktop = window.matchMedia('(min-width: 1024px)').matches; + + // Coin burst — small LytsUp coins fly toward the badges the first time they appear. + let bursted = false; + const burstCoins = () => { + const layer = coinLayerRef.current; + if (bursted || reduced || !desktop || !layer) return; + const targets = [badgeMag1.current, badgeMag2.current].filter(Boolean) as HTMLElement[]; + if (!targets.length) return; + bursted = true; + const layerRect = layer.getBoundingClientRect(); + for (let i = 0; i < 14; i++) { + const target = targets[i % targets.length]; + const tRect = target.getBoundingClientRect(); + const tx = tRect.left - layerRect.left + tRect.width / 2; + const ty = tRect.top - layerRect.top + tRect.height / 2; + const coin = document.createElement('span'); + coin.className = 'fcta-coin'; + layer.appendChild(coin); + gsap.set(coin, { + x: layerRect.width * (0.15 + Math.random() * 0.7), + y: -10 - Math.random() * 40, + scale: 0.45 + Math.random() * 0.55, + opacity: 0, + }); + gsap.timeline({ onComplete: () => coin.remove() }) + .to(coin, { opacity: 1, duration: 0.18 }, 0) + .to(coin, { + x: tx + (Math.random() * 24 - 12), + y: ty + (Math.random() * 24 - 12), + rotation: Math.random() * 240 - 120, + duration: 0.7 + Math.random() * 0.45, + ease: 'power2.in', + }, 0) + .to(coin, { opacity: 0, scale: 0.15, duration: 0.28 }, '-=0.26'); + } + }; + const ctx = gsap.context(() => { const tl = gsap.timeline({ scrollTrigger: { trigger: sectionRef.current, start: 'top 80%', toggleActions: 'play none none reset' }, @@ -56,29 +99,13 @@ export default function FinalCTASection() { { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.65, ease: 'power3.out' }, '-=0.4' ) - .fromTo('.fcta-hanging-logo', - { opacity: 0, y: -60, rotation: -16 }, - { opacity: 1, y: 0, rotation: 0, duration: 1.1, stagger: 0.16, ease: 'back.out(1.5)' }, - '-=0.45' + // Reveal on the magnet layer so the badge's own transform stays free for CSS hover + GSAP float. + .fromTo('.fcta-badge-magnet', + { opacity: 0, y: 30, scale: 0.94 }, + { opacity: 1, y: 0, scale: 1, duration: 0.7, stagger: 0.12, ease: 'back.out(1.6)' }, + '-=0.35' ) - .fromTo('.fcta-logo-link', - { rotation: 10 }, - { rotation: 0, duration: 1.2, stagger: 0.16, ease: 'back.out(1.6)' }, - '<' - ) - .call(() => { - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; - gsap.utils.toArray('.fcta-hanging-logo').forEach((el, i) => { - gsap.to(el, { - rotation: i % 2 === 0 ? 3.5 : -3.5, - duration: 2.4 + i * 0.3, - ease: 'sine.inOut', - yoyo: true, - repeat: -1, - transformOrigin: 'top center', - }); - }); - }) + .call(burstCoins) .fromTo('.fcta-btns', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.55, ease: 'power3.out' }, '-=0.45' @@ -91,6 +118,30 @@ export default function FinalCTASection() { return () => ctx.revert(); }, []); + // Continuous badge motion (desktop only): idle float + a glossy sweep & gentle pulse every ~6–8s. + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + if (!window.matchMedia('(min-width: 1024px)').matches) return; + + const ctx = gsap.context(() => { + gsap.utils.toArray('.fcta-badge-float').forEach((el, i) => { + gsap.to(el, { y: -6, duration: 2.6 + i * 0.3, ease: 'sine.inOut', yoyo: true, repeat: -1, delay: i * 0.4 }); + }); + gsap.utils.toArray('.fcta-store-badge').forEach((badge, i) => { + const sweep = badge.querySelector('.fcta-sweep'); + const floatEl = badge.parentElement; // .fcta-badge-float + const tl = gsap.timeline({ repeat: -1, repeatDelay: 6 + i, delay: 3 + i * 0.9 }); + if (sweep) tl.fromTo(sweep, { xPercent: -160 }, { xPercent: 260, duration: 1.1, ease: 'power2.inOut' }, 0); + if (floatEl) { + tl.to(floatEl, { scale: 1.03, duration: 0.5, ease: 'sine.out' }, 0) + .to(floatEl, { scale: 1, duration: 0.6, ease: 'sine.in' }, 0.5); + } + }); + }, sectionRef); + return () => ctx.revert(); + }, []); + return (
- Your rewards + Your lyts + {/* Lyts logo */} + {/* eslint-disable-next-line @next/next/no-img-element */} + Lyts + {/* Subtitle */}

- Download Loyaly and start earning on your very next purchase. Free. Instant. Yours. + Download LytsUp and start earning on your very next purchase.{' '} + Free · Instant · Yours

- {/* Hanging Logos */} -
- {/* Play Store Logo (hanging) */} -
- {/* Keyring loop */} -
- {/* String */} -
- {/* Logo Circle */} - - Google Play - - {/* Tooltip */} -
- Download In Playstore + {/* Coin-burst layer */} +
); diff --git a/src/components/for-people/FpCard.tsx b/src/components/for-people/FpCard.tsx new file mode 100644 index 0000000..510d93c --- /dev/null +++ b/src/components/for-people/FpCard.tsx @@ -0,0 +1,94 @@ +'use client'; + +import React from 'react'; + +/** + * The single card system for the "For People" page. + * Wraps the shared `.fp-card*` classes (defined in VerticalScrollWrapper) so + * every section imports one component and the markup can't drift. + */ + +type Theme = 'light' | 'dark' | 'fill'; + +const themeClass: Record = { + light: 'fp-card--light', + dark: 'fp-card--dark', + fill: 'fp-card--fill', +}; + +const DEFAULT_ICON_BG: Record = { + light: 'rgba(244,190,40,0.14)', + dark: 'rgba(244,190,40,0.14)', + fill: 'rgba(244,190,40,0.16)', +}; + +const sora = 'Sora, sans-serif'; + +interface TagCardProps { + theme?: Theme; + icon: React.ReactNode; + /** Icon-badge background; falls back to the theme's gold tint. */ + iconBg?: string; + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +} + +/** Compact icon + label pill (radius 12). */ +export function FpTagCard({ theme = 'light', icon, iconBg, children, className = '', style }: TagCardProps) { + return ( +
+ {icon} + + {children} + +
+ ); +} + +interface StatCardProps { + theme?: Theme; + layout?: 'row' | 'column'; + icon: React.ReactNode; + iconBg?: string; + /** May be a plain node or a count-up ref. */ + value: React.ReactNode; + label: string; + /** Value text color; defaults to theme-appropriate. */ + color?: string; + className?: string; + style?: React.CSSProperties; +} + +/** Larger icon + value + label card (radius 20). */ +export function FpStatCard({ + theme = 'light', layout = 'column', icon, iconBg, value, label, color, className = '', style, +}: StatCardProps) { + const dark = theme !== 'light'; + const column = layout === 'column'; + return ( +
+ {icon} +
+
+ {value} +
+
+ {label} +
+
+
+ ); +} diff --git a/src/components/for-people/HeroSection.tsx b/src/components/for-people/HeroSection.tsx index 417cdbf..0f0d7c2 100644 --- a/src/components/for-people/HeroSection.tsx +++ b/src/components/for-people/HeroSection.tsx @@ -63,15 +63,15 @@ export default function HeroSection() { fontSize: 'clamp(40px, 5.4vw, 82px)', fontWeight: 800, lineHeight: 1.02, letterSpacing: '-0.045em', color: '#fff', margin: '50px 0 28px', fontFamily: 'Sora, sans-serif', }}> - Shop Smart. - Live Rewarded. + Get Lit. + Live More.

- Meet LYTs (Loyaly Tokens): the single currency behind every reward. Earn them through daily challenges and games, then spend instantly at partner stores near you with zero expiry. + Meet Lyts: rewards for the things you already do. Walk, play, explore, shop, or complete a challenge, then spend your Lyts instantly at the brands you love. No waiting. No cashback. No complicated redemption.

@@ -93,9 +93,9 @@ export default function HeroSection() { maxWidth: 600, }}> {[ - { val: '1 LYT = ₹1', label: 'Fixed value' }, - { val: 'Zero Expiry', label: 'Ever' }, - { val: '10,000+', label: 'Partner stores' } + { val: '10+ Ways', label: 'To earn Lyts' }, + { val: 'Instant Spend', label: 'No delay at checkout' }, + { val: '10,000+', label: 'Partner brands' } ].map((item, idx) => (
- 50,000+ active shoppers earning daily + 50,000+ Lytians earning and living more, every day
diff --git a/src/components/for-people/LiveMoreSection.tsx b/src/components/for-people/LiveMoreSection.tsx new file mode 100644 index 0000000..99161da --- /dev/null +++ b/src/components/for-people/LiveMoreSection.tsx @@ -0,0 +1,95 @@ +'use client'; + +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { PhoneMockupImage } from './PhoneMockup'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +export default function LiveMoreSection() { + const panelRef = useRef(null); + const phoneRef = useRef(null); + + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const section = panelRef.current; + const phone = phoneRef.current; + if (!section || !phone) return; + + const ctx = gsap.context(() => { + gsap.fromTo(phone, + { y: 160, opacity: 0 }, + { + y: 0, opacity: 1, ease: 'power2.out', + scrollTrigger: { trigger: section, start: 'top 92%', end: 'top 25%', scrub: 1.2 }, + } + ); + }, section); + + return () => ctx.revert(); + }, []); + + return ( +
+
+
+ + {/* Left — phone mockup: lifestyle moments feed */} +
+ +
+ + {/* Right — headline + copy */} +
+
Live04
+ +

+ Live + More. +

+ +

+ Life isn't about collecting points. It's about collecting moments. +

+ +
+ {[ + { label: 'Walk more', icon: '🚶', bg: '#eef2ff' }, + { label: 'Explore more', icon: '🧭', bg: '#fdeef7' }, + { label: 'Meet friends', icon: '🤝', bg: '#eef9f1' }, + { label: 'Support local', icon: '🏪', bg: '#fef1e8' }, + { label: 'Discover new brands', icon: '🏷️', bg: '#fef7ea' }, + { label: 'Celebrate every day', icon: '🎊', bg: '#fdeeee' }, + ].map((tag) => ( +
+ {tag.icon} + + {tag.label} + +
+ ))} +
+ +
+ +

+ LytsUp rewards your lifestyle, not just your purchases. +

+
+
+
+
+
+ ); +} diff --git a/src/components/for-people/LytsUniverseSection.tsx b/src/components/for-people/LytsUniverseSection.tsx new file mode 100644 index 0000000..023b9b6 --- /dev/null +++ b/src/components/for-people/LytsUniverseSection.tsx @@ -0,0 +1,96 @@ +'use client'; + +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { PhoneMockupImage } from './PhoneMockup'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const CHALLENGES = [ + { label: 'Walk More Challenge', icon: '🚶' }, + { label: 'Selfie Quest', icon: '🤳' }, + { label: 'Weekend Explorer', icon: '🗺️' }, + { label: 'Coffee Week', icon: '☕' }, + { label: 'Sneaker Hunt', icon: '👟' }, +]; + +export default function LytsUniverseSection() { + const panelRef = useRef(null); + const phoneRef = useRef(null); + + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const section = panelRef.current; + const phone = phoneRef.current; + if (!section || !phone) return; + + const ctx = gsap.context(() => { + gsap.fromTo(phone, + { y: 160, opacity: 0 }, + { + y: 0, opacity: 1, ease: 'power2.out', + scrollTrigger: { trigger: section, start: 'top 92%', end: 'top 25%', scrub: 1.2 }, + } + ); + }, section); + + return () => ctx.revert(); + }, []); + + return ( +
+
+
+ + {/* Left — headline + copy */} +
+
The Lyts Universe05
+ +

+ Welcome to the + World of Lyts. +

+ +

+ A world where every action has value. Meet the Lytians, real people with real stories and real rewards, straight from the app. +

+ +
+ {CHALLENGES.map((tag) => ( +
+ {tag.icon} + + {tag.label} + +
+ ))} +
+ +
+ +

+ Become a Lytian. Walk. Earn. Spend. Live More. +

+
+
+ + {/* Right — phone mockup: Lytian profile + India lighting up */} +
+ +
+
+
+
+ ); +} diff --git a/src/components/for-people/PhoneMockup.tsx b/src/components/for-people/PhoneMockup.tsx index f801e9f..ba22425 100644 --- a/src/components/for-people/PhoneMockup.tsx +++ b/src/components/for-people/PhoneMockup.tsx @@ -56,7 +56,7 @@ export function MapIcon() { ); } -export function HeartLogoIcon({ size = 16, active = false }: { size?: number; active?: boolean }) { +export function HeartLogoIcon({ size = 16 }: { size?: number; active?: boolean }) { return ( ); @@ -94,12 +93,30 @@ export function FlameIcon({ color = '#f4be28' }: { color?: string }) { ); } -/* ── Bottom nav — pass the active tab key to highlight it ── */ -export function PhoneBottomNav({ active = 'home' }: { active?: string }) { +/* ── LytsUp brand mark — used in place of the Loyaly.AI heart+tag logo + wherever a screen is themed for the LytsUp sub-brand (Live, Lyts Universe). + No own background: the parent nav item already supplies the active-state + gold pill, same convention as every other bottom-nav icon. ── */ +export function LytsUpMarkIcon({ size = 18 }: { size?: number }) { + return ( + + L + + ); +} + +/* ── Bottom nav — pass the active tab key to highlight it ── + centerIcon lets LytsUp-branded sections swap the middle tab's mark + (default is the Loyaly.AI heart+tag logo) without touching every caller. */ +export function PhoneBottomNav({ active = 'home', centerIcon }: { active?: string; centerIcon?: (isActive: boolean) => React.ReactNode }) { const items: { key: string; icon: (isActive: boolean) => React.ReactNode }[] = [ { key: 'home', icon: () => }, { key: 'tag', icon: () => }, - { key: 'heart', icon: (isActive) => }, + { key: 'heart', icon: centerIcon ?? ((isActive) => ) }, { key: 'shop', icon: () => }, { key: 'user', icon: () => }, ]; @@ -122,11 +139,52 @@ export function PhoneBottomNav({ active = 'home' }: { active?: string }) { ); } +/* ── Static phone-mockup image — pre-rendered device + screen (used in place + of the hand-built PhoneFrame where a polished, final-design screenshot is + available). Keeps the same float animation and responsive width as + PhoneFrame so it drops into the same section markup unchanged. ── */ +export function PhoneMockupImage({ + src, alt, floatClassName = 'pm-phone-float', width = 300, aspectRatio = 1684 / 3196, +}: { src: string; alt: string; floatClassName?: string; width?: number; aspectRatio?: number }) { + return ( +
+ {alt} + +
+ ); +} + /* ── Phone frame — dark bezel + dynamic island + gentle float, white screen inside ── */ +/* Exact widget size on phone-width viewports — kept identical across every + section regardless of the (purely cosmetic) floatClassName passed in. */ +const MOBILE_WIDGET_WIDTH = 280; + export function PhoneFrame({ children, floatClassName = 'pm-phone-float', width = 300 }: { children: React.ReactNode; floatClassName?: string; width?: number }) { + // Convention across every section: is always passed as + // the last child. Splitting it out lets it sit pinned to the screen's + // bottom edge while the rest of the content fills the space above it — + // no scrollbar, no risk of the nav getting pushed out of view. + const items = React.Children.toArray(children); + const bottomNav = items[items.length - 1]; + const content = items.slice(0, -1); + return (
-
+ {/* Screen — locked to a real phone's aspect ratio (~9:19.5, e.g. iPhone 14) + so every section's widget reads as the same physical device regardless + of how much content it holds. */} +
{/* Dynamic island */}
- {children} +
+ {content} +
+ {bottomNav}
diff --git a/src/components/for-people/SpendAnywhereSection.tsx b/src/components/for-people/SpendAnywhereSection.tsx index 8c241fd..0cbfd48 100644 --- a/src/components/for-people/SpendAnywhereSection.tsx +++ b/src/components/for-people/SpendAnywhereSection.tsx @@ -1,211 +1,93 @@ 'use client'; import React, { useEffect, useLayoutEffect, useRef } from 'react'; -import Image from 'next/image'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { PhoneMockupImage } from './PhoneMockup'; if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; -const GroceryIcon = () => ; -const FashionIcon = () => ; -const CafeIcon = () => ; -const SportsIcon = () => ; -const ElectronicsIcon = () => ; -const BeautyIcon = () => ; - -const brands = [ - { name: 'Groceries', Icon: GroceryIcon, stores: ['DMart', 'Reliance Fresh', "Spencer's"], color: '#22c55e' }, - { name: 'Fashion', Icon: FashionIcon, stores: ['Zara', 'H&M', 'Lifestyle'], color: '#a855f7' }, - { name: 'Food & Cafe', Icon: CafeIcon, stores: ['Starbucks', 'KFC', 'McD'], color: '#f97316' }, - { name: 'Sports', Icon: SportsIcon, stores: ['Decathlon', 'Nike', 'Adidas'], color: '#3b82f6' }, - { name: 'Electronics', Icon: ElectronicsIcon, stores: ['Croma', 'Reliance Digital'], color: '#06b6d4' }, - { name: 'Beauty', Icon: BeautyIcon, stores: ['Health & Glow', 'MAC', 'Forest Essentials'], color: '#ec4899' }, -]; - export default function SpendAnywhereSection() { - const gridRef = useRef(null); - const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + const panelRef = useRef(null); + const phoneRef = useRef(null); - // Scroll-triggered blur/scale entrance for the cards + // Phone mockup rises in from the bottom as the section scrolls into view useIsomorphicLayoutEffect(() => { if (typeof window === 'undefined') return; - const grid = gridRef.current; - if (!grid) return; + const section = panelRef.current; + const phone = phoneRef.current; + if (!section || !phone) return; const ctx = gsap.context(() => { - gsap.fromTo('.spend-card', - { opacity: 0, y: 44, scale: 0.92, filter: 'blur(8px)' }, + gsap.fromTo(phone, + { y: 160, opacity: 0 }, { - opacity: 1, y: 0, scale: 1, filter: 'blur(0px)', - duration: 0.9, stagger: 0.09, ease: 'expo.out', - scrollTrigger: { trigger: grid, start: 'top 85%', toggleActions: 'play none none reset' }, + y: 0, opacity: 1, ease: 'power2.out', + scrollTrigger: { trigger: section, start: 'top 92%', end: 'top 25%', scrub: 1.2 }, } ); - }, grid); + }, section); return () => ctx.revert(); }, []); - // Cursor-tracked spotlight + tilt per card - const handleMove = (i: number) => (e: React.MouseEvent) => { - const el = cardRefs.current[i]; - if (!el) return; - const rect = el.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - el.style.setProperty('--mx', `${(x / rect.width) * 100}%`); - el.style.setProperty('--my', `${(y / rect.height) * 100}%`); - const rotateX = ((y - rect.height / 2) / rect.height) * -6; - const rotateY = ((x - rect.width / 2) / rect.width) * 6; - gsap.to(el, { rotateX, rotateY, duration: 0.4, ease: 'power2.out', transformPerspective: 700 }); - }; - - const handleLeave = (i: number) => () => { - const el = cardRefs.current[i]; - if (!el) return; - gsap.to(el, { rotateX: 0, rotateY: 0, duration: 0.5, ease: 'power3.out' }); - }; - return ( -
- {/* Background photo */} - -
- +
+
- {/* Header row */} -
+ {/* Left — headline + copy */}
-
Spend05
-

- Spend it - anywhere. +
Spend03
+ +

+ Your Lyts. + Your Choice.

-
-

- One balance. Every store. Your LYTs are universal: no restrictions, no expiry, no fine print. -

-
- {/* Category grid */} -
- {brands.map((brand, i) => { - const BrandIcon = brand.Icon; - return ( -
{ cardRefs.current[i] = el; }} - onMouseMove={handleMove(i)} - onMouseLeave={handleLeave(i)} - className="spend-card" - style={{ - position: 'relative', - padding: '26px 28px', - borderRadius: 22, - background: '#0d0d16', - border: '1px solid rgba(255,255,255,0.08)', - cursor: 'default', - overflow: 'hidden', - transformStyle: 'preserve-3d', - ['--card-color' as string]: brand.color, - }} - > - {/* Cursor-tracked spotlight */} -
- {/* Border glow ring */} -
+

+ Walk into a participating brand or store. Choose what you love. Tell the cashier you're paying with Lyts. Enjoy instant savings, no waiting, no cashback, no complicated redemption. +

-
-
-
- -
- {brand.name} -
- - {/* Explore arrow, slides in on hover */} - - +
+ {[ + { label: 'No waiting', icon: '⚡' }, + { label: 'No cashback', icon: '🚫' }, + { label: 'No complicated redemption', icon: '✅' }, + ].map((tag) => ( +
+ {tag.icon} + + {tag.label}
- -
- -
- {brand.stores.map((s, j) => ( - {s} - ))} -
-
- ); - })} -
- - {/* Stat strip */} -
- {[ - { val: '10,000+', lbl: 'Partner stores' }, - { val: 'Zero', lbl: 'Expiry on LYTs' }, - { val: 'Universal', lbl: 'One balance, all brands' }, - { val: 'Instant', lbl: 'Redeem at checkout' }, - ].map((s, i) => ( -
-
{s.val}
-
{s.lbl}
+ ))}
- ))} + +
+ +

+ One Wallet. Thousands of Rewards. +

+
+
+ + {/* Right — phone mockup: where to spend */} +
+ +
- -
); } diff --git a/src/components/for-people/VerticalScrollWrapper.tsx b/src/components/for-people/VerticalScrollWrapper.tsx index a08998f..f72f375 100644 --- a/src/components/for-people/VerticalScrollWrapper.tsx +++ b/src/components/for-people/VerticalScrollWrapper.tsx @@ -143,12 +143,36 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props --carbon: #171512; --warm: #4f463a; --violet: #8258c8; + /* ── Typography scale (Discover is the reference — one size everywhere) ── */ + --fp-h-hero: clamp(44px, 5.6vw, 88px); + --fp-h-weight: 800; + --fp-h-track: -0.045em; + --fp-desc-size: 16px; + --fp-desc-line: 1.8; position: relative; background: #ffffff; color: #fff; padding-top: 20px; } + /* ── Shared heading + description rhythm (one source of truth) ── */ + /* .fp-h1 / .fp-h2 are intentionally identical — Discover's scale on every section. */ + .fp-h1, .fp-h2 { + font-family: 'Sora', sans-serif; + font-weight: var(--fp-h-weight); + letter-spacing: var(--fp-h-track); + font-size: var(--fp-h-hero); + line-height: 0.94; + margin: 0 0 24px; + } + .fp-desc { + font-family: 'Sora', sans-serif; + font-size: var(--fp-desc-size); + line-height: var(--fp-desc-line); + margin: 0 0 24px; + max-width: 420px; + } + /* ── Film grain ── */ .fp-grain { position: fixed; inset: 0; z-index: 9; pointer-events: none; opacity: 0.04; @@ -300,6 +324,26 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props gap: clamp(40px, 6vw, 96px); align-items: center; } + /* ── Shared tag/stat card system (icon badge + label) ── */ + .fp-card { + display: flex; align-items: center; gap: 10px; + padding: 10px 14px; border-radius: 12px; + transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease; + } + .fp-card--light { background: #fff; border: 1.5px solid var(--carbon); } + .fp-card--light:hover { box-shadow: 3px 3px 0 var(--carbon); transform: translate(-2px, -2px); } + .fp-card--dark { background: rgba(255,255,255,0.04); border: 1.5px solid rgba(255,255,255,0.18); } + .fp-card--dark:hover { border-color: var(--gold); box-shadow: 3px 3px 0 rgba(244,190,40,0.35); transform: translate(-2px, -2px); } + .fp-card--fill { background: var(--carbon); border: 1.5px solid var(--carbon); } + .fp-card--fill:hover { box-shadow: 3px 3px 0 rgba(244,190,40,0.4); transform: translate(-2px, -2px); } + .fp-card-icon { + display: flex; align-items: center; justify-content: center; + width: 30px; height: 30px; border-radius: 9px; font-size: 14px; flex-shrink: 0; + } + /* Larger card for stat/feature content */ + .fp-card--stat { padding: 20px 16px; border-radius: 20px; gap: 12px; } + .fp-card-icon--lg { width: 40px; height: 40px; border-radius: 12px; font-size: 18px; } + /* ── Mobile ── */ @media (max-width: 900px) { .fp-cursor { display: none; } diff --git a/src/components/for-people/fpMotion.ts b/src/components/for-people/fpMotion.ts new file mode 100644 index 0000000..2e43b27 --- /dev/null +++ b/src/components/for-people/fpMotion.ts @@ -0,0 +1,153 @@ +'use client'; + +/** + * Shared GSAP + Lenis animation primitives for the "For People" page. + * The page's smooth scroll (Lenis) is already wired into GSAP's ticker inside + * VerticalScrollWrapper, so ScrollTrigger works everywhere without extra setup. + */ + +import { useRef, useEffect, useLayoutEffect } from 'react'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +// Run before paint on the client so initial hidden/zeroed states never flash. +const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const prefersReduced = () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +/** Run `cb` once when `el` enters the viewport — or immediately if already in view. */ +function onEnterOnce(el: HTMLElement, cb: () => void, startRatio = 0.9): () => void { + const rect = el.getBoundingClientRect(); + if (rect.top < window.innerHeight * startRatio && rect.bottom > 0) { + cb(); + return () => {}; + } + const st = ScrollTrigger.create({ + trigger: el, + start: `top ${startRatio * 100}%`, + once: true, + onEnter: cb, + }); + return () => st.kill(); +} + +/** Magnetic pull toward the cursor. Desktop only; no-op on touch / reduced-motion. */ +export function useMagnetic(strength = 12) { + const ref = useRef(null); + useEffect(() => { + const el = ref.current; + if (!el) return; + if (prefersReduced() || window.matchMedia('(pointer: coarse)').matches) return; + + const onMove = (e: MouseEvent) => { + const r = el.getBoundingClientRect(); + const x = ((e.clientX - r.left) / r.width - 0.5) * strength * 2; + const y = ((e.clientY - r.top) / r.height - 0.5) * strength * 2; + el.style.transform = `translate(${x}px, ${y}px)`; + }; + const onLeave = () => { el.style.transform = 'translate(0, 0)'; }; + + el.addEventListener('mousemove', onMove); + el.addEventListener('mouseleave', onLeave); + return () => { + el.removeEventListener('mousemove', onMove); + el.removeEventListener('mouseleave', onLeave); + }; + }, [strength]); + return ref; +} + +interface CountUpOpts { + duration?: number; + prefix?: string; + suffix?: string; + decimals?: number; + /** Custom formatter for the running value; overrides prefix/suffix/decimals body. */ + format?: (v: number) => string; +} + +/** Animate a number from 0 → `to` when it scrolls into view. Attach the returned ref to the text node. */ +export function useCountUp(to: number, opts: CountUpOpts = {}) { + const { duration = 1.6, prefix = '', suffix = '', decimals = 0, format } = opts; + const ref = useRef(null); + useIsoLayoutEffect(() => { + const el = ref.current; + if (!el) return; + + const render = (v: number) => { + let body: string; + if (format) body = format(v); + else if (decimals > 0) body = v.toFixed(decimals); + else body = Math.round(v).toLocaleString('en-IN'); + el.textContent = `${prefix}${body}${suffix}`; + }; + + if (prefersReduced()) { render(to); return; } + + render(0); + const obj = { v: 0 }; + return onEnterOnce(el, () => { + gsap.to(obj, { v: to, duration, ease: 'power2.out', onUpdate: () => render(obj.v) }); + }); + }, [to, duration, prefix, suffix, decimals, format]); + return ref; +} + +/** Scrub-linked vertical parallax drift as the element's section moves through the viewport. */ +export function useParallax(distance = 50) { + const ref = useRef(null); + useEffect(() => { + const el = ref.current; + if (!el || prefersReduced()) return; + + const anim = gsap.fromTo( + el, + { y: -distance }, + { + y: distance, + ease: 'none', + scrollTrigger: { + trigger: el.parentElement ?? el, + start: 'top bottom', + end: 'bottom top', + scrub: true, + }, + } + ); + return () => { anim.scrollTrigger?.kill(); anim.kill(); }; + }, [distance]); + return ref; +} + +interface StaggerOpts { + y?: number; + stagger?: number; + duration?: number; + startRatio?: number; +} + +/** Staggered fade-up of a container's direct children when it enters view. */ +export function useStaggerReveal(opts: StaggerOpts = {}) { + const { y = 22, stagger = 0.06, duration = 0.6, startRatio = 0.85 } = opts; + const ref = useRef(null); + useIsoLayoutEffect(() => { + const el = ref.current; + if (!el) return; + const items = Array.from(el.children) as HTMLElement[]; + if (!items.length) return; + + if (prefersReduced()) { gsap.set(items, { opacity: 1, y: 0 }); return; } + + gsap.set(items, { opacity: 0, y }); + return onEnterOnce( + el, + () => { gsap.to(items, { opacity: 1, y: 0, duration, ease: 'power3.out', stagger }); }, + startRatio + ); + }, [y, stagger, duration, startRatio]); + return ref; +} diff --git a/src/components/home/AnimatedDarkBg.tsx b/src/components/home/AnimatedDarkBg.tsx index cca88d9..a3bc0c4 100644 --- a/src/components/home/AnimatedDarkBg.tsx +++ b/src/components/home/AnimatedDarkBg.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; +import NextImage from 'next/image'; /** * Premium animated dark background — gold gradient mesh orbs, drifting grid, @@ -9,16 +10,31 @@ import React from 'react'; */ export default function AnimatedDarkBg({ variant = 'amber', -}: { variant?: 'amber' | 'cool' | 'mono' }) { + bgImage, + bgImageAlt = '', +}: { variant?: 'amber' | 'cool' | 'mono'; bgImage?: string; bgImageAlt?: string }) { const orbA = variant === 'cool' ? 'rgba(80,140,220,0.10)' : 'rgba(244,190,40,0.12)'; const orbB = variant === 'cool' ? 'rgba(140,90,210,0.10)' : 'rgba(255,149,0,0.08)'; return (
- {/* Base gradient */} + {bgImage && ( + + )} + + {/* Base gradient — top-weighted scrim so the headline stays legible while the + image reads clearly in the middle, plus a lift at the bottom for card contrast */}
{/* Drifting mesh orbs */} @@ -48,7 +64,9 @@ export default function AnimatedDarkBg({ {/* Vignette */}
+
+ ); +} diff --git a/src/components/home/HeroContent.tsx b/src/components/home/HeroContent.tsx new file mode 100644 index 0000000..ad75ef1 --- /dev/null +++ b/src/components/home/HeroContent.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useEffect, useMemo, useRef } from 'react'; +import { gsap } from 'gsap'; +import Link from 'next/link'; +import type { HeroSlideData } from './heroCarouselData'; + +interface HeroContentProps { + slide: HeroSlideData; +} + +export default function HeroContent({ slide }: HeroContentProps) { + const badgeRef = useRef(null); + const headlineRef = useRef(null); + const descRef = useRef(null); + const ctaRef = useRef(null); + + const words = useMemo(() => slide.headline.split(' '), [slide.headline]); + + useEffect(() => { + const wordEls = headlineRef.current + ? Array.from(headlineRef.current.querySelectorAll('.hero-word-inner')) + : []; + const desc = descRef.current; + const cta = ctaRef.current; + const badge = badgeRef.current; + + gsap.set(wordEls, { yPercent: 120, opacity: 0, rotateZ: 4 }); + gsap.set([desc, cta], { opacity: 0, y: 24 }); + gsap.set(badge, { opacity: 0, y: -12, scale: 0.92 }); + + const tl = gsap.timeline({ defaults: { ease: 'power4.out' } }); + tl.to(badge, { opacity: 1, y: 0, scale: 1, duration: 0.6 }, 0); + tl.to( + wordEls, + { yPercent: 0, opacity: 1, rotateZ: 0, duration: 1, stagger: 0.055 }, + 0.18 + ); + tl.to(desc, { opacity: 1, y: 0, duration: 0.8 }, '-=0.55'); + tl.to(cta, { opacity: 1, y: 0, duration: 0.8 }, '-=0.5'); + + return () => { tl.kill(); }; + }, [slide.id]); + + return ( +
+
+ + {slide.badge} +
+ +

+ {words.map((word, i) => { + const isHighlighted = slide.highlightWords.includes(word); + return ( + + + {word} + {i < words.length - 1 ? ' ' : ''} + + + ); + })} +

+ +

+ {slide.description} +

+ +
+ + {slide.ctaLabel} + + + + + + {slide.secondaryLabel} + + + + + +
+
+ ); +} diff --git a/src/components/home/HeroNavigation.tsx b/src/components/home/HeroNavigation.tsx new file mode 100644 index 0000000..ac335ac --- /dev/null +++ b/src/components/home/HeroNavigation.tsx @@ -0,0 +1,23 @@ +'use client'; + +interface HeroNavigationProps { + onPrev: () => void; + onNext: () => void; +} + +export default function HeroNavigation({ onPrev, onNext }: HeroNavigationProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/home/HeroProgress.tsx b/src/components/home/HeroProgress.tsx new file mode 100644 index 0000000..84b8c29 --- /dev/null +++ b/src/components/home/HeroProgress.tsx @@ -0,0 +1,34 @@ +'use client'; + +import type { MutableRefObject } from 'react'; + +interface HeroProgressProps { + count: number; + activeIndex: number; + onSelect: (index: number) => void; + fillRefs: MutableRefObject<(HTMLDivElement | null)[]>; +} + +export default function HeroProgress({ count, activeIndex, onSelect, fillRefs }: HeroProgressProps) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} diff --git a/src/components/home/HeroSlide.tsx b/src/components/home/HeroSlide.tsx new file mode 100644 index 0000000..49ea239 --- /dev/null +++ b/src/components/home/HeroSlide.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { gsap } from 'gsap'; +import NextImage from 'next/image'; +import type { HeroSlideData } from './heroCarouselData'; + +interface HeroSlideProps { + slide: HeroSlideData; + active: boolean; + priority?: boolean; + durationMs: number; +} + +export default function HeroSlide({ slide, active, priority, durationMs }: HeroSlideProps) { + const imgRef = useRef(null); + + // Slow zoom on the background image only, GSAP-driven. The crossfade/slide + // below is inline-style + inline-transition, so it can never depend on an + // external stylesheet having loaded/compiled correctly. + useEffect(() => { + const img = imgRef.current; + if (!img) return; + + gsap.killTweensOf(img); + + if (active) { + gsap.fromTo( + img, + { scale: 1 }, + { scale: 1.05, duration: durationMs / 1000, ease: 'none' } + ); + } else { + gsap.set(img, { scale: 1 }); + } + }, [active, durationMs]); + + return ( +
+
+ +
+ {/* Dark overlay for text readability — 40-50% per spec */} +
+
+ ); +} diff --git a/src/components/home/LytsUpSection.tsx b/src/components/home/LytsUpSection.tsx index aae0eb4..32c44b4 100644 --- a/src/components/home/LytsUpSection.tsx +++ b/src/components/home/LytsUpSection.tsx @@ -74,10 +74,11 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { 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)', + background: active ? 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)' : 'rgba(255,255,255,0.05)', + borderColor: active ? '#ffffff' : 'rgba(244,190,40,0.3)', borderWidth: active ? '3px' : '1px', - boxShadow: active ? '0 12px 26px -10px rgba(23,21,18,0.4)' : '0 0px 0px rgba(0,0,0,0)', + color: active ? '#171512' : 'rgba(255,255,255,0.6)', + boxShadow: active ? '0 12px 30px -8px rgba(244,190,40,0.55)' : '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)' }); @@ -158,8 +159,8 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { 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)', + background: 'radial-gradient(ellipse 110% 80% at 50% 0%, #17130d 0%, #0d0b09 55%, #0a0807 100%)', + boxShadow: 'inset 0 0 0 1px rgba(244,190,40,0.12)', }} > {/* Faint dot-grid texture for depth, matching the light sections elsewhere */} @@ -167,13 +168,14 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { aria-hidden style={{ position: 'absolute', inset: 0, pointerEvents: 'none', - backgroundImage: 'radial-gradient(rgba(23,21,18,0.06) 1px, transparent 1px)', + backgroundImage: 'radial-gradient(rgba(255,255,255,0.05) 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%)', }} /> -
+
+
{/* Oversized watermark type — big-type Gen-Z background flex */}
@@ -195,7 +197,7 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { margin: 0, padding: 'clamp(8px, 2vw, 20px) 0', fontSize: 'clamp(2.4rem, 5.4vw, 4.4rem)', - fontWeight: 800, color: '#171512', + fontWeight: 800, color: '#ffffff', lineHeight: 1, letterSpacing: '-0.04em', fontFamily: 'Sora, sans-serif', }} @@ -232,7 +234,7 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { 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', + color: 'rgba(255,255,255,0.62)', 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. @@ -247,16 +249,17 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { 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)', + background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(244,190,40,0.3)', + backdropFilter: 'blur(8px)', }} > { statRefs.current[i] = el; }} - style={{ fontSize: '0.92rem', fontWeight: 800, color: '#171512', fontFamily: 'Sora, sans-serif', minWidth: '2.6em', display: 'inline-block' }} + style={{ fontSize: '0.92rem', fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', minWidth: '2.6em', display: 'inline-block' }} > 0{stat.suffix} - {stat.label} + {stat.label}
))}
@@ -273,20 +276,20 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { 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', + color: i === 0 ? '#171512' : 'rgba(255,255,255,0.6)', boxSizing: 'border-box', + background: i === 0 ? 'linear-gradient(135deg, #f4be28 0%, #FF9800 100%)' : 'rgba(255,255,255,0.05)', + border: i === 0 ? '3px solid #ffffff' : '1px solid rgba(244,190,40,0.3)', + boxShadow: i === 0 ? '0 12px 30px -8px rgba(244,190,40,0.55)' : '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)' }} @@ -304,7 +307,9 @@ export default function LytsUpSection({ isLoaded = true }: LytsUpSectionProps) { maxWidth: 720, textAlign: 'center', padding: 'clamp(36px, 4vw, 52px) clamp(24px, 4vw, 48px)', borderRadius: 26, - background: '#171512', + background: 'linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%)', + border: '1px solid rgba(244,190,40,0.22)', + boxShadow: '0 30px 60px -24px rgba(0,0,0,0.6)', position: 'relative', overflow: 'hidden', }} > diff --git a/src/components/home/ProductsSection.tsx b/src/components/home/ProductsSection.tsx index 4b8b0da..114088e 100644 --- a/src/components/home/ProductsSection.tsx +++ b/src/components/home/ProductsSection.tsx @@ -450,7 +450,24 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro // Desktop / tablet: pinned horizontal scroll mm.add('(min-width: 901px)', () => { + const cards = gsap.utils.toArray('.products-card', track); const distance = () => Math.max(0, track.scrollWidth - section.clientWidth); + const steps = Math.max(1, PRODUCTS.length - 1); + + // Depth cue: the card nearest the viewport centre sits at full scale/ + // opacity, neighbours ease back slightly — makes the ride feel guided + // rather than a flat strip sliding past, and the eased response + // absorbs small scrub jitter instead of showing it as a raw jump. + const updateDepth = () => { + const centerX = section.clientWidth / 2; + cards.forEach((card) => { + const trackX = Number(gsap.getProperty(track, 'x', 'px')); + const cardCenter = card.offsetLeft + card.offsetWidth / 2 + trackX; + const dist = Math.abs(cardCenter - centerX); + const norm = gsap.utils.clamp(0, 1, dist / (section.clientWidth * 0.62)); + gsap.set(card, { scale: 1 - norm * 0.07, opacity: 1 - norm * 0.32 }); + }); + }; const tween = gsap.to(track, { x: () => -distance(), @@ -460,30 +477,42 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro start: 'top top', end: () => `+=${distance()}`, pin: true, - pinType: 'transform', pinSpacing: true, anticipatePin: 1, - scrub: 1, + scrub: 0.4, invalidateOnRefresh: true, refreshPriority: 5, + snap: { + snapTo: (value) => Math.round(value * steps) / steps, + duration: { min: 0.2, max: 0.5 }, + ease: 'power2.inOut', + }, onUpdate: (self) => { if (progressRef.current) progressRef.current.style.transform = `scaleX(${self.progress})`; if (counterRef.current) { - const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * (PRODUCTS.length - 1)) + 1)); + const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * steps) + 1)); counterRef.current.textContent = `0${idx}`; } + updateDepth(); }, }, }); + updateDepth(); ScrollTrigger.refresh(); - return () => { tween.scrollTrigger?.kill(); tween.kill(); gsap.set(track, { clearProps: 'transform' }); }; + return () => { + tween.scrollTrigger?.kill(); + tween.kill(); + gsap.set(track, { clearProps: 'transform' }); + gsap.set(cards, { clearProps: 'scale,opacity' }); + }; }); return () => mm.revert(); }, [isLoaded]); return ( +
-
+
Platform +
-

- Four products.
- One ecosystem. +

+ Four agents.{' '} + One AI.

-
-

+

+

Powering every stage of the retail journey with intelligent AI solutions.

-
- Built as one connected platform + +
+ Unified into one platform 04
@@ -604,11 +635,17 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro } } + /* Margin lives on this non-pinned wrapper, not on the pinned section + itself — GSAP's ScrollTrigger pin-spacer can miscalculate the + reserved space when the pinned element carries its own margin, + which shows up as a jump/shake right as the pin engages. */ + .products-section-wrapper { + margin: 20px; + } .products-section-root { --pt: clamp(230px, 28vh, 310px); --pb: clamp(64px, 8vh, 96px); - width: calc(100% - 40px); - margin: 20px 20px; + width: 100%; border-radius: 32px; background: #FAFAFA; height: 100vh; @@ -681,5 +718,6 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro } `}
+
); } diff --git a/src/components/home/ShiftSection.tsx b/src/components/home/ShiftSection.tsx index 408527e..27e9cf4 100644 --- a/src/components/home/ShiftSection.tsx +++ b/src/components/home/ShiftSection.tsx @@ -224,12 +224,12 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
{/* Papercraft Image */} -
@@ -238,11 +238,13 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { alt={card.label} width={400} height={400} + className="shift-card-art-img" style={{ objectFit: 'contain', + objectPosition: 'bottom right', width: '100%', height: '100%', - transform: i === 2 ? 'scale(1.4)' : 'scale(1.36)', + transform: i === 2 ? 'scale(1.1)' : 'scale(1.06)', transformOrigin: 'bottom right', }} /> @@ -291,11 +293,11 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { {/* Papercraft Image */} {(cardNumber === 7 || cardNumber === 8) ? ( -
) : ( -
@@ -328,11 +331,13 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { alt={card.title} width={400} height={400} + className="shift-card-art-img" style={{ objectFit: 'contain', + objectPosition: 'bottom right', width: '100%', height: '100%', - transform: 'scale(1.3)', + transform: 'scale(1.0)', transformOrigin: 'bottom right', }} /> @@ -429,6 +434,19 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { .shift-card-7 { min-height: 220px !important; } + /* Pin card art to the bottom-right corner on narrow screens, + where full-width cards would otherwise let it sprawl over the text */ + #shift .shift-card-art { + width: 62% !important; + height: 100% !important; + right: 0 !important; + bottom: 0 !important; + } + #shift .shift-card-art-img { + object-fit: contain !important; + object-position: bottom right !important; + transform: none !important; + } } @media (max-width: 900px) { diff --git a/src/components/home/WhatWeDo.tsx b/src/components/home/WhatWeDo.tsx index 3edf39c..5406b3c 100644 --- a/src/components/home/WhatWeDo.tsx +++ b/src/components/home/WhatWeDo.tsx @@ -156,39 +156,37 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) { } ); - // Cards: single staggered timeline triggered once the row enters view + // Cards: 3D flip-up reveal — each card rises and unfolds from its bottom + // edge, then its inner content cascades in. One scroll-triggered + // timeline keeps card + content in sync. const cards = gsap.utils.toArray('.wwd-card'); const grid = document.querySelector('.wwd-grid-container'); - gsap.fromTo(cards, - { opacity: 0, y: 56, scale: 0.93, filter: 'blur(4px)' }, - { - opacity: 1, y: 0, scale: 1, filter: 'blur(0px)', - duration: 0.8, stagger: 0.12, ease: 'expo.out', - scrollTrigger: { trigger: grid, start: 'top 82%', toggleActions: 'play none none reset' }, - } + const revealTl = gsap.timeline({ + scrollTrigger: { trigger: grid, start: 'top 82%', toggleActions: 'play none none reset' }, + }); + + revealTl.fromTo(cards, + { opacity: 0, y: 90, rotationX: -32, transformOrigin: '50% 100%', transformPerspective: 1100 }, + { opacity: 1, y: 0, rotationX: 0, duration: 0.9, ease: 'power4.out', stagger: 0.14 }, + 0 ); - // Cascade inner elements after cards land cards.forEach((card, i) => { - const tl = gsap.timeline({ delay: i * 0.12 + 0.25 }); - - tl.fromTo(card.querySelector('.wwd-icon'), - { opacity: 0, scale: 0.5, rotate: -15, filter: 'blur(3px)' }, - { opacity: 1, scale: 1, rotate: 0, filter: 'blur(0px)', duration: 0.55, ease: 'back.out(2)' } - ) - .fromTo(card.querySelector('.wwd-title'), - { opacity: 0, y: 14 }, - { opacity: 1, y: 0, duration: 0.4, ease: 'power3.out' }, '-=0.2' - ) - .fromTo(card.querySelector('.wwd-tagline'), - { opacity: 0, y: 10 }, - { opacity: 1, y: 0, duration: 0.38, ease: 'power3.out' }, '-=0.2' - ) - .fromTo(card.querySelector('.wwd-detail'), - { opacity: 0, y: 8 }, - { opacity: 1, y: 0, duration: 0.35, ease: 'power3.out' }, '-=0.18' - ); + const at = i * 0.14 + 0.35; + revealTl + .fromTo(card.querySelector('.wwd-icon'), + { opacity: 0, scale: 0.4, rotate: -20 }, + { opacity: 1, scale: 1, rotate: 0, duration: 0.5, ease: 'back.out(2.2)' }, at) + .fromTo(card.querySelector('.wwd-title'), + { opacity: 0, y: 16 }, + { opacity: 1, y: 0, duration: 0.45, ease: 'power3.out' }, at + 0.12) + .fromTo(card.querySelector('.wwd-tagline'), + { opacity: 0, y: 12 }, + { opacity: 1, y: 0, duration: 0.4, ease: 'power3.out' }, at + 0.22) + .fromTo(card.querySelector('.wwd-detail'), + { opacity: 0, y: 10 }, + { opacity: 1, y: 0, duration: 0.4, ease: 'power3.out' }, at + 0.3); }); }, containerRef); @@ -207,7 +205,7 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) { zIndex: 2, }} > - + {/* Header */}
@@ -224,6 +222,7 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) { fontWeight: 800, color: '#fff', lineHeight: 0.98, letterSpacing: '-0.04em', fontFamily: 'Sora, sans-serif', + textShadow: '0 4px 24px rgba(0,0,0,0.55), 0 1px 3px rgba(0,0,0,0.6)', }}> Every visit,
a loyal customer. @@ -242,8 +241,6 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {