feat: enhance landing page, for-people sections, blog main, and components including TheLoop and PhoneMockup
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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, Loyaly’s 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, Loyaly’s 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',
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
145
src/components/for-people/PhoneMockup.tsx
Normal file
145
src/components/for-people/PhoneMockup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user