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 = [
{
n: '01', tag: 'Analytics', readTime: '6 min',
@@ -52,6 +80,12 @@ const ARTICLES = [
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() {
const [activeFilter, setActiveFilter] = useState('All');
const [hovered, setHovered] = useState<number | null>(null);
@@ -66,46 +100,82 @@ export default function BlogMainPage() {
{/* ── HERO ── */}
<section style={{
padding: '120px 8% 100px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
padding: '120px 8% 90px', background: 'radial-gradient(ellipse 120% 90% at 50% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)',
margin: '20px', borderRadius: '32px',
position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center',
}}>
{/* Background photo */}
<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%)' }} />
{/* 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', 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: 'relative', zIndex: 2, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
<div style={{ maxWidth: 720 }}>
<div style={{ position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
<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' }}>
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>
<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.
</p>
</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>
</section>
{/* ── FILTERS ── */}
<section style={{ padding: '48px 8% 0', background: '#fff' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{FILTERS.map(f => (
<button key={f} onClick={() => setActiveFilter(f)} style={{
padding: '10px 22px', borderRadius: 100,
background: activeFilter === f ? '#0a0a0f' : 'transparent',
color: activeFilter === f ? '#f4be28' : 'rgba(0,0,0,0.4)',
fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif',
border: `1px solid ${activeFilter === f ? '#0a0a0f' : 'rgba(0,0,0,0.1)'}`,
cursor: 'pointer', transition: 'all 0.2s ease',
}}>{f}</button>
))}
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{FILTERS.map(f => {
const meta = TAG_META[f];
const active = activeFilter === f;
return (
<button key={f} onClick={() => setActiveFilter(f)} style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '10px 20px 10px 16px', borderRadius: 100,
background: active ? '#0a0a0f' : '#fafafa',
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>
</section>
@@ -113,42 +183,57 @@ export default function BlogMainPage() {
{featured && (
<section style={{ padding: '64px 8%', background: '#fff' }}>
<Reveal>
<div className="bm-featured-grid" style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6vw',
padding: '64px', background: '#07070d', borderRadius: 24,
alignItems: 'center', cursor: 'pointer',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
}}>
{/* Left — article info */}
<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',
<Link href="/blog_single_page" style={{ textDecoration: 'none', display: 'block' }}>
<div className="bm-featured-grid" style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6vw',
padding: '64px', background: '#07070d', borderRadius: 28,
alignItems: 'center', cursor: 'pointer', position: 'relative', overflow: 'hidden',
border: `1px solid ${(TAG_META[featured.tag]?.color || '#f4be28')}30`,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
}}>
<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={{
fontSize: 'clamp(80px, 12vw, 160px)', fontWeight: 900,
color: 'transparent', fontFamily: 'Sora, sans-serif',
WebkitTextStroke: '1px rgba(244,190,40,0.15)',
letterSpacing: '-0.06em', lineHeight: 1,
userSelect: 'none',
}}>{featured.n}</div>
aspectRatio: '4/3', borderRadius: 20, position: 'relative', zIndex: 2,
background: `linear-gradient(150deg, ${TAG_META[featured.tag]?.color || '#f4be28'}20 0%, transparent 70%)`,
border: `1px solid ${TAG_META[featured.tag]?.color || '#f4be28'}25`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
overflow: 'hidden',
}}>
<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>
</Link>
</Reveal>
</section>
)}
@@ -156,68 +241,123 @@ export default function BlogMainPage() {
{/* ── ARTICLE GRID ── */}
<section style={{ padding: '0 8% 140px', background: '#fff' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(340px,1fr))', gap: 20 }}>
{rest.map((article, i) => (
<Reveal key={article.n} delay={i * 0.08}>
<div
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
style={{
padding: '40px', borderRadius: 20,
background: '#fff',
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.3)' : 'rgba(0,0,0,0.08)'}`,
transition: 'all 0.3s cubic-bezier(0.22,1,0.36,1)',
transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)',
boxShadow: hovered === i ? '0 24px 60px rgba(0,0,0,0.1)' : '0 2px 12px rgba(0,0,0,0.04)',
cursor: 'pointer',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 280,
}}
>
<div>
<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>
{rest.map((article, i) => {
const meta = TAG_META[article.tag];
return (
<Reveal key={article.n} delay={i * 0.08}>
<Link href="/blog_single_page" style={{ textDecoration: 'none', display: 'block', height: '100%' }}>
<div
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
style={{
padding: '36px', borderRadius: 22,
background: '#fff',
border: `1.5px solid ${hovered === i ? `${meta?.color}45` : 'rgba(0,0,0,0.08)'}`,
transition: 'all 0.35s cubic-bezier(0.22,1,0.36,1)',
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',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 300,
}}
>
<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>
<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>
</div>
</Reveal>
))}
</Reveal>
);
})}
</div>
</section>
{/* ── NEWSLETTER CTA ── */}
<section style={{ padding: '120px 8%', background: '#07070d', margin: '0 20px 20px', borderRadius: 24, textAlign: 'center', position: 'relative', overflow: 'hidden' }}>
<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' }} />
<Reveal>
<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 }}>
Stay ahead of retail.
</h2>
<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 }}>
Get our latest insights on AI, footfall, and the future of physical retail, delivered monthly.
</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap', position: 'relative', zIndex: 2 }}>
<input placeholder="your@email.com" style={{
padding: '16px 24px', borderRadius: 100, border: '1px solid rgba(255,255,255,0.12)',
background: 'rgba(255,255,255,0.06)', color: '#fff',
fontSize: 15, fontFamily: 'Sora, sans-serif', outline: 'none',
backdropFilter: 'blur(12px)', width: 280,
}} />
<button style={{
padding: '16px 32px', borderRadius: 100, border: 'none',
background: '#f4be28', color: '#111',
fontSize: 15, fontWeight: 800, fontFamily: 'Sora, sans-serif', cursor: 'pointer',
}}>Subscribe </button>
</div>
</Reveal>
<section style={{ padding: 0, background: '#07070d', margin: '0 20px 20px', borderRadius: 28, overflow: 'hidden', position: 'relative' }}>
<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' }} />
<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' }} />
<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' }} />
<div className="bm-newsletter-grid" style={{
position: 'relative', zIndex: 2, display: 'grid', gridTemplateColumns: '1.1fr 0.9fr',
gap: '5vw', alignItems: 'center', padding: 'clamp(64px, 8vw, 110px) 8%',
}}>
<Reveal>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 18 }}>Newsletter</div>
<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' }}>
Stay ahead of retail.
</h2>
<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.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{NEWSLETTER_PERKS.map((perk) => (
<div key={perk} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{
width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
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>
<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) {
.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>
</main>

View File

@@ -10,11 +10,11 @@ const FAQS = [
category: 'For Customers',
color: '#f4be28',
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 much is 1 LYT worth?', a: '1 LYT = ₹1. So 284 LYT = ₹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: '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 LYTs = ₹284 redeemable value at any participating offline store.' },
{ 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: '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.' },
],
},

View File

@@ -313,14 +313,14 @@ export default function IndexPage() {
className="loyaly-home relative bg-white overflow-hidden"
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">
Turn Every <br />
Walk In Into a <br />
<span className="hero-gradient-text">Loyal Customer</span>
</h1>
<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>
<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">
@@ -445,7 +445,7 @@ export default function IndexPage() {
<span className="hero-gradient-text">Loyal Customer</span>
</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">
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>
<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">

View File

@@ -54,7 +54,7 @@ const PRODUCTS = [
},
{
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: [
'Daily login and step-challenge rewards that build a lasting habit loop',
'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 [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false);
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(() => {
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 });
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
useEffect(() => {
setMobileOpen(false);
@@ -210,17 +232,19 @@ export default function Header() {
className={transparentHeader ? 'header-transparent' : ''}
style={{
position: 'fixed',
top: 0,
top: isMobileViewport ? '14px' : 0,
left: '20px',
right: '20px',
width: 'auto',
zIndex: 99999999,
background: transparentHeader ? 'transparent' : '#ffffff',
borderTopLeftRadius: isMobileViewport ? '24px' : '0px',
borderTopRightRadius: isMobileViewport ? '24px' : '0px',
borderBottomLeftRadius: '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)'),
transition: 'background 0.3s ease, box-shadow 0.3s ease',
animation: scrolled ? 'slideDown 0.3s ease' : 'none',
boxShadow: transparentHeader ? 'none' : (scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 8px 28px rgba(23,21,18,0.12)'),
transform: hidden ? 'translateY(-130%)' : 'translateY(0)',
transition: 'transform 0.35s cubic-bezier(0.4,0,0.2,1), background 0.3s ease, box-shadow 0.3s ease',
}}
>
<div style={{
@@ -333,10 +357,6 @@ export default function Header() {
</header>
<style>{`
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 1023px) {
.desktop-nav { display: none !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>
{/* LYT balance card */}
{/* LYTs balance card */}
<div style={{
margin: '0 14px', borderRadius: 22,
background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)',
@@ -109,7 +109,7 @@ export default function PhoneWidget() {
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={{ 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: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 4 }}> 284 redeemable</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>
<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>
</div>

View File

@@ -44,7 +44,7 @@ const SOLUTIONS = [
title: 'Loyalty Management',
subtitle: 'Build Lasting Relationships',
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',
imageAlt: 'Loyalty management module showing customer tier performance',
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 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: '03', label: 'Decathlon weekend special', match: 91, tag: 'Sports · 1.4km' },
{ rank: '04', label: 'Zara new arrivals, 15% off', match: 87, tag: 'Your style' },
@@ -76,7 +76,7 @@ export default function AIRecommendationsSection() {
{/* Left — editorial */}
<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={{
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 Image from 'next/image';
import { PhoneFrame, PhoneBottomNav, BellIcon, PinIcon, ChevronIcon, SearchIcon, MicIcon, CameraIcon, MapIcon, HeartLogoIcon } from './PhoneMockup';
const deals = [
{ store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYT', savings: '₹120 saved', color: '#00704A', 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: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', 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: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#9ca3af', distance: '0.9 km', rating: 4.5 },
{ 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: '#171512', dot: '#d1d5db', distance: '0.7 km', rating: 4.9 },
{ 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', dot: '#3b82f6', distance: '1.4 km', rating: 4.7 },
{ 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() {
@@ -28,11 +29,11 @@ export default function DiscoverDealsSection() {
}} />
<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 */}
<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={{
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 800, lineHeight: 0.94,
@@ -46,7 +47,7 @@ export default function DiscoverDealsSection() {
<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,
}}>
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>
<div className="fp-rise" style={{
@@ -61,63 +62,97 @@ export default function DiscoverDealsSection() {
</div>
</div>
{/* Deal list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{deals.map((deal, i) => (
<div
key={i}
onMouseEnter={() => setHovered(i)}
onMouseLeave={() => setHovered(null)}
className="fp-rise"
style={{
position: 'relative', overflow: 'hidden',
display: 'grid', gridTemplateColumns: '46px 1fr auto', alignItems: 'center', gap: 16,
padding: '16px 20px', borderRadius: 16,
background: hovered === i ? 'rgba(244,190,40,0.06)' : 'rgba(255,255,255,0.03)',
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.28)' : 'rgba(255,255,255,0.07)'}`,
backdropFilter: 'blur(20px)', cursor: 'pointer',
transitionDelay: `${0.1 + i * 0.07}s`,
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>
{/* Phone mockup — white app screen, matches the in-app Discover experience */}
<div className="fp-rise" style={{ transitionDelay: '0.1s', display: 'flex', justifyContent: 'center' }}>
<PhoneFrame floatClassName="ddm-phone-shell">
{/* Amber header */}
<div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<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)' }}>
<HeartLogoIcon />
<span style={{ fontWeight: 800, fontSize: 11.5, color: '#171512', fontFamily: 'Sora, sans-serif' }}>1,250</span>
</div>
<div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<BellIcon />
</div>
</div>
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.38)', fontFamily: 'Sora, sans-serif' }}>{deal.category}</span>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 15, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{deal.offer}</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{deal.savings}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<PinIcon />
<span style={{ fontSize: 11.5, fontWeight: 600, color: '#171512', fontFamily: 'Sora, sans-serif' }}>R.S. Puram, Coimbatore</span>
<ChevronIcon color="#171512" />
</div>
</div>
))}
<div className="fp-rise" style={{
textAlign: 'right', paddingRight: 4, marginTop: 4, transitionDelay: '0.5s',
fontSize: 12, color: 'rgba(255,255,255,0.25)', fontFamily: 'Sora, sans-serif', letterSpacing: '0.06em',
}}>
+ 2,400 more deals near you
</div>
{/* Search bar */}
<div style={{ padding: '12px 14px 4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, background: '#f5f5f6', borderRadius: 100, padding: '9px 14px' }}>
<SearchIcon />
<span style={{ flex: 1, color: '#9ca3af', fontSize: 12, fontFamily: 'Sora, sans-serif' }}>Search</span>
<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>

View File

@@ -1,8 +1,10 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { PhoneFrame, PhoneBottomNav, BellIcon, FlameIcon } from './PhoneMockup';
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: 'Spin Wheel Game', pts: +120, time: '3h ago', 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() {
@@ -35,27 +34,20 @@ export default function EarnRewardsSection() {
rowRefs.current.forEach((row, i) => {
if (!row) return;
const active = i === idx;
row.style.background = active ? '#16a34a' : 'rgba(23,21,18,0.55)';
row.style.borderColor = active ? '#16a34a' : 'rgba(255,255,255,0.14)';
row.style.transform = active ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)';
row.style.boxShadow = active ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none';
row.style.zIndex = active ? '2' : '1';
row.style.background = active ? '#f0fdf4' : 'transparent';
row.style.borderColor = active ? '#16a34a' : 'transparent';
row.style.transform = active ? 'translateX(4px) scale(1.02)' : 'translateX(0) scale(1)';
row.style.boxShadow = active ? '0 10px 24px -12px rgba(22,163,74,0.35)' : 'none';
// 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');
if (icon) {
icon.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)';
icon.style.borderColor = active ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)';
icon.style.color = active ? '#ffffff' : '#4ade80';
icon.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(23,21,18,0.04)';
icon.style.borderColor = active ? '#16a34a' : 'rgba(23,21,18,0.08)';
icon.style.color = active ? '#16a34a' : '#9ca3af';
}
const points = row.querySelector<HTMLElement>('.feed-pts');
if (points) {
points.style.color = active ? '#ffffff' : '#4ade80';
points.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)';
points.style.background = active ? 'rgba(22,163,74,0.14)' : 'rgba(22,163,74,0.08)';
}
});
};
@@ -103,7 +95,7 @@ export default function EarnRewardsSection() {
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={{ 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>
{/* Giant gold number */}
@@ -125,7 +117,7 @@ export default function EarnRewardsSection() {
<span style={{ fontSize: 13, color: '#4f463a', fontFamily: 'Sora, sans-serif' }}>redeemable value</span>
</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={{
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 800, lineHeight: 1.08,
@@ -139,83 +131,107 @@ export default function EarnRewardsSection() {
<p className="fp-rise" style={{
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>
</div>
{/* Right — live feed */}
<div className="fp-rise" style={{ transitionDelay: '0.15s' }}>
{/* Feed header */}
<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>
{/* Right — phone mockup, matches the in-app Earn / Home experience */}
<div className="fp-rise" style={{ transitionDelay: '0.15s', display: 'flex', justifyContent: 'center' }}>
<PhoneFrame floatClassName="erm-phone-shell">
{/* Feed rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{feed.map((tx, i) => (
<div key={i} ref={el => { rowRefs.current[i] = el; }} style={{
position: 'relative', zIndex: i === 0 ? 2 : 1,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
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>
{/* Amber header */}
<div style={{ background: 'linear-gradient(180deg, #fcd34d 0%, #f4be28 100%)', padding: '34px 16px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<Image src="/images/loyalyai.png" alt="Loyaly.AI" width={100} height={26} style={{ objectFit: 'contain', height: 16, width: 'auto' }} />
<div style={{ width: 26, height: 26, borderRadius: '50%', background: 'rgba(255,255,255,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<BellIcon />
</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 }}>
<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 */}
<div style={{
marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
}}>
{[
{ label: 'Earned this month', val: '660 LYT', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' },
{ label: 'Redeemable value', val: '₹660', color: '#f4be28', bg: '#fff7e0', border: '#f4be28' },
].map((s, i) => (
<div key={i} style={{ padding: '14px 16px', borderRadius: 14, background: s.bg, border: `1.5px solid ${s.border}` }}>
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.5)', fontFamily: 'Sora, sans-serif', marginBottom: 4 }}>{s.label}</div>
<div style={{ fontSize: 18, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div>
</div>
))}
</div>
{/* Balance card */}
<div style={{ margin: '14px 14px 0', borderRadius: 18, padding: '16px 18px', background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)', position: 'relative', overflow: 'hidden' }}>
<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>
<div style={{ fontSize: 10.5, color: 'rgba(23,21,18,0.6)', fontFamily: 'Sora, sans-serif', marginTop: 3 }}> 284 redeemable</div>
</div>
{/* Feed header */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
margin: '16px 14px 6px',
}}>
<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>

View File

@@ -19,7 +19,7 @@ export default function ExperienceSection() {
<div className="fp-shell">
<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 className="fp-split" style={{ gridTemplateColumns: '1.1fr 0.9fr', alignItems: 'center' }}>

View File

@@ -1,7 +1,6 @@
'use client';
import React, { useRef, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
function useMagnetic<T extends HTMLElement>(strength = 12) {
@@ -27,7 +26,6 @@ function useMagnetic<T extends HTMLElement>(strength = 12) {
}
export default function HeroSection() {
const magPrimary = useMagnetic<HTMLAnchorElement>(10);
const magSecondary = useMagnetic<HTMLButtonElement>(8);
return (
@@ -71,19 +69,12 @@ export default function HeroSection() {
<p className="fp-content fp-subtext" style={{
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>
<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
ref={magSecondary}
className="fp-btn fp-btn-ghost fp-magnetic"
@@ -92,8 +83,41 @@ export default function HeroSection() {
</button>
</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 */}
<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' }}>
{['#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 }} />

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 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 'clamp(40px, 6vh, 64px)' }}>
<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={{
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.9,
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
@@ -92,7 +92,7 @@ export default function SpendAnywhereSection() {
<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,
}}>
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>
</div>
@@ -184,7 +184,7 @@ export default function SpendAnywhereSection() {
}}>
{[
{ 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: 'Instant', lbl: 'Redeem at checkout' },
].map((s, i) => (

View File

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

View File

@@ -63,7 +63,7 @@ const PRODUCTS: Product[] = [
title: 'Identiq',
subtitle: 'Consumer Loyalty Platform',
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',
accentRgb: '244, 190, 40',
tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'],
@@ -283,12 +283,12 @@ function ProductVisual({ product }: { product: Product }) {
</span>
</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 className="pv-sheen" />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Sora, sans-serif' }}>Reward 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>
<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 className="pv-progress-fill" style={{ height: '100%', width: '68%', borderRadius: 3, background: '#111' }} />
</div>
<div style={{ fontSize: 10, color: 'rgba(0,0,0,0.55)', fontFamily: 'Sora, sans-serif', marginTop: 6, fontWeight: 600 }}>116 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>
@@ -667,10 +667,16 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
}
.products-track::-webkit-scrollbar { display: 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-content { flex: 1 1 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-content { flex: 0 0 auto !important; }
.products-card-visual {
flex: 0 0 auto !important;
min-height: 300px;
padding: 32px 0;
border-left: none !important;
border-top: 1px solid rgba(255,255,255,0.08);
}
.products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; }
}
`}</style>

View File

@@ -37,7 +37,7 @@ const GREEN_CARDS = [
const BLACK_CARDS = [
{
title: 'Earn Daily',
description: 'Customers earn LYT through daily app activities',
description: 'Customers earn LYTs through daily app activities',
icon: (
<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>
@@ -226,23 +226,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
{/* Papercraft Image */}
<div style={{
position: 'absolute',
right: i === 2 ? '-18px' : '-22px',
bottom: i === 2 ? '-18px' : '-22px',
width: i === 2 ? '62%' : '58%',
height: '100%',
right: i === 2 ? '-20px' : '-24px',
bottom: i === 2 ? '-20px' : '-24px',
width: i === 2 ? '72%' : '68%',
height: '112%',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.label}
width={340}
height={340}
width={400}
height={400}
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
transform: i === 2 ? 'scale(1.32)' : 'scale(1.28)',
transform: i === 2 ? 'scale(1.4)' : 'scale(1.36)',
transformOrigin: 'bottom right',
}}
/>
@@ -263,7 +263,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
clickEffect={true}
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' }}>
{card.icon}
</div>
@@ -295,7 +295,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
position: 'absolute',
right: 0,
bottom: 0,
width: cardNumber === 7 ? '58%' : '54%',
width: cardNumber === 7 ? '66%' : '62%',
height: '100%',
pointerEvents: 'none',
zIndex: 1,
@@ -303,8 +303,8 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
<Image
src={card.image}
alt={card.title}
width={cardNumber === 7 ? 480 : 400}
height={cardNumber === 7 ? 480 : 400}
width={cardNumber === 7 ? 520 : 440}
height={cardNumber === 7 ? 520 : 440}
style={{
objectFit: 'cover',
objectPosition: cardNumber === 7 ? '62% 88%' : '68% 82%',
@@ -316,23 +316,23 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
) : (
<div style={{
position: 'absolute',
right: '-14px',
bottom: '-16px',
width: '56%',
height: '96%',
right: '-16px',
bottom: '-20px',
width: '66%',
height: '112%',
pointerEvents: 'none',
zIndex: 1,
}}>
<Image
src={card.image}
alt={card.title}
width={340}
height={340}
width={400}
height={400}
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
transform: 'scale(1.22)',
transform: 'scale(1.3)',
transformOrigin: 'bottom right',
}}
/>
@@ -366,7 +366,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
margin: 0 auto;
}
.shift-bento-card {
min-height: 220px;
min-height: 270px;
border-radius: 24px;
padding: clamp(20px, 2.4vw, 28px);
display: flex;
@@ -401,7 +401,7 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
.shift-card-4 { grid-column: span 1; }
.shift-card-5 { 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; }
@media (max-width: 1023px) {

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import AnimatedDarkBg from './AnimatedDarkBg';
@@ -12,6 +12,17 @@ if (typeof window !== 'undefined') {
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 {
step: string;
id: 'earn' | 'engage' | 'spend' | 'repeat';
@@ -20,10 +31,10 @@ interface 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: '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: '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: '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 LYTs and coupons at checkout to save money, then come back tomorrow to earn more and keep the loop going.' },
];
const IMPACTS = [
@@ -34,15 +45,16 @@ const IMPACTS = [
];
const IMAGE_PATHS = [
'/retail_cycle/Earn_online.png',
'/retail_cycle/unlock_deals.png',
'/retail_cycle/discover+visits.png',
'/retail_cycle/redeem+repeat.png',
'/retail_cycle/01_Earn_online.png',
'/retail_cycle/02_unlock_deals.png',
'/retail_cycle/03_discover+visits.png',
'/retail_cycle/04_redeem+repeat.png',
];
export default function TheLoop() {
// Only counts uses state — everything scroll-driven is ref-mutated
const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0));
const isMobile = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
const sectionRef = useRef<HTMLDivElement>(null);
const pinRef = useRef<HTMLDivElement>(null);
@@ -51,7 +63,7 @@ export default function TheLoop() {
const tipRef = useRef<HTMLDivElement>(null);
const ghostRef = useRef<HTMLDivElement>(null);
const scrollHintRef = useRef<HTMLDivElement>(null);
// Per-step refs
// Per-step refs (desktop)
const labelRefs = useRef<(HTMLDivElement | null)[]>([]);
const titleRefs = useRef<(HTMLHeadingElement | null)[]>([]);
const descRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -60,10 +72,19 @@ export default function TheLoop() {
const trackDotGlowRefs = 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(() => {
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) => {
// Growing line
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);
applyActiveMobile(0, 0);
const ctx = gsap.context(() => {
const totalSteps = STEPS.length;
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);
// Fill goes from 0 to 100% as progress goes 0→(n-1)/n
const lineFillPct = Math.min(p * totalSteps, totalSteps - 1) / (totalSteps - 1) * 100;
applyActive(idx, lineFillPct);
},
});
if (isMobile) {
ScrollTrigger.create({
trigger: mobileSectionRef.current,
start: 'top top',
end: () => `+=${window.innerHeight * (totalSteps - 1) * 1.5}`,
pin: mobilePinRef.current,
scrub: true,
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;
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
ScrollTrigger.create({
@@ -179,10 +258,13 @@ export default function TheLoop() {
}, sectionRef);
return () => ctx.revert();
}, []);
}, [isMobile]);
return (
<>
{/* ══════════════════════════════════════════
DESKTOP / LAPTOP — pinned two-column layout
══════════════════════════════════════════ */}
<section
ref={sectionRef}
id="the-loop"
@@ -194,6 +276,7 @@ export default function TheLoop() {
overflow: 'hidden',
zIndex: 2,
height: `${(STEPS.length - 1) * 180 + 100}vh`,
display: isMobile ? 'none' : 'block',
}}
>
<AnimatedDarkBg variant="amber" />
@@ -233,7 +316,7 @@ export default function TheLoop() {
color: 'rgba(255,255,255,0.38)', lineHeight: 1.75,
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>
</div>
@@ -372,23 +455,19 @@ export default function TheLoop() {
</div>
</div>
{/* RIGHT: visual panel */}
{/* RIGHT: visual panel — borderless, image floats directly on the section bg */}
<div style={{
flex: '0 0 clamp(300px,44%,500px)', minHeight: 360, position: 'relative', borderRadius: 28,
background: 'rgba(244,190,40,0.04)',
border: '1px solid rgba(244,190,40,0.12)',
backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)',
overflow: 'hidden',
flex: '0 0 clamp(340px,50%,560px)', minHeight: 420, position: 'relative',
}}>
<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%)',
pointerEvents: 'none',
}} />
{/* Ghost step number — text content set via ref */}
<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,
color: 'transparent',
WebkitTextStroke: '1px rgba(244,190,40,0.07)',
@@ -410,7 +489,6 @@ export default function TheLoop() {
justifyContent: 'center',
opacity: i === 0 ? 1 : 0,
transition: 'opacity 0.5s ease',
padding: '24px',
}}
>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
@@ -419,7 +497,7 @@ export default function TheLoop() {
alt={s.title}
fill
style={{ objectFit: 'contain' }}
sizes="(max-width: 768px) 100vw, 44vw"
sizes="(max-width: 768px) 100vw, 50vw"
priority={i === 0}
/>
</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>
</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>{`
@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 loopFloat2 { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-10px)} }
@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"] { animation: none !important; } }
@media (prefers-reduced-motion: reduce) { #the-loop [style*="animation"], #the-loop-mobile [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>
</section>
{/* Impact metrics — outside section to avoid overflow:hidden clipping */}
<div style={{