166 lines
8.3 KiB
TypeScript
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'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>
|
|
);
|
|
}
|