Cleanup old PHP source files and update Next.js pages and components
This commit is contained in:
@@ -61,7 +61,7 @@ export default function Footer() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer style={{
|
||||
<footer className="ly-footer" style={{
|
||||
background: 'linear-gradient(to top, #3a2800 0%, #1e1600 18%, #0e0b04 36%, #07070d 58%)',
|
||||
color: '#ffffff',
|
||||
position: 'relative',
|
||||
@@ -71,7 +71,7 @@ export default function Footer() {
|
||||
}}>
|
||||
|
||||
{/* ── TOP CTA STRIP ── */}
|
||||
<div style={{
|
||||
<div className="ly-footer-cta" style={{
|
||||
padding: '72px 64px 64px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
|
||||
@@ -83,15 +83,15 @@ export default function Footer() {
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 20, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ width: 24, height: 1, background: '#f4be28', display: 'block' }} />Let's build together
|
||||
</div>
|
||||
<h2 style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif' }}>
|
||||
<h2 className="ly-footer-heading" style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif' }}>
|
||||
Turn every visit<br />
|
||||
<span style={{ color: '#f4be28' }}>into value.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Link href="/contacts" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10,
|
||||
<div className="ly-footer-cta-actions" style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Link href="/contacts" className="ly-footer-cta-btn" style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 10,
|
||||
padding: '18px 40px', background: '#f4be28', color: '#111',
|
||||
fontSize: 16, fontWeight: 800, borderRadius: 100, textDecoration: 'none',
|
||||
fontFamily: 'Sora, sans-serif', boxShadow: '0 16px 48px rgba(244,190,40,0.25)',
|
||||
@@ -112,14 +112,14 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
{/* ── MAIN COLUMNS ── */}
|
||||
<div style={{
|
||||
<div className="ly-footer-columns" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.6fr 1fr 1fr 1fr',
|
||||
gap: '48px', padding: '64px 64px 48px',
|
||||
alignItems: 'start',
|
||||
}}>
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<div className="ly-footer-brand">
|
||||
<Link href="/" aria-label="Loyaly.AI home">
|
||||
<Image src="/images/logo-dark.png" alt="Loyaly.AI" width={560} height={160} style={{ height: 140, width: 'auto', marginBottom: 24, display: 'block' }} />
|
||||
</Link>
|
||||
@@ -145,7 +145,7 @@ export default function Footer() {
|
||||
|
||||
{/* Nav columns */}
|
||||
{NAV.map(col => (
|
||||
<nav key={col.title} aria-label={col.title}>
|
||||
<nav key={col.title} className="ly-footer-nav" aria-label={col.title}>
|
||||
<h5 style={{ margin: '0 0 20px', fontSize: 12, fontWeight: 700, color: 'rgba(255,255,255,0.3)', letterSpacing: '0.16em', textTransform: 'uppercase', fontFamily: 'Sora, sans-serif' }}>
|
||||
{col.title}
|
||||
</h5>
|
||||
@@ -164,7 +164,7 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM BAR ── */}
|
||||
<div style={{
|
||||
<div className="ly-footer-bottom" style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
flexWrap: 'wrap', gap: 12,
|
||||
padding: '20px 64px 36px',
|
||||
@@ -181,6 +181,25 @@ export default function Footer() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@media (max-width: 900px) {
|
||||
.ly-footer { margin: 0 12px 12px; border-radius: 0 0 20px 20px; }
|
||||
.ly-footer-cta { padding: 48px 28px 40px !important; align-items: flex-start !important; }
|
||||
.ly-footer-cta-actions { align-items: flex-start !important; width: 100%; }
|
||||
.ly-footer-cta-btn { width: 100%; padding: 16px 28px !important; }
|
||||
.ly-footer-columns { grid-template-columns: 1fr 1fr !important; gap: 36px 24px !important; padding: 48px 28px 36px !important; }
|
||||
.ly-footer-brand { grid-column: 1 / -1; }
|
||||
.ly-footer-bottom { padding: 20px 28px 28px !important; flex-direction: column !important; align-items: flex-start !important; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.ly-footer-heading { font-size: clamp(30px, 9vw, 44px) !important; }
|
||||
.ly-footer-columns { grid-template-columns: 1fr !important; gap: 32px !important; }
|
||||
.ly-footer-brand img { height: 110px !important; }
|
||||
.ly-footer-bottom { gap: 16px !important; }
|
||||
.ly-footer-bottom > div { gap: 18px !important; }
|
||||
}
|
||||
`}</style>
|
||||
</footer>
|
||||
|
||||
{/* Scroll-to-top */}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function GetInTouch() {
|
||||
}}
|
||||
>
|
||||
|
||||
<div style={{
|
||||
<div className="git-grid" style={{
|
||||
position: 'relative', zIndex: 2,
|
||||
maxWidth: 1200, margin: '0 auto',
|
||||
padding: 'clamp(56px, 8vh, 100px) clamp(28px, 5vw, 72px)',
|
||||
@@ -100,7 +100,7 @@ export default function GetInTouch() {
|
||||
|
||||
<h2 style={{
|
||||
margin: '0 0 24px',
|
||||
fontSize: 'clamp(2rem, 4vw, 3.4rem)', fontWeight: 900,
|
||||
fontSize: 'clamp(2rem, 4vw, 3.4rem)', fontWeight: 800,
|
||||
lineHeight: 0.98, letterSpacing: '-0.04em',
|
||||
color: '#fff', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
@@ -242,6 +242,9 @@ export default function GetInTouch() {
|
||||
color: rgba(255,255,255,0.5); transition: all 0.2s ease;
|
||||
}
|
||||
.git-social:hover { background: rgba(244,190,40,0.12); border-color: rgba(244,190,40,0.3); color: #f4be28; }
|
||||
@media (max-width: 420px) {
|
||||
.git-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function MapSection() {
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
}}>
|
||||
<div style={{
|
||||
<div className="map-section-grid" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
alignItems: 'stretch',
|
||||
@@ -37,7 +37,7 @@ export default function MapSection() {
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(32px, 4vw, 56px)', fontWeight: 900,
|
||||
fontSize: 'clamp(32px, 4vw, 56px)', fontWeight: 800,
|
||||
letterSpacing: '-0.04em', lineHeight: 1.0,
|
||||
color: '#0a0a0f', margin: '0 0 14px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function AIRecommendationsSection() {
|
||||
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence · 05</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.05em', lineHeight: 0.94,
|
||||
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.94,
|
||||
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Built for</span></span>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function DiscoverDealsSection() {
|
||||
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover · 02</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 900, lineHeight: 0.94,
|
||||
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94,
|
||||
letterSpacing: '-0.05em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Every</span></span>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function EarnRewardsSection() {
|
||||
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn · 03</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 900, lineHeight: 1.08,
|
||||
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08,
|
||||
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Every step.</span></span>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function FinalCTASection() {
|
||||
|
||||
{/* Headline */}
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(48px, 7.5vw, 112px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.92,
|
||||
fontSize: 'clamp(48px, 7.5vw, 112px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.92,
|
||||
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Your rewards</span></span>
|
||||
|
||||
@@ -61,10 +61,6 @@ export default function HeroSection() {
|
||||
|
||||
{/* Text column */}
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
<div className="fp-content fp-eyebrow" style={{ marginBottom: 26, display: 'flex', justifyContent: 'center' }}>
|
||||
<span className="fp-kicker">For People · By Loyaly</span>
|
||||
</div>
|
||||
|
||||
<h1 className="fp-headline" style={{
|
||||
fontSize: 'clamp(40px, 5.4vw, 82px)', fontWeight: 800, lineHeight: 1.02,
|
||||
letterSpacing: '-0.045em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function SpendAnywhereSection() {
|
||||
<div>
|
||||
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend · 04</span></div>
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.9,
|
||||
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9,
|
||||
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Spend it</span></span>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function WhyLoyalySection() {
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(30px, 3.6vw, 50px)', fontWeight: 900, lineHeight: 1.08,
|
||||
fontSize: 'clamp(30px, 3.6vw, 50px)', fontWeight: 800, lineHeight: 1.08,
|
||||
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Millions shop offline.</span></span>
|
||||
|
||||
@@ -234,8 +234,7 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
|
||||
scrollTrigger: { trigger: '.ps-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' },
|
||||
});
|
||||
bottomTl
|
||||
.fromTo('.ps-bottom-bar', { opacity: 0, y: 36, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: 'power3.out' })
|
||||
.fromTo('.ps-feature-item', { opacity: 0, y: 16 }, { opacity: 1, y: 0, stagger: 0.09, duration: 0.5, ease: 'power3.out' }, '-=0.5');
|
||||
.fromTo('.ps-feature-item', { opacity: 0, y: 36, scale: 0.88 }, { opacity: 1, y: 0, scale: 1, stagger: 0.1, duration: 0.65, ease: 'back.out(1.7)' });
|
||||
|
||||
ScrollTrigger.refresh();
|
||||
}, sectionRef);
|
||||
@@ -263,7 +262,7 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#f4be28', display: 'block' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Sora, sans-serif' }}>The Opportunity</span>
|
||||
</div>
|
||||
<h2 className="ps-title" style={{ color: '#171512', fontWeight: 900, fontSize: 'clamp(2.4rem, 6vw, 4.8rem)', letterSpacing: '-0.05em', lineHeight: 0.94, margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
|
||||
<h2 className="ps-title" style={{ color: '#171512', fontWeight: 800, fontSize: 'clamp(2.4rem, 6vw, 4.8rem)', letterSpacing: '-0.05em', lineHeight: 0.94, margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
|
||||
Retail is busy.<br /><span style={{ color: '#f4be28' }}>But blind.</span>
|
||||
</h2>
|
||||
<p className="ps-subtitle" style={{ color: '#4f463a', fontSize: 'clamp(1rem, 1.3vw, 1.2rem)', fontWeight: 500, maxWidth: 620, margin: '0 auto', lineHeight: 1.7, fontFamily: 'Sora, sans-serif' }}>
|
||||
@@ -305,26 +304,43 @@ export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
|
||||
</div>
|
||||
|
||||
{/* Bottom features bar */}
|
||||
<div className="ps-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(40px, 5vw, 72px)', background: '#fff', borderRadius: 20, border: '1px solid rgba(0,0,0,0.05)', boxShadow: '0 20px 50px -30px rgba(23,21,18,0.2)', padding: 'clamp(16px, 1.6vw, 22px) clamp(20px, 3vw, 44px)', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
|
||||
{BOTTOM_FEATURES.map((f, i) => (
|
||||
<React.Fragment key={f.label}>
|
||||
<div className="ps-feature-item" style={{ flex: '1 1 180px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 11, cursor: 'default' }}>
|
||||
<span style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 10, background: '#faf9f5', border: '1px solid rgba(0,0,0,0.05)' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2">{f.icon}</svg>
|
||||
</span>
|
||||
<span style={{ color: '#171512', fontWeight: 700, fontSize: '0.95rem', fontFamily: 'Sora, sans-serif' }}>{f.label}</span>
|
||||
</div>
|
||||
{i < BOTTOM_FEATURES.length - 1 && <span className="ps-sep" style={{ width: 1, height: 28, background: 'rgba(0,0,0,0.07)' }} />}
|
||||
</React.Fragment>
|
||||
<div className="ps-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(40px, 5vw, 72px)', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 'clamp(12px, 1.6vw, 18px)' }}>
|
||||
{BOTTOM_FEATURES.map((f) => (
|
||||
<div
|
||||
key={f.label}
|
||||
className="ps-feature-item"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
background: '#fff', borderRadius: 18,
|
||||
border: '1px solid rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.03), 0 16px 36px -22px rgba(23,21,18,0.22)',
|
||||
padding: 'clamp(16px, 1.6vw, 20px) clamp(16px, 2vw, 22px)',
|
||||
cursor: 'default',
|
||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 44, height: 44, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
borderRadius: 13, background: 'linear-gradient(145deg,#fff8e8 0%,#fff2c8 100%)',
|
||||
border: '1px solid rgba(244,190,40,0.25)',
|
||||
}}>
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#bc7122" strokeWidth="2">{f.icon}</svg>
|
||||
</span>
|
||||
<span style={{ color: '#171512', fontWeight: 700, fontSize: '0.95rem', fontFamily: 'Sora, sans-serif', lineHeight: 1.3 }}>{f.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Responsive: stack cards on narrow screens, rotate connector */}
|
||||
<style>{`
|
||||
#problems-shift .ps-feature-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 24px 48px -22px rgba(23,21,18,0.3);
|
||||
border-color: rgba(244,190,40,0.35);
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
#problems-shift .ps-cards-row { flex-direction: column; }
|
||||
#problems-shift .ps-connector-arrow { transform: rotate(90deg); }
|
||||
#problems-shift .ps-sep { display: none; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
|
||||
@@ -275,7 +275,7 @@ function ProductCard({ product, index }: { product: Product; index: number }) {
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.7rem, 2.8vw, 3rem)',
|
||||
fontWeight: 900, lineHeight: 0.98, letterSpacing: '-0.04em',
|
||||
fontWeight: 800, lineHeight: 0.98, letterSpacing: '-0.04em',
|
||||
color: '#ffffff', fontFamily: 'Sora, sans-serif',
|
||||
}}>{product.title}</h3>
|
||||
|
||||
@@ -410,7 +410,7 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
<span style={{ width: 32, height: 2, background: '#f4be28', display: 'block', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Sora, sans-serif' }}>Platform</span>
|
||||
</div>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 900, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 800, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
|
||||
Four products.<br />
|
||||
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One ecosystem.</span>
|
||||
</h2>
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function TheLoop() {
|
||||
<h2 style={{
|
||||
margin: '0 0 18px',
|
||||
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
|
||||
fontWeight: 900, color: '#fff',
|
||||
fontWeight: 800, color: '#fff',
|
||||
lineHeight: 0.92, letterSpacing: '-0.05em',
|
||||
fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
@@ -222,6 +222,7 @@ export default function TheLoop() {
|
||||
style={{
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
padding: 'clamp(26px, 2.2vw, 38px) clamp(20px, 1.8vw, 28px)',
|
||||
borderRadius: 26,
|
||||
background: isActive ? 'rgba(244,190,40,0.07)' : 'rgba(255,255,255,0.025)',
|
||||
@@ -276,14 +277,15 @@ export default function TheLoop() {
|
||||
width: 'clamp(28px, 3vw, 56px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
<div className="loop-connector-line" style={{
|
||||
position: 'absolute', left: 0, right: 0, height: 2, borderRadius: 2,
|
||||
background: 'linear-gradient(90deg, rgba(244,190,40,0.1), rgba(244,190,40,0.45), rgba(244,190,40,0.1))',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: revealed ? 'loopFlow 1.8s linear infinite' : 'none',
|
||||
}} />
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'relative', zIndex: 2, opacity: 0.8 }}>
|
||||
<svg className="loop-connector-arrow" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'relative', zIndex: 2, opacity: 0.8 }}>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -330,9 +332,25 @@ export default function TheLoop() {
|
||||
@keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
|
||||
@keyframes loopDash { to { stroke-dashoffset: -32; } }
|
||||
@keyframes loopFlow { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
|
||||
@keyframes loopFlowVertical { 0%{background-position:0 200%} 100%{background-position:0 -200%} }
|
||||
@keyframes loopArrowBounceH { 0%,100% { transform: translateX(0); } 50% { transform: translateX(5px); } }
|
||||
@keyframes loopArrowBounceV { 0%,100% { transform: rotate(90deg) translateX(0); } 50% { transform: rotate(90deg) translateX(5px); } }
|
||||
.loop-connector-arrow { animation: loopArrowBounceH 1.4s ease-in-out infinite; }
|
||||
@media (max-width: 880px) {
|
||||
.loop-row { flex-direction: column; }
|
||||
.loop-connector { width: 100% !important; height: 40px; transform: rotate(90deg); }
|
||||
.loop-card { z-index: 1; }
|
||||
.loop-connector {
|
||||
width: 100% !important; height: 40px; z-index: 0;
|
||||
}
|
||||
.loop-connector-line {
|
||||
left: 50% !important; right: auto !important; top: 0 !important; bottom: 0 !important;
|
||||
width: 2px !important; height: 100% !important;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(180deg, rgba(244,190,40,0.1), rgba(244,190,40,0.45), rgba(244,190,40,0.1)) !important;
|
||||
background-size: 100% 200% !important;
|
||||
animation-name: loopFlowVertical !important;
|
||||
}
|
||||
.loop-connector-arrow { animation: loopArrowBounceV 1.4s ease-in-out infinite; }
|
||||
.loop-arc, .loop-arc-label { display: none !important; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
|
||||
@@ -12,93 +12,204 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
interface CardProps {
|
||||
interface Stage {
|
||||
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) {
|
||||
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.',
|
||||
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>
|
||||
),
|
||||
},
|
||||
{
|
||||
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.',
|
||||
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>
|
||||
),
|
||||
},
|
||||
{
|
||||
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.',
|
||||
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>
|
||||
),
|
||||
},
|
||||
{
|
||||
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.',
|
||||
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 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;
|
||||
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)}%`);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
const el = cardRef.current;
|
||||
if (!el) return;
|
||||
el.style.setProperty('--rx', '0deg');
|
||||
el.style.setProperty('--ry', '0deg');
|
||||
onHover(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={`wwd-card relative group ${orderClass}`}
|
||||
data-stage-card={index}
|
||||
className={`wwd-card relative group ${active ? 'wwd-card-active' : ''}`}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseEnter={() => onHover(index)}
|
||||
onMouseLeave={onMouseLeave}
|
||||
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)';
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{/* 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
|
||||
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)',
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
|
||||
{/* 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)',
|
||||
}}>
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
</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: 'Sora, sans-serif',
|
||||
position: 'relative', zIndex: 2,
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -109,129 +220,43 @@ interface WhatWeDoProps {
|
||||
|
||||
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 [active, setActive] = useState<number | null>(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'
|
||||
}
|
||||
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: 0, y: 70, scale: 0.94 },
|
||||
{
|
||||
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'
|
||||
}
|
||||
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' },
|
||||
}
|
||||
);
|
||||
|
||||
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' },
|
||||
}
|
||||
);
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [isLoaded, updatePoints]);
|
||||
}, [isLoaded]);
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -241,136 +266,83 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
|
||||
style={{
|
||||
width: 'calc(100% - 40px)',
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: '32px',
|
||||
borderRadius: 32,
|
||||
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 */}
|
||||
{/* 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>
|
||||
|
||||
{/* 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: '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>
|
||||
|
||||
{/* 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"
|
||||
{/* 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={{
|
||||
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%)'
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{s.num} · {s.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Cascading 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@media (min-width: 1024px) {
|
||||
.wwd-cascade-item:nth-child(2n) .wwd-card { --cascade: 36px; }
|
||||
}
|
||||
.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;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wwd-card-active .wwd-card-inner::before { opacity: 1; }
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user