Files
loyalyweb/src/components/home/WhatWeDo.tsx
2026-06-29 18:07:43 +05:30

377 lines
14 KiB
TypeScript

'use client';
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
import AnimatedDarkBg from './AnimatedDarkBg';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface CardProps {
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) {
return (
<div
ref={cardRef}
className={`wwd-card relative group ${orderClass}`}
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)';
}}
>
{/* 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>
{/* 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: 'Manrope, sans-serif',
position: 'relative', zIndex: 2,
}}>
{description}
</p>
</div>
);
}
interface WhatWeDoProps {
isLoaded?: boolean;
}
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 [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'
}
}
);
// Card Stagger Animation
gsap.fromTo(
'.wwd-card',
{ opacity: 0, y: 60 },
{
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'
}
}
);
}, containerRef);
return () => ctx.revert();
}, [isLoaded, updatePoints]);
return (
<section
ref={containerRef}
id="what-we-do"
className="relative bg-[#0D0D0D] !pt-32 sm:!pt-40 md:!pt-44 pb-24 md:pb-32 px-6 md:px-12 xl:px-16 overflow-hidden select-none"
style={{
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: '32px',
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 */}
<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: 'Manrope, 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"
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%)'
}}
>
{/* 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>
)}
</div>
</section>
);
}