feat: enhance landing page, for-people sections, blog main, and components including TheLoop and PhoneMockup

This commit is contained in:
R-Bharathraj
2026-07-06 13:21:09 +05:30
parent 727a6bcb62
commit 57e64f86b6
18 changed files with 1037 additions and 386 deletions

View File

@@ -17,6 +17,34 @@ function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: nu
); );
} }
const TAG_META: Record<string, { color: string; icon: React.ReactNode }> = {
Analytics: {
color: '#f4be28',
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3 3v18h18M8 17V9m5 8V5m5 12v-6" />,
},
Strategy: {
color: '#a78bfa',
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />,
},
Technology: {
color: '#34d399',
icon: (
<>
<rect x="3" y="4" width="18" height="14" rx="2" strokeLinecap="round" strokeLinejoin="round" />
<path strokeLinecap="round" strokeLinejoin="round" d="M8 21h8M12 18v3" />
</>
),
},
Growth: {
color: '#3b82f6',
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3 17l6-6 4 4 8-8M21 7v6M21 7h-6" />,
},
Future: {
color: '#ec4899',
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z" />,
},
};
const ARTICLES = [ const ARTICLES = [
{ {
n: '01', tag: 'Analytics', readTime: '6 min', n: '01', tag: 'Analytics', readTime: '6 min',
@@ -52,6 +80,12 @@ const ARTICLES = [
const FILTERS = ['All', 'Analytics', 'Strategy', 'Technology', 'Growth', 'Future']; const FILTERS = ['All', 'Analytics', 'Strategy', 'Technology', 'Growth', 'Future'];
const NEWSLETTER_PERKS = [
'One email a month, no spam, ever',
'Original research on footfall & retail AI',
'Early access to new Loyaly product features',
];
export default function BlogMainPage() { export default function BlogMainPage() {
const [activeFilter, setActiveFilter] = useState('All'); const [activeFilter, setActiveFilter] = useState('All');
const [hovered, setHovered] = useState<number | null>(null); const [hovered, setHovered] = useState<number | null>(null);
@@ -66,46 +100,82 @@ export default function BlogMainPage() {
{/* ── HERO ── */} {/* ── HERO ── */}
<section style={{ <section style={{
padding: '120px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)', padding: '120px 8% 90px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
margin: '20px', borderRadius: '32px', margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden', position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center',
}}> }}>
{/* Background photo */} {/* Background photo */}
<Image src="/images/stock/blog-main.jpg" alt="" fill priority style={{ objectFit: 'cover', opacity: 0.9 }} /> <Image src="/images/stock/blog-main.jpg" alt="" fill priority style={{ objectFit: 'cover', opacity: 0.9 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(24,20,41,0.1) 0%, rgba(24,20,41,0.55) 85%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(24,20,41,0.1) 0%, rgba(24,20,41,0.6) 85%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 60% 65% at 50% 48%, rgba(18,14,30,0.78) 0%, rgba(18,14,30,0.35) 60%, transparent 100%)' }} /> <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 60% 65% at 50% 48%, rgba(18,14,30,0.78) 0%, rgba(18,14,30,0.35) 60%, transparent 100%)' }} />
{/* Accent blobs */} {/* Accent blobs */}
<div style={{ position: 'absolute', top: '-14%', left: '6%', width: 480, height: 480, borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.2) 0%, transparent 70%)', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', top: '-14%', left: '6%', width: 480, height: 480, borderRadius: '50%', background: 'radial-gradient(circle, rgba(244,190,40,0.2) 0%, transparent 70%)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: '-16%', right: '4%', width: 420, height: 420, borderRadius: '50%', background: 'radial-gradient(circle, rgba(167,139,250,0.18) 0%, transparent 70%)', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', bottom: '-16%', right: '4%', width: 420, height: 420, borderRadius: '50%', background: 'radial-gradient(circle, rgba(167,139,250,0.18) 0%, transparent 70%)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(244,190,40,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(244,190,40,0.025) 1px,transparent 1px)', backgroundSize: '60px 60px', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(244,190,40,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(244,190,40,0.025) 1px,transparent 1px)', backgroundSize: '60px 60px', pointerEvents: 'none' }} />
<div style={{ position: 'relative', zIndex: 2, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}> <div style={{ position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
<div style={{ maxWidth: 720 }}> <div className="bm-hero-badge" style={{
display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 26,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(244,190,40,0.3)',
borderRadius: 100, padding: '8px 18px', backdropFilter: 'blur(12px)',
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: '#f4be28', display: 'block', animation: 'bmPulse 1.6s ease-in-out infinite' }} />
<span style={{ fontSize: 12, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>The Loyaly Journal</span>
</div>
<div style={{ maxWidth: 760 }}>
<h1 data-gsap="heading" style={{ fontSize: 'clamp(48px, 8vw, 120px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.9, color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif' }}> <h1 data-gsap="heading" style={{ fontSize: 'clamp(48px, 8vw, 120px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.9, color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif' }}>
Insights for the<br /> Insights for the<br />
<span style={{ color: '#f4be28' }}>Future of Retail.</span> <span style={{
background: 'linear-gradient(105deg,#f4be28 0%,#fff8e0 45%,#f4be28 60%,#ff9500 100%)',
backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
animation: 'bmShimmer 3s linear infinite',
}}>Future of Retail.</span>
</h1> </h1>
<p data-gsap="text" style={{ fontSize: 'clamp(16px, 1.5vw, 20px)', lineHeight: 1.75, color: 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif', maxWidth: 520, margin: '0 auto' }}> <p data-gsap="text" style={{ fontSize: 'clamp(16px, 1.5vw, 20px)', lineHeight: 1.75, color: 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif', maxWidth: 520, margin: '0 auto 48px' }}>
Explore ideas, trends, and strategies shaping modern retail, from our team to yours. Explore ideas, trends, and strategies shaping modern retail, from our team to yours.
</p> </p>
</div> </div>
{/* Stat strip */}
<div style={{ display: 'flex', gap: 'clamp(20px, 4vw, 56px)', flexWrap: 'wrap', justifyContent: 'center' }}>
{[
{ val: `${ARTICLES.length}+`, lbl: 'Articles published' },
{ val: 'Weekly', lbl: 'New research' },
{ val: '5', lbl: 'Topics covered' },
].map((s, i) => (
<div key={i} style={{ textAlign: 'center' }}>
<div style={{ fontSize: 'clamp(22px, 2.6vw, 32px)', fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>{s.val}</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', marginTop: 4, letterSpacing: '0.04em' }}>{s.lbl}</div>
</div>
))}
</div>
</div> </div>
</section> </section>
{/* ── FILTERS ── */} {/* ── FILTERS ── */}
<section style={{ padding: '48px 8% 0', background: '#fff' }}> <section style={{ padding: '48px 8% 0', background: '#fff' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{FILTERS.map(f => ( {FILTERS.map(f => {
<button key={f} onClick={() => setActiveFilter(f)} style={{ const meta = TAG_META[f];
padding: '10px 22px', borderRadius: 100, const active = activeFilter === f;
background: activeFilter === f ? '#0a0a0f' : 'transparent', return (
color: activeFilter === f ? '#f4be28' : 'rgba(0,0,0,0.4)', <button key={f} onClick={() => setActiveFilter(f)} style={{
fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif', display: 'inline-flex', alignItems: 'center', gap: 8,
border: `1px solid ${activeFilter === f ? '#0a0a0f' : 'rgba(0,0,0,0.1)'}`, padding: '10px 20px 10px 16px', borderRadius: 100,
cursor: 'pointer', transition: 'all 0.2s ease', background: active ? '#0a0a0f' : '#fafafa',
}}>{f}</button> color: active ? (meta?.color || '#f4be28') : 'rgba(0,0,0,0.45)',
))} fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif',
border: `1.5px solid ${active ? '#0a0a0f' : 'rgba(0,0,0,0.08)'}`,
cursor: 'pointer', transition: 'all 0.25s cubic-bezier(0.22,1,0.36,1)',
}}>
{meta && (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={active ? meta.color : 'rgba(0,0,0,0.35)'} strokeWidth="2">{meta.icon}</svg>
)}
{f}
</button>
);
})}
</div> </div>
</section> </section>
@@ -113,42 +183,57 @@ export default function BlogMainPage() {
{featured && ( {featured && (
<section style={{ padding: '64px 8%', background: '#fff' }}> <section style={{ padding: '64px 8%', background: '#fff' }}>
<Reveal> <Reveal>
<div className="bm-featured-grid" style={{ <Link href="/blog_single_page" style={{ textDecoration: 'none', display: 'block' }}>
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6vw', <div className="bm-featured-grid" style={{
padding: '64px', background: '#07070d', borderRadius: 24, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6vw',
alignItems: 'center', cursor: 'pointer', padding: '64px', background: '#07070d', borderRadius: 28,
transition: 'transform 0.3s ease, box-shadow 0.3s ease', alignItems: 'center', cursor: 'pointer', position: 'relative', overflow: 'hidden',
}}> border: `1px solid ${(TAG_META[featured.tag]?.color || '#f4be28')}30`,
{/* Left — article info */} transition: 'transform 0.3s ease, box-shadow 0.3s ease',
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 32 }}>
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', background: 'rgba(244,190,40,0.1)', padding: '5px 12px', borderRadius: 100, border: '1px solid rgba(244,190,40,0.2)' }}>{featured.tag}</span>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontFamily: 'Sora, sans-serif' }}>{featured.readTime} read</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', background: 'rgba(244,190,40,0.08)', padding: '4px 10px', borderRadius: 100 }}>Featured</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.5vw, 52px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 1.05, color: '#fff', margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>{featured.title}</h2>
<p style={{ fontSize: 16, lineHeight: 1.75, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', margin: '0 0 36px' }}>{featured.excerpt}</p>
<Link href="/blog_single_page" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 15, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', textDecoration: 'none', borderBottom: '1px solid rgba(244,190,40,0.3)', paddingBottom: 4 }}>
Read Article
</Link>
</div>
{/* Right — visual */}
<div style={{
aspectRatio: '4/3', borderRadius: 16,
background: 'linear-gradient(135deg,rgba(244,190,40,0.08) 0%,rgba(244,190,40,0.02) 100%)',
border: '1px solid rgba(244,190,40,0.1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
position: 'relative', overflow: 'hidden',
}}> }}>
<div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(255,255,255,0.02) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.02) 1px,transparent 1px)', backgroundSize: '48px 48px', pointerEvents: 'none' }} />
{/* Left — article info */}
<div style={{ position: 'relative', zIndex: 2 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 32, flexWrap: 'wrap' }}>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase',
color: TAG_META[featured.tag]?.color || '#f4be28', fontFamily: 'Sora, sans-serif',
background: `${TAG_META[featured.tag]?.color || '#f4be28'}18`, padding: '5px 12px 5px 8px', borderRadius: 100,
border: `1px solid ${TAG_META[featured.tag]?.color || '#f4be28'}35`,
}}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={TAG_META[featured.tag]?.color || '#f4be28'} strokeWidth="2">{TAG_META[featured.tag]?.icon}</svg>
{featured.tag}
</span>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontFamily: 'Sora, sans-serif' }}>{featured.readTime} read</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', background: 'rgba(244,190,40,0.08)', padding: '4px 10px', borderRadius: 100 }}>Featured</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.5vw, 52px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 1.05, color: '#fff', margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>{featured.title}</h2>
<p style={{ fontSize: 16, lineHeight: 1.75, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', margin: '0 0 36px' }}>{featured.excerpt}</p>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 15, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', borderBottom: '1px solid rgba(244,190,40,0.3)', paddingBottom: 4 }}>
Read Article
</span>
</div>
{/* Right — visual */}
<div style={{ <div style={{
fontSize: 'clamp(80px, 12vw, 160px)', fontWeight: 900, aspectRatio: '4/3', borderRadius: 20, position: 'relative', zIndex: 2,
color: 'transparent', fontFamily: 'Sora, sans-serif', background: `linear-gradient(150deg, ${TAG_META[featured.tag]?.color || '#f4be28'}20 0%, transparent 70%)`,
WebkitTextStroke: '1px rgba(244,190,40,0.15)', border: `1px solid ${TAG_META[featured.tag]?.color || '#f4be28'}25`,
letterSpacing: '-0.06em', lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
userSelect: 'none', overflow: 'hidden',
}}>{featured.n}</div> }}>
<svg width="30%" height="30%" viewBox="0 0 24 24" fill="none" stroke={TAG_META[featured.tag]?.color || '#f4be28'} strokeWidth="1.2" style={{ opacity: 0.5 }}>{TAG_META[featured.tag]?.icon}</svg>
<div style={{
position: 'absolute', bottom: -14, right: -6,
fontSize: 'clamp(80px, 12vw, 160px)', fontWeight: 900,
color: 'transparent', fontFamily: 'Sora, sans-serif',
WebkitTextStroke: `1px ${TAG_META[featured.tag]?.color || '#f4be28'}30`,
letterSpacing: '-0.06em', lineHeight: 1,
userSelect: 'none',
}}>{featured.n}</div>
</div>
</div> </div>
</div> </Link>
</Reveal> </Reveal>
</section> </section>
)} )}
@@ -156,68 +241,123 @@ export default function BlogMainPage() {
{/* ── ARTICLE GRID ── */} {/* ── ARTICLE GRID ── */}
<section style={{ padding: '0 8% 140px', background: '#fff' }}> <section style={{ padding: '0 8% 140px', background: '#fff' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(340px,1fr))', gap: 20 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(340px,1fr))', gap: 20 }}>
{rest.map((article, i) => ( {rest.map((article, i) => {
<Reveal key={article.n} delay={i * 0.08}> const meta = TAG_META[article.tag];
<div return (
onMouseEnter={() => setHovered(i)} <Reveal key={article.n} delay={i * 0.08}>
onMouseLeave={() => setHovered(null)} <Link href="/blog_single_page" style={{ textDecoration: 'none', display: 'block', height: '100%' }}>
style={{ <div
padding: '40px', borderRadius: 20, onMouseEnter={() => setHovered(i)}
background: '#fff', onMouseLeave={() => setHovered(null)}
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.3)' : 'rgba(0,0,0,0.08)'}`, style={{
transition: 'all 0.3s cubic-bezier(0.22,1,0.36,1)', padding: '36px', borderRadius: 22,
transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)', background: '#fff',
boxShadow: hovered === i ? '0 24px 60px rgba(0,0,0,0.1)' : '0 2px 12px rgba(0,0,0,0.04)', border: `1.5px solid ${hovered === i ? `${meta?.color}45` : 'rgba(0,0,0,0.08)'}`,
cursor: 'pointer', transition: 'all 0.35s cubic-bezier(0.22,1,0.36,1)',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 280, transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)',
}} boxShadow: hovered === i ? `0 24px 60px -16px ${meta?.color}30` : '0 2px 12px rgba(0,0,0,0.04)',
> cursor: 'pointer', position: 'relative', overflow: 'hidden',
<div> display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 300,
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 24 }}> }}
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', background: 'rgba(244,190,40,0.08)', padding: '4px 10px', borderRadius: 100, border: '1px solid rgba(244,190,40,0.15)' }}>{article.tag}</span> >
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.3)', fontFamily: 'Sora, sans-serif' }}>{article.readTime}</span> <div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 22 }}>
<span style={{
width: 40, height: 40, borderRadius: 12, flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: `${meta?.color}14`, border: `1px solid ${meta?.color}35`,
}}>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={meta?.color} strokeWidth="2">{meta?.icon}</svg>
</span>
<span style={{ fontSize: 26, fontWeight: 900, color: 'rgba(0,0,0,0.06)', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{article.n}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: meta?.color, fontFamily: 'Sora, sans-serif' }}>{article.tag}</span>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: 'rgba(0,0,0,0.2)', display: 'block' }} />
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.35)', fontFamily: 'Sora, sans-serif' }}>{article.readTime} read</span>
</div>
<h3 style={{ fontSize: 'clamp(20px, 2vw, 26px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.22, color: '#0a0a0f', margin: '0 0 14px', fontFamily: 'Sora, sans-serif' }}>{article.title}</h3>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif', margin: 0 }}>{article.excerpt}</p>
</div>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6, width: 'fit-content',
fontSize: 13, fontWeight: 700, color: hovered === i ? meta?.color : 'rgba(0,0,0,0.35)',
fontFamily: 'Sora, sans-serif', marginTop: 28, transition: 'color 0.25s, transform 0.25s',
transform: hovered === i ? 'translateX(4px)' : 'translateX(0)',
}}>
Read Article
</span>
</div> </div>
<h3 style={{ fontSize: 'clamp(20px, 2vw, 28px)', fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.2, color: '#0a0a0f', margin: '0 0 16px', fontFamily: 'Sora, sans-serif' }}>{article.title}</h3>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif', margin: 0 }}>{article.excerpt}</p>
</div>
<Link href="/blog_single_page" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 700, color: hovered === i ? '#c9960f' : 'rgba(0,0,0,0.35)', fontFamily: 'Sora, sans-serif', textDecoration: 'none', marginTop: 28, transition: 'color 0.2s' }}>
Read Article
</Link> </Link>
</div> </Reveal>
</Reveal> );
))} })}
</div> </div>
</section> </section>
{/* ── NEWSLETTER CTA ── */} {/* ── NEWSLETTER CTA ── */}
<section style={{ padding: '120px 8%', background: '#07070d', margin: '0 20px 20px', borderRadius: 24, textAlign: 'center', position: 'relative', overflow: 'hidden' }}> <section style={{ padding: 0, background: '#07070d', margin: '0 20px 20px', borderRadius: 28, overflow: 'hidden', position: 'relative' }}>
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 600, height: 600, borderRadius: '50%', background: 'radial-gradient(circle,rgba(244,190,40,0.06) 0%,transparent 70%)', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', top: '20%', left: '10%', width: 420, height: 420, borderRadius: '50%', background: 'radial-gradient(circle,rgba(244,190,40,0.1) 0%,transparent 70%)', pointerEvents: 'none' }} />
<Reveal> <div style={{ position: 'absolute', bottom: '-10%', right: '5%', width: 380, height: 380, borderRadius: '50%', background: 'radial-gradient(circle,rgba(167,139,250,0.08) 0%,transparent 70%)', pointerEvents: 'none' }} />
<h2 style={{ fontSize: 'clamp(32px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: '0 0 20px', fontFamily: 'Sora, sans-serif', position: 'relative', zIndex: 2 }}> <div style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(rgba(244,190,40,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(244,190,40,0.025) 1px,transparent 1px)', backgroundSize: '56px 56px', pointerEvents: 'none' }} />
Stay ahead of retail.
</h2> <div className="bm-newsletter-grid" style={{
<p style={{ fontSize: 16, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', margin: '0 auto 44px', maxWidth: 400, position: 'relative', zIndex: 2 }}> position: 'relative', zIndex: 2, display: 'grid', gridTemplateColumns: '1.1fr 0.9fr',
Get our latest insights on AI, footfall, and the future of physical retail, delivered monthly. gap: '5vw', alignItems: 'center', padding: 'clamp(64px, 8vw, 110px) 8%',
</p> }}>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap', position: 'relative', zIndex: 2 }}> <Reveal>
<input placeholder="your@email.com" style={{ <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 18 }}>Newsletter</div>
padding: '16px 24px', borderRadius: 100, border: '1px solid rgba(255,255,255,0.12)', <h2 style={{ fontSize: 'clamp(32px, 4.6vw, 60px)', fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 0.98, color: '#fff', margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
background: 'rgba(255,255,255,0.06)', color: '#fff', Stay ahead of retail.
fontSize: 15, fontFamily: 'Sora, sans-serif', outline: 'none', </h2>
backdropFilter: 'blur(12px)', width: 280, <p style={{ fontSize: 16, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', margin: '0 0 32px', maxWidth: 420, lineHeight: 1.7 }}>
}} /> Get our latest insights on AI, footfall, and the future of physical retail, delivered monthly.
<button style={{ </p>
padding: '16px 32px', borderRadius: 100, border: 'none', <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
background: '#f4be28', color: '#111', {NEWSLETTER_PERKS.map((perk) => (
fontSize: 15, fontWeight: 800, fontFamily: 'Sora, sans-serif', cursor: 'pointer', <div key={perk} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
}}>Subscribe </button> <span style={{
</div> width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
</Reveal> display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(244,190,40,0.12)', border: '1px solid rgba(244,190,40,0.35)',
}}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
</span>
<span style={{ fontSize: 13.5, color: 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif' }}>{perk}</span>
</div>
))}
</div>
</Reveal>
<Reveal delay={0.12}>
<div style={{
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 22, padding: 'clamp(28px, 3vw, 40px)', backdropFilter: 'blur(12px)',
}}>
<label style={{ display: 'block', fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif', marginBottom: 10 }}>Email address</label>
<input placeholder="your@email.com" style={{
width: '100%', boxSizing: 'border-box', padding: '16px 20px', borderRadius: 14,
border: '1px solid rgba(255,255,255,0.14)', background: 'rgba(255,255,255,0.05)', color: '#fff',
fontSize: 15, fontFamily: 'Sora, sans-serif', outline: 'none', marginBottom: 14,
}} />
<button style={{
width: '100%', padding: '16px 24px', borderRadius: 14, border: 'none',
background: 'linear-gradient(135deg,#f4be28,#ff9500)', color: '#111',
fontSize: 15, fontWeight: 800, fontFamily: 'Sora, sans-serif', cursor: 'pointer',
boxShadow: '0 12px 32px rgba(244,190,40,0.3)',
}}>Subscribe </button>
<p style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)', fontFamily: 'Sora, sans-serif', margin: '14px 0 0', textAlign: 'center' }}>No spam. Unsubscribe anytime.</p>
</div>
</Reveal>
</div>
</section> </section>
<style>{` <style>{`
@keyframes bmShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
@keyframes bmPulse { 0%,100%{box-shadow:0 0 0 0 rgba(244,190,40,0.5)} 50%{box-shadow:0 0 0 5px rgba(244,190,40,0)} }
@media (max-width: 860px) { @media (max-width: 860px) {
.bm-featured-grid { grid-template-columns: 1fr !important; padding: 36px 28px !important; gap: 36px !important; } .bm-featured-grid { grid-template-columns: 1fr !important; padding: 36px 28px !important; gap: 36px !important; }
.bm-newsletter-grid { grid-template-columns: 1fr !important; gap: 40px !important; }
} }
`}</style> `}</style>
</main> </main>

View File

@@ -10,11 +10,11 @@ const FAQS = [
category: 'For Customers', category: 'For Customers',
color: '#f4be28', color: '#f4be28',
items: [ items: [
{ q: 'How do I earn LYT?', a: 'You earn LYT through daily app login (+50 LYT), completing 5,000-step walking challenges (+80 LYT), playing interactive games like Maze, Spin Wheel, Scratch Card, and Match Master, and visiting Selfie Booths at partner malls.' }, { q: 'How do I earn LYTs?', a: 'You earn LYTs through daily app login (+50 LYTs), completing 5,000-step walking challenges (+80 LYTs), playing interactive games like Maze, Spin Wheel, Scratch Card, and Match Master, and visiting Selfie Booths at partner malls.' },
{ q: 'How much is 1 LYT worth?', a: '1 LYT = ₹1. So 284 LYT = ₹284 redeemable value at any participating offline store.' }, { q: 'How much is 1 LYT worth?', a: '1 LYT = ₹1. So 284 LYTs = ₹284 redeemable value at any participating offline store.' },
{ q: 'Where can I redeem my LYT?', a: 'You can redeem LYT at any participating offline store near you. Open the app, browse nearby stores, visit in person, and use your LYT or coupons at checkout.' }, { q: 'Where can I redeem my LYTs?', a: 'You can redeem LYTs at any participating offline store near you. Open the app, browse nearby stores, visit in person, and use your LYTs or coupons at checkout.' },
{ q: 'What is the Battery Challenge?', a: 'The Battery Challenge is a daily surprise coupon based on your phone\'s battery percentage when you open the app. Every day brings a different discount — flat off, BOGO, festival specials, and more.' }, { q: 'What is the Battery Challenge?', a: 'The Battery Challenge is a daily surprise coupon based on your phone\'s battery percentage when you open the app. Every day brings a different discount — flat off, BOGO, festival specials, and more.' },
{ q: 'Does my LYT expire?', a: 'LYT is valid as long as your account is active. We recommend redeeming regularly to enjoy maximum savings.' }, { q: 'Do my LYTs expire?', a: 'LYTs are valid as long as your account is active. We recommend redeeming regularly to enjoy maximum savings.' },
{ q: 'How do I sign up?', a: 'Download the Loyaly app, sign up with your mobile number, and you\'re ready to start earning on day one. No credit card needed.' }, { q: 'How do I sign up?', a: 'Download the Loyaly app, sign up with your mobile number, and you\'re ready to start earning on day one. No credit card needed.' },
], ],
}, },

View File

@@ -313,14 +313,14 @@ export default function IndexPage() {
className="loyaly-home relative bg-white overflow-hidden" className="loyaly-home relative bg-white overflow-hidden"
style={{ margin: '20px', borderRadius: '25px', display: isDesktop ? 'none' : 'block' }} style={{ margin: '20px', borderRadius: '25px', display: isDesktop ? 'none' : 'block' }}
> >
<div className="px-6 sm:px-8 pt-12 pb-8 flex flex-col"> <div className="px-6 sm:px-8 pt-24 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"> <h1 className="text-[#171512] hero-heading text-3xl sm:text-4xl font-extrabold tracking-tight leading-[1.12] mb-6 select-none">
Turn Every <br /> Turn Every <br />
Walk In Into a <br /> Walk In Into a <br />
<span className="hero-gradient-text">Loyal Customer</span> <span className="hero-gradient-text">Loyal Customer</span>
</h1> </h1>
<p className="text-[#4f463a] hero-description text-sm sm:text-base mb-8 leading-relaxed select-none"> <p className="text-[#4f463a] hero-description text-sm sm:text-base mb-8 leading-relaxed select-none">
Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them. Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYTs through games and daily activities, then redeem them at offline stores near them.
</p> </p>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<Link href="/contacts" className="flex items-center gap-3 bg-[#f4be28] hover:bg-[#ffd95a] hero-btn-primary text-[#171512] px-6 py-3 rounded-xl font-bold text-base transition-all duration-300 shadow-lg shadow-yellow-500/30 hover:shadow-yellow-500/50 cursor-pointer no-underline"> <Link href="/contacts" className="flex items-center gap-3 bg-[#f4be28] hover:bg-[#ffd95a] hero-btn-primary text-[#171512] px-6 py-3 rounded-xl font-bold text-base transition-all duration-300 shadow-lg shadow-yellow-500/30 hover:shadow-yellow-500/50 cursor-pointer no-underline">
@@ -445,7 +445,7 @@ export default function IndexPage() {
<span className="hero-gradient-text">Loyal Customer</span> <span className="hero-gradient-text">Loyal Customer</span>
</h1> </h1>
<p className="text-[#4f463a] hero-description text-sm sm:text-base md:text-lg xl:text-xl mb-8 leading-relaxed max-w-2xl select-none"> <p className="text-[#4f463a] hero-description text-sm sm:text-base md:text-lg xl:text-xl mb-8 leading-relaxed max-w-2xl select-none">
Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them. Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYTs through games and daily activities, then redeem them at offline stores near them.
</p> </p>
<div className="flex flex-wrap gap-5"> <div className="flex flex-wrap gap-5">
<Link href="/contacts" className="flex items-center gap-3 bg-[#f4be28] hover:bg-[#ffd95a] hero-btn-primary text-[#171512] px-8 py-4 rounded-xl font-bold text-base md:text-lg transition-all duration-300 shadow-lg shadow-yellow-500/30 hover:shadow-yellow-500/50 hover:scale-[1.02] cursor-pointer no-underline"> <Link href="/contacts" className="flex items-center gap-3 bg-[#f4be28] hover:bg-[#ffd95a] hero-btn-primary text-[#171512] px-8 py-4 rounded-xl font-bold text-base md:text-lg transition-all duration-300 shadow-lg shadow-yellow-500/30 hover:shadow-yellow-500/50 hover:scale-[1.02] cursor-pointer no-underline">

View File

@@ -54,7 +54,7 @@ const PRODUCTS = [
}, },
{ {
n: '04', title: 'Identiq', subtitle: 'Consumer Loyalty Platform', color: '#f4be28', n: '04', title: 'Identiq', subtitle: 'Consumer Loyalty Platform', color: '#f4be28',
desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYT, Loyalys reward token, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.', desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYTs, Loyalys reward tokens, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.',
capabilities: [ capabilities: [
'Daily login and step-challenge rewards that build a lasting habit loop', 'Daily login and step-challenge rewards that build a lasting habit loop',
'Game based offers like Spin Wheel, Scratch Card, and Maze Challenge', 'Game based offers like Spin Wheel, Scratch Card, and Maze Challenge',

View File

@@ -29,15 +29,37 @@ export default function Header() {
const pathname = usePathname(); const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false);
const mobileMenuRef = useRef<HTMLDivElement>(null); const mobileMenuRef = useRef<HTMLDivElement>(null);
const lastScrollYRef = useRef(0);
// Detect scroll to show/hide shadow on header // Detect scroll to show/hide shadow on header, and hide the header on
// scroll-down / reveal it again on scroll-up.
useEffect(() => { useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50); lastScrollYRef.current = window.scrollY;
const onScroll = () => {
const y = window.scrollY;
setScrolled(y > 50);
const lastY = lastScrollYRef.current;
if (y <= 80) {
setHidden(false);
} else if (y > lastY + 4) {
setHidden(true);
} else if (y < lastY - 4) {
setHidden(false);
}
lastScrollYRef.current = y;
};
window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll);
}, []); }, []);
// Never hide the header while the mobile nav panel is open
useEffect(() => {
if (mobileOpen) setHidden(false);
}, [mobileOpen]);
// Close mobile menu on route change // Close mobile menu on route change
useEffect(() => { useEffect(() => {
setMobileOpen(false); setMobileOpen(false);
@@ -210,17 +232,19 @@ export default function Header() {
className={transparentHeader ? 'header-transparent' : ''} className={transparentHeader ? 'header-transparent' : ''}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: isMobileViewport ? '14px' : 0,
left: '20px', left: '20px',
right: '20px', right: '20px',
width: 'auto', width: 'auto',
zIndex: 99999999, zIndex: 99999999,
background: transparentHeader ? 'transparent' : '#ffffff', background: transparentHeader ? 'transparent' : '#ffffff',
borderTopLeftRadius: isMobileViewport ? '24px' : '0px',
borderTopRightRadius: isMobileViewport ? '24px' : '0px',
borderBottomLeftRadius: '24px', borderBottomLeftRadius: '24px',
borderBottomRightRadius: '24px', borderBottomRightRadius: '24px',
boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)'), boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 8px 28px rgba(23,21,18,0.12)'),
transition: 'background 0.3s ease, box-shadow 0.3s ease', transform: hidden ? 'translateY(-130%)' : 'translateY(0)',
animation: scrolled ? 'slideDown 0.3s ease' : 'none', transition: 'transform 0.35s cubic-bezier(0.4,0,0.2,1), background 0.3s ease, box-shadow 0.3s ease',
}} }}
> >
<div style={{ <div style={{
@@ -333,10 +357,6 @@ export default function Header() {
</header> </header>
<style>{` <style>{`
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 1023px) { @media (max-width: 1023px) {
.desktop-nav { display: none !important; } .desktop-nav { display: none !important; }
.menu-trigger { display: flex !important; } .menu-trigger { display: flex !important; }

View File

@@ -101,7 +101,7 @@ export default function PhoneWidget() {
<div style={{ fontSize: 16, color: '#fff', fontWeight: 800, fontFamily: 'Sora, sans-serif', marginTop: 2 }}>Loyaly </div> <div style={{ fontSize: 16, color: '#fff', fontWeight: 800, fontFamily: 'Sora, sans-serif', marginTop: 2 }}>Loyaly </div>
</div> </div>
{/* LYT balance card */} {/* LYTs balance card */}
<div style={{ <div style={{
margin: '0 14px', borderRadius: 22, margin: '0 14px', borderRadius: 22,
background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)', background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)',
@@ -109,7 +109,7 @@ export default function PhoneWidget() {
position: 'relative', overflow: 'hidden', position: 'relative', overflow: 'hidden',
}}> }}>
<div style={{ position: 'absolute', top: '-30%', right: '-10%', width: 110, height: 110, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} /> <div style={{ position: 'absolute', top: '-30%', right: '-10%', width: 110, height: 110, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} />
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYT</div> <div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYTs</div>
<div style={{ fontSize: 34, color: '#171512', fontWeight: 900, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div> <div style={{ fontSize: 34, color: '#171512', fontWeight: 900, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 4 }}> 284 redeemable</div> <div style={{ fontSize: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 4 }}> 284 redeemable</div>
</div> </div>
@@ -151,7 +151,7 @@ export default function PhoneWidget() {
}}> }}>
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(74,222,128,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4ade80', fontSize: 14, fontWeight: 800 }}></div> <div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(74,222,128,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4ade80', fontSize: 14, fontWeight: 800 }}></div>
<div> <div>
<div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>+120 LYT</div> <div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>+120 LYTs</div>
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>just now</div> <div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'Sora, sans-serif' }}>just now</div>
</div> </div>
</div> </div>

View File

@@ -44,7 +44,7 @@ const SOLUTIONS = [
title: 'Loyalty Management', title: 'Loyalty Management',
subtitle: 'Build Lasting Relationships', subtitle: 'Build Lasting Relationships',
excerpt: excerpt:
'Reward customer loyalty with smart programmes that adapt in real time — from LYT and tiers to personalised perks that feel genuinely earned.', 'Reward customer loyalty with smart programmes that adapt in real time — from LYTs and tiers to personalised perks that feel genuinely earned.',
image: '/loyaly_php/assets/images/cognivision-transformative-ai-for-intelligent-fi-640x640.jpg', image: '/loyaly_php/assets/images/cognivision-transformative-ai-for-intelligent-fi-640x640.jpg',
imageAlt: 'Loyalty management module showing customer tier performance', imageAlt: 'Loyalty management module showing customer tier performance',
href: '/solutions/loyalty-management', href: '/solutions/loyalty-management',

View File

@@ -9,7 +9,7 @@ if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const recs = [ const recs = [
{ rank: '01', label: 'Earn 2× LYT at Starbucks', match: 98, tag: 'Near you · 0.3km' }, { rank: '01', label: 'Earn 2× LYTs at Starbucks', match: 98, tag: 'Near you · 0.3km' },
{ rank: '02', label: 'Nike Air sale ends tonight', match: 94, tag: 'Trending · Fashion' }, { rank: '02', label: 'Nike Air sale ends tonight', match: 94, tag: 'Trending · Fashion' },
{ rank: '03', label: 'Decathlon weekend special', match: 91, tag: 'Sports · 1.4km' }, { rank: '03', label: 'Decathlon weekend special', match: 91, tag: 'Sports · 1.4km' },
{ rank: '04', label: 'Zara new arrivals, 15% off', match: 87, tag: 'Your style' }, { rank: '04', label: 'Zara new arrivals, 15% off', match: 87, tag: 'Your style' },
@@ -76,7 +76,7 @@ export default function AIRecommendationsSection() {
{/* Left — editorial */} {/* Left — editorial */}
<div> <div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence<span className="fp-kicker-num">05</span></span></div> <div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence<span className="fp-kicker-num">06</span></span></div>
<h2 style={{ <h2 style={{
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.94, fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.94,

View File

@@ -2,13 +2,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { PhoneFrame, PhoneBottomNav, BellIcon, PinIcon, ChevronIcon, SearchIcon, MicIcon, CameraIcon, MapIcon, HeartLogoIcon } from './PhoneMockup';
const deals = [ const deals = [
{ store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYT', savings: '₹120 saved', color: '#00704A', distance: '0.3 km', rating: 4.8 }, { store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYTs', savings: '₹120 saved', color: '#00704A', dot: '#22c55e', distance: '0.3 km', rating: 4.8 },
{ store: 'Nike', category: 'Footwear', offer: '30% OFF', savings: '₹890 saved', color: '#ffffff', distance: '0.7 km', rating: 4.9 }, { store: 'Nike', category: 'Footwear', offer: '30% OFF', savings: '₹890 saved', color: '#171512', dot: '#d1d5db', distance: '0.7 km', rating: 4.9 },
{ store: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', distance: '1.1 km', rating: 4.6 }, { store: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', dot: '#ef4444', distance: '1.1 km', rating: 4.6 },
{ store: 'Decathlon', category: 'Sports', offer: 'Free Gift', savings: '₹350 saved', color: '#0082C8', distance: '1.4 km', rating: 4.7 }, { store: 'Decathlon', category: 'Sports', offer: 'Free Gift', savings: '₹350 saved', color: '#0082C8', dot: '#3b82f6', distance: '1.4 km', rating: 4.7 },
{ store: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#9ca3af', distance: '0.9 km', rating: 4.5 }, { store: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#171512', dot: '#d1d5db', distance: '0.9 km', rating: 4.5 },
]; ];
export default function DiscoverDealsSection() { export default function DiscoverDealsSection() {
@@ -28,11 +29,11 @@ export default function DiscoverDealsSection() {
}} /> }} />
<div className="fp-shell"> <div className="fp-shell">
<div className="fp-split" style={{ gridTemplateColumns: '0.85fr 1.15fr', alignItems: 'center' }}> <div className="fp-split" style={{ gridTemplateColumns: '0.9fr 1.1fr', alignItems: 'center' }}>
{/* Headline column */} {/* Headline column */}
<div> <div>
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover<span className="fp-kicker-num">02</span></span></div> <div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover<span className="fp-kicker-num">03</span></span></div>
<h2 style={{ <h2 style={{
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94, fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94,
@@ -46,7 +47,7 @@ export default function DiscoverDealsSection() {
<p className="fp-rise" style={{ <p className="fp-rise" style={{
fontSize: 16, lineHeight: 1.8, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: '0 0 36px', maxWidth: 380, fontSize: 16, lineHeight: 1.8, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: '0 0 36px', maxWidth: 380,
}}> }}>
Find participating stores nearby and see their active game based discount campaigns, live coupons, and LYT earning opportunities, all surfaced in real time so you never miss a deal. Find participating stores nearby and see their active game based discount campaigns, live coupons, and LYTs earning opportunities, all surfaced in real time so you never miss a deal.
</p> </p>
<div className="fp-rise" style={{ <div className="fp-rise" style={{
@@ -61,63 +62,97 @@ export default function DiscoverDealsSection() {
</div> </div>
</div> </div>
{/* Deal list */} {/* Phone mockup — white app screen, matches the in-app Discover experience */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div className="fp-rise" style={{ transitionDelay: '0.1s', display: 'flex', justifyContent: 'center' }}>
{deals.map((deal, i) => ( <PhoneFrame floatClassName="ddm-phone-shell">
<div
key={i} {/* Amber header */}
onMouseEnter={() => setHovered(i)} <div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
onMouseLeave={() => setHovered(null)} <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
className="fp-rise" <Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
style={{ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
position: 'relative', overflow: 'hidden', <div style={{ display: 'flex', alignItems: 'center', gap: 5, background: '#fff', borderRadius: 100, padding: '5px 10px', boxShadow: '0 2px 6px rgba(0,0,0,0.12)' }}>
display: 'grid', gridTemplateColumns: '46px 1fr auto', alignItems: 'center', gap: 16, <HeartLogoIcon />
padding: '16px 20px', borderRadius: 16, <span style={{ fontWeight: 800, fontSize: 11.5, color: '#171512', fontFamily: 'Sora, sans-serif' }}>1,250</span>
background: hovered === i ? 'rgba(244,190,40,0.06)' : 'rgba(255,255,255,0.03)', </div>
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.28)' : 'rgba(255,255,255,0.07)'}`, <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
backdropFilter: 'blur(20px)', cursor: 'pointer', <BellIcon />
transitionDelay: `${0.1 + i * 0.07}s`, </div>
boxShadow: hovered === i ? '0 14px 40px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06)' : 'none',
transform: `${hovered === i ? 'translateX(8px)' : 'translateX(0)'}`,
}}
>
{/* Bottom border sweep, left to right on hover */}
<div style={{
position: 'absolute', left: 0, right: 0, bottom: 0, height: 2,
background: 'linear-gradient(90deg, #f4be28, #ff9500)',
transformOrigin: 'left center',
transform: hovered === i ? 'scaleX(1)' : 'scaleX(0)',
transition: 'transform 0.45s cubic-bezier(0.22,1,0.36,1)',
transitionDelay: '0s',
boxShadow: '0 0 10px rgba(244,190,40,0.5)',
pointerEvents: 'none',
}} />
<div style={{
width: 46, height: 46, borderRadius: 13, background: `${deal.color}22`, border: `1px solid ${deal.color}44`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ width: 15, height: 15, borderRadius: '50%', background: deal.color }} />
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<span style={{ fontSize: 16, fontWeight: 700, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{deal.store}</span>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Sora, sans-serif' }}>{deal.distance}</span>
<span style={{ fontSize: 11, color: '#f4be28', fontFamily: 'Sora, sans-serif' }}> {deal.rating}</span>
</div> </div>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.38)', fontFamily: 'Sora, sans-serif' }}>{deal.category}</span>
</div> </div>
<div style={{ textAlign: 'right' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ fontSize: 15, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{deal.offer}</div> <PinIcon />
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{deal.savings}</div> <span style={{ fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>R.S. Puram, Coimbatore</span>
<ChevronIcon color="#171512" />
</div> </div>
</div> </div>
))}
<div className="fp-rise" style={{ {/* Search bar */}
textAlign: 'right', paddingRight: 4, marginTop: 4, transitionDelay: '0.5s', <div style={{ padding: '12px 14px 4px' }}>
fontSize: 12, color: 'rgba(255,255,255,0.25)', fontFamily: 'Sora, sans-serif', letterSpacing: '0.06em', <div style={{ display: 'flex', alignItems: 'center', gap: 9, background: '#f5f5f6', borderRadius: 100, padding: '9px 14px' }}>
}}> <SearchIcon />
+ 2,400 more deals near you <span style={{ flex: 1, color: '#9ca3af', fontSize: 12, fontFamily: 'Sora, sans-serif' }}>Search</span>
</div> <MicIcon />
<CameraIcon />
</div>
</div>
{/* Deal rows */}
<div style={{ padding: '8px 12px 2px', display: 'flex', flexDirection: 'column', gap: 6 }}>
{deals.map((deal, i) => (
<div
key={deal.store}
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
style={{
display: 'grid', gridTemplateColumns: '34px minmax(0,1fr) 62px 12px', alignItems: 'center', gap: 8,
padding: '10px 8px', borderRadius: 14,
background: hovered === i ? '#fff7e0' : 'transparent',
transition: 'background 0.25s ease',
cursor: 'default',
}}
>
<div style={{ position: 'relative', width: 34, height: 34, borderRadius: 10, background: deal.color, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 12, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>{deal.store[0]}</span>
<span style={{ position: 'absolute', top: -2, left: -2, width: 9, height: 9, borderRadius: '50%', background: deal.dot, border: '2px solid #fff' }} />
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: 12.5, fontWeight: 700, color: '#171512', fontFamily: 'Sora, sans-serif',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.store}</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 5, marginTop: 2,
fontSize: 9.5, color: '#9ca3af', fontFamily: 'Sora, sans-serif', whiteSpace: 'nowrap',
}}>
<span>{deal.distance}</span>
<span style={{ color: '#f4be28' }}> {deal.rating}</span>
</div>
</div>
<div style={{ textAlign: 'right', minWidth: 0 }}>
<div style={{
fontSize: 11.5, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.01em',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.offer}</div>
<div style={{
fontSize: 8.5, color: '#9ca3af', fontFamily: 'Sora, sans-serif', marginTop: 1,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{deal.savings}</div>
</div>
<ChevronIcon />
</div>
))}
</div>
{/* More deals row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 18px', margin: '2px 10px 10px', borderRadius: 14, background: '#faf7ee' }}>
<MapIcon />
<span style={{ flex: 1, fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>+ 2,400 more deals near you</span>
<ChevronIcon />
</div>
<PhoneBottomNav active="heart" />
</PhoneFrame>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
'use client'; 'use client';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { PhoneFrame, PhoneBottomNav, BellIcon, FlameIcon } from './PhoneMockup';
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
@@ -11,9 +13,6 @@ const feed = [
{ store: 'Walking 5,000 steps', pts: +80, time: '1h ago', type: 'earn' }, { store: 'Walking 5,000 steps', pts: +80, time: '1h ago', type: 'earn' },
{ store: 'Spin Wheel Game', pts: +120, time: '3h ago', type: 'earn' }, { store: 'Spin Wheel Game', pts: +120, time: '3h ago', type: 'earn' },
{ store: 'Selfie Booth', pts: +200, time: 'Yesterday', type: 'earn' }, { store: 'Selfie Booth', pts: +200, time: 'Yesterday', type: 'earn' },
{ store: 'Scratch Card', pts: +40, time: 'Yesterday', type: 'earn' },
{ store: 'Match Master', pts: +75, time: '2 days ago', type: 'earn' },
{ store: 'Maze Challenge', pts: +95, time: '3 days ago', type: 'earn' },
]; ];
export default function EarnRewardsSection() { export default function EarnRewardsSection() {
@@ -35,27 +34,20 @@ export default function EarnRewardsSection() {
rowRefs.current.forEach((row, i) => { rowRefs.current.forEach((row, i) => {
if (!row) return; if (!row) return;
const active = i === idx; const active = i === idx;
row.style.background = active ? '#16a34a' : 'rgba(23,21,18,0.55)'; row.style.background = active ? '#f0fdf4' : 'transparent';
row.style.borderColor = active ? '#16a34a' : 'rgba(255,255,255,0.14)'; row.style.borderColor = active ? '#16a34a' : 'transparent';
row.style.transform = active ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)'; row.style.transform = active ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)';
row.style.boxShadow = active ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none'; row.style.boxShadow = active ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none';
row.style.zIndex = active ? '2' : '1';
// Flip inner text/icon colors for contrast per background
const title = row.querySelector<HTMLElement>('.feed-title');
if (title) title.style.color = '#ffffff';
const time = row.querySelector<HTMLElement>('.feed-time');
if (time) time.style.color = active ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.55)';
const icon = row.querySelector<HTMLElement>('.feed-icon'); const icon = row.querySelector<HTMLElement>('.feed-icon');
if (icon) { if (icon) {
icon.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)'; icon.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)';
icon.style.borderColor = active ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)'; icon.style.borderColor = active ? '#16a34a' : 'rgba(23,21,18,0.08)';
icon.style.color = active ? '#ffffff' : '#4ade80'; icon.style.color = active ? '#16a34a' : '#9ca3af';
} }
const points = row.querySelector<HTMLElement>('.feed-pts'); const points = row.querySelector<HTMLElement>('.feed-pts');
if (points) { if (points) {
points.style.color = active ? '#ffffff' : '#4ade80'; points.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)';
points.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)';
} }
}); });
}; };
@@ -103,7 +95,7 @@ export default function EarnRewardsSection() {
background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '7px 16px', background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '7px 16px',
}}> }}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0, animation: 'fpPulse 1.5s infinite' }} /> <span style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0, animation: 'fpPulse 1.5s infinite' }} />
<span style={{ fontSize: 11, fontWeight: 700, color: '#16a34a', fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live LYT balance</span> <span style={{ fontSize: 11, fontWeight: 700, color: '#16a34a', fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live LYTs balance</span>
</div> </div>
{/* Giant gold number */} {/* Giant gold number */}
@@ -125,7 +117,7 @@ export default function EarnRewardsSection() {
<span style={{ fontSize: 13, color: '#4f463a', fontFamily: 'Sora, sans-serif' }}>redeemable value</span> <span style={{ fontSize: 13, color: '#4f463a', fontFamily: 'Sora, sans-serif' }}>redeemable value</span>
</div> </div>
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn<span className="fp-kicker-num">03</span></span></div> <div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn<span className="fp-kicker-num">04</span></span></div>
<h2 style={{ <h2 style={{
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08, fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08,
@@ -139,83 +131,107 @@ export default function EarnRewardsSection() {
<p className="fp-rise" style={{ <p className="fp-rise" style={{
fontSize: 15, lineHeight: 1.8, color: '#4f463a', fontFamily: 'Sora, sans-serif', maxWidth: 380, margin: 0, fontSize: 15, lineHeight: 1.8, color: '#4f463a', fontFamily: 'Sora, sans-serif', maxWidth: 380, margin: 0,
}}> }}>
Earn LYT (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYT to your balance. Earn LYTs (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYTs to your balance.
</p> </p>
</div> </div>
{/* Right — live feed */} {/* Right — phone mockup, matches the in-app Earn / Home experience */}
<div className="fp-rise" style={{ transitionDelay: '0.15s' }}> <div className="fp-rise" style={{ transitionDelay: '0.15s', display: 'flex', justifyContent: 'center' }}>
{/* Feed header */} <PhoneFrame floatClassName="erm-phone-shell">
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 14, paddingBottom: 14, borderBottom: '2px solid #f4be28',
}}>
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Sora, sans-serif' }}>Activity feed</span>
<span style={{ fontSize: 11, color: '#16a34a', fontWeight: 700, fontFamily: 'Sora, sans-serif', display: 'flex', alignItems: 'center', gap: 5,
background: '#f0fdf4', border: '1px solid #16a34a', borderRadius: 100, padding: '4px 10px',
}}>
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite' }} />
LIVE
</span>
</div>
{/* Feed rows */} {/* Amber header */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
{feed.map((tx, i) => ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<div key={i} ref={el => { rowRefs.current[i] = el; }} style={{ <Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
position: 'relative', zIndex: i === 0 ? 2 : 1, <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
display: 'flex', alignItems: 'center', justifyContent: 'space-between', <BellIcon />
padding: '12px 14px', borderRadius: 12,
background: i === 0 ? '#16a34a' : 'rgba(23,21,18,0.55)',
border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(255,255,255,0.14)'}`,
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
transform: i === 0 ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)',
boxShadow: i === 0 ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none',
transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div className="feed-icon" style={{
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
background: i === 0 ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)',
border: `1.5px solid ${i === 0 ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 900,
color: i === 0 ? '#ffffff' : '#4ade80',
transition: 'background 0.35s ease, border-color 0.35s ease, color 0.35s ease',
}}>{tx.type === 'earn' ? '↑' : '↓'}</div>
<div>
<div className="feed-title" style={{ fontSize: 13, fontWeight: 700, color: '#ffffff', fontFamily: 'Sora, sans-serif', transition: 'color 0.35s ease' }}>{tx.store}</div>
<div className="feed-time" style={{ fontSize: 11, color: i === 0 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 1, transition: 'color 0.35s ease' }}>{tx.time}</div>
</div>
</div> </div>
<span className="feed-pts" style={{
fontSize: 15, fontWeight: 900, fontFamily: 'Sora, sans-serif',
color: i === 0 ? '#ffffff' : '#4ade80', letterSpacing: '-0.02em',
background: i === 0 ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)',
padding: '4px 10px', borderRadius: 8,
transition: 'color 0.35s ease, background 0.35s ease',
}}>
{tx.pts > 0 ? '+' : ''}{tx.pts}
</span>
</div> </div>
))} <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
</div> <FlameIcon />
<span style={{ fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>6-day streak · keep it going!</span>
</div>
</div>
{/* Summary strip */} {/* Balance card */}
<div style={{ <div style={{ margin: '14px 14px 0', borderRadius: 18, padding: '16px 18px', background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)', position: 'relative', overflow: 'hidden' }}>
marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, <div style={{ position: 'absolute', top: '-30%', right: '-8%', width: 90, height: 90, borderRadius: '50%', background: 'rgba(255,255,255,0.14)' }} />
}}> <div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total LYTs</div>
{[ <div style={{ fontSize: 30, color: '#171512', fontWeight: 900, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
{ label: 'Earned this month', val: '660 LYT', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' }, <div style={{ fontSize: 10.5, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 3 }}> 284 redeemable</div>
{ label: 'Redeemable value', val: '₹660', color: '#f4be28', bg: '#fff7e0', border: '#f4be28' }, </div>
].map((s, i) => (
<div key={i} style={{ padding: '14px 16px', borderRadius: 14, background: s.bg, border: `1.5px solid ${s.border}` }}> {/* Feed header */}
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginBottom: 4 }}>{s.label}</div> <div style={{
<div style={{ fontSize: 18, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div> display: 'flex', justifyContent: 'space-between', alignItems: 'center',
</div> margin: '16px 14px 6px',
))} }}>
</div> <span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Sora, sans-serif' }}>Activity feed</span>
<span style={{ fontSize: 9.5, color: '#16a34a', fontWeight: 700, fontFamily: 'Sora, sans-serif', display: 'flex', alignItems: 'center', gap: 4,
background: '#f0fdf4', border: '1px solid #16a34a', borderRadius: 100, padding: '3px 8px',
}}>
<span style={{ width: 4.5, height: 4.5, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite' }} />
LIVE
</span>
</div>
{/* Feed rows */}
<div style={{ padding: '2px 10px', display: 'flex', flexDirection: 'column', gap: 3 }}>
{feed.map((tx, i) => (
<div key={i} ref={el => { rowRefs.current[i] = el; }} style={{
display: 'grid', gridTemplateColumns: '32px minmax(0,1fr) auto', alignItems: 'center', gap: 10,
padding: '8px 8px', borderRadius: 12,
background: i === 0 ? '#f0fdf4' : 'transparent',
border: `1px solid ${i === 0 ? '#16a34a' : 'transparent'}`,
transform: i === 0 ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)',
boxShadow: i === 0 ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none',
transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease',
}}>
<div className="feed-icon" style={{
width: 32, height: 32, borderRadius: 9, flexShrink: 0,
background: i === 0 ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)',
border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(23,21,18,0.08)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: i === 0 ? '#16a34a' : '#9ca3af',
transition: 'background 0.35s ease, border-color 0.35s ease, color 0.35s ease',
}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5" /><path d="M5 12l7-7 7 7" /></svg>
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: 11.5, fontWeight: 700, color: '#171512', fontFamily: 'Sora, sans-serif',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{tx.store}</div>
<div style={{ fontSize: 9, color: '#9ca3af', fontFamily: 'Sora, sans-serif', marginTop: 1 }}>{tx.time}</div>
</div>
<span className="feed-pts" style={{
fontSize: 12, fontWeight: 900, fontFamily: 'Sora, sans-serif',
color: '#16a34a', letterSpacing: '-0.01em',
background: i === 0 ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)',
padding: '3px 8px', borderRadius: 8, whiteSpace: 'nowrap',
transition: 'background 0.35s ease',
}}>
+{tx.pts}
</span>
</div>
))}
</div>
{/* Summary strip */}
<div style={{ margin: '10px 14px 12px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{[
{ label: 'Earned this month', val: '660 LYTs', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' },
{ label: 'Redeemable value', val: '₹660', color: '#bc7122', bg: '#fff7e0', border: '#f4be28' },
].map((s, i) => (
<div key={i} style={{ padding: '10px 12px', borderRadius: 12, background: s.bg, border: `1.5px solid ${s.border}` }}>
<div style={{ fontSize: 8.5, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginBottom: 3 }}>{s.label}</div>
<div style={{ fontSize: 14, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div>
</div>
))}
</div>
<PhoneBottomNav active="home" />
</PhoneFrame>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,7 +19,7 @@ export default function ExperienceSection() {
<div className="fp-shell"> <div className="fp-shell">
<div style={{ marginBottom: 'clamp(36px, 5vh, 60px)' }}> <div style={{ marginBottom: 'clamp(36px, 5vh, 60px)' }}>
<span className="fp-kicker fp-rise">The Experience<span className="fp-kicker-num">06</span></span> <span className="fp-kicker fp-rise">The Experience<span className="fp-kicker-num">07</span></span>
</div> </div>
<div className="fp-split" style={{ gridTemplateColumns: '1.1fr 0.9fr', alignItems: 'center' }}> <div className="fp-split" style={{ gridTemplateColumns: '1.1fr 0.9fr', alignItems: 'center' }}>

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
function useMagnetic<T extends HTMLElement>(strength = 12) { function useMagnetic<T extends HTMLElement>(strength = 12) {
@@ -27,7 +26,6 @@ function useMagnetic<T extends HTMLElement>(strength = 12) {
} }
export default function HeroSection() { export default function HeroSection() {
const magPrimary = useMagnetic<HTMLAnchorElement>(10);
const magSecondary = useMagnetic<HTMLButtonElement>(8); const magSecondary = useMagnetic<HTMLButtonElement>(8);
return ( return (
@@ -71,19 +69,12 @@ export default function HeroSection() {
<p className="fp-content fp-subtext" style={{ <p className="fp-content fp-subtext" style={{
fontSize: 'clamp(16px, 1.4vw, 19px)', lineHeight: 1.7, fontWeight: 400, fontSize: 'clamp(16px, 1.4vw, 19px)', lineHeight: 1.7, fontWeight: 400,
color: 'rgba(255,255,255,0.58)', maxWidth: 560, margin: '0 auto 40px', fontFamily: 'Sora, sans-serif', color: 'rgba(255,255,255,0.58)', maxWidth: 640, margin: '0 auto 40px', fontFamily: 'Sora, sans-serif',
}}> }}>
Earn LYT through daily activities, interactive games, and walking challenges, then redeem them at participating stores near you. Earn Online. Spend Offline. Meet <strong>LYT (Loyaly Token)</strong>: the single currency behind every reward in the app. Earn them through daily tasks, walking challenges, and games, then spend them instantly at partner stores near you with zero expiry.
</p> </p>
<div className="fp-content fp-cta" style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center' }}> <div className="fp-content fp-cta" style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center' }}>
<Link
href="/contacts"
ref={magPrimary}
className="fp-btn fp-btn-primary fp-magnetic"
>
Download Free
</Link>
<button <button
ref={magSecondary} ref={magSecondary}
className="fp-btn fp-btn-ghost fp-magnetic" className="fp-btn fp-btn-ghost fp-magnetic"
@@ -92,8 +83,41 @@ export default function HeroSection() {
</button> </button>
</div> </div>
{/* LYT Quick Facts */}
<div className="fp-content fp-cta" style={{
display: 'flex',
gap: 16,
justifyContent: 'center',
flexWrap: 'wrap',
margin: '36px auto 0',
maxWidth: 600,
}}>
{[
{ val: '1 LYT = ₹1', label: 'Fixed value' },
{ val: 'Zero Expiry', label: 'Ever' },
{ val: '10,000+', label: 'Partner stores' }
].map((item, idx) => (
<div key={idx} style={{
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 13,
fontFamily: 'Sora, sans-serif',
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(244, 190, 40, 0.22)',
padding: '8px 16px',
borderRadius: '100px',
backdropFilter: 'blur(8px)',
}}>
<span style={{ color: '#f4be28', fontWeight: 800 }}>{item.val}</span>
<span style={{ width: 4, height: 4, borderRadius: '50%', background: 'rgba(255,255,255,0.25)' }} />
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 11.5 }}>{item.label}</span>
</div>
))}
</div>
{/* Trust row */} {/* Trust row */}
<div className="fp-content fp-cta" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, marginTop: 40 }}> <div className="fp-content fp-cta" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, marginTop: 32 }}>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
{['#f4be28', '#4ade80', '#a855f7', '#3b82f6'].map((c, i) => ( {['#f4be28', '#4ade80', '#a855f7', '#3b82f6'].map((c, i) => (
<div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid #0a0a12', marginLeft: i > 0 ? -9 : 0, opacity: 0.9 }} /> <div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid #0a0a12', marginLeft: i > 0 ? -9 : 0, opacity: 0.9 }} />

View File

@@ -0,0 +1,145 @@
'use client';
import React from 'react';
import Image from 'next/image';
/* ── Shared icon set — used across the for-people phone mockups ── */
export function BellIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
}
export function PinIcon() {
return (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
<circle cx="12" cy="9" r="2.4" />
</svg>
);
}
export function ChevronIcon({ color = '#9ca3af' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 6l6 6-6 6" />
</svg>
);
}
export function SearchIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" />
</svg>
);
}
export function MicIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="2" width="6" height="12" rx="3" /><path d="M5 10a7 7 0 0 0 14 0" /><path d="M12 19v3" />
</svg>
);
}
export function CameraIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
);
}
export function MapIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 6v16l7-4 8 4 7-4V2l-7 4-8-4z" /><path d="M8 2v16" /><path d="M16 6v16" />
</svg>
);
}
export function HeartLogoIcon({ size = 16 }: { size?: number }) {
return (
<Image src="/images/loyalyai-logo.png" alt="" width={32} height={32} style={{ width: size, height: size, objectFit: 'contain' }} />
);
}
export function UpArrowIcon({ color = '#16a34a' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 19V5" /><path d="M5 12l7-7 7 7" />
</svg>
);
}
export function DownArrowIcon({ color = '#ef4444' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14" /><path d="M5 12l7 7 7-7" />
</svg>
);
}
export function FlameIcon({ color = '#f4be28' }: { color?: string }) {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill={color} stroke="none">
<path d="M12 2c1 3-2 4-2 7a3 3 0 0 0 6 0c1.5 1.5 2 3.5 2 5a6 6 0 0 1-12 0c0-4 3-6 3-9 0-1 1-2 3-3z" />
</svg>
);
}
/* ── Bottom nav — pass the active tab key to highlight it ── */
export function PhoneBottomNav({ active = 'home' }: { active?: string }) {
const items: { key: string; icon: React.ReactNode }[] = [
{ key: 'home', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 10.5L12 3l9 7.5" /><path d="M5 9.5V21h14V9.5" /></svg> },
{ key: 'tag', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41 12 22l-9-9V4h9z" /><circle cx="7.5" cy="8.5" r="1.2" /></svg> },
{ key: 'heart', icon: <HeartLogoIcon size={18} /> },
{ key: 'shop', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l1.5-5h15L21 9" /><path d="M3 9h18v11H3z" /><path d="M9 20v-6h6v6" /></svg> },
{ key: 'user', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="4" /><path d="M4 21c1.5-4 5-6 8-6s6.5 2 8 6" /></svg> },
];
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-around', padding: '10px 8px', borderTop: '1px solid #f0f0f0' }}>
{items.map((n) => {
const isActive = n.key === active;
return (
<div key={n.key} style={{
width: 34, height: 34, borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: isActive && n.key !== 'heart' ? '#171512' : '#9ca3af',
background: isActive ? '#fff7e0' : 'transparent',
border: isActive ? '1.5px solid #f4be28' : 'none',
}}>
{n.icon}
</div>
);
})}
</div>
);
}
/* ── Phone frame — dark bezel + dynamic island + gentle float, white screen inside ── */
export function PhoneFrame({ children, floatClassName = 'pm-phone-float', width = 300 }: { children: React.ReactNode; floatClassName?: string; width?: number }) {
return (
<div
className={floatClassName}
style={{
width, borderRadius: 42, padding: 10,
background: 'linear-gradient(160deg, #2a2a2e 0%, #0a0a0c 100%)',
boxShadow: '0 60px 120px -24px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06)',
position: 'relative',
}}
>
<div style={{ borderRadius: 32, overflow: 'hidden', background: '#ffffff', position: 'relative' }}>
{/* Dynamic island */}
<div style={{
position: 'absolute', top: 8, left: '50%', transform: 'translateX(-50%)',
width: 82, height: 22, borderRadius: 14, background: '#000', zIndex: 5,
}} />
{children}
</div>
<style>{`
@keyframes pmPhoneFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.pm-phone-float { animation: pmPhoneFloat 7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .pm-phone-float { animation: none !important; } }
@media (max-width: 900px) {
.pm-phone-float { width: 280px !important; }
}
`}</style>
</div>
);
}

View File

@@ -80,7 +80,7 @@ export default function SpendAnywhereSection() {
{/* Header row */} {/* Header row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 'clamp(40px, 6vh, 64px)' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 'clamp(40px, 6vh, 64px)' }}>
<div> <div>
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend<span className="fp-kicker-num">04</span></span></div> <div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend<span className="fp-kicker-num">05</span></span></div>
<h2 style={{ <h2 style={{
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9, fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9,
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif', color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
@@ -92,7 +92,7 @@ export default function SpendAnywhereSection() {
<p className="fp-rise" style={{ <p className="fp-rise" style={{
fontSize: 15, lineHeight: 1.75, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: 0, maxWidth: 320, fontSize: 15, lineHeight: 1.75, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', margin: 0, maxWidth: 320,
}}> }}>
One balance. Every store. Your LYT is universal: no restrictions, no expiry, no fine print. One balance. Every store. Your LYTs are universal: no restrictions, no expiry, no fine print.
</p> </p>
</div> </div>
@@ -184,7 +184,7 @@ export default function SpendAnywhereSection() {
}}> }}>
{[ {[
{ val: '10,000+', lbl: 'Partner stores' }, { val: '10,000+', lbl: 'Partner stores' },
{ val: 'Zero', lbl: 'Expiry on LYT' }, { val: 'Zero', lbl: 'Expiry on LYTs' },
{ val: 'Universal', lbl: 'One balance, all brands' }, { val: 'Universal', lbl: 'One balance, all brands' },
{ val: 'Instant', lbl: 'Redeem at checkout' }, { val: 'Instant', lbl: 'Redeem at checkout' },
].map((s, i) => ( ].map((s, i) => (

View File

@@ -178,7 +178,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
minHeight: '180px', minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
@@ -196,8 +196,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute', position: 'absolute',
right: '-18px', right: '-18px',
bottom: '-18px', bottom: '-18px',
width: '210px', width: '250px',
height: '210px', height: '250px',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
@@ -210,7 +210,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: 'scale(2.0)', transform: 'scale(1.85)',
transformOrigin: 'center center', transformOrigin: 'center center',
}} }}
/> />
@@ -233,7 +233,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
minHeight: '180px', minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
@@ -251,8 +251,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute', position: 'absolute',
right: '-18px', right: '-18px',
bottom: '-18px', bottom: '-18px',
width: '210px', width: '250px',
height: '210px', height: '250px',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
@@ -265,7 +265,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: 'scale(2.0)', transform: 'scale(1.85)',
transformOrigin: 'center center', transformOrigin: 'center center',
}} }}
/> />
@@ -288,7 +288,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
minHeight: '180px', minHeight: '230px',
boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)', boxShadow: '0 10px 30px -12px rgba(225, 45, 45, 0.4)',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
@@ -306,8 +306,8 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute', position: 'absolute',
right: '-18px', right: '-18px',
bottom: '-18px', bottom: '-18px',
width: '210px', width: '250px',
height: '210px', height: '250px',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
@@ -320,7 +320,7 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: 'scale(2.0)', transform: 'scale(1.85)',
transformOrigin: 'center center', transformOrigin: 'center center',
}} }}
/> />
@@ -347,12 +347,12 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
minHeight: '190px', minHeight: '250px',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<div style={{ width: card.wide ? '46%' : '54%', paddingRight: '12px', position: 'relative', zIndex: 2 }}> <div style={{ width: card.wide ? '42%' : '52%', paddingRight: '12px', position: 'relative', zIndex: 2 }}>
{card.icon} {card.icon}
<h3 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#ffffff', fontFamily: 'Sora, sans-serif', margin: '0 0 6px' }}> <h3 style={{ fontSize: '1.1rem', fontWeight: 800, color: '#ffffff', fontFamily: 'Sora, sans-serif', margin: '0 0 6px' }}>
{card.title} {card.title}
@@ -366,16 +366,16 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
position: 'absolute', position: 'absolute',
right: '-6px', right: '-6px',
bottom: '-6px', bottom: '-6px',
width: '300px', width: '380px',
height: '180px', height: '230px',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
<Image <Image
src={card.image} src={card.image}
alt={card.title} alt={card.title}
width={400} width={480}
height={400} height={480}
style={{ style={{
objectFit: 'cover', objectFit: 'cover',
objectPosition: '52% 34%', objectPosition: '52% 34%',
@@ -387,23 +387,23 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
) : ( ) : (
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
right: '-24px', right: '-22px',
bottom: '-24px', bottom: '-22px',
width: '230px', width: '280px',
height: '230px', height: '280px',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
<Image <Image
src={card.image} src={card.image}
alt={card.title} alt={card.title}
width={400} width={440}
height={400} height={440}
style={{ style={{
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: 'scale(2.1)', transform: 'scale(2.0)',
transformOrigin: 'center center', transformOrigin: 'center center',
}} }}
/> />

View File

@@ -63,7 +63,7 @@ const PRODUCTS: Product[] = [
title: 'Identiq', title: 'Identiq',
subtitle: 'Consumer Loyalty Platform', subtitle: 'Consumer Loyalty Platform',
description: description:
'Earn LYT through daily logins and games, then redeem rewards at real offline stores.', 'Earn LYTs through daily logins and games, then redeem rewards at real offline stores.',
accent: '#f4be28', accent: '#f4be28',
accentRgb: '244, 190, 40', accentRgb: '244, 190, 40',
tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'], tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'],
@@ -283,12 +283,12 @@ function ProductVisual({ product }: { product: Product }) {
</span> </span>
</div> </div>
{/* LYT balance card with progress to next tier */} {/* LYTs balance card with progress to next tier */}
<div style={{ borderRadius: 18, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)`, position: 'relative', overflow: 'hidden' }}> <div style={{ borderRadius: 18, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)`, position: 'relative', overflow: 'hidden' }}>
<div className="pv-sheen" /> <div className="pv-sheen" />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div> <div>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward LYT</div> <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward LYTs</div>
<div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div> <div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
</div> </div>
<span style={{ width: 34, height: 34, borderRadius: 10, background: 'rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif' }}></span> <span style={{ width: 34, height: 34, borderRadius: 10, background: 'rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif' }}></span>
@@ -297,7 +297,7 @@ function ProductVisual({ product }: { product: Product }) {
<div style={{ height: 5, borderRadius: 3, background: 'rgba(0,0,0,0.15)', overflow: 'hidden' }}> <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 className="pv-progress-fill" style={{ height: '100%', width: '68%', borderRadius: 3, background: '#111' }} />
</div> </div>
<div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 LYT to Platinum</div> <div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 LYTs to Platinum</div>
</div> </div>
</div> </div>
@@ -667,10 +667,16 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
} }
.products-track::-webkit-scrollbar { display: none; } .products-track::-webkit-scrollbar { display: none; }
.products-track { scrollbar-width: none; } .products-track { scrollbar-width: none; }
.products-card { height: 560px; width: min(86vw, 460px); } .products-card { height: auto; width: min(86vw, 460px); }
.products-card { flex-direction: column !important; } .products-card { flex-direction: column !important; }
.products-card-content { flex: 1 1 auto !important; } .products-card-content { flex: 0 0 auto !important; }
.products-card-visual { flex: 0 0 200px !important; border-left: none !important; border-top: 1px solid rgba(255,255,255,0.08); } .products-card-visual {
flex: 0 0 auto !important;
min-height: 300px;
padding: 32px 0;
border-left: none !important;
border-top: 1px solid rgba(255,255,255,0.08);
}
.products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; } .products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; }
} }
`}</style> `}</style>

View File

@@ -37,7 +37,7 @@ const GREEN_CARDS = [
const BLACK_CARDS = [ const BLACK_CARDS = [
{ {
title: 'Earn Daily', title: 'Earn Daily',
description: 'Customers earn LYT through daily app activities', description: 'Customers earn LYTs through daily app activities',
icon: ( icon: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
@@ -226,23 +226,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
{/* Papercraft Image */} {/* Papercraft Image */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
right: i === 2 ? '-18px' : '-22px', right: i === 2 ? '-20px' : '-24px',
bottom: i === 2 ? '-18px' : '-22px', bottom: i === 2 ? '-20px' : '-24px',
width: i === 2 ? '62%' : '58%', width: i === 2 ? '72%' : '68%',
height: '100%', height: '112%',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
<Image <Image
src={card.image} src={card.image}
alt={card.label} alt={card.label}
width={340} width={400}
height={340} height={400}
style={{ style={{
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: i === 2 ? 'scale(1.32)' : 'scale(1.28)', transform: i === 2 ? 'scale(1.4)' : 'scale(1.36)',
transformOrigin: 'bottom right', transformOrigin: 'bottom right',
}} }}
/> />
@@ -263,7 +263,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
clickEffect={true} clickEffect={true}
particleCount={6} particleCount={6}
> >
<div style={{ position: 'relative', zIndex: 2, width: cardNumber === 7 ? '40%' : (cardNumber === 8 ? '46%' : '52%'), display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ position: 'relative', zIndex: 2, width: cardNumber === 7 ? '36%' : (cardNumber === 8 ? '42%' : '48%'), display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ color: iconAccent, marginBottom: 14, display: 'flex', alignItems: 'center' }}> <div style={{ color: iconAccent, marginBottom: 14, display: 'flex', alignItems: 'center' }}>
{card.icon} {card.icon}
</div> </div>
@@ -295,7 +295,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
position: 'absolute', position: 'absolute',
right: 0, right: 0,
bottom: 0, bottom: 0,
width: cardNumber === 7 ? '58%' : '54%', width: cardNumber === 7 ? '66%' : '62%',
height: '100%', height: '100%',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
@@ -303,8 +303,8 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
<Image <Image
src={card.image} src={card.image}
alt={card.title} alt={card.title}
width={cardNumber === 7 ? 480 : 400} width={cardNumber === 7 ? 520 : 440}
height={cardNumber === 7 ? 480 : 400} height={cardNumber === 7 ? 520 : 440}
style={{ style={{
objectFit: 'cover', objectFit: 'cover',
objectPosition: cardNumber === 7 ? '62% 88%' : '68% 82%', objectPosition: cardNumber === 7 ? '62% 88%' : '68% 82%',
@@ -316,23 +316,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
) : ( ) : (
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
right: '-14px', right: '-16px',
bottom: '-16px', bottom: '-20px',
width: '56%', width: '66%',
height: '96%', height: '112%',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 1, zIndex: 1,
}}> }}>
<Image <Image
src={card.image} src={card.image}
alt={card.title} alt={card.title}
width={340} width={400}
height={340} height={400}
style={{ style={{
objectFit: 'contain', objectFit: 'contain',
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: 'scale(1.22)', transform: 'scale(1.3)',
transformOrigin: 'bottom right', transformOrigin: 'bottom right',
}} }}
/> />
@@ -366,7 +366,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
margin: 0 auto; margin: 0 auto;
} }
.shift-bento-card { .shift-bento-card {
min-height: 220px; min-height: 270px;
border-radius: 24px; border-radius: 24px;
padding: clamp(20px, 2.4vw, 28px); padding: clamp(20px, 2.4vw, 28px);
display: flex; display: flex;
@@ -401,7 +401,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
.shift-card-4 { grid-column: span 1; } .shift-card-4 { grid-column: span 1; }
.shift-card-5 { grid-column: span 1; } .shift-card-5 { grid-column: span 1; }
.shift-card-6 { grid-column: span 1; } .shift-card-6 { grid-column: span 1; }
.shift-card-7 { grid-column: span 2; min-height: 240px; } .shift-card-7 { grid-column: span 2; min-height: 300px; }
.shift-card-8 { grid-column: span 1; } .shift-card-8 { grid-column: span 1; }
@media (max-width: 1023px) { @media (max-width: 1023px) {

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
import AnimatedDarkBg from './AnimatedDarkBg'; import AnimatedDarkBg from './AnimatedDarkBg';
@@ -12,6 +12,17 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const MOBILE_QUERY = '(max-width: 768px)';
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 layout; the client's
// first render must match that to avoid a hydration mismatch.
const getMobileServerSnapshot = () => false;
interface Step { interface Step {
step: string; step: string;
id: 'earn' | 'engage' | 'spend' | 'repeat'; id: 'earn' | 'engage' | 'spend' | 'repeat';
@@ -20,10 +31,10 @@ interface Step {
} }
const STEPS: Step[] = [ const STEPS: Step[] = [
{ step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYT and unlock surprise coupons.' }, { step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn LYTs and unlock surprise coupons.' },
{ step: '02', id: 'engage', title: 'Unlock Deals', desc: 'Merchants create game based discount campaigns: customers play once to reveal their exclusive reward on a specific product.' }, { step: '02', id: 'engage', title: 'Unlock Deals', desc: 'Merchants create game based discount campaigns: customers play once to reveal their exclusive reward on a specific product.' },
{ step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYT-earning offers, then walk in and shop in person.' }, { step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and LYTs-earning offers, then walk in and shop in person.' },
{ step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYT and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' }, { step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use LYTs and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' },
]; ];
const IMPACTS = [ const IMPACTS = [
@@ -34,15 +45,16 @@ const IMPACTS = [
]; ];
const IMAGE_PATHS = [ const IMAGE_PATHS = [
'/retail_cycle/Earn_online.png', '/retail_cycle/01_Earn_online.png',
'/retail_cycle/unlock_deals.png', '/retail_cycle/02_unlock_deals.png',
'/retail_cycle/discover+visits.png', '/retail_cycle/03_discover+visits.png',
'/retail_cycle/redeem+repeat.png', '/retail_cycle/04_redeem+repeat.png',
]; ];
export default function TheLoop() { export default function TheLoop() {
// Only counts uses state — everything scroll-driven is ref-mutated // Only counts uses state — everything scroll-driven is ref-mutated
const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0)); const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0));
const isMobile = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
const sectionRef = useRef<HTMLDivElement>(null); const sectionRef = useRef<HTMLDivElement>(null);
const pinRef = useRef<HTMLDivElement>(null); const pinRef = useRef<HTMLDivElement>(null);
@@ -51,7 +63,7 @@ export default function TheLoop() {
const tipRef = useRef<HTMLDivElement>(null); const tipRef = useRef<HTMLDivElement>(null);
const ghostRef = useRef<HTMLDivElement>(null); const ghostRef = useRef<HTMLDivElement>(null);
const scrollHintRef = useRef<HTMLDivElement>(null); const scrollHintRef = useRef<HTMLDivElement>(null);
// Per-step refs // Per-step refs (desktop)
const labelRefs = useRef<(HTMLDivElement | null)[]>([]); const labelRefs = useRef<(HTMLDivElement | null)[]>([]);
const titleRefs = useRef<(HTMLHeadingElement | null)[]>([]); const titleRefs = useRef<(HTMLHeadingElement | null)[]>([]);
const descRefs = useRef<(HTMLDivElement | null)[]>([]); const descRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -60,10 +72,19 @@ export default function TheLoop() {
const trackDotGlowRefs = useRef<(HTMLDivElement | null)[]>([]); const trackDotGlowRefs = useRef<(HTMLDivElement | null)[]>([]);
const visualRefs = useRef<(HTMLDivElement | null)[]>([]); const visualRefs = useRef<(HTMLDivElement | null)[]>([]);
// Mobile-only refs — same scroll-scrub mechanic, vertical single-column layout
const mobileSectionRef = useRef<HTMLDivElement>(null);
const mobilePinRef = useRef<HTMLDivElement>(null);
const mobileLineRef = useRef<HTMLDivElement>(null);
const mobileVisualRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileTextRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileDotRefs = useRef<(HTMLDivElement | null)[]>([]);
const mobileDotTextRefs = useRef<(HTMLSpanElement | null)[]>([]);
useIsomorphicLayoutEffect(() => { useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
// Set initial active state directly in DOM // Set initial active state directly in DOM (desktop)
const applyActive = (idx: number, lineFillPct: number) => { const applyActive = (idx: number, lineFillPct: number) => {
// Growing line // Growing line
if (lineRef.current) if (lineRef.current)
@@ -131,26 +152,84 @@ export default function TheLoop() {
}); });
}; };
// Seed with step 0 // Same progression logic, driving the vertical mobile layout
const applyActiveMobile = (idx: number, lineFillPct: number) => {
if (mobileLineRef.current)
mobileLineRef.current.style.width = `calc((100% - 20px) * ${lineFillPct / 100})`;
STEPS.forEach((_, i) => {
const isActive = i === idx;
const isPassed = i < idx;
if (mobileTextRefs.current[i])
mobileTextRefs.current[i]!.style.opacity = isActive ? '1' : '0';
if (mobileVisualRefs.current[i])
mobileVisualRefs.current[i]!.style.opacity = isActive ? '1' : '0';
const dot = mobileDotRefs.current[i];
const txt = mobileDotTextRefs.current[i];
if (dot) {
if (isActive) {
dot.style.background = '#f4be28';
dot.style.borderColor = '#f4be28';
dot.style.transform = 'scale(1.15)';
dot.style.boxShadow = '0 0 14px rgba(244,190,40,0.7)';
} else if (isPassed) {
dot.style.background = 'rgba(244,190,40,0.25)';
dot.style.borderColor = 'rgba(244,190,40,0.5)';
dot.style.transform = 'scale(1)';
dot.style.boxShadow = 'none';
} else {
dot.style.background = 'transparent';
dot.style.borderColor = 'rgba(244,190,40,0.2)';
dot.style.transform = 'scale(1)';
dot.style.boxShadow = 'none';
}
}
if (txt) {
txt.style.color = isActive ? '#1a1200' : isPassed ? 'rgba(244,190,40,0.7)' : 'rgba(244,190,40,0.22)';
}
});
};
// Seed initial state for whichever layout is active
applyActive(0, 0); applyActive(0, 0);
applyActiveMobile(0, 0);
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
const totalSteps = STEPS.length; const totalSteps = STEPS.length;
ScrollTrigger.create({ if (isMobile) {
trigger: sectionRef.current, ScrollTrigger.create({
start: 'top top', trigger: mobileSectionRef.current,
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`, start: 'top top',
pin: pinRef.current, end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.5}`,
scrub: true, // No smoothing lag — direct 1:1 with scroll position pin: mobilePinRef.current,
onUpdate: (self) => { scrub: true,
const p = self.progress; onUpdate: (self) => {
const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1); const p = self.progress;
// Fill goes from 0 to 100% as progress goes 0→(n-1)/n const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1);
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100; const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActive(idx, lineFillPct); applyActiveMobile(idx, lineFillPct);
}, },
}); });
} else {
ScrollTrigger.create({
trigger: sectionRef.current,
start: 'top top',
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.8}`,
pin: pinRef.current,
scrub: true, // No smoothing lag — direct 1:1 with scroll position
onUpdate: (self) => {
const p = self.progress;
const idx = Math.min(Math.floor(p * totalSteps), totalSteps - 1);
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActive(idx, lineFillPct);
},
});
}
// Count-up metrics // Count-up metrics
ScrollTrigger.create({ ScrollTrigger.create({
@@ -179,10 +258,13 @@ export default function TheLoop() {
}, sectionRef); }, sectionRef);
return () => ctx.revert(); return () => ctx.revert();
}, []); }, [isMobile]);
return ( return (
<> <>
{/* ══════════════════════════════════════════
DESKTOP / LAPTOP — pinned two-column layout
══════════════════════════════════════════ */}
<section <section
ref={sectionRef} ref={sectionRef}
id="the-loop" id="the-loop"
@@ -194,6 +276,7 @@ export default function TheLoop() {
overflow: 'hidden', overflow: 'hidden',
zIndex: 2, zIndex: 2,
height: `${(STEPS.length - 1) * 180 + 100}vh`, height: `${(STEPS.length - 1) * 180 + 100}vh`,
display: isMobile ? 'none' : 'block',
}} }}
> >
<AnimatedDarkBg variant="amber" /> <AnimatedDarkBg variant="amber" />
@@ -233,7 +316,7 @@ export default function TheLoop() {
color: 'rgba(255,255,255,0.38)', lineHeight: 1.75, color: 'rgba(255,255,255,0.38)', lineHeight: 1.75,
fontFamily: 'Sora, sans-serif', fontFamily: 'Sora, sans-serif',
}}> }}>
Earn LYT online, unlock game based deals, discover nearby stores, and redeem offline. Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline.
</p> </p>
</div> </div>
@@ -372,23 +455,19 @@ export default function TheLoop() {
</div> </div>
</div> </div>
{/* RIGHT: visual panel */} {/* RIGHT: visual panel — borderless, image floats directly on the section bg */}
<div style={{ <div style={{
flex: '0 0 clamp(300px,44%,500px)', minHeight: 360, position: 'relative', borderRadius: 28, flex: '0 0 clamp(340px,50%,560px)', minHeight: 420, position: 'relative',
background: 'rgba(244,190,40,0.04)',
border: '1px solid rgba(244,190,40,0.12)',
backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)',
overflow: 'hidden',
}}> }}>
<div style={{ <div style={{
position: 'absolute', inset: 0, position: 'absolute', inset: '-10%',
background: 'radial-gradient(ellipse 60% 50% at 50% 50%, rgba(244,190,40,0.08) 0%, transparent 70%)', background: 'radial-gradient(ellipse 60% 50% at 50% 50%, rgba(244,190,40,0.08) 0%, transparent 70%)',
pointerEvents: 'none', pointerEvents: 'none',
}} /> }} />
{/* Ghost step number — text content set via ref */} {/* Ghost step number — text content set via ref */}
<div ref={ghostRef} style={{ <div ref={ghostRef} style={{
position: 'absolute', bottom: -10, right: 24, position: 'absolute', bottom: -10, right: 0,
fontSize: 'clamp(7rem,14vw,12rem)', fontWeight: 900, lineHeight: 1, fontSize: 'clamp(7rem,14vw,12rem)', fontWeight: 900, lineHeight: 1,
color: 'transparent', color: 'transparent',
WebkitTextStroke: '1px rgba(244,190,40,0.07)', WebkitTextStroke: '1px rgba(244,190,40,0.07)',
@@ -410,7 +489,6 @@ export default function TheLoop() {
justifyContent: 'center', justifyContent: 'center',
opacity: i === 0 ? 1 : 0, opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.5s ease', transition: 'opacity 0.5s ease',
padding: '24px',
}} }}
> >
<div style={{ position: 'relative', width: '100%', height: '100%' }}> <div style={{ position: 'relative', width: '100%', height: '100%' }}>
@@ -419,7 +497,7 @@ export default function TheLoop() {
alt={s.title} alt={s.title}
fill fill
style={{ objectFit: 'contain' }} style={{ objectFit: 'contain' }}
sizes="(max-width: 768px) 100vw, 44vw" sizes="(max-width: 768px) 100vw, 50vw"
priority={i === 0} priority={i === 0}
/> />
</div> </div>
@@ -446,6 +524,189 @@ export default function TheLoop() {
<div style={{ width: 1.5, height: 28, background: 'linear-gradient(180deg,rgba(244,190,40,0.6),transparent)', animation: 'loopScrollHint 1.4s ease-in-out infinite' }} /> <div style={{ width: 1.5, height: 28, background: 'linear-gradient(180deg,rgba(244,190,40,0.6),transparent)', animation: 'loopScrollHint 1.4s ease-in-out infinite' }} />
</div> </div>
</div> </div>
</section>
{/* ══════════════════════════════════════════
MOBILE — pinned single-column layout: big
image on top, one legible step at a time,
horizontal progress tracker below
══════════════════════════════════════════ */}
<section
ref={mobileSectionRef}
id="the-loop-mobile"
style={{
position: 'relative',
width: 'calc(100% - 40px)',
margin: '0 20px 0',
borderRadius: '32px 32px 0 0',
overflow: 'hidden',
zIndex: 2,
height: `${(STEPS.length - 1) * 130 + 100}vh`,
display: isMobile ? 'block' : 'none',
}}
>
<AnimatedDarkBg variant="amber" />
<div
ref={mobilePinRef}
style={{
position: 'relative',
height: '100vh',
display: 'flex',
flexDirection: 'column',
padding: '96px 24px 28px',
boxSizing: 'border-box',
zIndex: 10,
}}
>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 12, flexShrink: 0 }}>
<h2 style={{
margin: '0 0 8px',
fontSize: 'clamp(1.5rem,6.8vw,2rem)',
fontWeight: 800, color: '#fff',
lineHeight: 1.05, letterSpacing: '-0.04em',
fontFamily: 'Sora, sans-serif',
}}>
A smarter{' '}
<span style={{
background: 'linear-gradient(105deg,#f4be28 0%,#fff8e0 48%,#f4be28 62%,#ff9500 100%)',
backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
animation: 'loopShimmer 3s linear infinite',
}}>retail cycle.</span>
</h2>
<p style={{
margin: '0 auto', maxWidth: 300,
fontSize: '0.78rem',
color: 'rgba(255,255,255,0.4)', lineHeight: 1.5,
fontFamily: 'Sora, sans-serif',
}}>
Earn LYTs online, unlock game based deals, discover nearby stores, and redeem offline.
</p>
</div>
{/* Visual panel — borderless, image floats directly on the section bg */}
<div style={{
flex: '0 0 auto',
height: 'clamp(230px,32vh,300px)',
position: 'relative',
marginBottom: 14,
}}>
<div style={{
position: 'absolute', inset: '-10%',
background: 'radial-gradient(ellipse 60% 50% at 50% 50%, rgba(244,190,40,0.1) 0%, transparent 70%)',
pointerEvents: 'none',
}} />
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileVisualRefs.current[i] = el; }}
style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.5s ease',
}}
>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Image
src={IMAGE_PATHS[i]}
alt={s.title}
fill
style={{ objectFit: 'contain' }}
sizes="100vw"
priority={i === 0}
/>
</div>
</div>
))}
</div>
{/* Step text — one clear step at a time, cross-faded */}
<div style={{ flex: 1, position: 'relative', minHeight: 150 }}>
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileTextRefs.current[i] = el; }}
style={{
position: 'absolute', inset: 0,
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.4s ease',
textAlign: 'center',
}}
>
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '0.18em',
textTransform: 'uppercase', color: 'rgba(244,190,40,0.85)',
fontFamily: 'Sora, sans-serif', marginBottom: 6,
}}>
Step {s.step}
</div>
<h3 style={{
margin: '0 0 8px',
fontSize: 'clamp(1.25rem,6.4vw,1.55rem)',
fontWeight: 800, color: '#fff',
letterSpacing: '-0.03em', lineHeight: 1.15,
fontFamily: 'Sora, sans-serif',
}}>
{s.title}
</h3>
<p style={{
margin: '0 auto', maxWidth: 340,
fontSize: '0.85rem', lineHeight: 1.55,
color: 'rgba(255,255,255,0.6)',
fontFamily: 'Sora, sans-serif',
}}>
{s.desc}
</p>
</div>
))}
</div>
{/* Horizontal progress tracker */}
<div style={{ position: 'relative', flexShrink: 0, padding: '0 12px', marginTop: 10 }}>
<div style={{
position: 'absolute', left: 22, right: 22, top: 11,
height: 2, background: 'rgba(244,190,40,0.12)', borderRadius: 2,
}} />
<div ref={mobileLineRef} style={{
position: 'absolute', left: 22, top: 11,
width: '0%', height: 2,
background: 'linear-gradient(90deg,#f4be28 0%,rgba(244,190,40,0.55) 100%)',
borderRadius: 2,
boxShadow: '0 0 10px rgba(244,190,40,0.5)',
}} />
<div style={{ display: 'flex', justifyContent: 'space-between', position: 'relative', zIndex: 2 }}>
{STEPS.map((s, i) => (
<div
key={s.id}
ref={el => { mobileDotRefs.current[i] = el; }}
style={{
width: 24, height: 24, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: i === 0 ? '#f4be28' : 'transparent',
border: `1.5px solid ${i === 0 ? '#f4be28' : 'rgba(244,190,40,0.2)'}`,
transition: 'background 0.3s ease, border-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease',
}}
>
<span
ref={el => { mobileDotTextRefs.current[i] = el; }}
style={{
fontSize: 9, fontWeight: 800, lineHeight: 1,
fontFamily: 'Sora, sans-serif', letterSpacing: '0.04em',
color: i === 0 ? '#1a1200' : 'rgba(244,190,40,0.22)',
transition: 'color 0.3s ease',
userSelect: 'none',
}}
>
{String(i + 1).padStart(2, '0')}
</span>
</div>
))}
</div>
</div>
</div>
</section>
<style>{` <style>{`
@keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} } @keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
@@ -456,10 +717,14 @@ export default function TheLoop() {
@keyframes loopFloat1 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} } @keyframes loopFloat1 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
@keyframes loopFloat2 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} } @keyframes loopFloat2 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} }
@keyframes loopFloat3 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} } @keyframes loopFloat3 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} }
@media (max-width: 768px) { #the-loop { height: auto !important; } } @media (prefers-reduced-motion: reduce) { #the-loop [style*="animation"], #the-loop-mobile [style*="animation"] { animation: none !important; } }
@media (prefers-reduced-motion: reduce) { #the-loop [style*="animation"] { animation: none !important; } }
/* Metrics grid collapses to a single column on narrow screens — the
right-hand column divider no longer applies, so drop it. */
@media (max-width: 480px) {
.loop-metric { border-right: none !important; }
}
`}</style> `}</style>
</section>
{/* Impact metrics — outside section to avoid overflow:hidden clipping */} {/* Impact metrics — outside section to avoid overflow:hidden clipping */}
<div style={{ <div style={{