377 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|