Update homepage, use-cases, and people pages; add preloader and new section components
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB |
BIN
public/images/stock/deals-retail.png
Normal file
BIN
public/images/stock/deals-retail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 985 KiB |
BIN
public/images/stock/physical retail.png
Normal file
BIN
public/images/stock/physical retail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/images/stock/spend-anywhere.png
Normal file
BIN
public/images/stock/spend-anywhere.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
@@ -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 */}
|
||||
|
||||
@@ -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',
|
||||
}}>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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',
|
||||
}}>
|
||||
|
||||
@@ -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',
|
||||
}}>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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}
|
||||
|
||||
152
src/app/page.tsx
152
src/app/page.tsx
@@ -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 />
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
91
src/components/Preloader.tsx
Normal file
91
src/components/Preloader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
225
src/components/home/ProblemSection.tsx
Normal file
225
src/components/home/ProblemSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
325
src/components/home/ShiftSection.tsx
Normal file
325
src/components/home/ShiftSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user