From 57e64f86b68e6d557593577e9c83d7a0301c5402 Mon Sep 17 00:00:00 2001 From: R-Bharathraj Date: Mon, 6 Jul 2026 13:21:09 +0530 Subject: [PATCH] feat: enhance landing page, for-people sections, blog main, and components including TheLoop and PhoneMockup --- src/app/blog_main/page.tsx | 344 +++++++++++------ src/app/faq/page.tsx | 8 +- src/app/page.tsx | 6 +- src/app/solutions_page/page.tsx | 2 +- src/components/Header.tsx | 40 +- src/components/PhoneWidget.tsx | 6 +- src/components/Solutions.tsx | 2 +- .../for-people/AIRecommendationsSection.tsx | 4 +- .../for-people/DiscoverDealsSection.tsx | 157 ++++---- .../for-people/EarnRewardsSection.tsx | 196 +++++----- .../for-people/ExperienceSection.tsx | 2 +- src/components/for-people/HeroSection.tsx | 48 ++- src/components/for-people/PhoneMockup.tsx | 145 ++++++++ .../for-people/SpendAnywhereSection.tsx | 6 +- src/components/home/ProblemSection.tsx | 50 +-- src/components/home/ProductsSection.tsx | 20 +- src/components/home/ShiftSection.tsx | 42 +-- src/components/home/TheLoop.tsx | 345 ++++++++++++++++-- 18 files changed, 1037 insertions(+), 386 deletions(-) create mode 100644 src/components/for-people/PhoneMockup.tsx diff --git a/src/app/blog_main/page.tsx b/src/app/blog_main/page.tsx index ea9ceca..8da9b36 100644 --- a/src/app/blog_main/page.tsx +++ b/src/app/blog_main/page.tsx @@ -17,6 +17,34 @@ function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: nu ); } +const TAG_META: Record = { + Analytics: { + color: '#f4be28', + icon: , + }, + Strategy: { + color: '#a78bfa', + icon: , + }, + Technology: { + color: '#34d399', + icon: ( + <> + + + + ), + }, + Growth: { + color: '#3b82f6', + icon: , + }, + Future: { + color: '#ec4899', + icon: , + }, +}; + const ARTICLES = [ { n: '01', tag: 'Analytics', readTime: '6 min', @@ -52,6 +80,12 @@ const ARTICLES = [ const FILTERS = ['All', 'Analytics', 'Strategy', 'Technology', 'Growth', 'Future']; +const NEWSLETTER_PERKS = [ + 'One email a month, no spam, ever', + 'Original research on footfall & retail AI', + 'Early access to new Loyaly product features', +]; + export default function BlogMainPage() { const [activeFilter, setActiveFilter] = useState('All'); const [hovered, setHovered] = useState(null); @@ -66,46 +100,82 @@ export default function BlogMainPage() { {/* ── HERO ── */}
{/* Background photo */} -
+
{/* Accent blobs */}
-
-
+
+
+ + The Loyaly Journal +
+ +

Insights for the
- Future of Retail. + Future of Retail.

-

+

Explore ideas, trends, and strategies shaping modern retail, from our team to yours.

+ + {/* Stat strip */} +
+ {[ + { val: `${ARTICLES.length}+`, lbl: 'Articles published' }, + { val: 'Weekly', lbl: 'New research' }, + { val: '5', lbl: 'Topics covered' }, + ].map((s, i) => ( +
+
{s.val}
+
{s.lbl}
+
+ ))} +
{/* ── FILTERS ── */}
-
- {FILTERS.map(f => ( - - ))} +
+ {FILTERS.map(f => { + const meta = TAG_META[f]; + const active = activeFilter === f; + return ( + + ); + })}
@@ -113,42 +183,57 @@ export default function BlogMainPage() { {featured && (
-
- {/* Left — article info */} -
-
- {featured.tag} - {featured.readTime} read - Featured -
-

{featured.title}

-

{featured.excerpt}

- - Read Article → - -
- {/* Right — visual */} -
+
+
+ {/* Left — article info */} +
+
+ + {TAG_META[featured.tag]?.icon} + {featured.tag} + + {featured.readTime} read + Featured +
+

{featured.title}

+

{featured.excerpt}

+ + Read Article → + +
+ {/* Right — visual */}
{featured.n}
+ aspectRatio: '4/3', borderRadius: 20, position: 'relative', zIndex: 2, + background: `linear-gradient(150deg, ${TAG_META[featured.tag]?.color || '#f4be28'}20 0%, transparent 70%)`, + border: `1px solid ${TAG_META[featured.tag]?.color || '#f4be28'}25`, + display: 'flex', alignItems: 'center', justifyContent: 'center', + overflow: 'hidden', + }}> + {TAG_META[featured.tag]?.icon} +
{featured.n}
+
-
+
)} @@ -156,68 +241,123 @@ export default function BlogMainPage() { {/* ── ARTICLE GRID ── */}
- {rest.map((article, i) => ( - -
setHovered(i)} - onMouseLeave={() => setHovered(null)} - style={{ - padding: '40px', borderRadius: 20, - background: '#fff', - border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.3)' : 'rgba(0,0,0,0.08)'}`, - transition: 'all 0.3s cubic-bezier(0.22,1,0.36,1)', - transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)', - boxShadow: hovered === i ? '0 24px 60px rgba(0,0,0,0.1)' : '0 2px 12px rgba(0,0,0,0.04)', - cursor: 'pointer', - display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 280, - }} - > -
-
- {article.tag} - {article.readTime} + {rest.map((article, i) => { + const meta = TAG_META[article.tag]; + return ( + + +
setHovered(i)} + onMouseLeave={() => setHovered(null)} + style={{ + padding: '36px', borderRadius: 22, + background: '#fff', + border: `1.5px solid ${hovered === i ? `${meta?.color}45` : 'rgba(0,0,0,0.08)'}`, + transition: 'all 0.35s cubic-bezier(0.22,1,0.36,1)', + transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)', + boxShadow: hovered === i ? `0 24px 60px -16px ${meta?.color}30` : '0 2px 12px rgba(0,0,0,0.04)', + cursor: 'pointer', position: 'relative', overflow: 'hidden', + display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 300, + }} + > +
+
+ + {meta?.icon} + + {article.n} +
+
+ {article.tag} + + {article.readTime} read +
+

{article.title}

+

{article.excerpt}

+
+ + Read Article → +
-

{article.title}

-

{article.excerpt}

-
- - Read Article → -
- - ))} + + ); + })}
{/* ── NEWSLETTER CTA ── */} -
-
- -

- Stay ahead of retail. -

-

- Get our latest insights on AI, footfall, and the future of physical retail, delivered monthly. -

-
- - -
-
+
+
+
+
+ +
+ +
Newsletter
+

+ Stay ahead of retail. +

+

+ Get our latest insights on AI, footfall, and the future of physical retail, delivered monthly. +

+
+ {NEWSLETTER_PERKS.map((perk) => ( +
+ + + + {perk} +
+ ))} +
+
+ + +
+ + + +

No spam. Unsubscribe anytime.

+
+
+
diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx index 6c63489..c1fdef7 100644 --- a/src/app/faq/page.tsx +++ b/src/app/faq/page.tsx @@ -10,11 +10,11 @@ const FAQS = [ category: 'For Customers', color: '#f4be28', items: [ - { q: 'How do I earn LYT?', a: 'You earn LYT through daily app login (+50 LYT), completing 5,000-step walking challenges (+80 LYT), playing interactive games like Maze, Spin Wheel, Scratch Card, and Match Master, and visiting Selfie Booths at partner malls.' }, - { q: 'How much is 1 LYT worth?', a: '1 LYT = ₹1. So 284 LYT = ₹284 redeemable value at any participating offline store.' }, - { q: 'Where can I redeem my LYT?', a: 'You can redeem LYT at any participating offline store near you. Open the app, browse nearby stores, visit in person, and use your LYT or coupons at checkout.' }, + { q: 'How do I earn LYTs?', a: 'You earn LYTs through daily app login (+50 LYTs), completing 5,000-step walking challenges (+80 LYTs), playing interactive games like Maze, Spin Wheel, Scratch Card, and Match Master, and visiting Selfie Booths at partner malls.' }, + { q: 'How much is 1 LYT worth?', a: '1 LYT = ₹1. So 284 LYTs = ₹284 redeemable value at any participating offline store.' }, + { q: 'Where can I redeem my LYTs?', a: 'You can redeem LYTs at any participating offline store near you. Open the app, browse nearby stores, visit in person, and use your LYTs or coupons at checkout.' }, { q: 'What is the Battery Challenge?', a: 'The Battery Challenge is a daily surprise coupon based on your phone\'s battery percentage when you open the app. Every day brings a different discount — flat off, BOGO, festival specials, and more.' }, - { q: 'Does my LYT expire?', a: 'LYT is valid as long as your account is active. We recommend redeeming regularly to enjoy maximum savings.' }, + { q: 'Do my LYTs expire?', a: 'LYTs are valid as long as your account is active. We recommend redeeming regularly to enjoy maximum savings.' }, { q: 'How do I sign up?', a: 'Download the Loyaly app, sign up with your mobile number, and you\'re ready to start earning on day one. No credit card needed.' }, ], }, diff --git a/src/app/page.tsx b/src/app/page.tsx index 9a3349d..fb6770c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -313,14 +313,14 @@ export default function IndexPage() { className="loyaly-home relative bg-white overflow-hidden" style={{ margin: '20px', borderRadius: '25px', display: isDesktop ? 'none' : 'block' }} > -
+

Turn Every
Walk In Into a
Loyal Customer

- Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them. + Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYTs through games and daily activities, then redeem them at offline stores near them.

@@ -445,7 +445,7 @@ export default function IndexPage() { Loyal Customer

- Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them. + Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYTs through games and daily activities, then redeem them at offline stores near them.

diff --git a/src/app/solutions_page/page.tsx b/src/app/solutions_page/page.tsx index 03c7d2a..9833af1 100644 --- a/src/app/solutions_page/page.tsx +++ b/src/app/solutions_page/page.tsx @@ -54,7 +54,7 @@ const PRODUCTS = [ }, { n: '04', title: 'Identiq', subtitle: 'Consumer Loyalty Platform', color: '#f4be28', - desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYT, Loyaly’s reward token, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.', + desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYTs, Loyaly’s reward tokens, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.', capabilities: [ 'Daily login and step-challenge rewards that build a lasting habit loop', 'Game based offers like Spin Wheel, Scratch Card, and Maze Challenge', diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 871ed58..750cc2f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -29,15 +29,37 @@ export default function Header() { const pathname = usePathname(); const [mobileOpen, setMobileOpen] = useState(false); const [scrolled, setScrolled] = useState(false); + const [hidden, setHidden] = useState(false); const mobileMenuRef = useRef(null); + const lastScrollYRef = useRef(0); - // Detect scroll to show/hide shadow on header + // Detect scroll to show/hide shadow on header, and hide the header on + // scroll-down / reveal it again on scroll-up. useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 50); + lastScrollYRef.current = window.scrollY; + const onScroll = () => { + const y = window.scrollY; + setScrolled(y > 50); + + const lastY = lastScrollYRef.current; + if (y <= 80) { + setHidden(false); + } else if (y > lastY + 4) { + setHidden(true); + } else if (y < lastY - 4) { + setHidden(false); + } + lastScrollYRef.current = y; + }; window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); + // Never hide the header while the mobile nav panel is open + useEffect(() => { + if (mobileOpen) setHidden(false); + }, [mobileOpen]); + // Close mobile menu on route change useEffect(() => { setMobileOpen(false); @@ -210,17 +232,19 @@ export default function Header() { className={transparentHeader ? 'header-transparent' : ''} style={{ position: 'fixed', - top: 0, + top: isMobileViewport ? '14px' : 0, left: '20px', right: '20px', width: 'auto', zIndex: 99999999, background: transparentHeader ? 'transparent' : '#ffffff', + borderTopLeftRadius: isMobileViewport ? '24px' : '0px', + borderTopRightRadius: isMobileViewport ? '24px' : '0px', borderBottomLeftRadius: '24px', borderBottomRightRadius: '24px', - boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)'), - transition: 'background 0.3s ease, box-shadow 0.3s ease', - animation: scrolled ? 'slideDown 0.3s ease' : 'none', + boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 8px 28px rgba(23,21,18,0.12)'), + transform: hidden ? 'translateY(-130%)' : 'translateY(0)', + transition: 'transform 0.35s cubic-bezier(0.4,0,0.2,1), background 0.3s ease, box-shadow 0.3s ease', }} >
+
+ ); +} diff --git a/src/components/for-people/SpendAnywhereSection.tsx b/src/components/for-people/SpendAnywhereSection.tsx index 64ed15b..8c241fd 100644 --- a/src/components/for-people/SpendAnywhereSection.tsx +++ b/src/components/for-people/SpendAnywhereSection.tsx @@ -80,7 +80,7 @@ export default function SpendAnywhereSection() { {/* Header row */}
-
Spend04
+
Spend05

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

@@ -184,7 +184,7 @@ export default function SpendAnywhereSection() { }}> {[ { val: '10,000+', lbl: 'Partner stores' }, - { val: 'Zero', lbl: 'Expiry on LYT' }, + { val: 'Zero', lbl: 'Expiry on LYTs' }, { val: 'Universal', lbl: 'One balance, all brands' }, { val: 'Instant', lbl: 'Redeem at checkout' }, ].map((s, i) => ( diff --git a/src/components/home/ProblemSection.tsx b/src/components/home/ProblemSection.tsx index 96d0ee3..703749e 100644 --- a/src/components/home/ProblemSection.tsx +++ b/src/components/home/ProblemSection.tsx @@ -178,7 +178,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) display: 'flex', alignItems: 'center', justifyContent: 'space-between', - minHeight: '180px', + minHeight: '230px', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', position: 'relative', overflow: 'hidden', @@ -196,8 +196,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) position: 'absolute', right: '-18px', bottom: '-18px', - width: '210px', - height: '210px', + width: '250px', + height: '250px', pointerEvents: 'none', zIndex: 1, }}> @@ -210,7 +210,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) objectFit: 'contain', width: '100%', height: '100%', - transform: 'scale(2.0)', + transform: 'scale(1.85)', transformOrigin: 'center center', }} /> @@ -233,7 +233,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) display: 'flex', alignItems: 'center', justifyContent: 'space-between', - minHeight: '180px', + minHeight: '230px', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', position: 'relative', overflow: 'hidden', @@ -251,8 +251,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) position: 'absolute', right: '-18px', bottom: '-18px', - width: '210px', - height: '210px', + width: '250px', + height: '250px', pointerEvents: 'none', zIndex: 1, }}> @@ -265,7 +265,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) objectFit: 'contain', width: '100%', height: '100%', - transform: 'scale(2.0)', + transform: 'scale(1.85)', transformOrigin: 'center center', }} /> @@ -288,7 +288,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) display: 'flex', alignItems: 'center', justifyContent: 'space-between', - minHeight: '180px', + minHeight: '230px', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', position: 'relative', overflow: 'hidden', @@ -306,8 +306,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) position: 'absolute', right: '-18px', bottom: '-18px', - width: '210px', - height: '210px', + width: '250px', + height: '250px', pointerEvents: 'none', zIndex: 1, }}> @@ -320,7 +320,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) objectFit: 'contain', width: '100%', height: '100%', - transform: 'scale(2.0)', + transform: 'scale(1.85)', transformOrigin: 'center center', }} /> @@ -347,12 +347,12 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) display: 'flex', alignItems: 'center', justifyContent: 'space-between', - minHeight: '190px', + minHeight: '250px', position: 'relative', overflow: 'hidden', }} > -
+
{card.icon}

{card.title} @@ -366,16 +366,16 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) position: 'absolute', right: '-6px', bottom: '-6px', - width: '300px', - height: '180px', + width: '380px', + height: '230px', pointerEvents: 'none', zIndex: 1, }}> {card.title} {card.title} diff --git a/src/components/home/ProductsSection.tsx b/src/components/home/ProductsSection.tsx index df0e71d..4b8b0da 100644 --- a/src/components/home/ProductsSection.tsx +++ b/src/components/home/ProductsSection.tsx @@ -63,7 +63,7 @@ const PRODUCTS: Product[] = [ title: 'Identiq', subtitle: 'Consumer Loyalty Platform', description: - 'Earn LYT through daily logins and games, then redeem rewards at real offline stores.', + 'Earn LYTs through daily logins and games, then redeem rewards at real offline stores.', accent: '#f4be28', accentRgb: '244, 190, 40', tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'], @@ -283,12 +283,12 @@ function ProductVisual({ product }: { product: Product }) {

- {/* LYT balance card with progress to next tier */} + {/* LYTs balance card with progress to next tier */}
-
Reward LYT
+
Reward LYTs
284
@@ -297,7 +297,7 @@ function ProductVisual({ product }: { product: Product }) {
-
116 LYT to Platinum
+
116 LYTs to Platinum
@@ -667,10 +667,16 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro } .products-track::-webkit-scrollbar { display: none; } .products-track { scrollbar-width: none; } - .products-card { height: 560px; width: min(86vw, 460px); } + .products-card { height: auto; width: min(86vw, 460px); } .products-card { flex-direction: column !important; } - .products-card-content { flex: 1 1 auto !important; } - .products-card-visual { flex: 0 0 200px !important; border-left: none !important; border-top: 1px solid rgba(255,255,255,0.08); } + .products-card-content { flex: 0 0 auto !important; } + .products-card-visual { + flex: 0 0 auto !important; + min-height: 300px; + padding: 32px 0; + border-left: none !important; + border-top: 1px solid rgba(255,255,255,0.08); + } .products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; } } `} diff --git a/src/components/home/ShiftSection.tsx b/src/components/home/ShiftSection.tsx index 0ecfb9c..408527e 100644 --- a/src/components/home/ShiftSection.tsx +++ b/src/components/home/ShiftSection.tsx @@ -37,7 +37,7 @@ const GREEN_CARDS = [ const BLACK_CARDS = [ { title: 'Earn Daily', - description: 'Customers earn LYT through daily app activities', + description: 'Customers earn LYTs through daily app activities', icon: ( @@ -226,23 +226,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { {/* Papercraft Image */}
{card.label} @@ -263,7 +263,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { clickEffect={true} particleCount={6} > -
+
{card.icon}
@@ -295,7 +295,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { position: 'absolute', right: 0, bottom: 0, - width: cardNumber === 7 ? '58%' : '54%', + width: cardNumber === 7 ? '66%' : '62%', height: '100%', pointerEvents: 'none', zIndex: 1, @@ -303,8 +303,8 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { {card.title} {card.title} @@ -366,7 +366,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { margin: 0 auto; } .shift-bento-card { - min-height: 220px; + min-height: 270px; border-radius: 24px; padding: clamp(20px, 2.4vw, 28px); display: flex; @@ -401,7 +401,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { .shift-card-4 { grid-column: span 1; } .shift-card-5 { grid-column: span 1; } .shift-card-6 { grid-column: span 1; } - .shift-card-7 { grid-column: span 2; min-height: 240px; } + .shift-card-7 { grid-column: span 2; min-height: 300px; } .shift-card-8 { grid-column: span 1; } @media (max-width: 1023px) { diff --git a/src/components/home/TheLoop.tsx b/src/components/home/TheLoop.tsx index 035f566..4a89158 100644 --- a/src/components/home/TheLoop.tsx +++ b/src/components/home/TheLoop.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import AnimatedDarkBg from './AnimatedDarkBg'; @@ -12,6 +12,17 @@ if (typeof window !== 'undefined') { const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; +const MOBILE_QUERY = '(max-width: 768px)'; +const subscribeMobileQuery = (callback: () => void) => { + const mql = window.matchMedia(MOBILE_QUERY); + mql.addEventListener('change', callback); + return () => mql.removeEventListener('change', callback); +}; +const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches; +// Server has no viewport, so it always renders the desktop layout; the client's +// first render must match that to avoid a hydration mismatch. +const getMobileServerSnapshot = () => false; + interface Step { step: string; id: 'earn' | 'engage' | 'spend' | 'repeat'; @@ -20,10 +31,10 @@ interface Step { } const STEPS: Step[] = [ - { step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYT and unlock surprise coupons.' }, + { step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYTs and unlock surprise coupons.' }, { step: '02', id: 'engage', title: 'Unlock Deals', desc: 'Merchants create game based discount campaigns: customers play once to reveal their exclusive reward on a specific product.' }, - { step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYT-earning offers, then walk in and shop in person.' }, - { step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYT and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' }, + { step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYTs-earning offers, then walk in and shop in person.' }, + { step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYTs and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' }, ]; const IMPACTS = [ @@ -34,15 +45,16 @@ const IMPACTS = [ ]; const IMAGE_PATHS = [ - '/retail_cycle/Earn_online.png', - '/retail_cycle/unlock_deals.png', - '/retail_cycle/discover+visits.png', - '/retail_cycle/redeem+repeat.png', + '/retail_cycle/01_Earn_online.png', + '/retail_cycle/02_unlock_deals.png', + '/retail_cycle/03_discover+visits.png', + '/retail_cycle/04_redeem+repeat.png', ]; export default function TheLoop() { // Only counts uses state — everything scroll-driven is ref-mutated const [counts, setCounts] = useState(IMPACTS.map(() => 0)); + const isMobile = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot); const sectionRef = useRef(null); const pinRef = useRef(null); @@ -51,7 +63,7 @@ export default function TheLoop() { const tipRef = useRef(null); const ghostRef = useRef(null); const scrollHintRef = useRef(null); - // Per-step refs + // Per-step refs (desktop) const labelRefs = useRef<(HTMLDivElement | null)[]>([]); const titleRefs = useRef<(HTMLHeadingElement | null)[]>([]); const descRefs = useRef<(HTMLDivElement | null)[]>([]); @@ -60,10 +72,19 @@ export default function TheLoop() { const trackDotGlowRefs = useRef<(HTMLDivElement | null)[]>([]); const visualRefs = useRef<(HTMLDivElement | null)[]>([]); + // Mobile-only refs — same scroll-scrub mechanic, vertical single-column layout + const mobileSectionRef = useRef(null); + const mobilePinRef = useRef(null); + const mobileLineRef = useRef(null); + const mobileVisualRefs = useRef<(HTMLDivElement | null)[]>([]); + const mobileTextRefs = useRef<(HTMLDivElement | null)[]>([]); + const mobileDotRefs = useRef<(HTMLDivElement | null)[]>([]); + const mobileDotTextRefs = useRef<(HTMLSpanElement | null)[]>([]); + useIsomorphicLayoutEffect(() => { if (typeof window === 'undefined') return; - // Set initial active state directly in DOM + // Set initial active state directly in DOM (desktop) const applyActive = (idx: number, lineFillPct: number) => { // Growing line if (lineRef.current) @@ -131,26 +152,84 @@ export default function TheLoop() { }); }; - // Seed with step 0 + // Same progression logic, driving the vertical mobile layout + const applyActiveMobile = (idx: number, lineFillPct: number) => { + if (mobileLineRef.current) + mobileLineRef.current.style.width = `calc((100% - 20px) * ${lineFillPct / 100})`; + + STEPS.forEach((_, i) => { + const isActive = i === idx; + const isPassed = i < idx; + + if (mobileTextRefs.current[i]) + mobileTextRefs.current[i]!.style.opacity = isActive ? '1' : '0'; + + if (mobileVisualRefs.current[i]) + mobileVisualRefs.current[i]!.style.opacity = isActive ? '1' : '0'; + + const dot = mobileDotRefs.current[i]; + const txt = mobileDotTextRefs.current[i]; + + if (dot) { + if (isActive) { + dot.style.background = '#f4be28'; + dot.style.borderColor = '#f4be28'; + dot.style.transform = 'scale(1.15)'; + dot.style.boxShadow = '0 0 14px rgba(244,190,40,0.7)'; + } else if (isPassed) { + dot.style.background = 'rgba(244,190,40,0.25)'; + dot.style.borderColor = 'rgba(244,190,40,0.5)'; + dot.style.transform = 'scale(1)'; + dot.style.boxShadow = 'none'; + } else { + dot.style.background = 'transparent'; + dot.style.borderColor = 'rgba(244,190,40,0.2)'; + dot.style.transform = 'scale(1)'; + dot.style.boxShadow = 'none'; + } + } + if (txt) { + txt.style.color = isActive ? '#1a1200' : isPassed ? 'rgba(244,190,40,0.7)' : 'rgba(244,190,40,0.22)'; + } + }); + }; + + // Seed initial state for whichever layout is active applyActive(0, 0); + applyActiveMobile(0, 0); const ctx = gsap.context(() => { const totalSteps = STEPS.length; - ScrollTrigger.create({ - trigger: sectionRef.current, - start: 'top top', - end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`, - pin: pinRef.current, - scrub: true, // No smoothing lag — direct 1:1 with scroll position - onUpdate: (self) => { - const p = self.progress; - const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1); - // Fill goes from 0 to 100% as progress goes 0→(n-1)/n - const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100; - applyActive(idx, lineFillPct); - }, - }); + if (isMobile) { + ScrollTrigger.create({ + trigger: mobileSectionRef.current, + start: 'top top', + end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.5}`, + pin: mobilePinRef.current, + scrub: true, + onUpdate: (self) => { + const p = self.progress; + const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1); + const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100; + applyActiveMobile(idx, lineFillPct); + }, + }); + } else { + ScrollTrigger.create({ + trigger: sectionRef.current, + start: 'top top', + end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`, + pin: pinRef.current, + scrub: true, // No smoothing lag — direct 1:1 with scroll position + onUpdate: (self) => { + const p = self.progress; + const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1); + const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100; + applyActive(idx, lineFillPct); + }, + }); + } // Count-up metrics ScrollTrigger.create({ @@ -179,10 +258,13 @@ export default function TheLoop() { }, sectionRef); return () => ctx.revert(); - }, []); + }, [isMobile]); return ( <> + {/* ══════════════════════════════════════════ + DESKTOP / LAPTOP — pinned two-column layout + ══════════════════════════════════════════ */}
@@ -233,7 +316,7 @@ export default function TheLoop() { color: 'rgba(255,255,255,0.38)', lineHeight: 1.75, fontFamily: 'Sora, sans-serif', }}> - Earn LYT online, unlock game based deals, discover nearby stores, and redeem offline. + Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline.

@@ -372,23 +455,19 @@ export default function TheLoop() {
- {/* RIGHT: visual panel */} + {/* RIGHT: visual panel — borderless, image floats directly on the section bg */}
{/* Ghost step number — text content set via ref */}
@@ -419,7 +497,7 @@ export default function TheLoop() { alt={s.title} fill style={{ objectFit: 'contain' }} - sizes="(max-width: 768px) 100vw, 44vw" + sizes="(max-width: 768px) 100vw, 50vw" priority={i === 0} />
@@ -446,6 +524,189 @@ export default function TheLoop() {
+
+ + {/* ══════════════════════════════════════════ + MOBILE — pinned single-column layout: big + image on top, one legible step at a time, + horizontal progress tracker below + ══════════════════════════════════════════ */} +
+ + +
+ {/* Header */} +
+

+ A smarter{' '} + retail cycle. +

+

+ Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline. +

+
+ + {/* Visual panel — borderless, image floats directly on the section bg */} +
+
+ {STEPS.map((s, i) => ( +
{ mobileVisualRefs.current[i] = el; }} + style={{ + position: 'absolute', inset: 0, + display: 'flex', alignItems: 'center', justifyContent: 'center', + opacity: i === 0 ? 1 : 0, + transition: 'opacity 0.5s ease', + }} + > +
+ {s.title} +
+
+ ))} +
+ + {/* Step text — one clear step at a time, cross-faded */} +
+ {STEPS.map((s, i) => ( +
{ mobileTextRefs.current[i] = el; }} + style={{ + position: 'absolute', inset: 0, + opacity: i === 0 ? 1 : 0, + transition: 'opacity 0.4s ease', + textAlign: 'center', + }} + > +
+ Step {s.step} +
+

+ {s.title} +

+

+ {s.desc} +

+
+ ))} +
+ + {/* Horizontal progress tracker */} +
+
+
+
+ {STEPS.map((s, i) => ( +
{ mobileDotRefs.current[i] = el; }} + style={{ + width: 24, height: 24, borderRadius: '50%', + display: 'flex', alignItems: 'center', justifyContent: 'center', + background: i === 0 ? '#f4be28' : 'transparent', + border: `1.5px solid ${i === 0 ? '#f4be28' : 'rgba(244,190,40,0.2)'}`, + transition: 'background 0.3s ease, border-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease', + }} + > + { mobileDotTextRefs.current[i] = el; }} + style={{ + fontSize: 9, fontWeight: 800, lineHeight: 1, + fontFamily: 'Sora, sans-serif', letterSpacing: '0.04em', + color: i === 0 ? '#1a1200' : 'rgba(244,190,40,0.22)', + transition: 'color 0.3s ease', + userSelect: 'none', + }} + > + {String(i + 1).padStart(2, '0')} + +
+ ))} +
+
+
+
- {/* Impact metrics — outside section to avoid overflow:hidden clipping */}