Update homepage, use-cases, and people pages; add preloader and new section components

This commit is contained in:
R-Bharathraj
2026-07-01 18:06:06 +05:30
parent 7a15ee2b52
commit abcefbb9c9
26 changed files with 1178 additions and 897 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

View File

@@ -36,7 +36,7 @@ export default function AboutUsPage() {
minHeight: '100vh', background: 'radial-gradient(ellipse 120% 90% at 22% 0%, #3a2b5e 0%, #251c40 38%, #1a1530 100%)',
display: 'flex', alignItems: 'center',
padding: '120px 8% 80px',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
}}>
{/* Background photo */}

View File

@@ -67,7 +67,7 @@ export default function BlogMainPage() {
{/* ── HERO ── */}
<section style={{
padding: '120px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center',
}}>

View File

@@ -43,7 +43,7 @@ export default function BlogSinglePage() {
{/* Hero */}
<section style={{
padding: '140px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
}}>
{/* Background photo */}

View File

@@ -15,7 +15,7 @@ export default function ContactsPage() {
{/* ── HERO ── */}
<section style={{
padding: '120px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 78% 0%, #2e2350 0%, #221b3e 38%, #191529 100%)',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center',
}}>

View File

@@ -51,7 +51,7 @@ export default function FAQPage() {
{/* Hero */}
<section style={{
padding: '140px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center',
}}>

View File

@@ -14,6 +14,7 @@
html, body {
margin: 0;
padding: 0;
background: #ffffff;
}
html.lenis, html.lenis body {
@@ -246,11 +247,6 @@ body.is-home-page.scroll-up .sticky-active {
z-index: 999999999 !important;
}
/* Ensure page loader covers everything during initial render */
.page-loader-container {
z-index: 1000000000 !important;
}
/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
/* Disable utility animation classes used across the project */

View File

@@ -4,6 +4,7 @@ import Header from '@/components/Header';
import Footer from '@/components/Footer';
import GetInTouch from '@/components/GetInTouch';
import BodyClassManager from '@/components/BodyClassManager';
import Preloader from '@/components/Preloader';
export const metadata: Metadata = {
title: 'Loyaly.AI | AI Powered Retail Intelligence',
@@ -46,6 +47,7 @@ export default function RootLayout({
</head>
<body suppressHydrationWarning>
<BodyClassManager />
<Preloader />
<div className="body-container">
<Header />
{children}

View File

@@ -7,7 +7,8 @@ import Lenis from 'lenis';
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
import Link from 'next/link';
import NextImage from 'next/image';
import ProblemsShift from '@/components/home/ProblemsShift';
import ProblemSection from '@/components/home/ProblemSection';
import ShiftSection from '@/components/home/ShiftSection';
import WhatWeDo from '@/components/home/WhatWeDo';
import ProductsSection from '@/components/home/ProductsSection';
import TheLoop from '@/components/home/TheLoop';
@@ -32,7 +33,6 @@ const getDesktopServerSnapshot = () => true;
export default function IndexPage() {
const [imagesLoaded, setImagesLoaded] = useState(false);
const [loadProgress, setLoadProgress] = useState(0);
// Desktop pinned canvas hero vs. mobile static hero.
const isDesktop = useSyncExternalStore(subscribeDesktopQuery, getDesktopSnapshot, getDesktopServerSnapshot);
@@ -40,7 +40,6 @@ export default function IndexPage() {
const heroRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const preloaderRef = useRef<HTMLDivElement | null>(null);
const welcomeRef = useRef<HTMLDivElement | null>(null);
const imagesRef = useRef<HTMLImageElement[]>([]);
@@ -110,13 +109,15 @@ export default function IndexPage() {
drawFrame(currentFrameIndexRef.current);
}, [drawFrame]);
/* ── lock scroll until preloader exits ───────────────────────── */
/* ── lock scroll on desktop until frames are ready to avoid the
pinned ScrollTrigger engaging mid-scroll and snapping the page ─ */
useEffect(() => {
if (imagesLoaded || !isDesktop) return;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);
}, [imagesLoaded, isDesktop]);
/* ── load frame 0 immediately (canvas visible behind preloader) ─ */
/* ── load frame 0 immediately (canvas visible on first paint) ─── */
useEffect(() => {
const firstImg = new Image();
firstImg.onload = () => {
@@ -144,16 +145,14 @@ export default function IndexPage() {
const images: HTMLImageElement[] = [];
// Use RAF-throttled progress updates to avoid 192 rapid re-renders
// Use RAF-throttled updates to avoid 192 rapid re-renders
let rafId = 0;
const scheduleUpdate = (pct: number, done: boolean) => {
const scheduleUpdate = (done: boolean) => {
if (!done) return;
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
setLoadProgress(pct);
if (done) {
imagesRef.current = images;
setImagesLoaded(true);
}
imagesRef.current = images;
setImagesLoaded(true);
});
};
@@ -162,9 +161,8 @@ export default function IndexPage() {
const paddedIndex = String(i).padStart(3, '0');
const onLoad = () => {
loadedCountRef.current++;
const pct = Math.min(100, Math.round((loadedCountRef.current / frameCount) * 100));
const done = loadedCountRef.current >= frameCount;
scheduleUpdate(pct, done);
scheduleUpdate(done);
};
img.onload = onLoad;
img.onerror = onLoad;
@@ -195,30 +193,6 @@ export default function IndexPage() {
return () => { clearTimeout(t1); clearTimeout(t2); };
}, [imagesLoaded, handleResize]);
/* ── animate preloader out when all frames are ready ─────────── */
useEffect(() => {
if (!imagesLoaded) return;
const preloader = preloaderRef.current;
if (!preloader) return;
const tl = gsap.timeline({
delay: 0.2,
onComplete: () => {
preloader.style.display = 'none';
document.body.style.overflow = '';
},
});
tl.to('.pl-number', { opacity: 0, y: -50, duration: 0.55, ease: 'power3.in' }, 0)
.to('.pl-pct', { opacity: 0, duration: 0.4, ease: 'power2.in' }, 0.05)
.to('.pl-label', { opacity: 0, duration: 0.4, ease: 'power2.in' }, 0.1)
.to('.pl-bar', { scaleX: 1, duration: 0.25, ease: 'power3.out' }, 0)
.to(preloader, { yPercent: -100, duration: 1.05, ease: 'expo.inOut' }, 0.4);
// Kill the timeline on unmount so stale onComplete never runs after navigation
return () => { tl.kill(); };
}, [imagesLoaded]);
/* ── homepage header show/hide on scroll direction ───────────── */
useEffect(() => {
document.body.classList.add('is-home-page', 'scroll-down');
@@ -332,99 +306,12 @@ export default function IndexPage() {
/* ── render ───────────────────────────────────────────────────── */
return (
<>
{/* ══════════════════════════════════════════
PRELOADER
Fixed overlay — slides up when images done
══════════════════════════════════════════ */}
<div
ref={preloaderRef}
style={{
position: 'fixed', inset: 0, zIndex: 9999,
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center',
overflow: 'hidden',
}}
>
{/* Background: frame_000 blurred + dark overlay */}
<div style={{
position: 'absolute', inset: 0,
backgroundImage: `url('/img_frames/frame_000_delay-0.041s.jpg')`,
backgroundSize: 'cover', backgroundPosition: 'center',
filter: 'blur(3px)', transform: 'scale(1.06)',
}} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(160deg,rgba(7,7,10,0.88) 0%,rgba(10,8,5,0.92) 100%)' }} />
{/* Noise grain */}
<div style={{
position: 'absolute', inset: 0, opacity: 0.035, mixBlendMode: 'overlay',
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
}} />
{/* Brand wordmark */}
<div className="pl-label" style={{
position: 'relative', zIndex: 2,
fontSize: 11, fontWeight: 800, letterSpacing: '0.3em',
textTransform: 'uppercase', color: 'rgba(244,190,40,0.75)',
fontFamily: 'Sora, sans-serif', marginBottom: 36,
userSelect: 'none',
}}>
Loyaly
</div>
{/* Big percentage counter */}
<div style={{ position: 'relative', zIndex: 2, display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<span className="pl-number" style={{
fontSize: 'clamp(88px, 14vw, 156px)',
fontWeight: 900, lineHeight: 0.86,
letterSpacing: '-0.05em', color: '#ffffff',
fontFamily: 'Sora, sans-serif',
fontVariantNumeric: 'tabular-nums',
userSelect: 'none',
willChange: 'transform, opacity',
}}>
{loadProgress}
</span>
<span className="pl-pct" style={{
fontSize: 'clamp(22px, 3.5vw, 38px)',
fontWeight: 800, color: 'rgba(255,255,255,0.35)',
fontFamily: 'Sora, sans-serif', marginTop: 10,
userSelect: 'none',
}}>%</span>
</div>
{/* Sub-label */}
<div className="pl-label" style={{
position: 'relative', zIndex: 2,
fontSize: 11, fontWeight: 600, letterSpacing: '0.2em',
textTransform: 'uppercase', color: 'rgba(255,255,255,0.28)',
fontFamily: 'Sora, sans-serif', marginTop: 28,
userSelect: 'none',
}}>
Loading experience
</div>
{/* Progress bar (full width, bottom) */}
<div className="pl-bar-track" style={{
position: 'absolute', bottom: 0, left: 0, right: 0, height: 3,
background: 'rgba(255,255,255,0.06)',
}}>
<div className="pl-bar" style={{
height: '100%',
width: `${loadProgress}%`,
background: 'linear-gradient(90deg, #f4be28, #FF9800)',
transition: 'width 0.12s linear',
borderRadius: '0 2px 2px 0',
}} />
</div>
</div>
{/* ══════════════════════════════════════════
MOBILE HERO — static image below text content
══════════════════════════════════════════ */}
{!isDesktop && (
<section
className="loyaly-home relative bg-white overflow-hidden"
style={{ margin: '0 20px 20px', borderRadius: '25px' }}
style={{ margin: '20px', borderRadius: '25px', display: isDesktop ? 'none' : 'block' }}
>
<div className="px-6 sm:px-8 pt-12 pb-8 flex flex-col">
<h1 className="text-[#171512] hero-heading text-3xl sm:text-4xl font-extrabold tracking-tight leading-[1.12] mb-6 select-none">
@@ -457,20 +344,19 @@ export default function IndexPage() {
className="w-full h-auto block"
/>
</section>
)}
{/* ══════════════════════════════════════════
HERO — pinned, drives canvas frame scrub
══════════════════════════════════════════ */}
{isDesktop && (
<main
ref={containerRef}
className="loyaly-home relative bg-white overflow-hidden"
style={{
width: 'calc(100% - 40px)',
height: 'calc(100vh - 20px)',
margin: '0 20px 20px',
height: 'calc(100vh - 40px)',
margin: '20px',
borderRadius: '25px',
display: isDesktop ? 'block' : 'none',
}}
>
{/* Canvas */}
@@ -575,10 +461,10 @@ export default function IndexPage() {
</div>
</div>
</main>
)}
{/* Other sections */}
<ProblemsShift isLoaded={imagesLoaded} />
<ProblemSection isLoaded={imagesLoaded} />
<ShiftSection isLoaded={imagesLoaded} />
<WhatWeDo isLoaded={imagesLoaded} />
<ProductsSection isLoaded={imagesLoaded} />
<TheLoop />

View File

@@ -52,7 +52,7 @@ export default function SolutionsPage() {
minHeight: '92vh', background: 'radial-gradient(ellipse 120% 90% at 70% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
display: 'flex', alignItems: 'center',
padding: '120px 8% 100px',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
}}>
{/* Background photo */}

View File

@@ -91,7 +91,7 @@ export default function ForBusinessPage() {
minHeight: '100vh', background: 'radial-gradient(ellipse 120% 90% at 30% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
display: 'flex', alignItems: 'center',
padding: '120px 8% 100px', position: 'relative', overflow: 'hidden',
margin: '0 20px 20px', borderRadius: '0 0 32px 32px',
margin: '20px', borderRadius: '32px',
}}>
{/* Background photo */}
<Image src="/images/stock/use-cases.jpg" alt="" fill priority style={{ objectFit: 'cover', opacity: 0.9 }} />
@@ -264,27 +264,33 @@ export default function ForBusinessPage() {
</section>
{/* ── WHO IT'S FOR */}
<section style={{ padding: '140px 8%', background: '#07070d', margin: '0 20px 20px', borderRadius: 24 }}>
<RevealBlock>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 20 }}>Who It's For</div>
<h2 style={{ fontSize: 'clamp(36px, 5vw, 68px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: '0 0 80px', fontFamily: 'Sora, sans-serif', maxWidth: 600 }}>
Built for every format of physical retail.
</h2>
</RevealBlock>
<div className="uc-who-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
{WHO.map((w, i) => (
<RevealBlock key={i} delay={i * 0.1}>
<div style={{
padding: '48px 32px',
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 20, backdropFilter: 'blur(12px)',
transition: 'all 0.3s ease',
}}>
<div style={{ marginBottom: 20 }}>{w.icon}</div>
<div style={{ fontSize: 20, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{w.label}</div>
</div>
</RevealBlock>
))}
<section style={{ padding: '140px 8%', background: '#07070d', margin: '0 20px 20px', borderRadius: 24, position: 'relative', overflow: 'hidden' }}>
{/* Background photo */}
<Image src="/images/stock/physical retail.png" alt="" fill style={{ objectFit: 'cover', opacity: 0.35 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(7,7,13,0.8) 0%, rgba(7,7,13,0.95) 100%)', pointerEvents: 'none' }} />
<div style={{ position: 'relative', zIndex: 1 }}>
<RevealBlock>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 20 }}>Who It's For</div>
<h2 style={{ fontSize: 'clamp(36px, 5vw, 68px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: '0 0 80px', fontFamily: 'Sora, sans-serif', maxWidth: 600 }}>
Built for every format of physical retail.
</h2>
</RevealBlock>
<div className="uc-who-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
{WHO.map((w, i) => (
<RevealBlock key={i} delay={i * 0.1}>
<div style={{
padding: '48px 32px',
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 20, backdropFilter: 'blur(12px)',
transition: 'all 0.3s ease',
}}>
<div style={{ marginBottom: 20 }}>{w.icon}</div>
<div style={{ fontSize: 20, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{w.label}</div>
</div>
</RevealBlock>
))}
</div>
</div>
</section>

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useSyncExternalStore } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
@@ -14,22 +14,26 @@ const NAV_LINKS = [
{ label: 'Meet the Crew', href: '/about_us' },
];
const MOBILE_QUERY = '(max-width: 1023px)';
const subscribeMobileQuery = (callback: () => void) => {
const mql = window.matchMedia(MOBILE_QUERY);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
};
const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches;
// Server has no viewport, so it always renders the desktop header; the client's
// first render must match that to avoid a hydration mismatch.
const getMobileServerSnapshot = () => false;
export default function Header() {
const pathname = usePathname();
const [loading, setLoading] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const mobileMenuRef = useRef<HTMLDivElement>(null);
// Hide loader after brief mount delay
useEffect(() => {
const t = setTimeout(() => setLoading(false), 400);
return () => clearTimeout(t);
}, []);
// Detect scroll to show/hide shadow on header
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 300);
const onScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
@@ -60,28 +64,15 @@ export default function Header() {
const isActive = (href: string) =>
href === '/' ? pathname === '/' : pathname.startsWith(href);
// Mobile keeps the original plain white header — the transparent hero overlay is desktop-only.
const isMobileViewport = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
// Every page's hero is built with top clearance for an overlaid header, so let it
// show through transparently until the user scrolls (desktop only).
const transparentHeader = !scrolled && !isMobileViewport;
return (
<div suppressHydrationWarning>
{/* Page pre-loader */}
{loading && (
<div
className="page-loader-container"
aria-hidden="true"
style={{ position: 'fixed', inset: 0, zIndex: 1000000000, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff8e8' }}
>
<div className="page-loader">
<Image
src="/images/loyalyai-logo.png"
alt="Loyaly.AI"
width={100}
height={100}
className="page-loader-logo"
priority
/>
</div>
</div>
)}
{/* Mobile menu overlay */}
{mobileOpen && (
<div
@@ -214,23 +205,21 @@ export default function Header() {
</Link>
</div>
{/* Spacer only needed when header is fixed */}
{scrolled && <div style={{ height: '88px' }} aria-hidden="true" />}
{/* ——— Header: in-flow until 100px scroll, then fixed ——— */}
{/* ——— Header: always overlays the hero; goes solid once scrolled ——— */}
<header
className={transparentHeader ? 'header-transparent' : ''}
style={{
position: scrolled ? 'fixed' : 'relative',
position: 'fixed',
top: 0,
left: 0,
right: 0,
left: '20px',
right: '20px',
width: 'auto',
zIndex: 99999999,
background: '#ffffff',
background: transparentHeader ? 'transparent' : '#ffffff',
borderBottomLeftRadius: '24px',
borderBottomRightRadius: '24px',
boxShadow: scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)',
transition: 'box-shadow 0.3s ease',
boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)'),
transition: 'background 0.3s ease, box-shadow 0.3s ease',
animation: scrolled ? 'slideDown 0.3s ease' : 'none',
}}
>
@@ -239,20 +228,21 @@ export default function Header() {
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 48px',
height: '88px',
height: transparentHeader ? '136px' : '88px',
width: '100%',
boxSizing: 'border-box',
transition: 'height 0.3s ease',
}}>
{/* Logo */}
<div style={{ flexShrink: 0, flex: 1, minWidth: 120 }}>
<Link href="/" aria-label="Loyaly.AI home" style={{ display: 'inline-flex', alignItems: 'center', textDecoration: 'none' }}>
<Image
src="/images/loyalyai.png"
src={transparentHeader ? '/images/logo-dark.png' : '/images/loyalyai.png'}
alt="Loyaly.AI"
width={160}
height={48}
width={460}
height={138}
className="header-logo-img"
style={{ objectFit: 'contain', height: '42px', width: 'auto' }}
style={{ objectFit: 'contain', height: transparentHeader ? '110px' : '42px', width: 'auto' }}
priority
/>
</Link>
@@ -310,7 +300,7 @@ export default function Header() {
display: 'inline-flex',
alignItems: 'center',
padding: '12px 28px',
background: 'transparent',
background: '#ffffff',
color: '#1a1a1a',
border: '1.5px solid #c9960f',
borderRadius: '50px',
@@ -356,12 +346,14 @@ export default function Header() {
.header-nav-link { padding: 8px 12px !important; font-size: 11px !important; letter-spacing: 0.02em !important; }
.header-cta { padding: 9px 16px !important; font-size: 12px !important; }
.header-logo-img { height: 32px !important; }
.header-transparent .header-logo-img { height: 92px !important; }
}
@media (min-width: 1281px) and (max-width: 1440px) {
.header-nav-pill { padding: 6px 8px !important; }
.header-nav-link { padding: 9px 16px !important; font-size: 12px !important; }
.header-cta { padding: 10px 20px !important; font-size: 13px !important; }
.header-logo-img { height: 36px !important; }
.header-transparent .header-logo-img { height: 100px !important; }
}
.header-cta:hover {
background: #f4be28 !important;

View File

@@ -0,0 +1,91 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { gsap } from 'gsap';
import Image from 'next/image';
export default function Preloader() {
const [visible, setVisible] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const logoRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let cancelled = false;
let dismissed = false;
let minHoldTimer: ReturnType<typeof setTimeout>;
let hardCapTimer: ReturnType<typeof setTimeout>;
let onLoad: (() => void) | null = null;
const dismiss = () => {
if (dismissed || cancelled) return;
dismissed = true;
const container = containerRef.current;
const logo = logoRef.current;
if (!container) {
setVisible(false);
return;
}
const tl = gsap.timeline({
onComplete: () => setVisible(false),
});
if (logo) {
tl.to(logo, { opacity: 0, scale: 0.9, duration: 0.35, ease: 'power2.in' }, 0);
}
tl.to(container, { opacity: 0, duration: 0.5, ease: 'power2.inOut' }, logo ? 0.1 : 0);
};
// Wait for the actual page load (images, fonts, etc.), with a small
// minimum hold so the screen doesn't just flicker, and a hard cap so
// a slow asset never traps the visitor behind the screen.
const minHold = new Promise<void>((resolve) => { minHoldTimer = setTimeout(resolve, 900); });
const pageLoad = document.readyState === 'complete'
? Promise.resolve()
: new Promise<void>((resolve) => {
onLoad = () => resolve();
window.addEventListener('load', onLoad, { once: true });
});
Promise.all([minHold, pageLoad]).then(() => { if (!cancelled) dismiss(); });
hardCapTimer = setTimeout(dismiss, 4000);
return () => {
cancelled = true;
clearTimeout(minHoldTimer);
clearTimeout(hardCapTimer);
if (onLoad) window.removeEventListener('load', onLoad);
};
}, []);
if (!visible) return null;
return (
<div
ref={containerRef}
aria-hidden="true"
style={{
position: 'fixed',
inset: 0,
zIndex: 999999,
background: '#ffffff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
}}
>
<div ref={logoRef} className="preloader-logo">
<Image
src="/images/loyalyai-logo.png"
alt="Loyaly.AI"
width={110}
height={110}
priority
/>
</div>
</div>
);
}

View File

@@ -18,7 +18,7 @@ export default function DiscoverDealsSection() {
<section className="fp-panel" style={{ background: '#08080f' }}>
{/* Background photo */}
<Image src="/images/stock/deals-retail.jpg" alt="" fill style={{ objectFit: 'cover', opacity: 0.25, zIndex: 0 }} />
<Image src="/images/stock/deals-retail.png" alt="" fill style={{ objectFit: 'cover', opacity: 0.25, zIndex: 0 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(8,8,15,0.3) 0%, rgba(8,8,15,0.75) 80%)', zIndex: 0 }} />
{/* Soft radial */}

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useState } from 'react';
import Image from 'next/image';
const GroceryIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>;
const FashionIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.38 3.46L16 2a4 4 0 01-8 0L3.62 3.46a2 2 0 00-1.34 2.23l.58 3.57a1 1 0 00.99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 002-2V10h2.15a1 1 0 00.99-.84l.58-3.57a2 2 0 00-1.34-2.23z"/></svg>;
@@ -23,6 +24,10 @@ export default function SpendAnywhereSection() {
return (
<section className="fp-panel" style={{ background: '#080810' }}>
{/* Background photo */}
<Image src="/images/stock/spend-anywhere.png" alt="" fill style={{ objectFit: 'cover', opacity: 0.25, zIndex: 0 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(8,8,16,0.3) 0%, rgba(8,8,16,0.75) 80%)', zIndex: 0 }} />
<div className="fp-shell">
{/* Header row */}
@@ -45,40 +50,90 @@ export default function SpendAnywhereSection() {
</div>
{/* Category grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 14 }}>
{brands.map((brand, i) => (
<div
key={i}
onMouseEnter={() => setActive(i)}
onMouseLeave={() => setActive(null)}
className="fp-rise"
style={{
padding: '22px 24px', borderRadius: 18,
background: active === i ? `${brand.color}10` : 'rgba(255,255,255,0.03)',
border: `1px solid ${active === i ? `${brand.color}40` : 'rgba(255,255,255,0.07)'}`,
backdropFilter: 'blur(12px)', cursor: 'default',
transitionDelay: `${0.08 + i * 0.06}s`,
transform: active === i ? 'translateY(-5px)' : 'translateY(0)',
boxShadow: active === i ? `0 22px 60px ${brand.color}1f, inset 0 1px 0 rgba(255,255,255,0.08)` : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 11, marginBottom: 14 }}>
{(() => { const BrandIcon = brand.Icon; return <span style={{ color: active === i ? brand.color : 'rgba(255,255,255,0.45)', transition: 'color 0.3s' }}><BrandIcon /></span>; })()}
<span style={{ fontSize: 14, fontWeight: 700, color: active === i ? '#fff' : 'rgba(255,255,255,0.75)', fontFamily: 'Sora, sans-serif', transition: 'color 0.2s' }}>{brand.name}</span>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 16 }}>
{brands.map((brand, i) => {
const BrandIcon = brand.Icon;
return (
<div
key={i}
onMouseEnter={() => setActive(i)}
onMouseLeave={() => setActive(null)}
className="fp-rise"
style={{
position: 'relative',
padding: '24px 28px',
borderRadius: 22,
background: active === i ? 'rgba(12, 12, 22, 0.65)' : 'rgba(10, 10, 18, 0.45)',
border: `1px solid ${active === i ? `${brand.color}45` : 'rgba(255,255,255,0.08)'}`,
backdropFilter: 'blur(20px) saturate(140%)',
WebkitBackdropFilter: 'blur(20px) saturate(140%)',
cursor: 'default',
transitionDelay: `${0.08 + i * 0.06}s`,
transform: active === i ? 'translateY(-6px)' : 'translateY(0)',
boxShadow: active === i
? `0 24px 50px -12px rgba(0,0,0,0.65), 0 0 25px -4px ${brand.color}25, inset 0 1px 0 rgba(255,255,255,0.1)`
: '0 8px 30px -10px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255,255,255,0.05)',
transition: 'all 0.4s cubic-bezier(0.25, 1, 0.22, 1)',
overflow: 'hidden',
}}
>
{/* Glowing Left Accent Bar */}
<span style={{
position: 'absolute', left: 0, top: '20%', bottom: '20%', width: 3,
borderRadius: '0 4px 4px 0',
background: brand.color,
opacity: active === i ? 1 : 0,
transform: active === i ? 'scaleY(1.2)' : 'scaleY(0.8)',
transition: 'all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)',
boxShadow: `0 0 12px ${brand.color}`,
}} />
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 18 }}>
{/* Icon Container with subtle background shape */}
<div style={{
width: 42,
height: 42,
borderRadius: 13,
background: active === i ? `${brand.color}20` : 'rgba(255,255,255,0.04)',
border: `1px solid ${active === i ? `${brand.color}45` : 'rgba(255,255,255,0.09)'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: active === i ? brand.color : 'rgba(255,255,255,0.45)',
transition: 'all 0.35s cubic-bezier(0.25, 1, 0.22, 1)',
}}>
<BrandIcon />
</div>
<span style={{
fontSize: 15,
fontWeight: 800,
color: active === i ? '#fff' : 'rgba(255,255,255,0.75)',
fontFamily: 'Sora, sans-serif',
letterSpacing: '-0.01em',
transition: 'color 0.3s ease',
}}>{brand.name}</span>
</div>
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
{brand.stores.map((s, j) => (
<span key={j} style={{
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.04em',
fontFamily: 'Sora, sans-serif',
color: active === i ? '#ffffff' : 'rgba(255,255,255,0.3)',
background: active === i ? `${brand.color}22` : 'rgba(255,255,255,0.04)',
border: `1px solid ${active === i ? `${brand.color}40` : 'rgba(255,255,255,0.06)'}`,
borderRadius: 100,
padding: '5px 12px',
transition: 'all 0.3s cubic-bezier(0.25, 1, 0.22, 1)',
}}>{s}</span>
))}
</div>
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{brand.stores.map((s, j) => (
<span key={j} style={{
fontSize: 11, fontWeight: 600, fontFamily: 'Sora, sans-serif',
color: active === i ? brand.color : 'rgba(255,255,255,0.3)',
background: active === i ? `${brand.color}18` : 'rgba(255,255,255,0.04)',
border: `1px solid ${active === i ? `${brand.color}30` : 'rgba(255,255,255,0.06)'}`,
borderRadius: 100, padding: '4px 10px', transition: 'all 0.25s ease',
}}>{s}</span>
))}
</div>
</div>
))}
);
})}
</div>
{/* Stat strip */}

View File

@@ -129,25 +129,6 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props
{/* Stacked sections */}
{children}
{/* Side section rail (desktop) */}
{sections.length > 0 && (
<nav className="fp-rail" aria-label="Sections">
{sections.map((label, i) => (
<button
key={label}
className={`fp-rail-item${i === active ? ' is-active' : ''}`}
onClick={() => goTo(i)}
>
<span className="fp-rail-label">
<span className="fp-rail-idx">{String(i + 1).padStart(2, '0')}</span>
{label}
</span>
<span className="fp-rail-dot" />
</button>
))}
</nav>
)}
{/* Scroll progress (bottom) */}
<div className="fp-progress" aria-hidden>
<div className="fp-progress-bar" style={{ transform: `scaleX(${progress})` }} />
@@ -280,34 +261,6 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props
box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06);
}
/* ── Side rail (dot sits in the gutter; label is a floating pill) ── */
.fp-rail {
position: fixed; right: 13px; top: 50%; transform: translateY(-50%);
z-index: 40; display: flex; flex-direction: column; gap: 6px;
}
.fp-rail-item {
display: flex; align-items: center; justify-content: flex-end; gap: 10px;
background: none; border: none; cursor: pointer; padding: 6px 0; color: inherit;
}
.fp-rail-dot {
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
background: rgba(150,150,160,0.55); transition: all 0.35s cubic-bezier(0.34,1.56,0.64,1);
}
.fp-rail-item.is-active .fp-rail-dot { background: var(--gold); box-shadow: 0 0 12px rgba(244,190,40,0.8); transform: scale(1.3); }
.fp-rail-label {
display: flex; align-items: center; gap: 8px;
font-family: 'Sora', sans-serif; font-size: 11px; font-weight: 600;
letter-spacing: 0.04em; color: rgba(255,255,255,0.8); white-space: nowrap;
padding: 5px 12px; border-radius: 100px;
background: rgba(12,12,18,0.72); border: 1px solid rgba(255,255,255,0.08);
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
opacity: 0; transform: translateX(8px); pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fp-rail-idx { font-family: 'Sora', sans-serif; font-size: 9px; color: var(--gold); }
.fp-rail-item:hover .fp-rail-label,
.fp-rail-item.is-active .fp-rail-label { opacity: 1; transform: translateX(0); }
/* ── Scroll progress ── */
.fp-progress {
position: fixed; left: 0; right: 0; bottom: 0; height: 2px; z-index: 40;
@@ -339,7 +292,6 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props
/* ── Mobile ── */
@media (max-width: 900px) {
.fp-rail { display: none; }
.fp-cursor { display: none; }
.fp-panel {
padding: 96px 0 80px; min-height: auto;

View File

@@ -31,7 +31,7 @@ export default function WhyLoyalySection() {
const bars = [
{ label: 'Online retail loyalty', pct: 78, color: '#171512' },
{ label: 'Offline retail loyalty', pct: 2, color: '#ef4444' },
{ label: 'Offline retail loyalty', pct: 2, color: '#bc7122' },
{ label: 'Loyaly target', pct: 60, color: '#f4be28' },
];
@@ -67,7 +67,7 @@ export default function WhyLoyalySection() {
<div className="fp-rise" style={{ marginTop: 20, marginBottom: 32 }}>
<div style={{
display: 'inline-block', padding: '8px 16px', borderRadius: 10,
background: '#ef4444', color: '#fff',
background: '#bc7122', color: '#fff',
fontSize: 12, fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em', textTransform: 'uppercase',
}}>Loyalty penetration in offline retail</div>
</div>
@@ -76,7 +76,7 @@ export default function WhyLoyalySection() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{[
{ val: '₹2.1L Cr', label: 'Offline retail market size', accent: '#171512' },
{ val: '<2%', label: 'Currently getting rewards', accent: '#ef4444' },
{ val: '<2%', label: 'Currently getting rewards', accent: '#bc7122' },
{ val: '10Cr+', label: 'Shoppers Loyaly can reach', accent: '#16a34a' },
].map((m, i) => (
<div key={i} className="fp-rise" style={{
@@ -109,7 +109,7 @@ export default function WhyLoyalySection() {
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
}}>
<span className="fp-mask"><span>Millions shop offline.</span></span>
<span className="fp-mask"><span style={{ color: '#ef4444' }}>Nobody rewards them.</span></span>
<span className="fp-mask"><span style={{ color: '#bc7122' }}>Nobody rewards them.</span></span>
</h2>
<p className="fp-rise" style={{

View File

@@ -0,0 +1,225 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface ProblemSectionProps {
isLoaded?: boolean;
}
interface Item {
text: string;
icon: React.ReactNode;
}
const PROBLEM_ITEMS: Item[] = [
{
text: 'No compelling reason for customers to visit your store',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
),
},
{
text: 'Traditional discounts erode profit margins',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
),
},
{
text: 'Digital ads rarely convert into store footfall',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6l8.25 8.25 4.5-4.5 6.75 6.75M21.75 16.5V11.25m0 5.25H16.5" />
),
},
{
text: 'No real-time connection between online activity and offline shopping',
icon: (
<>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
</>
),
},
{
text: 'Customers forget about stores between visits',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6l4 2M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
),
},
];
const STATS = [
{ value: '68%', label: 'walk in once, never return' },
{ value: '0', label: 'visibility after they leave' },
];
export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) {
const sectionRef = useRef<HTMLDivElement>(null);
const accent = '#ef4444';
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return;
if (!isLoaded) return;
const ctx = gsap.context(() => {
const headerTl = gsap.timeline({
scrollTrigger: { trigger: '.problem-header', start: 'top 85%', toggleActions: 'play none none reset' },
});
headerTl
.fromTo('.problem-eyebrow', { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.6, ease: 'power3.out' })
.fromTo('.problem-title', { opacity: 0, y: 40, clipPath: 'polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)' }, { opacity: 1, y: 0, clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', duration: 1.1, ease: 'power4.out' }, '-=0.3')
.fromTo('.problem-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8');
const rowTl = gsap.timeline({
scrollTrigger: { trigger: '.problem-list', start: 'top 82%', toggleActions: 'play none none reset' },
});
rowTl.fromTo('.problem-row', { opacity: 0, x: -30 }, { opacity: 1, x: 0, duration: 0.6, stagger: 0.09, ease: 'power3.out' });
const visualTl = gsap.timeline({
scrollTrigger: { trigger: '.problem-visual', start: 'top 78%', toggleActions: 'play none none reset' },
});
visualTl
.fromTo('.problem-visual-stage', { opacity: 0, scale: 0.9, y: 40 }, { opacity: 1, scale: 1, y: 0, duration: 1, ease: 'power3.out' })
.fromTo('.problem-chip', { opacity: 0, y: 18, scale: 0.85 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, stagger: 0.15, ease: 'back.out(1.6)' }, '-=0.5');
gsap.to('.problem-visual-img', {
y: -22,
ease: 'none',
scrollTrigger: { trigger: '.problem-visual', start: 'top bottom', end: 'bottom top', scrub: 1 },
});
ScrollTrigger.refresh();
}, sectionRef);
return () => ctx.revert();
}, [isLoaded]);
return (
<section
ref={sectionRef}
id="problem"
className="relative overflow-hidden problem-section"
style={{
background: '#ffffff',
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: '32px',
padding: 'clamp(72px, 9vw, 130px) clamp(20px, 4vw, 64px) clamp(64px, 7vw, 96px)',
zIndex: 1,
boxShadow: 'inset 0 0 0 1px rgba(23,21,18,0.05)',
}}
>
{/* Faint dot-grid texture, just enough to keep the white from feeling flat */}
<div
style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
backgroundImage: 'radial-gradient(rgba(23,21,18,0.05) 1px, transparent 1px)',
backgroundSize: '28px 28px',
maskImage: 'radial-gradient(ellipse 75% 60% at 50% 25%, #000 0%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse 75% 60% at 50% 25%, #000 0%, transparent 75%)',
}}
/>
{/* Single whisper-soft accent glow, top right */}
<div style={{ position: 'absolute', top: '-8%', right: '-6%', width: 480, height: 480, borderRadius: '50%', background: `radial-gradient(circle, ${accent}12 0%, transparent 72%)`, pointerEvents: 'none' }} />
{/* Header */}
<div className="problem-header relative z-10 mx-auto problem-header-grid" style={{ maxWidth: 1200, marginBottom: 'clamp(48px, 6vw, 80px)', display: 'grid', gridTemplateColumns: 'minmax(0,1.3fr) minmax(0,1fr)', gap: 'clamp(28px, 4vw, 60px)', alignItems: 'end' }}>
<div>
<div className="problem-eyebrow" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 22, padding: '6px 16px 6px 8px', borderRadius: 100, background: '#ffffff', border: `1px solid ${accent}2a`, 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: `${accent}0d` }}>
<img src="/images/loyalyai-logo.png" alt="" width={15} height={15} style={{ display: 'block', objectFit: 'contain' }} />
</span>
<span style={{ color: accent, fontSize: 12, fontWeight: 800, letterSpacing: '0.14em', textTransform: 'uppercase', fontFamily: 'Sora, sans-serif' }}>The Problem</span>
</div>
<h2 className="problem-title" style={{ color: '#171512', fontWeight: 800, fontSize: 'clamp(2.6rem, 5.6vw, 4.6rem)', letterSpacing: '-0.05em', lineHeight: 0.96, margin: 0, fontFamily: 'Sora, sans-serif' }}>
Retail is busy.<br /><span style={{ color: accent }}>But blind.</span>
</h2>
</div>
<div className="problem-header-right" style={{ paddingBottom: 'clamp(4px, 1vw, 10px)', borderLeft: '2px solid rgba(23,21,18,0.1)', paddingLeft: 'clamp(20px, 2.4vw, 32px)' }}>
<p className="problem-subtitle" style={{ color: '#4f463a', fontSize: 'clamp(1rem, 1.2vw, 1.15rem)', fontWeight: 500, margin: '0 0 18px', lineHeight: 1.7, fontFamily: 'Sora, sans-serif' }}>
Retailers struggle to attract new customers and retain existing ones without sacrificing profit margins on generic discounts.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.6rem', color: accent, letterSpacing: '-0.02em' }}>05</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.85rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Recurring pain points, every quarter</span>
</div>
</div>
</div>
{/* Split layout */}
<div className="relative z-10 mx-auto grid problem-grid" style={{ maxWidth: 1200, display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 'clamp(32px, 5vw, 72px)', alignItems: 'center' }}>
{/* Visual */}
<div className="problem-visual order-2 md:order-1" style={{ position: 'relative' }}>
<div
className="problem-visual-stage"
style={{
position: 'relative',
borderRadius: 28,
background: 'transparent',
padding: 'clamp(12px, 2vw, 24px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
minHeight: 'clamp(340px, 38vw, 520px)',
overflow: 'hidden',
}}
>
<div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse 65% 55% at 50% 55%, ${accent}1c 0%, transparent 72%)`, pointerEvents: 'none' }} />
<img
src="/Problems+shift/Problems.png"
alt="Customers walking past a store without engaging"
className="problem-visual-img"
style={{ maxWidth: '112%', maxHeight: '118%', objectFit: 'contain', filter: 'drop-shadow(0 26px 34px rgba(23,21,18,0.18))', pointerEvents: 'none', userSelect: 'none' }}
/>
</div>
{/* Floating stat chips */}
<div className="problem-chip" style={{ position: 'absolute', top: '8%', left: '-6%', display: 'flex', alignItems: 'baseline', gap: 8, background: '#ffffff', borderRadius: 16, padding: '12px 18px', boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 20px 40px -20px rgba(23,21,18,0.28)', border: '1px solid rgba(23,21,18,0.05)' }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.35rem', color: accent }}>{STATS[0].value}</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: '#6b6255', lineHeight: 1.3, maxWidth: 110 }}>{STATS[0].label}</span>
</div>
<div className="problem-chip" style={{ position: 'absolute', bottom: '6%', right: '-4%', display: 'flex', alignItems: 'baseline', gap: 8, background: '#171512', borderRadius: 16, padding: '12px 18px', boxShadow: '0 20px 44px -18px rgba(23,21,18,0.45)' }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.35rem', color: '#fff' }}>{STATS[1].value}</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: 'rgba(255,255,255,0.6)', lineHeight: 1.3, maxWidth: 100 }}>{STATS[1].label}</span>
</div>
</div>
{/* List */}
<div className="problem-list order-1 md:order-2" style={{ display: 'flex', flexDirection: 'column' }}>
{PROBLEM_ITEMS.map((item, idx) => (
<div
key={idx}
className="problem-row"
style={{
display: 'flex', alignItems: 'center', gap: 16,
padding: 'clamp(16px, 1.8vw, 20px) 4px',
borderBottom: idx !== PROBLEM_ITEMS.length - 1 ? '1px solid rgba(23,21,18,0.08)' : 'none',
}}
>
<span style={{ width: 42, height: 42, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 13, background: `${accent}12`, border: `1px solid ${accent}26` }}>
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2">{item.icon}</svg>
</span>
<span style={{ color: '#2e2a24', fontSize: '1rem', fontWeight: 600, lineHeight: 1.4, fontFamily: 'Sora, sans-serif' }}>{item.text}</span>
</div>
))}
</div>
</div>
<style>{`
@media (max-width: 900px) {
#problem .problem-grid { grid-template-columns: 1fr; }
#problem .problem-chip { display: none; }
#problem .problem-header-grid { grid-template-columns: 1fr; }
#problem .problem-header-right { border-left: none; padding-left: 0; border-top: 2px solid rgba(23,21,18,0.1); padding-top: 20px; margin-top: 20px; }
}
`}</style>
</section>
);
}

View File

@@ -1,348 +0,0 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface ProblemsShiftProps {
isLoaded?: boolean;
}
/* ── Data ──────────────────────────────────────────────────────────── */
interface Item {
text: string;
icon: React.ReactNode;
}
const PROBLEM_ITEMS: Item[] = [
{
text: 'No compelling reason for customers to visit your store',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
),
},
{
text: 'Traditional discounts erode profit margins',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
),
},
{
text: 'Digital ads rarely convert into store footfall',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6l8.25 8.25 4.5-4.5 6.75 6.75M21.75 16.5V11.25m0 5.25H16.5" />
),
},
{
text: 'No real-time connection between online activity and offline shopping',
icon: (
<>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
</>
),
},
{
text: 'Customers forget about stores between visits',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6l4 2M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
),
},
];
const SHIFT_ITEMS: Item[] = [
{
text: 'Customers earn Loyaly Coins through daily app activities',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
),
},
{
text: 'Game-based campaigns drive footfall with excitement',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" />
),
},
{
text: 'Smart surprise coupons create a daily reason to engage',
icon: (
<>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="5" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" />
</>
),
},
{
text: 'Real-time discovery of nearby stores and live offers',
icon: (
<>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
</>
),
},
{
text: 'Every store visit earns more rewards and deepens loyalty',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
),
},
];
const BOTTOM_FEATURES = [
{ label: 'Earn Daily', icon: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" /></> },
{ label: 'Play & Win', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> },
{ label: 'Smart Coupons', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" /> },
{ label: 'Shop Offline', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" /> },
];
/* ── Card ──────────────────────────────────────────────────────────── */
function CompareCard({
variant,
badge,
desc,
items,
image,
imageAlt,
accent,
}: {
variant: 'problem' | 'shift';
badge: string;
desc: string;
items: Item[];
image: string;
imageAlt: string;
accent: string;
}) {
const isProblem = variant === 'problem';
return (
<div
className={`ps-card ps-card-${variant}`}
style={{
position: 'relative',
flex: 1,
background: '#ffffff',
borderRadius: 28,
border: '1px solid rgba(0,0,0,0.05)',
padding: 'clamp(28px, 2.6vw, 44px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 24px 60px -28px rgba(23,21,18,0.18)',
willChange: 'transform, opacity',
}}
>
{/* accent top edge */}
<span style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 4, background: `linear-gradient(90deg, transparent, ${accent}, transparent)` }} />
{/* soft accent glow */}
<div style={{ position: 'absolute', top: -80, [isProblem ? 'left' : 'right']: -80, width: 260, height: 260, borderRadius: '50%', background: `radial-gradient(circle, ${accent}1f 0%, transparent 70%)`, pointerEvents: 'none' }} />
{/* Badge */}
<div className="ps-reveal" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 18 }}>
<span style={{ width: 30, height: 30, borderRadius: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', background: `${accent}1a`, border: `1px solid ${accent}33` }}>
{isProblem ? (
<svg width="16" height="16" viewBox="0 0 20 20" fill={accent}><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /></svg>
) : (
<svg width="16" height="16" viewBox="0 0 20 20" fill={accent}><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>
)}
</span>
<span style={{ color: accent, fontSize: 12, fontWeight: 800, letterSpacing: '0.14em', textTransform: 'uppercase', fontFamily: 'Sora, sans-serif' }}>{badge}</span>
</div>
{/* Description */}
<h3 className="ps-reveal" style={{ margin: '0 0 20px', color: '#171512', fontSize: 'clamp(1.15rem, 1.5vw, 1.45rem)', fontWeight: 800, lineHeight: 1.3, letterSpacing: '-0.01em', fontFamily: 'Sora, sans-serif' }}>
{desc}
</h3>
{/* 3D visual */}
<div className="ps-reveal ps-visual-wrap" style={{ position: 'relative', height: 'clamp(150px, 17vw, 210px)', margin: '4px 0 22px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse 60% 60% at 50% 60%, ${accent}22 0%, transparent 70%)`, pointerEvents: 'none' }} />
<img src={image} alt={imageAlt} className="ps-visual-img" style={{ maxHeight: '100%', maxWidth: '80%', objectFit: 'contain', pointerEvents: 'none', userSelect: 'none', filter: 'drop-shadow(0 22px 30px rgba(23,21,18,0.16))', willChange: 'transform' }} />
</div>
{/* Items */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 'auto' }}>
{items.map((item, idx) => (
<div
key={idx}
className={`ps-item ps-item-${variant}`}
style={{ display: 'flex', alignItems: 'center', gap: 13 }}
>
<span style={{ width: 38, height: 38, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 11, background: `${accent}12`, border: `1px solid ${accent}26` }}>
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2" style={{ color: accent }}>{item.icon}</svg>
</span>
<span style={{ color: '#3f3a32', fontSize: '0.95rem', fontWeight: 600, lineHeight: 1.35, fontFamily: 'Sora, sans-serif' }}>{item.text}</span>
</div>
))}
</div>
</div>
);
}
/* ── Section ───────────────────────────────────────────────────────── */
export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
const sectionRef = useRef<HTMLDivElement>(null);
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return;
if (!isLoaded) return;
const ctx = gsap.context(() => {
// Header reveal — clip-path line wipe
const headerTl = gsap.timeline({
scrollTrigger: { trigger: '.ps-header', start: 'top 85%', toggleActions: 'play none none reset' },
});
headerTl
.fromTo('.ps-eyebrow', { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.6, ease: 'power3.out' })
.fromTo('.ps-title', { opacity: 0, y: 40, clipPath: 'polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)' }, { opacity: 1, y: 0, clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', duration: 1.1, ease: 'power4.out' }, '-=0.3')
.fromTo('.ps-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8');
// Cards reveal + their inner reveal elements + connector
const cardsTl = gsap.timeline({
scrollTrigger: { trigger: '.ps-cards-row', start: 'top 80%', toggleActions: 'play none none reset' },
});
cardsTl
.fromTo('.ps-card', { opacity: 0, y: 70, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 1.0, stagger: 0.14, ease: 'power3.out' })
.fromTo('.ps-connector', { opacity: 0, scale: 0.4 }, { opacity: 1, scale: 1, duration: 0.7, ease: 'back.out(1.8)' }, '-=0.7')
.fromTo('.ps-card-problem .ps-reveal', { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.08, ease: 'power3.out' }, '-=0.8')
.fromTo('.ps-card-shift .ps-reveal', { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.08, ease: 'power3.out' }, '<')
.fromTo('.ps-item-problem', { opacity: 0, x: -24 }, { opacity: 1, x: 0, duration: 0.5, stagger: 0.07, ease: 'power3.out' }, '-=0.5')
.fromTo('.ps-item-shift', { opacity: 0, x: 24 }, { opacity: 1, x: 0, duration: 0.5, stagger: 0.07, ease: 'power3.out' }, '<');
// Parallax drift on the 3D visuals while scrolling through the section
gsap.utils.toArray<HTMLElement>('.ps-visual-img').forEach((img) => {
gsap.fromTo(img, { y: 26 }, {
y: -26,
ease: 'none',
scrollTrigger: { trigger: img, start: 'top bottom', end: 'bottom top', scrub: 1 },
});
});
// Bottom bar
const bottomTl = gsap.timeline({
scrollTrigger: { trigger: '.ps-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' },
});
bottomTl
.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);
return () => ctx.revert();
}, [isLoaded]);
return (
<section
ref={sectionRef}
id="problems-shift"
className="relative overflow-hidden"
style={{
background: 'linear-gradient(180deg, #faf9f5 0%, #f4f2ea 100%)',
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: '32px',
padding: 'clamp(72px, 9vw, 130px) clamp(20px, 4vw, 56px) clamp(56px, 7vw, 90px)',
zIndex: 1,
}}
>
{/* Header */}
<div className="ps-header relative z-10 text-center max-w-3xl mx-auto" style={{ marginBottom: 'clamp(40px, 5vw, 72px)' }}>
<div className="ps-eyebrow" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, padding: '7px 16px', borderRadius: 999, background: 'rgba(244,190,40,0.1)', border: '1px solid rgba(244,190,40,0.28)', marginBottom: 24 }}>
<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: 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' }}>
Customers have no compelling reason to choose your store over the next one. Loyaly creates that reason, with daily rewards, interactive games, and surprise coupons redeemable in person.
</p>
</div>
{/* Comparison row */}
<div className="ps-cards-row relative z-10 mx-auto" style={{ maxWidth: 1240, display: 'flex', alignItems: 'stretch', gap: 'clamp(16px, 2vw, 28px)' }}>
<CompareCard
variant="problem"
badge="The Problem"
desc="Retailers struggle to attract new customers and retain existing ones without sacrificing profit margins on generic discounts."
items={PROBLEM_ITEMS}
image="/Problems+shift/Problems.png"
imageAlt="In-store behavior problems"
accent="#ef4444"
/>
{/* Connector */}
<div className="ps-connector" style={{ flexShrink: 0, alignSelf: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div className="animate-float" style={{ width: 56, height: 56, borderRadius: '50%', background: '#fff', border: '3px solid rgba(244,190,40,0.8)', boxShadow: '0 10px 30px rgba(0,0,0,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<div className="absolute inset-[-3px] rounded-full animate-pulse-ring-1" style={{ background: 'rgba(244,190,40,0.25)', zIndex: -1 }} />
<svg className="ps-connector-arrow" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</div>
</div>
<CompareCard
variant="shift"
badge="The Shift"
desc="Loyaly bridges digital engagement and offline shopping. Customers earn coins and coupons through fun activities, then walk into nearby stores to redeem them in person."
items={SHIFT_ITEMS}
image="/Problems+shift/Shift.png"
imageAlt="In-store intelligence shift"
accent="#22c55e"
/>
</div>
{/* Bottom features bar */}
<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); }
}
`}</style>
</section>
);
}

View File

@@ -60,10 +60,10 @@ const PRODUCTS: Product[] = [
{
id: 4,
href: '/solutions/loyaly-app',
title: 'Loyaly App',
title: 'Identiq',
subtitle: 'Consumer Loyalty Platform',
description:
'Earn Loyaly Coins through daily login, walking challenges, and interactive games. Unlock surprise coupons, discover nearby store campaigns, and redeem rewards at real offline stores.',
'Earn Loyaly Coins through daily logins and games, then redeem rewards at real offline stores.',
accent: '#f4be28',
accentRgb: '244, 190, 40',
tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'],
@@ -132,14 +132,23 @@ function ProductVisual({ product }: { product: Product }) {
<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 style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'block' }} />2,847
<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} style={{ aspectRatio: '1 / 1', borderRadius: 5, background: `rgba(${accentRgb}, ${alpha})` }} />;
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 }}>
@@ -177,8 +186,17 @@ function ProductVisual({ product }: { product: Product }) {
</linearGradient>
</defs>
<polygon points={`0,${h} ${path} ${w},${h}`} fill={`url(#sg${id})`} />
<polyline points={path} fill="none" stroke={accent} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
{pts.map((p, i) => <circle key={i} cx={(i / (pts.length - 1)) * w} cy={h - (p / max) * h} r={i === pts.length - 1 ? 4 : 0} fill={accent} />)}
<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>
);
@@ -187,43 +205,115 @@ function ProductVisual({ product }: { product: Product }) {
// 3 — Dyscount: smart offer + conversion uplift
if (id === 3) {
return (
<div style={{ width: '82%', display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ borderRadius: 16, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #c0392b 100%)`, position: 'relative', overflow: 'hidden' }}>
<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 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 style={{ fontSize: 13, fontWeight: 800, color: accent, fontFamily: 'Sora, sans-serif' }}>+41%</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 style={{ height: '100%', width: '72%', borderRadius: 3, background: accent }} />
<div className="pv-progress-fill" style={{ height: '100%', width: '72%', borderRadius: 3, background: accent }} />
</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.35)', fontFamily: 'Sora, sans-serif', marginTop: 10 }}>Margin protected · 0 waste</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: '80%', borderRadius: 22, padding: 22, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.1)', display: 'flex', flexDirection: 'column', gap: 16 }}>
<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' }}>
<span style={{ fontSize: 12, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>Loyaly </span>
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>GOLD MEMBER</span>
</div>
<div style={{ borderRadius: 16, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)` }}>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward Points</div>
<div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
</div>
{[['Starbucks', '+120'], ['Nike Store', '+85']].map(([s, p]) => (
<div key={s} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', borderRadius: 10, background: 'rgba(255,255,255,0.04)' }}>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.7)', fontFamily: 'Sora, sans-serif' }}>{s}</span>
<span style={{ fontSize: 12, fontWeight: 700, color: '#4ade80', fontFamily: 'Sora, sans-serif' }}>{p}</span>
<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',
}}>A</span>
<div>
<div style={{ fontSize: 13, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', lineHeight: 1.1 }}>Loyaly</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>
{/* Points 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' }}>Reward Points</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: '50%', background: 'rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16 }}>🪙</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 pts 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>
);
}
@@ -405,10 +495,12 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
{/* Heading (fixed within the pinned viewport) */}
<div className="products-heading">
<div style={{ maxWidth: 480 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
<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 style={{ maxWidth: 520 }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 18, 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>
</div>
<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 />
@@ -416,15 +508,13 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
</h2>
</div>
<div className="products-heading-hint" style={{ maxWidth: 280, textAlign: 'right' }}>
<p style={{ margin: '0 0 14px', fontSize: 'clamp(0.85rem, 1vw, 1rem)', lineHeight: 1.6, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif' }}>
<div className="products-heading-hint" style={{ maxWidth: 300, textAlign: 'right', borderRight: '2px solid rgba(244,190,40,0.3)', paddingRight: 'clamp(18px, 2.2vw, 28px)' }}>
<p style={{ margin: '0 0 16px', 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: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.32)', fontFamily: 'Sora, sans-serif' }}>
Scroll to explore
<svg viewBox="0 0 24 16" fill="none" width="22" height="14">
<path d="M2 8h18M14 3l5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 10 }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.85rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Built as one connected platform</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.6rem', color: '#f4be28', letterSpacing: '-0.02em' }}>04</span>
</div>
</div>
</div>
@@ -448,6 +538,72 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
<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;
}
}
.products-section-root {
--pt: clamp(230px, 28vh, 310px);
--pb: clamp(64px, 8vh, 96px);

View File

@@ -0,0 +1,325 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface ShiftSectionProps {
isLoaded?: boolean;
}
interface Item {
text: string;
icon: React.ReactNode;
}
const SHIFT_ITEMS: Item[] = [
{
text: 'Customers earn Loyaly Coins through daily app activities',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
),
},
{
text: 'Game-based campaigns drive footfall with excitement',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" />
),
},
{
text: 'Smart surprise coupons create a daily reason to engage',
icon: (
<>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="5" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" />
</>
),
},
{
text: 'Real-time discovery of nearby stores and live offers',
icon: (
<>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
</>
),
},
{
text: 'Every store visit earns more rewards and deepens loyalty',
icon: (
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
),
},
];
const BOTTOM_FEATURES = [
{ label: 'Earn Daily', icon: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" /></> },
{ label: 'Play & Win', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> },
{ label: 'Smart Coupons', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" /> },
{ label: 'Shop Offline', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" /> },
];
const STATS = [
{ value: '3.4x', label: 'more repeat visits' },
{ value: '20%', label: 'live in-app offer' },
];
export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
const sectionRef = useRef<HTMLDivElement>(null);
const accent = '#f4be28';
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return;
if (!isLoaded) return;
const ctx = gsap.context(() => {
const headerTl = gsap.timeline({
scrollTrigger: { trigger: '.shift-header', start: 'top 85%', toggleActions: 'play none none reset' },
});
headerTl
.fromTo('.shift-eyebrow', { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.6, ease: 'power3.out' })
.fromTo('.shift-title', { opacity: 0, y: 40, clipPath: 'polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)' }, { opacity: 1, y: 0, clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', duration: 1.1, ease: 'power4.out' }, '-=0.3')
.fromTo('.shift-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8');
const rowTl = gsap.timeline({
scrollTrigger: { trigger: '.shift-list', start: 'top 82%', toggleActions: 'play none none reset' },
});
rowTl.fromTo('.shift-row', { opacity: 0, x: 30 }, { opacity: 1, x: 0, duration: 0.6, stagger: 0.09, ease: 'power3.out' });
const visualTl = gsap.timeline({
scrollTrigger: { trigger: '.shift-visual', start: 'top 78%', toggleActions: 'play none none reset' },
});
visualTl
.fromTo('.shift-visual-stage', { opacity: 0, scale: 0.9, y: 40 }, { opacity: 1, scale: 1, y: 0, duration: 1, ease: 'power3.out' })
.fromTo('.shift-chip', { opacity: 0, y: 18, scale: 0.85 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, stagger: 0.15, ease: 'back.out(1.6)' }, '-=0.5');
gsap.to('.shift-visual-img', {
y: -22,
ease: 'none',
scrollTrigger: { trigger: '.shift-visual', start: 'top bottom', end: 'bottom top', scrub: 1 },
});
const bottomTl = gsap.timeline({
scrollTrigger: { trigger: '.shift-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' },
});
bottomTl.fromTo('.shift-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);
return () => ctx.revert();
}, [isLoaded]);
return (
<section
ref={sectionRef}
id="shift"
className="relative overflow-hidden shift-section"
style={{
background: 'linear-gradient(155deg, #fffdf6 0%, #fbf5e4 48%, #f6ecd2 100%)',
width: 'calc(100% - 40px)',
margin: '0 20px 20px',
borderRadius: '32px',
padding: 'clamp(72px, 9vw, 130px) clamp(20px, 4vw, 64px) clamp(56px, 7vw, 90px)',
zIndex: 1,
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.7), inset 0 0 0 1px rgba(23,21,18,0.04)',
}}
>
{/* Fine dot-grid texture */}
<div
style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
backgroundImage: 'radial-gradient(rgba(23,21,18,0.06) 1px, transparent 1px)',
backgroundSize: '26px 26px',
maskImage: 'radial-gradient(ellipse 80% 65% at 50% 30%, #000 0%, transparent 78%)',
WebkitMaskImage: 'radial-gradient(ellipse 80% 65% at 50% 30%, #000 0%, transparent 78%)',
}}
/>
{/* Subtle grain */}
<div
style={{
position: 'absolute', inset: 0, pointerEvents: 'none', opacity: 0.5, mixBlendMode: 'multiply',
backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E\")",
}}
/>
{/* Decorative backdrop — slow-drifting glow orbs for a living, premium feel */}
<div className="shift-orb shift-orb-1" style={{ position: 'absolute', top: '-10%', left: '-8%', width: 560, height: 560, borderRadius: '50%', background: `radial-gradient(circle, ${accent}2a 0%, transparent 70%)`, filter: 'blur(10px)', pointerEvents: 'none' }} />
<div className="shift-orb shift-orb-2" style={{ position: 'absolute', bottom: '-15%', right: '-10%', width: 460, height: 460, borderRadius: '50%', background: 'radial-gradient(circle, rgba(34,197,94,0.12) 0%, transparent 70%)', pointerEvents: 'none' }} />
<div className="shift-orb shift-orb-3" style={{ position: 'absolute', top: '32%', right: '36%', width: 320, height: 320, borderRadius: '50%', background: `radial-gradient(circle, ${accent}14 0%, transparent 72%)`, pointerEvents: 'none' }} />
{/* Twinkling sparkle accents */}
{[
{ top: '14%', left: '58%', delay: '0s', size: 6 },
{ top: '68%', left: '48%', delay: '1.1s', size: 4 },
{ top: '40%', left: '8%', delay: '2s', size: 5 },
{ top: '78%', left: '82%', delay: '0.6s', size: 4 },
].map((s, i) => (
<span
key={i}
className="shift-sparkle"
style={{
position: 'absolute', top: s.top, left: s.left, width: s.size, height: s.size,
borderRadius: '50%', background: '#f4be28', pointerEvents: 'none',
animationDelay: s.delay,
}}
/>
))}
{/* Inner edge vignette for a framed, premium finish */}
<div style={{ position: 'absolute', inset: 0, borderRadius: 32, boxShadow: 'inset 0 0 90px rgba(23,21,18,0.05)', pointerEvents: 'none' }} />
{/* Header */}
<div className="shift-header relative z-10 mx-auto shift-header-grid" style={{ maxWidth: 1200, marginBottom: 'clamp(48px, 6vw, 80px)', display: 'grid', gridTemplateColumns: 'minmax(0,1.3fr) minmax(0,1fr)', gap: 'clamp(28px, 4vw, 60px)', alignItems: 'end' }}>
<div>
<div className="shift-eyebrow" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 22, padding: '6px 16px 6px 8px', borderRadius: 100, background: '#ffffff', border: `1px solid ${accent}44`, 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: `${accent}1a` }}>
<img src="/images/loyalyai-logo.png" alt="" width={15} height={15} style={{ display: 'block', objectFit: 'contain' }} />
</span>
<span style={{ color: '#8a5a14', fontSize: 12, fontWeight: 800, letterSpacing: '0.14em', textTransform: 'uppercase', fontFamily: 'Sora, sans-serif' }}>The Shift</span>
</div>
<h2 className="shift-title" style={{ color: '#171512', fontWeight: 800, fontSize: 'clamp(2.6rem, 5.6vw, 4.6rem)', letterSpacing: '-0.05em', lineHeight: 0.96, margin: 0, fontFamily: 'Sora, sans-serif' }}>
Loyaly makes it<br /><span style={{ color: accent }}>simple.</span>
</h2>
</div>
<div className="shift-header-right" style={{ paddingBottom: 'clamp(4px, 1vw, 10px)', borderLeft: `2px solid ${accent}44`, paddingLeft: 'clamp(20px, 2.4vw, 32px)' }}>
<p className="shift-subtitle" style={{ color: '#4f463a', fontSize: 'clamp(1rem, 1.2vw, 1.15rem)', fontWeight: 500, margin: '0 0 18px', lineHeight: 1.7, fontFamily: 'Sora, sans-serif' }}>
Loyaly bridges digital engagement and offline shopping, with daily rewards, interactive games, and surprise coupons redeemable in person.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.6rem', color: '#bc7122', letterSpacing: '-0.02em' }}>05</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.85rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Everyday moments, turned into loyalty</span>
</div>
</div>
</div>
{/* Split layout */}
<div className="relative z-10 mx-auto shift-grid" style={{ maxWidth: 1200, display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 'clamp(32px, 5vw, 72px)', alignItems: 'center' }}>
{/* List */}
<div className="shift-list" style={{ display: 'flex', flexDirection: 'column' }}>
{SHIFT_ITEMS.map((item, idx) => (
<div
key={idx}
className="shift-row"
style={{
display: 'flex', alignItems: 'center', gap: 16,
padding: 'clamp(16px, 1.8vw, 20px) 4px',
borderBottom: idx !== SHIFT_ITEMS.length - 1 ? '1px solid rgba(23,21,18,0.08)' : 'none',
}}
>
<span style={{ width: 42, height: 42, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 13, background: `${accent}1f`, border: `1px solid ${accent}55` }}>
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#8a5a14" strokeWidth="2">{item.icon}</svg>
</span>
<span style={{ color: '#2e2a24', fontSize: '1rem', fontWeight: 600, lineHeight: 1.4, fontFamily: 'Sora, sans-serif' }}>{item.text}</span>
</div>
))}
</div>
{/* Visual */}
<div className="shift-visual" style={{ position: 'relative' }}>
<div
className="shift-visual-stage"
style={{
position: 'relative',
borderRadius: 28,
background: 'transparent',
padding: 'clamp(12px, 2vw, 24px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
minHeight: 'clamp(340px, 38vw, 520px)',
overflow: 'hidden',
}}
>
<div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse 65% 55% at 50% 55%, ${accent}2a 0%, transparent 72%)`, pointerEvents: 'none' }} />
<img
src="/Problems+shift/Shift.png"
alt="Customers engaging with the Loyaly app inside a store with live offers"
className="shift-visual-img"
style={{ maxWidth: '112%', maxHeight: '118%', objectFit: 'contain', filter: 'drop-shadow(0 26px 34px rgba(23,21,18,0.16))', pointerEvents: 'none', userSelect: 'none' }}
/>
</div>
{/* Floating stat chips */}
<div className="shift-chip" style={{ position: 'absolute', top: '8%', right: '-6%', display: 'flex', alignItems: 'baseline', gap: 8, background: '#171512', borderRadius: 16, padding: '12px 18px', boxShadow: '0 20px 44px -18px rgba(23,21,18,0.45)' }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.35rem', color: accent }}>{STATS[0].value}</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: 'rgba(255,255,255,0.65)', lineHeight: 1.3, maxWidth: 100 }}>{STATS[0].label}</span>
</div>
<div className="shift-chip" style={{ position: 'absolute', bottom: '6%', left: '-4%', display: 'flex', alignItems: 'baseline', gap: 8, background: '#ffffff', borderRadius: 16, padding: '12px 18px', boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 20px 40px -20px rgba(23,21,18,0.28)', border: '1px solid rgba(23,21,18,0.05)' }}>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.35rem', color: '#22c55e' }}>{STATS[1].value}</span>
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: '#6b6255', lineHeight: 1.3, maxWidth: 110 }}>{STATS[1].label}</span>
</div>
</div>
</div>
{/* Bottom features bar */}
<div className="shift-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(48px, 6vw, 80px)', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 'clamp(12px, 1.6vw, 18px)' }}>
{BOTTOM_FEATURES.map((f) => (
<div
key={f.label}
className="shift-feature-item"
style={{
display: 'flex', alignItems: 'center', gap: 14,
background: '#fff', borderRadius: 18,
border: '1px solid rgba(23,21,18,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>
<style>{`
#shift .shift-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);
}
#shift .shift-orb { animation: shiftOrbDrift 16s ease-in-out infinite; }
#shift .shift-orb-2 { animation-duration: 20s; animation-direction: reverse; }
#shift .shift-orb-3 { animation-duration: 13s; }
@keyframes shiftOrbDrift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(22px, -18px) scale(1.06); }
}
#shift .shift-sparkle {
opacity: 0;
box-shadow: 0 0 8px 2px rgba(244,190,40,0.6);
animation: shiftSparkleTwinkle 3.2s ease-in-out infinite;
}
@keyframes shiftSparkleTwinkle {
0%, 100% { opacity: 0; transform: scale(0.6); }
50% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
#shift .shift-orb, #shift .shift-sparkle { animation: none; }
}
@media (max-width: 900px) {
#shift .shift-grid { grid-template-columns: 1fr; }
#shift .shift-visual { order: -1; }
#shift .shift-chip { display: none; }
#shift .shift-header-grid { grid-template-columns: 1fr; }
#shift .shift-header-right { border-left: none; padding-left: 0; border-top: 2px solid rgba(244,190,40,0.3); padding-top: 20px; margin-top: 20px; }
}
`}</style>
</section>
);
}

View File

@@ -143,15 +143,6 @@ export default function TheLoop() {
{/* ── Header ── */}
<div style={{ position: 'relative', zIndex: 10, textAlign: 'center', maxWidth: 760, margin: '0 auto' }}>
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 24,
padding: '7px 18px', borderRadius: 999,
background: 'rgba(244,190,40,0.08)', border: '1px solid rgba(244,190,40,0.22)',
}}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#f4be28', display: 'block' }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif' }}>The Loop</span>
</div>
<h2 style={{
margin: '0 0 18px',
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
@@ -180,11 +171,11 @@ export default function TheLoop() {
{/* ── Flow diagram ── */}
<div className="loop-diagram" style={{
position: 'relative', zIndex: 10,
maxWidth: 1180, margin: 'clamp(140px, 16vh, 200px) auto 0',
maxWidth: 1180, margin: 'clamp(170px, 19vh, 230px) auto 0',
}}>
{/* Loop-back arc (desktop) */}
<svg className="loop-arc" viewBox="0 0 1000 120" preserveAspectRatio="none" aria-hidden style={{
position: 'absolute', top: -64, left: '11%', width: '78%', height: 70, overflow: 'visible',
position: 'absolute', top: -46, left: '11%', width: '78%', height: 70, overflow: 'visible',
}}>
<defs>
<linearGradient id="loopArcGrad" x1="0" y1="0" x2="1" y2="0">
@@ -200,17 +191,6 @@ export default function TheLoop() {
<path d="M 8 104 L 15 120 L 22 104" fill="none" stroke="rgba(244,190,40,0.6)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{/* Center "continuous" label on the arc */}
<div className="loop-arc-label" style={{
position: 'absolute', top: -78, left: '50%', transform: 'translateX(-50%)',
fontSize: 10, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase',
color: 'rgba(244,190,40,0.7)', fontFamily: 'Sora, sans-serif',
background: 'rgba(244,190,40,0.06)', border: '1px solid rgba(244,190,40,0.18)',
padding: '5px 14px', borderRadius: 999, whiteSpace: 'nowrap',
}}>
Continuous loop
</div>
{/* Steps row */}
<div className="loop-row" style={{ display: 'flex', alignItems: 'stretch', gap: 0 }}>
{STEPS.map((s, i) => {
@@ -304,27 +284,30 @@ export default function TheLoop() {
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 2, borderRadius: 24, overflow: 'hidden' }}>
{IMPACTS.map((m, i) => (
{IMPACTS.map((m, i) => {
const isEven = i % 2 === 1;
return (
<div key={m.label} className="loop-metric" style={{
padding: 'clamp(28px, 3vw, 44px) 28px',
background: 'rgba(255,255,255,0.025)',
background: isEven ? '#f4be28' : 'rgba(255,255,255,0.025)',
borderRight: i < IMPACTS.length - 1 ? '1px solid rgba(255,255,255,0.06)' : 'none',
textAlign: 'center',
}}>
<div style={{
fontSize: 'clamp(2.4rem, 4vw, 3.4rem)', fontWeight: 900,
color: '#f4be28', lineHeight: 1, letterSpacing: '-0.04em',
color: isEven ? '#171512' : '#f4be28', lineHeight: 1, letterSpacing: '-0.04em',
fontVariantNumeric: 'tabular-nums', fontFamily: 'Sora, sans-serif',
}}>
<span style={{ fontSize: '55%', verticalAlign: 'middle', marginRight: 1 }}>{m.prefix}</span>
{counts[i].toFixed(m.decimals)}
<span style={{ fontSize: '50%', verticalAlign: 'middle', marginLeft: 1 }}>{m.suffix}</span>
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'rgba(255,255,255,0.5)', fontFamily: 'Sora, sans-serif', marginTop: 12, lineHeight: 1.4 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: isEven ? 'rgba(23,21,18,0.7)' : 'rgba(255,255,255,0.5)', fontFamily: 'Sora, sans-serif', marginTop: 12, lineHeight: 1.4 }}>
{m.label}
</div>
</div>
))}
);
})}
</div>
</div>
@@ -351,7 +334,7 @@ export default function TheLoop() {
animation-name: loopFlowVertical !important;
}
.loop-connector-arrow { animation: loopArrowBounceV 1.4s ease-in-out infinite; }
.loop-arc, .loop-arc-label { display: none !important; }
.loop-arc { display: none !important; }
}
@media (prefers-reduced-motion: reduce) {
#the-loop [style*="animation"] { animation: none !important; }

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
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';
@@ -13,17 +13,17 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
interface Stage {
num: string;
title: string;
description: string;
tagline: string;
detail: string;
icon: React.ReactNode;
}
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.',
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" />
@@ -32,9 +32,9 @@ const STAGES: Stage[] = [
),
},
{
num: '02',
title: 'Engage',
description: 'Customers earn Loyaly Coins through daily login, 5,000-step walking challenges, and interactive games, including Maze, Spin Wheel, Scratch Card, and Match Master, keeping them engaged every day.',
tagline: 'Playable daily rewards 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" />
@@ -42,9 +42,9 @@ const STAGES: Stage[] = [
),
},
{
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.',
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" />
@@ -52,9 +52,9 @@ const STAGES: Stage[] = [
),
},
{
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.',
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" />
@@ -75,6 +75,7 @@ function StageCard({
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;
@@ -82,133 +83,50 @@ function StageCard({
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)}%`);
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 = () => {
const el = cardRef.current;
if (!el) return;
el.style.setProperty('--rx', '0deg');
el.style.setProperty('--ry', '0deg');
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}
data-stage-card={index}
className={`wwd-card relative group ${active ? 'wwd-card-active' : ''}`}
className={`wwd-card relative group ${isYellow ? 'wwd-card-yellow' : 'wwd-card-white'} ${active ? 'wwd-card-active' : ''}`}
onMouseMove={onMouseMove}
onMouseEnter={() => onHover(index)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{
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',
}}
>
<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)',
}}
>
<div className="wwd-card-inner">
{/* 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>
<div className="wwd-card-glow" style={{ opacity: active ? 1 : 0 }} />
{/* 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)',
}}>
<div ref={iconRef} className="wwd-icon">
{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>
<h3 className="wwd-title">{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>
{/* 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>
);
@@ -236,23 +154,41 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
}
);
gsap.fromTo(
'.wwd-card',
{ opacity: 0, y: 70, scale: 0.94 },
{
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' },
}
);
// Per-card reveal: each card gets its own scrubbed-in timeline
// (clip-path wipe + rise), staggered across the row, with the
// icon and copy cascading in right after.
gsap.utils.toArray<HTMLElement>('.wwd-card').forEach((card, i) => {
const tl = gsap.timeline({
scrollTrigger: { trigger: card, start: 'top 85%', toggleActions: 'play none none reset' },
delay: i * 0.1,
});
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' },
}
);
tl.fromTo(card,
{ opacity: 0, y: 70, scale: 0.94, clipPath: 'inset(0% 0% 100% 0% round 22px)' },
{ opacity: 1, y: 0, scale: 1, clipPath: 'inset(0% 0% 0% 0% round 22px)', duration: 0.75, ease: 'power3.out' },
0
)
.fromTo(card.querySelector('.wwd-icon'),
{ opacity: 0, scale: 0.4, rotate: -20 },
{ opacity: 1, scale: 1, rotate: 0, duration: 0.55, ease: 'back.out(2.2)' },
0.3
)
.fromTo(card.querySelector('.wwd-title'),
{ opacity: 0, y: 16 },
{ opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' },
0.5
)
.fromTo(card.querySelector('.wwd-tagline'),
{ opacity: 0, y: 12 },
{ opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' },
0.58
)
.fromTo(card.querySelector('.wwd-detail'),
{ opacity: 0, y: 10 },
{ opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' },
0.66
);
});
}, containerRef);
return () => ctx.revert();
@@ -274,74 +210,98 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
{/* 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>
<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 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 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',
}}>
Every visit,<br />
a <span className="wwd-shimmer">loyal customer.</span>
</h2>
<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>
{/* 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={{
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',
}}
>
{s.num} · {s.title}
</button>
))}
</div>
{/* Cascading 4-up grid */}
{/* 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>
<StageCard key={stage.title} stage={stage} index={i} active={active === i} onHover={setActive} />
))}
</div>
</div>
<style>{`
@media (min-width: 1024px) {
.wwd-cascade-item:nth-child(2n) .wwd-card { --cascade: 36px; }
.wwd-card {
position: relative;
transform: perspective(1100px) rotateX(0deg) rotateY(0deg);
transition: transform 0.45s cubic-bezier(0.22,1,0.36,1);
}
.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;
.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; }
.wwd-card-yellow .wwd-card-inner { background: #F4C21A; }
.wwd-card-active .wwd-card-inner {
transform: translateY(-6px);
box-shadow: 0 24px 50px -18px rgba(0,0,0,0.35);
}
.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;
pointer-events: none;
}
.wwd-card-active .wwd-card-inner::before { opacity: 1; }
.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,#f4be28 0%,#fff8e0 48%,#f4be28 62%,#ff9500 100%);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
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>
);