Update homepage, use-cases, and people pages; add preloader and new section components

This commit is contained in:
R-Bharathraj
2026-07-01 18:06:06 +05:30
parent 7a15ee2b52
commit abcefbb9c9
26 changed files with 1178 additions and 897 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
@@ -13,17 +13,17 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface Stage {
num: string;
title: string;
description: string;
tagline: string;
detail: string;
icon: React.ReactNode;
}
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.',
tagline: 'See exactly who walks in, when, and why they buy.',
detail: 'Real-time footfall & behavior insights',
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" />
@@ -32,9 +32,9 @@ const STAGES: Stage[] = [
),
},
{
num: '02',
title: 'Engage',
description: 'Customers earn Loyaly Coins through daily login, 5,000-step walking challenges, and interactive games, including Maze, Spin Wheel, Scratch Card, and Match Master, keeping them engaged every day.',
tagline: 'Playable daily rewards turn one visit into a habit.',
detail: 'Daily streaks, challenges & mini games',
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" />
@@ -42,9 +42,9 @@ const STAGES: Stage[] = [
),
},
{
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.',
tagline: 'Customers play to unlock the deal, then buy on the spot.',
detail: 'Higher-converting, game-based offers',
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" />
@@ -52,9 +52,9 @@ const STAGES: Stage[] = [
),
},
{
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.',
tagline: 'Every redemption resets the loop for the next visit.',
detail: 'More repeat visits, compounding loyalty',
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" />
@@ -75,6 +75,7 @@ function StageCard({
onHover: (i: number | null) => void;
}) {
const cardRef = useRef<HTMLDivElement>(null);
const iconRef = useRef<HTMLDivElement>(null);
const onMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const el = cardRef.current;
@@ -82,133 +83,50 @@ function StageCard({
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)}%`);
gsap.to(el, { '--mx': `${((px + 0.5) * 100).toFixed(1)}%`, '--my': `${((py + 0.5) * 100).toFixed(1)}%`, duration: 0.3, overwrite: 'auto' } as gsap.TweenVars);
};
const onMouseEnter = () => {
onHover(index);
if (iconRef.current) gsap.to(iconRef.current, { rotate: -8, scale: 1.08, duration: 0.4, ease: 'back.out(2)' });
};
const onMouseLeave = () => {
const el = cardRef.current;
if (!el) return;
el.style.setProperty('--rx', '0deg');
el.style.setProperty('--ry', '0deg');
onHover(null);
if (iconRef.current) gsap.to(iconRef.current, { rotate: 0, scale: 1, duration: 0.4, ease: 'power3.out' });
};
const isYellow = index % 2 === 1;
return (
<div
ref={cardRef}
data-stage-card={index}
className={`wwd-card relative group ${active ? 'wwd-card-active' : ''}`}
className={`wwd-card relative group ${isYellow ? 'wwd-card-yellow' : 'wwd-card-white'} ${active ? 'wwd-card-active' : ''}`}
onMouseMove={onMouseMove}
onMouseEnter={() => onHover(index)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{
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',
}}
>
<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)',
}}
>
<div className="wwd-card-inner">
{/* 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>
<div className="wwd-card-glow" style={{ opacity: active ? 1 : 0 }} />
{/* 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)',
}}>
<div ref={iconRef} className="wwd-icon">
{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>
<h3 className="wwd-title">{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>
{/* Tagline — single concise line */}
<p className="wwd-tagline">{stage.tagline}</p>
{/* Detail pill — quick supporting fact */}
<div className="wwd-detail">
<span className="wwd-detail-dot" />
{stage.detail}
</div>
</div>
</div>
);
@@ -236,23 +154,41 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
}
);
gsap.fromTo(
'.wwd-card',
{ opacity: 0, y: 70, scale: 0.94 },
{
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' },
}
);
// Per-card reveal: each card gets its own scrubbed-in timeline
// (clip-path wipe + rise), staggered across the row, with the
// icon and copy cascading in right after.
gsap.utils.toArray<HTMLElement>('.wwd-card').forEach((card, i) => {
const tl = gsap.timeline({
scrollTrigger: { trigger: card, start: 'top 85%', toggleActions: 'play none none reset' },
delay: i * 0.1,
});
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' },
}
);
tl.fromTo(card,
{ opacity: 0, y: 70, scale: 0.94, clipPath: 'inset(0% 0% 100% 0% round 22px)' },
{ opacity: 1, y: 0, scale: 1, clipPath: 'inset(0% 0% 0% 0% round 22px)', duration: 0.75, ease: 'power3.out' },
0
)
.fromTo(card.querySelector('.wwd-icon'),
{ opacity: 0, scale: 0.4, rotate: -20 },
{ opacity: 1, scale: 1, rotate: 0, duration: 0.55, ease: 'back.out(2.2)' },
0.3
)
.fromTo(card.querySelector('.wwd-title'),
{ opacity: 0, y: 16 },
{ opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' },
0.5
)
.fromTo(card.querySelector('.wwd-tagline'),
{ opacity: 0, y: 12 },
{ opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' },
0.58
)
.fromTo(card.querySelector('.wwd-detail'),
{ opacity: 0, y: 10 },
{ opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' },
0.66
);
});
}, containerRef);
return () => ctx.revert();
@@ -274,74 +210,98 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
{/* 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>
<span className="wwd-header-el" style={{
fontSize: 11, fontWeight: 800, letterSpacing: '0.2em', textTransform: 'uppercase',
color: 'rgba(244,190,40,0.7)', fontFamily: 'Sora, sans-serif', marginBottom: 18,
}}>
One Platform
</span>
<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 className="wwd-header-el" style={{
margin: 0,
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
fontWeight: 800, color: '#fff',
lineHeight: 0.98, letterSpacing: '-0.04em',
fontFamily: 'Sora, sans-serif',
}}>
Every visit,<br />
a <span className="wwd-shimmer">loyal customer.</span>
</h2>
<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>
{/* 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={{
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',
}}
>
{s.num} · {s.title}
</button>
))}
</div>
{/* Cascading 4-up grid */}
{/* 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>
<StageCard key={stage.title} stage={stage} index={i} active={active === i} onHover={setActive} />
))}
</div>
</div>
<style>{`
@media (min-width: 1024px) {
.wwd-cascade-item:nth-child(2n) .wwd-card { --cascade: 36px; }
.wwd-card {
position: relative;
transform: perspective(1100px) rotateX(0deg) rotateY(0deg);
transition: transform 0.45s cubic-bezier(0.22,1,0.36,1);
}
.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;
.wwd-card-inner {
position: relative;
border-radius: 22px;
padding: clamp(24px, 2vw, 30px);
overflow: hidden;
height: 100%;
transition: transform 0.4s cubic-bezier(0.22,1,0.36,1), box-shadow 0.4s ease;
}
.wwd-card-white .wwd-card-inner { background: #ffffff; }
.wwd-card-yellow .wwd-card-inner { background: #F4C21A; }
.wwd-card-active .wwd-card-inner {
transform: translateY(-6px);
box-shadow: 0 24px 50px -18px rgba(0,0,0,0.35);
}
.wwd-card-glow {
position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(260px circle at var(--mx,50%) var(--my,50%), rgba(0,0,0,0.06), transparent 70%);
transition: opacity 0.4s ease;
pointer-events: none;
}
.wwd-card-active .wwd-card-inner::before { opacity: 1; }
.wwd-icon {
width: 50px; height: 50px; border-radius: 14px;
background: #0D0D0D; color: #F4C21A;
display: flex; align-items: center; justify-content: center;
margin-bottom: 22px; position: relative; z-index: 2;
}
.wwd-title {
margin: 0 0 10px; font-size: clamp(1.25rem, 1.8vw, 1.5rem);
font-weight: 800; color: #0D0D0D; letter-spacing: -0.02em;
font-family: 'Sora', sans-serif; position: relative; z-index: 2;
}
.wwd-tagline {
margin: 0 0 16px; font-size: 0.9rem; color: rgba(13,13,13,0.62);
line-height: 1.55; font-family: 'Sora', sans-serif;
position: relative; z-index: 2;
}
.wwd-detail {
display: flex; align-items: center; gap: 8px;
font-size: 0.76rem; font-weight: 700; letter-spacing: 0.02em;
color: rgba(13,13,13,0.75); font-family: 'Sora', sans-serif;
padding-top: 14px; border-top: 1px solid rgba(13,13,13,0.1);
position: relative; z-index: 2;
}
.wwd-detail-dot {
width: 6px; height: 6px; border-radius: 50%; background: #0D0D0D;
flex-shrink: 0;
}
.wwd-shimmer {
background: linear-gradient(105deg,#f4be28 0%,#fff8e0 48%,#f4be28 62%,#ff9500 100%);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: wwdShimmer 3s linear infinite;
}
@keyframes wwdShimmer { 0% { background-position: 200% center; } 100% { background-position: -200% center; } }
@media (prefers-reduced-motion: reduce) {
.wwd-shimmer { animation: none; }
}
`}</style>
</section>
);