'use client'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import AnimatedDarkBg from './AnimatedDarkBg'; if (typeof window !== 'undefined') { gsap.registerPlugin(ScrollTrigger); } const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; interface Step { step: string; id: 'earn' | 'engage' | 'spend' | 'repeat'; title: string; desc: string; } 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 Loyaly Coins 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 coin-earning offers, then walk in and shop in person.' }, { step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use Loyaly Coins and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' }, ]; const IMPACTS = [ { label: 'Conversion Rates', value: 42, suffix: '%', prefix: '+', decimals: 0 }, { label: 'Repeat Customers', value: 3.8, suffix: 'x', prefix: '+', decimals: 1 }, { label: 'Discount Optimization', value: 28, suffix: '%', prefix: '+', decimals: 0 }, { label: 'Real-Time Decisions', value: 500, suffix: 'ms', prefix: '<', decimals: 0 }, ]; function StepIcon({ id }: { id: Step['id'] }) { const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const }; if (id === 'earn') return ( ); if (id === 'engage') return ( ); if (id === 'spend') return ( ); return ( ); } export default function TheLoop() { const [active, setActive] = useState(0); const [counts, setCounts] = useState(IMPACTS.map(() => 0)); const [revealed, setRevealed] = useState(false); const sectionRef = useRef(null); const metricsRef = useRef(null); // Sequential highlight cycle (client-only → no hydration mismatch) useEffect(() => { const t = setInterval(() => setActive(a => (a + 1) % STEPS.length), 1900); return () => clearInterval(t); }, []); // Scroll-triggered reveals + count-up, all via GSAP ScrollTrigger useIsomorphicLayoutEffect(() => { if (typeof window === 'undefined') return; const ctx = gsap.context(() => { // Reveal the flow diagram cards on scroll-in ScrollTrigger.create({ trigger: '.loop-diagram', start: 'top 78%', once: true, onEnter: () => setRevealed(true), }); // Subtle parallax drift on the diagram + metrics as the section scrolls gsap.fromTo('.loop-diagram', { y: 40 }, { y: -40, ease: 'none', scrollTrigger: { trigger: '.loop-diagram', start: 'top bottom', end: 'bottom top', scrub: 1 }, }); // Count-up metrics when they scroll into view ScrollTrigger.create({ trigger: metricsRef.current, start: 'top 82%', once: true, onEnter: () => { const start = performance.now(); const dur = 1800; const tick = (now: number) => { const t = Math.min((now - start) / dur, 1); const ease = 1 - Math.pow(1 - t, 3); setCounts(IMPACTS.map(m => m.value * ease)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, }); // Metric cells stagger in gsap.fromTo('.loop-metric', { opacity: 0, y: 26 }, { opacity: 1, y: 0, duration: 0.7, stagger: 0.1, ease: 'power3.out', scrollTrigger: { trigger: metricsRef.current, start: 'top 82%', toggleActions: 'play none none reset' }, }); ScrollTrigger.refresh(); }, sectionRef); return () => ctx.revert(); }, []); return (
{/* ── Header ── */}
The Loop

A smarter
retail cycle.

Earn coins online, unlock game based deals, discover nearby stores, and redeem offline: a self reinforcing loop that keeps customers engaged and drives repeat footfall.

{/* ── Flow diagram ── */}
{/* Loop-back arc (desktop) */} {/* Center "continuous" label on the arc */}
↻ Continuous loop
{/* Steps row */}
{STEPS.map((s, i) => { const isActive = i === active; return (
{/* big ghost number */} {s.step} {/* icon */}
Step {s.step}

{s.title}

{s.desc}

{/* connector arrow between cards */} {i < STEPS.length - 1 && (
)} ); })}
{/* ── Impact metrics ── */}
Measured Impact
{IMPACTS.map((m, i) => (
{m.prefix} {counts[i].toFixed(m.decimals)} {m.suffix}
{m.label}
))}
); }