292 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|