Files
loyalyweb/src/components/home/WhatWeDo.tsx

292 lines
10 KiB
TypeScript

'use client';
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';
import AnimatedDarkBg from './AnimatedDarkBg';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface Stage {
title: string;
tagline: string;
detail: string;
icon: React.ReactNode;
}
const STAGES: Stage[] = [
{
title: 'Understand',
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" />
<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>
),
},
{
title: 'Engage',
tagline: 'Playable daily Lyts 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" />
</svg>
),
},
{
title: 'Convert',
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" />
</svg>
),
},
{
title: 'Retain',
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" />
</svg>
),
},
];
function StageCard({
stage,
index,
active,
onHover,
}: {
stage: Stage;
index: number;
active: boolean;
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;
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;
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 = () => {
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}
className={`wwd-card relative group ${isYellow ? 'wwd-card-yellow' : 'wwd-card-white'} ${active ? 'wwd-card-active' : ''}`}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className="wwd-card-inner">
{/* Cursor-following glow */}
<div className="wwd-card-glow" style={{ opacity: active ? 1 : 0 }} />
{/* Icon */}
<div ref={iconRef} className="wwd-icon">
{stage.icon}
</div>
{/* Title */}
<h3 className="wwd-title">{stage.title}</h3>
{/* 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>
);
}
interface WhatWeDoProps {
isLoaded?: boolean;
}
export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [active, setActive] = useState<number | null>(null);
useIsomorphicLayoutEffect(() => {
if (!isLoaded) return;
if (typeof window === 'undefined') return;
const ctx = gsap.context(() => {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Header: clean fade-up with a refined blur transition
gsap.fromTo(
'.wwd-header-el',
{
opacity: 0,
y: prefersReducedMotion ? 0 : 30,
filter: prefersReducedMotion ? 'none' : 'blur(8px)'
},
{
opacity: 1,
y: 0,
filter: 'blur(0px)',
duration: DEFAULT_DURATION,
stagger: 0.15,
ease: DEFAULT_EASE,
scrollTrigger: {
trigger: '.wwd-header',
start: 'top 85%',
toggleActions: 'play none none reset'
},
}
);
// The cards have no reveal animation by design — they render in place.
// Hover interactions live in StageCard.
}, containerRef);
return () => ctx.revert();
}, [isLoaded]);
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% - (var(--sec-gutter) * 2))',
margin: '0 var(--sec-gutter) var(--sec-gap)',
borderRadius: 32,
zIndex: 2,
}}
>
<AnimatedDarkBg variant="amber" bgImage="/assets/images/stock/one_platform_bg.png" bgImageAlt="" />
{/* 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">
<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" style={{
margin: 0,
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
fontWeight: 800, color: '#fff',
lineHeight: 0.98, letterSpacing: '-0.04em',
fontFamily: 'Sora, sans-serif',
textShadow: '0 4px 24px rgba(0,0,0,0.55), 0 1px 3px rgba(0,0,0,0.6)',
}}>
Every visit,<br />
a <span className="wwd-shimmer">loyal customer.</span>
</h2>
</div>
{/* 4-up grid */}
<div className="relative z-10 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) => (
<StageCard key={stage.title} stage={stage} index={i} active={active === i} onHover={setActive} />
))}
</div>
</div>
<style>{`
.wwd-card {
position: relative;
}
.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; box-shadow: 0 18px 40px -14px rgba(0,0,0,0.5); }
.wwd-card-yellow .wwd-card-inner { background: #F4C21A; box-shadow: 0 18px 40px -14px rgba(0,0,0,0.45); }
.wwd-card-active .wwd-card-inner {
transform: translateY(-6px);
box-shadow: 0 24px 50px -18px rgba(0,0,0,0.55);
}
.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;
}
.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,#ffd95a 0%,#fff8e0 35%,#f4c21a 55%,#ffb020 75%,#ffd95a 100%);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: none;
filter: drop-shadow(0 3px 14px rgba(0,0,0,0.55)) drop-shadow(0 0 22px rgba(244,194,26,0.35));
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>
);
}