724 lines
37 KiB
TypeScript
724 lines
37 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||
import Link from 'next/link';
|
||
import { gsap } from 'gsap';
|
||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||
|
||
if (typeof window !== 'undefined') {
|
||
gsap.registerPlugin(ScrollTrigger);
|
||
}
|
||
|
||
const useIsomorphicLayoutEffect =
|
||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||
|
||
interface Product {
|
||
id: number;
|
||
href: string;
|
||
title: string;
|
||
subtitle: string;
|
||
description: string;
|
||
accent: string;
|
||
accentRgb: string;
|
||
tags: string[];
|
||
}
|
||
|
||
const PRODUCTS: Product[] = [
|
||
{
|
||
id: 1,
|
||
href: '/solutions/behavision',
|
||
title: 'Behavision',
|
||
subtitle: 'AI Powered Footfall Analytics',
|
||
description:
|
||
'AI powered footfall analytics and behavioral intelligence. Understand how customers move, dwell, and engage, turning raw foot traffic into rich, actionable insights.',
|
||
accent: '#f4be28',
|
||
accentRgb: '244, 190, 40',
|
||
tags: ['Footfall Tracking', 'Behavior Analysis'],
|
||
},
|
||
{
|
||
id: 2,
|
||
href: '/solutions/spendsense',
|
||
title: 'SpendSense',
|
||
subtitle: 'Customer Spending Intelligence',
|
||
description:
|
||
'Deep insights into customer spending patterns. Identify high-value segments, predict buying behaviour, and unlock incremental revenue hiding in plain sight.',
|
||
accent: '#10B981',
|
||
accentRgb: '16, 185, 129',
|
||
tags: ['Spending Trends', 'Revenue Analytics', 'Customer Value'],
|
||
},
|
||
{
|
||
id: 3,
|
||
href: '/solutions/dyscount',
|
||
title: 'Dyscount',
|
||
subtitle: 'Smart Discount Engine',
|
||
description:
|
||
'Smart discount engine that maximises conversions without killing margins. Deliver the right offer to the right customer at exactly the right moment.',
|
||
accent: '#EF4444',
|
||
accentRgb: '239, 68, 68',
|
||
tags: ['Smart Discounts', 'Offer Automation', 'Margin Protection'],
|
||
},
|
||
{
|
||
id: 4,
|
||
href: '/solutions/loyaly-app',
|
||
title: 'Identiq',
|
||
subtitle: 'Consumer Loyalty Platform',
|
||
description:
|
||
'Earn LYTs through daily logins and games, then redeem Lyts at real offline stores.',
|
||
accent: '#f4be28',
|
||
accentRgb: '244, 190, 40',
|
||
tags: ['Daily Lyts', 'Game Based Offers', 'Offline Redemption'],
|
||
},
|
||
];
|
||
|
||
function ProductIcon({ id, accent }: { id: number; accent: string }) {
|
||
if (id === 1) {
|
||
return (
|
||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||
<path d="M14 34 C14 28 18 24 24 24 C30 24 34 28 34 34" stroke={accent} strokeWidth="2.5" strokeLinecap="round" fill="none" />
|
||
<circle cx="24" cy="18" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||
<circle cx="14" cy="20" r="3.5" stroke={accent} strokeWidth="2" fill="none" />
|
||
<circle cx="34" cy="20" r="3.5" stroke={accent} strokeWidth="2" fill="none" />
|
||
<path d="M8 34 C8 30 10 27 14 27" stroke={accent} strokeWidth="2" strokeLinecap="round" opacity="0.6" fill="none" />
|
||
<path d="M40 34 C40 30 38 27 34 27" stroke={accent} strokeWidth="2" strokeLinecap="round" opacity="0.6" fill="none" />
|
||
</svg>
|
||
);
|
||
}
|
||
if (id === 2) {
|
||
return (
|
||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||
<rect x="10" y="28" width="6" height="10" rx="2" fill={accent} opacity="0.5" />
|
||
<rect x="19" y="22" width="6" height="16" rx="2" fill={accent} opacity="0.7" />
|
||
<rect x="28" y="16" width="6" height="22" rx="2" fill={accent} />
|
||
<path d="M12 26 L21 18 L30 12 L36 10" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" opacity="0.8" fill="none" />
|
||
<circle cx="36" cy="10" r="2.5" fill={accent} />
|
||
</svg>
|
||
);
|
||
}
|
||
if (id === 3) {
|
||
return (
|
||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||
<path d="M14 14 L34 34" stroke={accent} strokeWidth="2.2" strokeLinecap="round" opacity="0.4" fill="none" />
|
||
<circle cx="16" cy="16" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||
<circle cx="32" cy="32" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||
<path d="M30 14 L34 14 L34 18" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||
<path d="M18 34 L14 34 L14 30" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||
<circle cx="16" cy="16" r="2" fill={accent} />
|
||
<circle cx="32" cy="32" r="2" fill={accent} />
|
||
</svg>
|
||
);
|
||
}
|
||
return (
|
||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||
<path d="M24 36 C24 36 10 28 10 19 C10 14.6 13.6 11 18 11 C20.6 11 22.9 12.3 24 14.3 C25.1 12.3 27.4 11 30 11 C34.4 11 38 14.6 38 19 C38 28 24 36 24 36Z" stroke={accent} strokeWidth="2.5" fill={accent} fillOpacity="0.12" strokeLinejoin="round" />
|
||
<circle cx="36" cy="34" r="6" fill={accent} />
|
||
<path d="M33 34 L35 36 L39 32" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/* Real product-preview visual per product — a believable mini-UI, not generic decoration */
|
||
function ProductVisual({ product }: { product: Product }) {
|
||
const { id, accent, accentRgb } = product;
|
||
|
||
// 1 — Behavision: footfall heatmap + live counter
|
||
if (id === 1) {
|
||
const cells = Array.from({ length: 48 });
|
||
return (
|
||
<div style={{ width: '86%', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', fontWeight: 600 }}>Live footfall</span>
|
||
<span style={{ fontSize: 11, color: accent, fontFamily: 'Sora, sans-serif', fontWeight: 700, display: 'flex', alignItems: 'center', gap: 5 }}>
|
||
<span className="pv-live-dot" style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'block' }} />2,847
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8,1fr)', gap: 5 }}>
|
||
{cells.map((_, i) => {
|
||
const intensity = Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.6));
|
||
const alpha = (0.08 + intensity * 0.7).toFixed(3);
|
||
return (
|
||
<div
|
||
key={i}
|
||
className="pv-cell"
|
||
style={{
|
||
aspectRatio: '1 / 1', borderRadius: 5, background: `rgba(${accentRgb}, ${alpha})`,
|
||
animationDelay: `${(i % 8) * 0.12 + Math.floor(i / 8) * 0.07}s`,
|
||
}}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 10 }}>
|
||
{[['Peak', '6–8 PM'], ['Dwell', '14 min'], ['Zones', '12']].map(([k, v]) => (
|
||
<div key={k} style={{ flex: 1, padding: '10px 12px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>{k}</div>
|
||
<div style={{ fontSize: 15, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{v}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 2 — SpendSense: spend trend chart
|
||
if (id === 2) {
|
||
const pts = [22, 30, 26, 42, 38, 54, 48, 66, 72];
|
||
const max = 80;
|
||
const w = 280, h = 120;
|
||
const path = pts.map((p, i) => `${(i / (pts.length - 1)) * w},${h - (p / max) * h}`).join(' ');
|
||
return (
|
||
<div style={{ width: '86%', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<div>
|
||
<div style={{ fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', fontWeight: 600 }}>Avg basket value</div>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginTop: 4 }}>
|
||
<span style={{ fontSize: 34, fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>₹1,240</span>
|
||
<span style={{ fontSize: 13, fontWeight: 700, color: accent, fontFamily: 'Sora, sans-serif' }}>+18%</span>
|
||
</div>
|
||
</div>
|
||
<svg viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
|
||
<defs>
|
||
<linearGradient id={`sg${id}`} x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={accent} stopOpacity="0.35" />
|
||
<stop offset="100%" stopColor={accent} stopOpacity="0" />
|
||
</linearGradient>
|
||
</defs>
|
||
<polygon points={`0,${h} ${path} ${w},${h}`} fill={`url(#sg${id})`} />
|
||
<polyline className="pv-chart-line" points={path} fill="none" stroke={accent} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" pathLength={100} />
|
||
{pts.map((p, i) => (
|
||
i === pts.length - 1
|
||
? (
|
||
<g key={i}>
|
||
<circle className="pv-chart-pulse-ring" cx={(i / (pts.length - 1)) * w} cy={h - (p / max) * h} r={4} fill="none" stroke={accent} strokeWidth="2" />
|
||
<circle cx={(i / (pts.length - 1)) * w} cy={h - (p / max) * h} r={4} fill={accent} />
|
||
</g>
|
||
)
|
||
: null
|
||
))}
|
||
</svg>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 3 — Dyscount: smart offer + conversion uplift
|
||
if (id === 3) {
|
||
return (
|
||
<div style={{ width: '82%', position: 'relative', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||
{/* Live campaign badge, floating on the card edge */}
|
||
<div className="pv-streak-badge" style={{ position: 'absolute', top: -34, right: 18, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', borderRadius: 99, background: '#171512', border: `1px solid ${accent}55`, boxShadow: '0 8px 20px -8px rgba(0,0,0,0.6)' }}>
|
||
<span className="pv-live-dot" style={{ width: 6, height: 6, borderRadius: '50%', background: accent, display: 'block' }} />
|
||
<span style={{ fontSize: 11, fontWeight: 800, color: accent, fontFamily: 'Sora, sans-serif' }}>Live campaign</span>
|
||
</div>
|
||
|
||
{/* Coupon-style offer card with perforated divider */}
|
||
<div style={{ borderRadius: 16, background: `linear-gradient(135deg, ${accent} 0%, #c0392b 100%)`, position: 'relative', overflow: 'hidden', display: 'flex' }}>
|
||
<div className="pv-sheen" />
|
||
<div style={{ flex: 1, padding: '16px 18px' }}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Smart Offer</div>
|
||
<div style={{ fontSize: 30, fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em', marginTop: 2 }}>15% OFF</div>
|
||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.85)', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>Targeted · high-intent shoppers</div>
|
||
</div>
|
||
{/* Perforated tear-off edge */}
|
||
<div style={{ position: 'relative', width: 0, borderLeft: '2px dashed rgba(0,0,0,0.25)' }}>
|
||
<span style={{ position: 'absolute', top: -8, left: -7, width: 14, height: 14, borderRadius: '50%', background: '#171512' }} />
|
||
<span style={{ position: 'absolute', bottom: -8, left: -7, width: 14, height: 14, borderRadius: '50%', background: '#171512' }} />
|
||
</div>
|
||
<div style={{ width: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<span style={{ fontSize: 11, fontWeight: 800, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', writingMode: 'vertical-rl', letterSpacing: '0.1em' }}>DYSCOUNT</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ padding: '14px 16px', borderRadius: 14, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.5)', fontFamily: 'Sora, sans-serif' }}>Conversion uplift</span>
|
||
<span className="pv-uplift-pulse" style={{ fontSize: 13, fontWeight: 800, color: accent, fontFamily: 'Sora, sans-serif' }}>+41%</span>
|
||
</div>
|
||
<div style={{ height: 6, borderRadius: 3, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
|
||
<div className="pv-progress-fill" style={{ height: '100%', width: '72%', borderRadius: 3, background: accent }} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mini stat grid */}
|
||
<div style={{ display: 'flex', gap: 10 }}>
|
||
{[['Margin protected', '100%'], ['Waste', '0'], ['Redemptions', '1.2k']].map(([k, v]) => (
|
||
<div key={k} style={{ flex: 1, padding: '10px 12px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>{k}</div>
|
||
<div style={{ fontSize: 14, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{v}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 4 — Loyaly App: rewards card
|
||
const REDEMPTIONS: [string, string, string][] = [
|
||
['Starbucks', '+120', '#1e6b52'],
|
||
['Nike Store', '+85', '#374151'],
|
||
];
|
||
return (
|
||
<div style={{ width: '82%', borderRadius: 24, padding: 22, background: 'linear-gradient(165deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.1)', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.06)', position: 'relative', display: 'flex', flexDirection: 'column', gap: 18 }}>
|
||
{/* Streak badge, floating on the card edge */}
|
||
<div className="pv-streak-badge" style={{ position: 'absolute', top: -14, right: 18, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', borderRadius: 99, background: '#171512', border: `1px solid ${accent}55`, boxShadow: '0 8px 20px -8px rgba(0,0,0,0.6)' }}>
|
||
<span style={{ fontSize: 12 }}>🔥</span>
|
||
<span style={{ fontSize: 11, fontWeight: 800, color: accent, fontFamily: 'Sora, sans-serif' }}>6-day streak</span>
|
||
</div>
|
||
|
||
{/* Member header */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<span style={{
|
||
width: 34, height: 34, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)`, color: '#111', fontWeight: 800, fontSize: 13, fontFamily: 'Sora, sans-serif',
|
||
}}>L</span>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', lineHeight: 1.1 }}>LytsUp</div>
|
||
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>Member since 2024</div>
|
||
</div>
|
||
</div>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 10, fontWeight: 800, letterSpacing: '0.06em', color: accent, background: `${accent}1a`, border: `1px solid ${accent}40`, padding: '5px 10px', borderRadius: 99, fontFamily: 'Sora, sans-serif' }}>
|
||
✦ GOLD
|
||
</span>
|
||
</div>
|
||
|
||
{/* LYTs balance card with progress to next tier */}
|
||
<div style={{ borderRadius: 18, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)`, position: 'relative', overflow: 'hidden' }}>
|
||
<div className="pv-sheen" />
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||
<div>
|
||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Lyts Balance</div>
|
||
<div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
|
||
</div>
|
||
<span style={{ width: 34, height: 34, borderRadius: 10, background: 'rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif' }}>⬡</span>
|
||
</div>
|
||
<div style={{ marginTop: 12 }}>
|
||
<div style={{ height: 5, borderRadius: 3, background: 'rgba(0,0,0,0.15)', overflow: 'hidden' }}>
|
||
<div className="pv-progress-fill" style={{ height: '100%', width: '68%', borderRadius: 3, background: '#111' }} />
|
||
</div>
|
||
<div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 LYTs to Platinum</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recent redemptions */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', fontFamily: 'Sora, sans-serif' }}>Recently redeemed</span>
|
||
{REDEMPTIONS.map(([s, p, dot], i) => (
|
||
<div key={s} className="pv-row" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 12px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)', animationDelay: `${i * 0.9}s` }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
|
||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: dot, flexShrink: 0 }} />
|
||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.75)', fontFamily: 'Sora, sans-serif', fontWeight: 600 }}>{s}</span>
|
||
</div>
|
||
<span style={{ fontSize: 12, fontWeight: 700, color: '#4ade80', fontFamily: 'Sora, sans-serif' }}>{p}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Gallery card (horizontal panel) ──────────────────────────────── */
|
||
function ProductCard({ product, index }: { product: Product; index: number }) {
|
||
return (
|
||
<article
|
||
className="products-card"
|
||
data-index={index}
|
||
style={{
|
||
borderRadius: '28px',
|
||
background: 'linear-gradient(145deg, #0e0e13 0%, #16161d 100%)',
|
||
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.05)`,
|
||
border: `1px solid rgba(${product.accentRgb}, 0.16)`,
|
||
display: 'flex',
|
||
flexDirection: 'row',
|
||
overflow: 'hidden',
|
||
scrollSnapAlign: 'center',
|
||
}}
|
||
>
|
||
{/* ── Left content ── */}
|
||
<div className="products-card-content" style={{
|
||
flex: '0 0 54%',
|
||
padding: 'clamp(22px, 2.6vw, 46px)',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'space-between',
|
||
gap: 14,
|
||
minWidth: 0,
|
||
}}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(11px, 1.5vh, 18px)' }}>
|
||
<div style={{
|
||
width: 48, height: 48, borderRadius: 14, padding: 10,
|
||
background: `rgba(${product.accentRgb}, 0.1)`,
|
||
border: `1px solid rgba(${product.accentRgb}, 0.22)`,
|
||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.06)',
|
||
flexShrink: 0,
|
||
}}>
|
||
<ProductIcon id={product.id} accent={product.accent} />
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||
<span style={{ fontSize: 13, fontWeight: 800, color: product.accent, fontFamily: 'Sora, sans-serif', letterSpacing: '0.05em' }}>0{product.id}</span>
|
||
<span style={{ width: 24, height: 1, background: `rgba(${product.accentRgb},0.4)`, display: 'block' }} />
|
||
<span style={{ fontSize: '0.68rem', fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: product.accent, fontFamily: 'Sora, sans-serif' }}>{product.subtitle}</span>
|
||
</div>
|
||
|
||
<h3 style={{
|
||
margin: 0,
|
||
fontSize: 'clamp(1.7rem, 2.8vw, 3rem)',
|
||
fontWeight: 800, lineHeight: 0.98, letterSpacing: '-0.04em',
|
||
color: '#ffffff', fontFamily: 'Sora, sans-serif',
|
||
}}>{product.title}</h3>
|
||
|
||
<p style={{
|
||
margin: 0,
|
||
fontSize: 'clamp(0.82rem, 0.95vw, 0.98rem)',
|
||
lineHeight: 1.6, color: 'rgba(255,255,255,0.5)',
|
||
maxWidth: '420px', fontFamily: 'Sora, sans-serif',
|
||
}}>{product.description}</p>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(11px, 1.5vh, 16px)' }}>
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||
{product.tags.map((tag) => (
|
||
<span key={tag} style={{
|
||
padding: '5px 13px', borderRadius: '99px', fontSize: '0.7rem', fontWeight: 600,
|
||
color: 'rgba(255,255,255,0.7)', background: 'rgba(255,255,255,0.05)',
|
||
border: '1px solid rgba(255,255,255,0.1)', fontFamily: 'Sora, sans-serif',
|
||
}}>{tag}</span>
|
||
))}
|
||
</div>
|
||
|
||
<Link
|
||
href="/solutions_page"
|
||
style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: '8px', width: 'max-content',
|
||
padding: '11px 22px', borderRadius: '99px', fontSize: '0.85rem', fontWeight: 800,
|
||
textDecoration: 'none', color: '#111111', background: product.accent,
|
||
fontFamily: 'Sora, sans-serif', boxShadow: `0 12px 36px rgba(${product.accentRgb},0.25)`,
|
||
transition: 'transform 0.3s cubic-bezier(0.34,1.56,0.64,1)',
|
||
}}
|
||
onMouseEnter={(e) => { (e.currentTarget as HTMLAnchorElement).style.transform = 'scale(1.04)'; }}
|
||
onMouseLeave={(e) => { (e.currentTarget as HTMLAnchorElement).style.transform = 'scale(1)'; }}
|
||
>
|
||
Explore {product.title}
|
||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '15px', height: '15px' }}>
|
||
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
|
||
</svg>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Right visual panel ── */}
|
||
<div className="products-card-visual" style={{
|
||
flex: '0 0 46%',
|
||
position: 'relative', overflow: 'hidden',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
borderLeft: `1px solid rgba(${product.accentRgb}, 0.1)`,
|
||
background: `radial-gradient(ellipse 90% 80% at 60% 40%, rgba(${product.accentRgb},0.08) 0%, transparent 70%)`,
|
||
}}>
|
||
<div style={{
|
||
position: 'absolute', inset: 0,
|
||
backgroundImage: `linear-gradient(rgba(${product.accentRgb},0.05) 1px,transparent 1px),linear-gradient(90deg,rgba(${product.accentRgb},0.05) 1px,transparent 1px)`,
|
||
backgroundSize: '40px 40px',
|
||
maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 40%, transparent 85%)',
|
||
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 40%, transparent 85%)',
|
||
}} />
|
||
<ProductVisual product={product} />
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
interface ProductsSectionProps {
|
||
isLoaded?: boolean;
|
||
}
|
||
|
||
export default function ProductsSection({ isLoaded = false }: ProductsSectionProps) {
|
||
const sectionRef = useRef<HTMLDivElement>(null);
|
||
const trackRef = useRef<HTMLDivElement>(null);
|
||
const progressRef = useRef<HTMLDivElement>(null);
|
||
const counterRef = useRef<HTMLSpanElement>(null);
|
||
|
||
useIsomorphicLayoutEffect(() => {
|
||
if (!isLoaded) return;
|
||
if (typeof window === 'undefined') return;
|
||
const section = sectionRef.current;
|
||
const track = trackRef.current;
|
||
if (!section || !track) return;
|
||
|
||
const mm = gsap.matchMedia();
|
||
|
||
// Desktop / tablet: pinned horizontal scroll
|
||
mm.add('(min-width: 901px)', () => {
|
||
const cards = gsap.utils.toArray<HTMLElement>('.products-card', track);
|
||
const distance = () => Math.max(0, track.scrollWidth - section.clientWidth);
|
||
const steps = Math.max(1, PRODUCTS.length - 1);
|
||
|
||
// Depth cue: the card nearest the viewport centre sits at full scale/
|
||
// opacity, neighbours ease back slightly — makes the ride feel guided
|
||
// rather than a flat strip sliding past, and the eased response
|
||
// absorbs small scrub jitter instead of showing it as a raw jump.
|
||
const updateDepth = () => {
|
||
const centerX = section.clientWidth / 2;
|
||
cards.forEach((card) => {
|
||
const trackX = Number(gsap.getProperty(track, 'x', 'px'));
|
||
const cardCenter = card.offsetLeft + card.offsetWidth / 2 + trackX;
|
||
const dist = Math.abs(cardCenter - centerX);
|
||
const norm = gsap.utils.clamp(0, 1, dist / (section.clientWidth * 0.62));
|
||
gsap.set(card, { scale: 1 - norm * 0.07, opacity: 1 - norm * 0.32 });
|
||
});
|
||
};
|
||
|
||
const tween = gsap.to(track, {
|
||
x: () => -distance(),
|
||
ease: 'none',
|
||
scrollTrigger: {
|
||
trigger: section,
|
||
start: 'top top',
|
||
end: () => `+=${distance()}`,
|
||
pin: true,
|
||
pinSpacing: true,
|
||
anticipatePin: 1,
|
||
scrub: 0.4,
|
||
invalidateOnRefresh: true,
|
||
refreshPriority: 5,
|
||
snap: {
|
||
snapTo: (value) => Math.round(value * steps) / steps,
|
||
duration: { min: 0.2, max: 0.5 },
|
||
ease: 'power2.inOut',
|
||
},
|
||
onUpdate: (self) => {
|
||
if (progressRef.current) progressRef.current.style.transform = `scaleX(${self.progress})`;
|
||
if (counterRef.current) {
|
||
const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * steps) + 1));
|
||
counterRef.current.textContent = `0${idx}`;
|
||
}
|
||
updateDepth();
|
||
},
|
||
},
|
||
});
|
||
|
||
updateDepth();
|
||
ScrollTrigger.refresh();
|
||
return () => {
|
||
tween.scrollTrigger?.kill();
|
||
tween.kill();
|
||
gsap.set(track, { clearProps: 'transform' });
|
||
gsap.set(cards, { clearProps: 'scale,opacity' });
|
||
};
|
||
});
|
||
|
||
return () => mm.revert();
|
||
}, [isLoaded]);
|
||
|
||
return (
|
||
<div className="products-section-wrapper">
|
||
<section
|
||
ref={sectionRef}
|
||
id="products-section"
|
||
className="products-section-root"
|
||
style={{ position: 'relative', zIndex: 3 }}
|
||
>
|
||
{/* ambient glow */}
|
||
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 80% 60% at 50% 0%, rgba(244, 190, 40, 0.07) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||
|
||
{/* Heading (fixed within the pinned viewport) */}
|
||
<div className="products-heading">
|
||
<div style={{ maxWidth: 520 }}>
|
||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 20, padding: '6px 16px 6px 8px', borderRadius: 100, background: '#ffffff', border: '1px solid rgba(244,190,40,0.3)', boxShadow: '0 1px 2px rgba(0,0,0,0.03), 0 12px 30px -18px rgba(23,21,18,0.2)' }}>
|
||
<span style={{ width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(244,190,40,0.15)' }}>
|
||
<img src="/images/loyalyai-logo.png" alt="" width={15} height={15} style={{ display: 'block', objectFit: 'contain' }} />
|
||
</span>
|
||
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#8a5a14', fontFamily: 'Sora, sans-serif' }}>Platform</span>
|
||
<span className="pv-live-dot" style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'block', marginLeft: 2 }} />
|
||
</div>
|
||
<h2 style={{ margin: 0, padding: '0 0 clamp(10px, 1.6vw, 22px)', fontSize: 'clamp(1.6rem, 3vw, 2.7rem)', fontWeight: 800, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif', whiteSpace: 'nowrap' }}>
|
||
Four agents.{' '}
|
||
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One AI.</span>
|
||
</h2>
|
||
</div>
|
||
|
||
<div className="products-heading-hint" style={{ maxWidth: 320, textAlign: 'right', borderRight: '2px solid rgba(244,190,40,0.3)', paddingRight: 'clamp(18px, 2.2vw, 28px)' }}>
|
||
<p style={{ margin: '0 0 18px', fontSize: 'clamp(0.85rem, 1vw, 1rem)', lineHeight: 1.6, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif' }}>
|
||
Powering every stage of the retail journey with intelligent AI solutions.
|
||
</p>
|
||
|
||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'flex-end', gap: 10 }}>
|
||
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Unified into one platform</span>
|
||
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.6rem', color: '#f4be28', letterSpacing: '-0.02em' }}>04</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Horizontal track */}
|
||
<div ref={trackRef} className="products-track">
|
||
{PRODUCTS.map((product, index) => (
|
||
<ProductCard key={product.id} product={product} index={index} />
|
||
))}
|
||
</div>
|
||
|
||
{/* Progress bar + counter */}
|
||
<div className="products-progress-wrap">
|
||
<span ref={counterRef} style={{ fontSize: 12, fontWeight: 800, color: '#C49600', fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em', minWidth: 22 }}>01</span>
|
||
<div style={{ position: 'relative', flex: 1, height: 3, borderRadius: 99, background: 'rgba(0,0,0,0.08)', overflow: 'hidden' }}>
|
||
<div ref={progressRef} style={{ position: 'absolute', inset: 0, borderRadius: 99, background: 'linear-gradient(90deg,#f4be28,#FF9800)', transformOrigin: 'left center', transform: 'scaleX(0)' }} />
|
||
</div>
|
||
<span style={{ fontSize: 12, fontWeight: 800, color: 'rgba(0,0,0,0.3)', fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em' }}>0{PRODUCTS.length}</span>
|
||
</div>
|
||
|
||
<style>{`
|
||
@keyframes prodShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
|
||
|
||
/* ── Product-visual loop animations ── */
|
||
.pv-cell { animation: pvCellPulse 3.6s ease-in-out infinite; }
|
||
@keyframes pvCellPulse {
|
||
0%, 100% { opacity: 0.55; transform: scale(1); }
|
||
50% { opacity: 1; transform: scale(1.06); }
|
||
}
|
||
.pv-live-dot { animation: pvLiveDot 1.8s ease-in-out infinite; }
|
||
@keyframes pvLiveDot {
|
||
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74,222,128,0.5); }
|
||
50% { opacity: 0.6; box-shadow: 0 0 0 4px rgba(74,222,128,0); }
|
||
}
|
||
.pv-chart-line {
|
||
stroke-dasharray: 100;
|
||
stroke-dashoffset: 100;
|
||
animation: pvDrawLine 3s ease-out infinite;
|
||
}
|
||
@keyframes pvDrawLine {
|
||
0% { stroke-dashoffset: 100; }
|
||
55%, 100% { stroke-dashoffset: 0; }
|
||
}
|
||
.pv-chart-pulse-ring {
|
||
transform-origin: center;
|
||
transform-box: fill-box;
|
||
animation: pvRingPulse 2.4s ease-out infinite;
|
||
}
|
||
@keyframes pvRingPulse {
|
||
0% { opacity: 0.9; transform: scale(1); }
|
||
100% { opacity: 0; transform: scale(2.6); }
|
||
}
|
||
.pv-uplift-pulse { display: inline-block; animation: pvUpliftPulse 2.6s ease-in-out infinite; }
|
||
@keyframes pvUpliftPulse {
|
||
0%, 100% { opacity: 0.75; }
|
||
50% { opacity: 1; }
|
||
}
|
||
.pv-progress-fill { position: relative; animation: pvProgressGrow 3.2s ease-in-out infinite; transform-origin: left center; }
|
||
@keyframes pvProgressGrow {
|
||
0%, 100% { transform: scaleX(0.94); }
|
||
50% { transform: scaleX(1); }
|
||
}
|
||
.pv-sheen {
|
||
position: absolute; inset: 0;
|
||
background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.35) 48%, transparent 66%);
|
||
background-size: 200% 100%;
|
||
animation: pvSheenSweep 4.5s ease-in-out infinite;
|
||
pointer-events: none;
|
||
}
|
||
@keyframes pvSheenSweep {
|
||
0% { background-position: 160% 0; }
|
||
60%, 100% { background-position: -60% 0; }
|
||
}
|
||
.pv-row { animation: pvRowGlow 3.6s ease-in-out infinite; }
|
||
@keyframes pvRowGlow {
|
||
0%, 100% { background: rgba(255,255,255,0.04); }
|
||
50% { background: rgba(74,222,128,0.08); }
|
||
}
|
||
.pv-streak-badge { animation: pvStreakFloat 2.8s ease-in-out infinite; }
|
||
@keyframes pvStreakFloat {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-3px); }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.pv-cell, .pv-live-dot, .pv-chart-line, .pv-chart-pulse-ring, .pv-uplift-pulse, .pv-progress-fill, .pv-sheen, .pv-row, .pv-streak-badge {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* Margin lives on this non-pinned wrapper, not on the pinned section
|
||
itself — GSAP's ScrollTrigger pin-spacer can miscalculate the
|
||
reserved space when the pinned element carries its own margin,
|
||
which shows up as a jump/shake right as the pin engages. */
|
||
.products-section-wrapper {
|
||
margin: 20px;
|
||
}
|
||
.products-section-root {
|
||
--pt: clamp(230px, 28vh, 310px);
|
||
--pb: clamp(64px, 8vh, 96px);
|
||
width: 100%;
|
||
border-radius: 32px;
|
||
background: #FAFAFA;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
.products-card {
|
||
flex: 0 0 auto;
|
||
width: clamp(320px, 90vw, 1280px);
|
||
/* Fill the band between heading and progress bar, capped on huge screens */
|
||
height: min(740px, calc(100vh - var(--pt) - var(--pb)));
|
||
}
|
||
.products-heading {
|
||
position: absolute; top: 0; left: 0; right: 0; z-index: 20;
|
||
display: flex; align-items: flex-end; justify-content: space-between;
|
||
padding: clamp(32px, 5vh, 56px) clamp(24px, 5vw, 68px) 0;
|
||
box-sizing: border-box; pointer-events: none;
|
||
}
|
||
.products-heading a { pointer-events: auto; }
|
||
.products-track {
|
||
position: absolute; top: 0; left: 0; height: 100%;
|
||
display: flex; align-items: center;
|
||
gap: clamp(20px, 2.4vw, 40px);
|
||
padding-left: clamp(24px, 5vw, 68px);
|
||
padding-right: clamp(24px, 5vw, 68px);
|
||
padding-top: var(--pt);
|
||
padding-bottom: var(--pb);
|
||
box-sizing: border-box;
|
||
will-change: transform;
|
||
}
|
||
.products-progress-wrap {
|
||
position: absolute; bottom: clamp(24px, 4vh, 44px);
|
||
left: 50%; transform: translateX(-50%); z-index: 20;
|
||
width: min(420px, 70vw);
|
||
display: flex; align-items: center; gap: 14px;
|
||
}
|
||
|
||
/* Mobile / small tablet: native horizontal swipe, no pin */
|
||
@media (max-width: 900px) {
|
||
.products-section-root {
|
||
height: auto;
|
||
overflow: visible;
|
||
padding-bottom: 28px;
|
||
}
|
||
.products-heading {
|
||
position: relative;
|
||
flex-direction: column; align-items: flex-start; gap: 16px;
|
||
padding: clamp(40px, 8vw, 64px) 24px 24px;
|
||
}
|
||
.products-heading-hint { text-align: left !important; }
|
||
.products-track {
|
||
position: relative; height: auto;
|
||
padding-top: 8px; padding-bottom: 8px;
|
||
overflow-x: auto; overflow-y: hidden;
|
||
scroll-snap-type: x mandatory;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
.products-track::-webkit-scrollbar { display: none; }
|
||
.products-track { scrollbar-width: none; }
|
||
.products-card { height: auto; width: min(90vw, 520px); }
|
||
.products-card { flex-direction: column !important; }
|
||
.products-card-content { flex: 0 0 auto !important; }
|
||
.products-card-visual {
|
||
flex: 0 0 auto !important;
|
||
min-height: 300px;
|
||
padding: 32px 0;
|
||
border-left: none !important;
|
||
border-top: 1px solid rgba(255,255,255,0.08);
|
||
}
|
||
.products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; }
|
||
}
|
||
`}</style>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|