Cleanup old PHP source files and update Next.js pages and components

This commit is contained in:
R-Bharathraj
2026-06-30 17:02:35 +05:30
parent 7175adb0db
commit 7cf5035c89
1223 changed files with 438 additions and 168007 deletions

View File

@@ -61,7 +61,7 @@ export default function Footer() {
return (
<>
<footer style={{
<footer className="ly-footer" style={{
background: 'linear-gradient(to top, #3a2800 0%, #1e1600 18%, #0e0b04 36%, #07070d 58%)',
color: '#ffffff',
position: 'relative',
@@ -71,7 +71,7 @@ export default function Footer() {
}}>
{/* ── TOP CTA STRIP ── */}
<div style={{
<div className="ly-footer-cta" style={{
padding: '72px 64px 64px',
borderBottom: '1px solid rgba(255,255,255,0.06)',
display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
@@ -83,15 +83,15 @@ export default function Footer() {
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 20, display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ width: 24, height: 1, background: '#f4be28', display: 'block' }} />Let's build together
</div>
<h2 style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif' }}>
<h2 className="ly-footer-heading" style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif' }}>
Turn every visit<br />
<span style={{ color: '#f4be28' }}>into value.</span>
</h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }}>
<Link href="/contacts" style={{
display: 'inline-flex', alignItems: 'center', gap: 10,
<div className="ly-footer-cta-actions" style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }}>
<Link href="/contacts" className="ly-footer-cta-btn" style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 10,
padding: '18px 40px', background: '#f4be28', color: '#111',
fontSize: 16, fontWeight: 800, borderRadius: 100, textDecoration: 'none',
fontFamily: 'Sora, sans-serif', boxShadow: '0 16px 48px rgba(244,190,40,0.25)',
@@ -112,14 +112,14 @@ export default function Footer() {
</div>
{/* ── MAIN COLUMNS ── */}
<div style={{
<div className="ly-footer-columns" style={{
display: 'grid',
gridTemplateColumns: '1.6fr 1fr 1fr 1fr',
gap: '48px', padding: '64px 64px 48px',
alignItems: 'start',
}}>
{/* Brand */}
<div>
<div className="ly-footer-brand">
<Link href="/" aria-label="Loyaly.AI home">
<Image src="/images/logo-dark.png" alt="Loyaly.AI" width={560} height={160} style={{ height: 140, width: 'auto', marginBottom: 24, display: 'block' }} />
</Link>
@@ -145,7 +145,7 @@ export default function Footer() {
{/* Nav columns */}
{NAV.map(col => (
<nav key={col.title} aria-label={col.title}>
<nav key={col.title} className="ly-footer-nav" aria-label={col.title}>
<h5 style={{ margin: '0 0 20px', fontSize: 12, fontWeight: 700, color: 'rgba(255,255,255,0.3)', letterSpacing: '0.16em', textTransform: 'uppercase', fontFamily: 'Sora, sans-serif' }}>
{col.title}
</h5>
@@ -164,7 +164,7 @@ export default function Footer() {
</div>
{/* ── BOTTOM BAR ── */}
<div style={{
<div className="ly-footer-bottom" style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexWrap: 'wrap', gap: 12,
padding: '20px 64px 36px',
@@ -181,6 +181,25 @@ export default function Footer() {
))}
</div>
</div>
<style>{`
@media (max-width: 900px) {
.ly-footer { margin: 0 12px 12px; border-radius: 0 0 20px 20px; }
.ly-footer-cta { padding: 48px 28px 40px !important; align-items: flex-start !important; }
.ly-footer-cta-actions { align-items: flex-start !important; width: 100%; }
.ly-footer-cta-btn { width: 100%; padding: 16px 28px !important; }
.ly-footer-columns { grid-template-columns: 1fr 1fr !important; gap: 36px 24px !important; padding: 48px 28px 36px !important; }
.ly-footer-brand { grid-column: 1 / -1; }
.ly-footer-bottom { padding: 20px 28px 28px !important; flex-direction: column !important; align-items: flex-start !important; }
}
@media (max-width: 560px) {
.ly-footer-heading { font-size: clamp(30px, 9vw, 44px) !important; }
.ly-footer-columns { grid-template-columns: 1fr !important; gap: 32px !important; }
.ly-footer-brand img { height: 110px !important; }
.ly-footer-bottom { gap: 16px !important; }
.ly-footer-bottom > div { gap: 18px !important; }
}
`}</style>
</footer>
{/* Scroll-to-top */}

View File

@@ -79,7 +79,7 @@ export default function GetInTouch() {
}}
>
<div style={{
<div className="git-grid" style={{
position: 'relative', zIndex: 2,
maxWidth: 1200, margin: '0 auto',
padding: 'clamp(56px, 8vh, 100px) clamp(28px, 5vw, 72px)',
@@ -100,7 +100,7 @@ export default function GetInTouch() {
<h2 style={{
margin: '0 0 24px',
fontSize: 'clamp(2rem, 4vw, 3.4rem)', fontWeight: 900,
fontSize: 'clamp(2rem, 4vw, 3.4rem)', fontWeight: 800,
lineHeight: 0.98, letterSpacing: '-0.04em',
color: '#fff', fontFamily: 'Sora, sans-serif',
}}>
@@ -242,6 +242,9 @@ export default function GetInTouch() {
color: rgba(255,255,255,0.5); transition: all 0.2s ease;
}
.git-social:hover { background: rgba(244,190,40,0.12); border-color: rgba(244,190,40,0.3); color: #f4be28; }
@media (max-width: 420px) {
.git-grid { grid-template-columns: 1fr !important; }
}
`}</style>
</section>
);

View File

@@ -14,7 +14,7 @@ export default function MapSection() {
overflow: 'hidden',
background: '#fff',
}}>
<div style={{
<div className="map-section-grid" style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
alignItems: 'stretch',
@@ -37,7 +37,7 @@ export default function MapSection() {
</div>
<h2 style={{
fontSize: 'clamp(32px, 4vw, 56px)', fontWeight: 900,
fontSize: 'clamp(32px, 4vw, 56px)', fontWeight: 800,
letterSpacing: '-0.04em', lineHeight: 1.0,
color: '#0a0a0f', margin: '0 0 14px', fontFamily: 'Sora, sans-serif',
}}>

View File

@@ -35,7 +35,7 @@ export default function AIRecommendationsSection() {
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence · 05</span></div>
<h2 style={{
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.05em', lineHeight: 0.94,
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.94,
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Built for</span></span>

View File

@@ -35,7 +35,7 @@ export default function DiscoverDealsSection() {
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover · 02</span></div>
<h2 style={{
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 900, lineHeight: 0.94,
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94,
letterSpacing: '-0.05em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Every</span></span>

View File

@@ -73,7 +73,7 @@ export default function EarnRewardsSection() {
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn · 03</span></div>
<h2 style={{
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 900, lineHeight: 1.08,
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08,
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Every step.</span></span>

View File

@@ -65,7 +65,7 @@ export default function FinalCTASection() {
{/* Headline */}
<h2 style={{
fontSize: 'clamp(48px, 7.5vw, 112px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.92,
fontSize: 'clamp(48px, 7.5vw, 112px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.92,
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Your rewards</span></span>

View File

@@ -61,10 +61,6 @@ export default function HeroSection() {
{/* Text column */}
<div style={{ maxWidth: 720 }}>
<div className="fp-content fp-eyebrow" style={{ marginBottom: 26, display: 'flex', justifyContent: 'center' }}>
<span className="fp-kicker">For People · By Loyaly</span>
</div>
<h1 className="fp-headline" style={{
fontSize: 'clamp(40px, 5.4vw, 82px)', fontWeight: 800, lineHeight: 1.02,
letterSpacing: '-0.045em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',

View File

@@ -30,7 +30,7 @@ export default function SpendAnywhereSection() {
<div>
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend · 04</span></div>
<h2 style={{
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.9,
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9,
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Spend it</span></span>

View File

@@ -105,7 +105,7 @@ export default function WhyLoyalySection() {
</div>
<h2 style={{
fontSize: 'clamp(30px, 3.6vw, 50px)', fontWeight: 900, lineHeight: 1.08,
fontSize: 'clamp(30px, 3.6vw, 50px)', fontWeight: 800, lineHeight: 1.08,
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Millions shop offline.</span></span>

View File

@@ -234,8 +234,7 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
scrollTrigger: { trigger: '.ps-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' },
});
bottomTl
.fromTo('.ps-bottom-bar', { opacity: 0, y: 36, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: 'power3.out' })
.fromTo('.ps-feature-item', { opacity: 0, y: 16 }, { opacity: 1, y: 0, stagger: 0.09, duration: 0.5, ease: 'power3.out' }, '-=0.5');
.fromTo('.ps-feature-item', { opacity: 0, y: 36, scale: 0.88 }, { opacity: 1, y: 0, scale: 1, stagger: 0.1, duration: 0.65, ease: 'back.out(1.7)' });
ScrollTrigger.refresh();
}, sectionRef);
@@ -263,7 +262,7 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#f4be28', display: 'block' }} />
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Sora, sans-serif' }}>The Opportunity</span>
</div>
<h2 className="ps-title" style={{ color: '#171512', fontWeight: 900, fontSize: 'clamp(2.4rem, 6vw, 4.8rem)', letterSpacing: '-0.05em', lineHeight: 0.94, margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
<h2 className="ps-title" style={{ color: '#171512', fontWeight: 800, fontSize: 'clamp(2.4rem, 6vw, 4.8rem)', letterSpacing: '-0.05em', lineHeight: 0.94, margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
Retail is busy.<br /><span style={{ color: '#f4be28' }}>But blind.</span>
</h2>
<p className="ps-subtitle" style={{ color: '#4f463a', fontSize: 'clamp(1rem, 1.3vw, 1.2rem)', fontWeight: 500, maxWidth: 620, margin: '0 auto', lineHeight: 1.7, fontFamily: 'Sora, sans-serif' }}>
@@ -305,26 +304,43 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
</div>
{/* Bottom features bar */}
<div className="ps-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(40px, 5vw, 72px)', background: '#fff', borderRadius: 20, border: '1px solid rgba(0,0,0,0.05)', boxShadow: '0 20px 50px -30px rgba(23,21,18,0.2)', padding: 'clamp(16px, 1.6vw, 22px) clamp(20px, 3vw, 44px)', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
{BOTTOM_FEATURES.map((f, i) => (
<React.Fragment key={f.label}>
<div className="ps-feature-item" style={{ flex: '1 1 180px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 11, cursor: 'default' }}>
<span style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 10, background: '#faf9f5', border: '1px solid rgba(0,0,0,0.05)' }}>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2">{f.icon}</svg>
</span>
<span style={{ color: '#171512', fontWeight: 700, fontSize: '0.95rem', fontFamily: 'Sora, sans-serif' }}>{f.label}</span>
</div>
{i < BOTTOM_FEATURES.length - 1 && <span className="ps-sep" style={{ width: 1, height: 28, background: 'rgba(0,0,0,0.07)' }} />}
</React.Fragment>
<div className="ps-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(40px, 5vw, 72px)', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 'clamp(12px, 1.6vw, 18px)' }}>
{BOTTOM_FEATURES.map((f) => (
<div
key={f.label}
className="ps-feature-item"
style={{
display: 'flex', alignItems: 'center', gap: 14,
background: '#fff', borderRadius: 18,
border: '1px solid rgba(0,0,0,0.06)',
boxShadow: '0 1px 2px rgba(0,0,0,0.03), 0 16px 36px -22px rgba(23,21,18,0.22)',
padding: 'clamp(16px, 1.6vw, 20px) clamp(16px, 2vw, 22px)',
cursor: 'default',
transition: 'transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease',
}}
>
<span style={{
width: 44, height: 44, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderRadius: 13, background: 'linear-gradient(145deg,#fff8e8 0%,#fff2c8 100%)',
border: '1px solid rgba(244,190,40,0.25)',
}}>
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#bc7122" strokeWidth="2">{f.icon}</svg>
</span>
<span style={{ color: '#171512', fontWeight: 700, fontSize: '0.95rem', fontFamily: 'Sora, sans-serif', lineHeight: 1.3 }}>{f.label}</span>
</div>
))}
</div>
{/* Responsive: stack cards on narrow screens, rotate connector */}
<style>{`
#problems-shift .ps-feature-item:hover {
transform: translateY(-4px);
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 24px 48px -22px rgba(23,21,18,0.3);
border-color: rgba(244,190,40,0.35);
}
@media (max-width: 900px) {
#problems-shift .ps-cards-row { flex-direction: column; }
#problems-shift .ps-connector-arrow { transform: rotate(90deg); }
#problems-shift .ps-sep { display: none; }
}
`}</style>
</section>

View File

@@ -275,7 +275,7 @@ function ProductCard({ product, index }: { product: Product; index: number }) {
<h3 style={{
margin: 0,
fontSize: 'clamp(1.7rem, 2.8vw, 3rem)',
fontWeight: 900, lineHeight: 0.98, letterSpacing: '-0.04em',
fontWeight: 800, lineHeight: 0.98, letterSpacing: '-0.04em',
color: '#ffffff', fontFamily: 'Sora, sans-serif',
}}>{product.title}</h3>
@@ -410,7 +410,7 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
<span style={{ width: 32, height: 2, background: '#f4be28', display: 'block', borderRadius: 2 }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Sora, sans-serif' }}>Platform</span>
</div>
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 900, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 800, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
Four products.<br />
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One ecosystem.</span>
</h2>

View File

@@ -155,7 +155,7 @@ export default function TheLoop() {
<h2 style={{
margin: '0 0 18px',
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
fontWeight: 900, color: '#fff',
fontWeight: 800, color: '#fff',
lineHeight: 0.92, letterSpacing: '-0.05em',
fontFamily: 'Sora, sans-serif',
}}>
@@ -222,6 +222,7 @@ export default function TheLoop() {
style={{
flex: 1,
position: 'relative',
zIndex: 1,
padding: 'clamp(26px, 2.2vw, 38px) clamp(20px, 1.8vw, 28px)',
borderRadius: 26,
background: isActive ? 'rgba(244,190,40,0.07)' : 'rgba(255,255,255,0.025)',
@@ -276,14 +277,15 @@ export default function TheLoop() {
width: 'clamp(28px, 3vw, 56px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
position: 'relative',
zIndex: 0,
}}>
<div style={{
<div className="loop-connector-line" style={{
position: 'absolute', left: 0, right: 0, height: 2, borderRadius: 2,
background: 'linear-gradient(90deg, rgba(244,190,40,0.1), rgba(244,190,40,0.45), rgba(244,190,40,0.1))',
backgroundSize: '200% 100%',
animation: revealed ? 'loopFlow 1.8s linear infinite' : 'none',
}} />
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'relative', zIndex: 2, opacity: 0.8 }}>
<svg className="loop-connector-arrow" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'relative', zIndex: 2, opacity: 0.8 }}>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</div>
@@ -330,9 +332,25 @@ export default function TheLoop() {
@keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
@keyframes loopDash { to { stroke-dashoffset: -32; } }
@keyframes loopFlow { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
@keyframes loopFlowVertical { 0%{background-position:0 200%} 100%{background-position:0 -200%} }
@keyframes loopArrowBounceH { 0%,100% { transform: translateX(0); } 50% { transform: translateX(5px); } }
@keyframes loopArrowBounceV { 0%,100% { transform: rotate(90deg) translateX(0); } 50% { transform: rotate(90deg) translateX(5px); } }
.loop-connector-arrow { animation: loopArrowBounceH 1.4s ease-in-out infinite; }
@media (max-width: 880px) {
.loop-row { flex-direction: column; }
.loop-connector { width: 100% !important; height: 40px; transform: rotate(90deg); }
.loop-card { z-index: 1; }
.loop-connector {
width: 100% !important; height: 40px; z-index: 0;
}
.loop-connector-line {
left: 50% !important; right: auto !important; top: 0 !important; bottom: 0 !important;
width: 2px !important; height: 100% !important;
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(244,190,40,0.1), rgba(244,190,40,0.45), rgba(244,190,40,0.1)) !important;
background-size: 100% 200% !important;
animation-name: loopFlowVertical !important;
}
.loop-connector-arrow { animation: loopArrowBounceV 1.4s ease-in-out infinite; }
.loop-arc, .loop-arc-label { display: none !important; }
}
@media (prefers-reduced-motion: reduce) {

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
@@ -12,93 +12,204 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface CardProps {
interface Stage {
num: string;
title: string;
description: string;
icon: React.ReactNode;
cardRef: React.RefObject<HTMLDivElement | null>;
orderClass: string;
}
function StageCard({ num, title, description, icon, cardRef, orderClass }: CardProps) {
const STAGES: Stage[] = [
{
num: '01',
title: 'Understand',
description: 'Track store footfall, customer behavior, and real-time engagement with AI — giving merchants the intelligence to run smarter campaigns and targeted offers that actually convert.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
),
},
{
num: '02',
title: 'Engage',
description: 'Customers earn Loyaly Coins through daily login, 5,000-step walking challenges, and interactive games — Maze, Spin Wheel, Scratch Card, and Match Master — keeping them engaged every day.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
),
},
{
num: '03',
title: 'Convert',
description: 'Merchants create game-based discount campaigns where customers play once to reveal their reward — turning product discovery into an exciting, high-conversion in-store experience.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
},
{
num: '04',
title: 'Retain',
description: 'Daily rewards, surprise coupons, and game-based offers keep customers returning to earn, play, and redeem — creating a self-reinforcing loyalty loop that compounds value with every visit.',
icon: (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
),
},
];
function StageCard({
stage,
index,
active,
onHover,
}: {
stage: Stage;
index: number;
active: boolean;
onHover: (i: number | null) => void;
}) {
const cardRef = useRef<HTMLDivElement>(null);
const onMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const el = cardRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width - 0.5;
const py = (e.clientY - r.top) / r.height - 0.5;
el.style.setProperty('--rx', `${(-py * 6).toFixed(2)}deg`);
el.style.setProperty('--ry', `${(px * 8).toFixed(2)}deg`);
el.style.setProperty('--mx', `${((px + 0.5) * 100).toFixed(1)}%`);
el.style.setProperty('--my', `${((py + 0.5) * 100).toFixed(1)}%`);
};
const onMouseLeave = () => {
const el = cardRef.current;
if (!el) return;
el.style.setProperty('--rx', '0deg');
el.style.setProperty('--ry', '0deg');
onHover(null);
};
return (
<div
ref={cardRef}
className={`wwd-card relative group ${orderClass}`}
data-stage-card={index}
className={`wwd-card relative group ${active ? 'wwd-card-active' : ''}`}
onMouseMove={onMouseMove}
onMouseEnter={() => onHover(index)}
onMouseLeave={onMouseLeave}
style={{
background: 'rgba(255,255,255,0.025)',
backdropFilter: 'blur(24px) saturate(160%)',
WebkitBackdropFilter: 'blur(24px) saturate(160%)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: '28px',
padding: 'clamp(30px, 3vw, 48px)',
transition: 'transform 0.5s cubic-bezier(0.22,1,0.36,1), box-shadow 0.5s ease, border-color 0.5s ease',
cursor: 'default',
overflow: 'hidden',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(-8px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 32px 80px rgba(244, 190, 40,0.12), 0 0 0 1px rgba(244, 190, 40,0.25), inset 0 1px 0 rgba(255,255,255,0.08)';
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(244, 190, 40,0.3)';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
(e.currentTarget as HTMLElement).style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05)';
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.08)';
position: 'relative',
transform: 'perspective(1100px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(var(--cascade, 0px))',
transformStyle: 'preserve-3d',
transition: 'transform 0.45s cubic-bezier(0.22,1,0.36,1), box-shadow 0.45s ease, border-color 0.45s ease',
}}
>
{/* Giant editorial number watermark */}
<div style={{
position: 'absolute', top: '-24px', right: '8px',
fontSize: 'clamp(8rem, 11vw, 13rem)', fontWeight: 900, lineHeight: 1,
color: 'transparent',
WebkitTextStroke: '1px rgba(244, 190, 40,0.06)',
fontFamily: 'Sora, sans-serif',
letterSpacing: '-0.06em',
userSelect: 'none', pointerEvents: 'none',
}}>
{num}
<div
className="wwd-card-inner"
style={{
position: 'relative',
background: 'rgba(255,255,255,0.025)',
backdropFilter: 'blur(24px) saturate(160%)',
WebkitBackdropFilter: 'blur(24px) saturate(160%)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 26,
padding: 'clamp(28px, 2.4vw, 38px)',
overflow: 'hidden',
cursor: 'default',
height: '100%',
boxShadow: active
? '0 32px 70px -20px rgba(244,190,40,0.22), 0 0 0 1px rgba(244,190,40,0.3), inset 0 1px 0 rgba(255,255,255,0.08)'
: 'inset 0 1px 0 rgba(255,255,255,0.05)',
borderColor: active ? 'rgba(244,190,40,0.32)' : 'rgba(255,255,255,0.08)',
}}
>
{/* Cursor-following glow */}
<div
className="wwd-card-glow"
style={{
position: 'absolute', inset: 0, pointerEvents: 'none', opacity: active ? 1 : 0,
background: 'radial-gradient(280px circle at var(--mx,50%) var(--my,50%), rgba(244,190,40,0.12), transparent 70%)',
transition: 'opacity 0.4s ease',
}}
/>
{/* Giant editorial number, bleeding off the top edge */}
<div style={{
position: 'absolute', top: '-0.32em', right: '-0.06em',
fontSize: 'clamp(6rem, 8vw, 9rem)', fontWeight: 900, lineHeight: 1,
color: 'transparent',
WebkitTextStroke: active ? '1px rgba(244,190,40,0.18)' : '1px rgba(244,190,40,0.07)',
fontFamily: 'Sora, sans-serif',
letterSpacing: '-0.06em',
userSelect: 'none', pointerEvents: 'none',
transition: 'all 0.45s ease',
}}>
{stage.num}
</div>
{/* Step rail dot */}
<div style={{
display: 'flex', alignItems: 'center', gap: 10, marginBottom: 22,
position: 'relative', zIndex: 2,
}}>
<span style={{
fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase',
color: active ? '#f4be28' : 'rgba(255,255,255,0.32)',
fontFamily: 'Sora, sans-serif', transition: 'color 0.4s ease',
}}>
Stage {stage.num}
</span>
<span style={{ flex: 1, height: 1, background: active ? 'rgba(244,190,40,0.35)' : 'rgba(255,255,255,0.08)', transition: 'background 0.4s ease' }} />
</div>
{/* Icon */}
<div style={{
width: 54, height: 54,
borderRadius: 16,
background: active ? 'rgba(244,190,40,0.16)' : 'rgba(244,190,40,0.1)',
border: `1px solid rgba(244,190,40,${active ? 0.4 : 0.22})`,
color: '#f4be28',
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: 28,
position: 'relative', zIndex: 2,
boxShadow: active ? '0 0 28px rgba(244,190,40,0.3), inset 0 1px 0 rgba(255,255,255,0.08)' : 'inset 0 1px 0 rgba(255,255,255,0.08)',
transform: active ? 'rotate(-6deg) scale(1.05)' : 'none',
transition: 'all 0.45s cubic-bezier(0.22,1,0.36,1)',
}}>
{stage.icon}
</div>
{/* Title */}
<h3 style={{
margin: '0 0 14px',
fontSize: 'clamp(1.4rem, 2vw, 1.85rem)',
fontWeight: 800, color: '#ffffff',
lineHeight: 1.1, letterSpacing: '-0.03em',
fontFamily: 'Sora, sans-serif',
position: 'relative', zIndex: 2,
}}>
{stage.title}
</h3>
{/* Description */}
<p style={{
margin: 0,
fontSize: '0.92rem',
color: 'rgba(255,255,255,0.45)',
lineHeight: 1.75,
fontFamily: 'Sora, sans-serif',
position: 'relative', zIndex: 2,
}}>
{stage.description}
</p>
</div>
{/* Icon */}
<div style={{
width: '56px', height: '56px',
borderRadius: '16px',
background: 'rgba(244, 190, 40,0.1)',
border: '1px solid rgba(244, 190, 40,0.22)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: '32px',
position: 'relative', zIndex: 2,
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.08)',
}}>
{icon}
</div>
{/* Title */}
<h3 style={{
margin: '0 0 16px',
fontSize: 'clamp(1.5rem, 2.2vw, 2rem)',
fontWeight: 800, color: '#ffffff',
lineHeight: 1.1, letterSpacing: '-0.03em',
fontFamily: 'Sora, sans-serif',
position: 'relative', zIndex: 2,
}}>
{title}
</h3>
{/* Description */}
<p style={{
margin: 0,
fontSize: 'clamp(0.9rem, 1vw, 1rem)',
color: 'rgba(255,255,255,0.45)',
lineHeight: 1.75, maxWidth: '360px',
fontFamily: 'Sora, sans-serif',
position: 'relative', zIndex: 2,
}}>
{description}
</p>
</div>
);
}
@@ -109,129 +220,43 @@ interface WhatWeDoProps {
export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
const containerRef = useRef<HTMLDivElement>(null);
const gridContainerRef = useRef<HTMLDivElement>(null);
const card1Ref = useRef<HTMLDivElement>(null);
const card2Ref = useRef<HTMLDivElement>(null);
const card3Ref = useRef<HTMLDivElement>(null);
const card4Ref = useRef<HTMLDivElement>(null);
const [active, setActive] = useState<number | null>(null);
const [points, setPoints] = useState({
p1: { x: 0, y: 0 },
p2: { x: 0, y: 0 },
p3: { x: 0, y: 0 },
p4: { x: 0, y: 0 }
});
const [hasMeasured, setHasMeasured] = useState(false);
const updatePoints = useCallback(() => {
if (!gridContainerRef.current || !card1Ref.current || !card2Ref.current || !card3Ref.current || !card4Ref.current) return;
const containerRect = gridContainerRef.current.getBoundingClientRect();
const r1 = card1Ref.current.getBoundingClientRect();
const r2 = card2Ref.current.getBoundingClientRect();
const r3 = card3Ref.current.getBoundingClientRect();
const r4 = card4Ref.current.getBoundingClientRect();
// Check that width is non-zero to verify the elements have rendered and have dimensions
if (r1.width === 0 || r2.width === 0 || r3.width === 0 || r4.width === 0) return;
setPoints({
p1: {
x: r1.left - containerRect.left + r1.width / 2,
y: r1.top - containerRect.top + r1.height / 2
},
p2: {
x: r2.left - containerRect.left + r2.width / 2,
y: r2.top - containerRect.top + r2.height / 2
},
p3: {
x: r3.left - containerRect.left + r3.width / 2,
y: r3.top - containerRect.top + r3.height / 2
},
p4: {
x: r4.left - containerRect.left + r4.width / 2,
y: r4.top - containerRect.top + r4.height / 2
}
});
setHasMeasured(true);
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
// Use ResizeObserver for highly responsive layout tracking
const observer = new ResizeObserver(() => {
updatePoints();
});
if (gridContainerRef.current) observer.observe(gridContainerRef.current);
if (card1Ref.current) observer.observe(card1Ref.current);
if (card2Ref.current) observer.observe(card2Ref.current);
if (card3Ref.current) observer.observe(card3Ref.current);
if (card4Ref.current) observer.observe(card4Ref.current);
// Initial calculation
updatePoints();
// Secondary delay to ensure clean alignment post-font/css loading
const timer = setTimeout(updatePoints, 200);
window.addEventListener('resize', updatePoints);
return () => {
observer.disconnect();
window.removeEventListener('resize', updatePoints);
clearTimeout(timer);
};
}, [updatePoints]);
// GSAP animations for stagger entrances on entering viewport
useIsomorphicLayoutEffect(() => {
if (!isLoaded) return;
if (typeof window === 'undefined') return;
const ctx = gsap.context(() => {
// Header Animation
gsap.fromTo(
'.wwd-header-el',
{ opacity: 0, y: 40 },
{
opacity: 1,
y: 0,
duration: DEFAULT_DURATION,
stagger: 0.15,
ease: DEFAULT_EASE,
scrollTrigger: {
trigger: '.wwd-header',
start: 'top 85%',
toggleActions: 'play none none reset'
}
opacity: 1, y: 0, duration: DEFAULT_DURATION, stagger: 0.15, ease: DEFAULT_EASE,
scrollTrigger: { trigger: '.wwd-header', start: 'top 85%', toggleActions: 'play none none reset' },
}
);
// Card Stagger Animation
gsap.fromTo(
'.wwd-card',
{ opacity: 0, y: 60 },
{ opacity: 0, y: 70, scale: 0.94 },
{
opacity: 1,
y: 0,
duration: DEFAULT_DURATION,
stagger: 0.15,
ease: DEFAULT_EASE,
onComplete: updatePoints,
scrollTrigger: {
trigger: '.wwd-grid-container',
start: 'top 80%',
toggleActions: 'play none none reset'
}
opacity: 1, y: 0, scale: 1, duration: 0.9, stagger: 0.12, ease: 'power3.out',
scrollTrigger: { trigger: '.wwd-grid-container', start: 'top 80%', toggleActions: 'play none none reset' },
}
);
gsap.fromTo(
'.wwd-rail-step',
{ opacity: 0, y: 14 },
{
opacity: 1, y: 0, duration: 0.5, stagger: 0.08, ease: 'power3.out',
scrollTrigger: { trigger: '.wwd-rail', start: 'top 85%', toggleActions: 'play none none reset' },
}
);
}, containerRef);
return () => ctx.revert();
}, [isLoaded, updatePoints]);
}, [isLoaded]);
return (
<section
@@ -241,136 +266,83 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
style={{
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: '32px',
borderRadius: 32,
zIndex: 2,
}}
>
{/* Animated dark background (replaces video) */}
<AnimatedDarkBg variant="amber" />
{/* Header Container */}
<div className="wwd-header relative z-10 text-center max-w-4xl mx-auto mb-24 sm:mb-28 md:mb-32 lg:mb-36 flex flex-col items-center">
{/* Pill Badge */}
{/* Header */}
<div className="wwd-header relative z-10 text-center max-w-4xl mx-auto mb-16 sm:mb-20 flex flex-col items-center">
<div className="wwd-header-el inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-white/10 bg-white/5 text-[#f4be28] text-xs font-bold tracking-wider uppercase mb-5">
<span className="w-1.5 h-1.5 rounded-full bg-[#f4be28] animate-pulse" />
What We Do
</div>
{/* Main Heading */}
<h2 className="wwd-header-el text-white font-black mb-6" style={{ fontSize: 'clamp(2.8rem, 6vw, 5.5rem)', letterSpacing: '-0.05em', lineHeight: 0.9, fontFamily: 'Sora, sans-serif' }}>
Understand.<br />
<span className="text-[#f4be28]">Engage. Convert.</span><br />
Retain.
</h2>
{/* Subtitle */}
<p className="wwd-header-el text-gray-400 text-base sm:text-lg max-w-xl font-medium" style={{ fontFamily: 'Sora, sans-serif', lineHeight: 1.75 }}>
Four stages. One platform. A smarter retail cycle that turns every walk-in into a loyal customer.
</p>
</div>
{/* Grid and Paths Container */}
<div ref={gridContainerRef} className="relative z-10 wwd-grid-container max-w-6xl mx-auto">
{/* 2 x 2 Responsive Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 lg:gap-24 relative z-20 px-2 md:px-0">
{/* Card 1 — Understand */}
<StageCard
num="01"
title="Understand"
description="Track store footfall, customer behavior, and real-time engagement with AI — giving merchants the intelligence to run smarter campaigns and targeted offers that actually convert."
cardRef={card1Ref}
orderClass="order-1 md:order-1"
icon={
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
}
/>
{/* Card 2 — Engage */}
<StageCard
num="02"
title="Engage"
description="Customers earn Loyaly Coins through daily login, 5,000-step walking challenges, and interactive games — Maze, Spin Wheel, Scratch Card, and Match Master — keeping them engaged every day."
cardRef={card2Ref}
orderClass="order-2 md:order-2"
icon={
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
}
/>
{/* Card 4 — Retain (positioned bottom-left on desktop via order-3) */}
<StageCard
num="04"
title="Retain"
description="Daily rewards, surprise coupons, and game-based offers keep customers returning to earn, play, and redeem — creating a self-reinforcing loyalty loop that compounds value with every visit."
cardRef={card4Ref}
orderClass="order-4 md:order-3"
icon={
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
}
/>
{/* Card 3 — Convert (positioned bottom-right on desktop via order-4) */}
<StageCard
num="03"
title="Convert"
description="Merchants create game-based discount campaigns where customers play once to reveal their reward — turning product discovery into an exciting, high-conversion in-store experience."
cardRef={card3Ref}
orderClass="order-3 md:order-4"
icon={
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
}
/>
</div>
{/* Center Area Ornament */}
{hasMeasured && (
<div
className="absolute z-30 hidden md:flex items-center justify-center"
{/* Step rail — desktop flow indicator synced to card hover, aligned to the card columns below */}
<div className="wwd-rail relative z-10 max-w-6xl mx-auto mb-6 hidden lg:grid lg:grid-cols-4 gap-7">
{STAGES.map((s, i) => (
<button
key={s.num}
type="button"
onMouseEnter={() => setActive(i)}
onMouseLeave={() => setActive(null)}
className="wwd-rail-step"
style={{
pointerEvents: 'none',
left: `${(points.p1.x + points.p2.x + points.p3.x + points.p4.x) / 4}px`,
top: `${(points.p1.y + points.p2.y + points.p3.y + points.p4.y) / 4}px`,
transform: 'translate(-50%, -50%)'
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
padding: '6px 14px', borderRadius: 100, border: '1px solid',
borderColor: active === i ? 'rgba(244,190,40,0.4)' : 'rgba(255,255,255,0.1)',
background: active === i ? 'rgba(244,190,40,0.1)' : 'rgba(255,255,255,0.03)',
color: active === i ? '#f4be28' : 'rgba(255,255,255,0.4)',
fontSize: 11, fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase',
fontFamily: 'Sora, sans-serif', cursor: 'default',
transition: 'all 0.3s ease',
}}
>
{/* Glowing container */}
<div className="relative w-16 h-16 rounded-full bg-[#0D0D0D] border border-[#f4be28]/35 flex items-center justify-center shadow-[0_0_35px_rgba(244, 190, 40,0.22)]">
{/* Pulsing outer aura */}
<div className="absolute inset-0 rounded-full border border-[#f4be28]/20 animate-ping opacity-60 pointer-events-none" />
{/* Rotating ↻ Icon */}
<svg
className="w-7 h-7 text-[#f4be28] animate-[spin_12s_linear_infinite]"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2.5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</div>
</div>
)}
{s.num} · {s.title}
</button>
))}
</div>
{/* Cascading 4-up grid */}
<div className="relative z-10 wwd-grid-container max-w-6xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-7">
{STAGES.map((stage, i) => (
<div key={stage.num} className="wwd-cascade-item" style={{ ['--cascade-offset' as string]: `${(i % 2 === 1 ? 32 : 0)}px` }}>
<StageCard stage={stage} index={i} active={active === i} onHover={setActive} />
</div>
))}
</div>
</div>
<style>{`
@media (min-width: 1024px) {
.wwd-cascade-item:nth-child(2n) .wwd-card { --cascade: 36px; }
}
.wwd-card-inner::before {
content: '';
position: absolute; inset: 0; border-radius: inherit; padding: 1px;
background: linear-gradient(135deg, rgba(244,190,40,0.5), transparent 40%, transparent 60%, rgba(244,190,40,0.25));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.wwd-card-active .wwd-card-inner::before { opacity: 1; }
`}</style>
</section>
);
}