Files
loyalyweb/src/components/for-people/SpendAnywhereSection.tsx
2026-09-22 16:12:19 +05:30

166 lines
8.3 KiB
TypeScript

'use client';
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import FpIcon, { type FpIconName } from './FpIcons';
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
/* Logical viewport the embedded LytsUp web app is designed against — see
EarnRewardsSection for the full rationale. */
const DESIGN_HEIGHT = 844;
const NOTCH_STRIP = 32;
export default function SpendAnywhereSection() {
const panelRef = useRef<HTMLDivElement>(null);
const phoneRef = useRef<HTMLDivElement>(null);
// Phone mockup rises in from the bottom as the section scrolls into view
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') return;
const section = panelRef.current;
const phone = phoneRef.current;
if (!section || !phone) return;
const ctx = gsap.context(() => {
gsap.fromTo(phone,
{ y: 160, opacity: 0 },
{
y: 0, opacity: 1, ease: 'power2.out',
scrollTrigger: { trigger: section, start: 'top 92%', end: 'top 25%', scrub: 1.2 },
}
);
}, section);
return () => ctx.revert();
}, []);
return (
<section ref={panelRef} className="fp-panel" style={{ background: '#080810' }}>
<div className="fp-shell">
<div className="fp-split" style={{ gridTemplateColumns: '1.05fr 0.95fr' }}>
{/* Left — headline + copy */}
<div>
<div style={{ marginBottom: 20 }}><span className="fp-kicker fp-rise">Spend<span className="fp-kicker-num">03</span></span></div>
<h2 className="fp-h1" style={{ color: '#fff' }}>
<span className="fp-mask"><span>Your Lyts.</span></span>
<span className="fp-mask"><span className="fp-shimmer">Your Choice.</span></span>
</h2>
<p className="fp-rise" style={{ fontSize: 15, lineHeight: 1.8, color: 'rgba(255,255,255,0.45)', fontFamily: 'Sora, sans-serif', maxWidth: 420, margin: '0 0 24px' }}>
Walk into a participating brand or store. Choose what you love. Tell the cashier you&apos;re paying with Lyts. Enjoy instant savings, no waiting, no cashback, no complicated redemption.
</p>
<div className="fp-rise" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
{([
{ label: 'No waiting', icon: 'bolt' },
{ label: 'No cashback', icon: 'ban' },
{ label: 'No complicated redemption', icon: 'check' },
] as { label: string; icon: FpIconName }[]).map((tag) => (
<div key={tag.label} className="fp-card fp-card--dark">
<span className="fp-card-icon" style={{ background: 'rgba(244,190,40,0.14)', color: '#f4be28' }}><FpIcon name={tag.icon} /></span>
<span style={{ fontSize: 13, fontWeight: 700, fontFamily: 'Sora, sans-serif', color: '#fff', letterSpacing: '-0.01em', whiteSpace: 'nowrap' }}>
{tag.label}
</span>
</div>
))}
</div>
<div className="fp-rise" style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ width: 34, height: 3, borderRadius: 2, background: '#f4be28', flexShrink: 0 }} />
<p style={{
fontSize: 'clamp(17px, 1.7vw, 21px)', lineHeight: 1.3, color: '#fff', fontFamily: 'Sora, sans-serif',
margin: 0, fontWeight: 800, letterSpacing: '-0.02em',
}}>
One Wallet. <span style={{ color: '#f4be28' }}>Thousands of Lyts.</span>
</p>
</div>
</div>
{/* Right — live LytsUp Spend preview embedded in a real phone frame */}
<div ref={phoneRef} style={{ display: 'flex', justifyContent: 'center', opacity: 0, width: '100%' }}>
<div
className="spa-phone-shell"
style={{
width: 300, borderRadius: 42, padding: 10,
background: 'linear-gradient(160deg, #2a2a2e 0%, #0a0a0c 100%)',
boxShadow: '0 28px 60px -22px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.06)',
position: 'relative',
}}
>
{/* Screen — height is derived, not a fixed aspect ratio: see
EarnRewardsSection. A hard 9/19.5 left the iframe ~47 logical px
short once the notch strip was subtracted, clipping the app's
bottom nav; this lands it on exactly 390x844 at every breakpoint. */}
<div style={{
borderRadius: 32, overflow: 'hidden', background: '#ffffff',
position: 'relative', height: `calc(${NOTCH_STRIP}px + ${DESIGN_HEIGHT}px * var(--pm-scale))`,
display: 'flex', flexDirection: 'column',
}}>
{/* Dynamic island */}
<div style={{
position: 'absolute', top: 8, left: '50%', transform: 'translateX(-50%)',
width: 82, height: 22, borderRadius: 14, background: '#000', zIndex: 5,
}} />
{/* Safe area — reserves the notch strip above the app's top bar */}
<div style={{ height: NOTCH_STRIP, flexShrink: 0, background: '#ffffff' }} />
{/* The embed is a responsive Flutter web app. We render it at a
comfortable logical width (390px) and scale it down to fill the
screen exactly, so the UI stays proportioned as designed instead
of cramped at ~260px. The iframe width is exactly the design
width (no horizontal overscan) so the app lays out edge-to-edge
and stays centered — nothing on the right (e.g. the Profile tab)
gets clipped. Flutter's thin right-edge list scrollbar is hidden
by a slim mask strip rather than by cutting into the layout. */}
<div className="spa-phone-viewport" style={{ position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden' }}>
<iframe
src="https://anbarasu22004.github.io/lytsup_view/rewards"
title="LytsUp Spend screen live preview"
loading="lazy"
scrolling="no"
className="spa-phone-iframe"
style={{
position: 'absolute', top: 0, left: 0,
border: 'none', display: 'block', background: '#ffffff',
transformOrigin: 'top left',
}}
/>
{/* Masks Flutter's canvas-painted scrollbar at the far right edge */}
<div className="spa-scrollbar-mask" aria-hidden />
</div>
</div>
<style>{`
@keyframes spaPhoneFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.spa-phone-shell { animation: spaPhoneFloat 7s ease-in-out infinite; }
/* --pm-scale = screen width (frame - 2*10px padding) ÷ 390px design width */
.spa-phone-shell { --pm-scale: calc(280 / 390); }
.spa-phone-iframe {
width: calc(100% / var(--pm-scale));
height: calc(100% / var(--pm-scale));
transform: scale(var(--pm-scale));
}
.spa-scrollbar-mask {
position: absolute; top: 0; right: 0; z-index: 2;
width: 7px; height: 100%; pointer-events: none;
background: linear-gradient(to right, rgba(255,255,255,0), #ffffff 75%);
}
@media (prefers-reduced-motion: reduce) { .spa-phone-shell { animation: none !important; } }
@media (max-width: 900px) {
/* min(...,100%): see EarnRewardsSection — a hard 280px overflows
the ~256px available at 320px instead of centring. */
.spa-phone-shell { width: min(280px, 100%) !important; --pm-scale: calc(260 / 390); }
}
`}</style>
</div>
</div>
</div>
</div>
</section>
);
}