updated mobile widget
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useSyncExternalStore } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
@@ -15,15 +15,6 @@ const NAV_LINKS = [
|
||||
];
|
||||
|
||||
const MOBILE_QUERY = '(max-width: 1023px)';
|
||||
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 header; the client's
|
||||
// first render must match that to avoid a hydration mismatch.
|
||||
const getMobileServerSnapshot = () => false;
|
||||
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
@@ -87,7 +78,15 @@ export default function Header() {
|
||||
href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
|
||||
// Mobile keeps the original plain white header — the transparent hero overlay is desktop-only.
|
||||
const isMobileViewport = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
|
||||
const [isMobileViewport, setIsMobileViewport] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(MOBILE_QUERY);
|
||||
setIsMobileViewport(mql.matches);
|
||||
const update = () => setIsMobileViewport(mql.matches);
|
||||
mql.addEventListener('change', update);
|
||||
return () => mql.removeEventListener('change', update);
|
||||
}, []);
|
||||
|
||||
// Every page's hero is built with top clearance for an overlaid header, so let it
|
||||
// show through transparently until the user scrolls (desktop only).
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { PhoneMockupImage } from './PhoneMockup';
|
||||
|
||||
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
@@ -77,14 +76,76 @@ export default function SpendAnywhereSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right — phone mockup: where to spend */}
|
||||
{/* Right — live LytsUp Spend preview embedded in a real phone frame */}
|
||||
<div ref={phoneRef} style={{ display: 'flex', justifyContent: 'center', opacity: 0, width: '100%' }}>
|
||||
<PhoneMockupImage
|
||||
src="/assets/For_people/02_spend-cropped.png"
|
||||
alt="LytsUp Spend screen showing redeem-at categories"
|
||||
floatClassName="spa-phone-shell"
|
||||
aspectRatio={1516 / 3040}
|
||||
<div
|
||||
className="spa-phone-shell"
|
||||
style={{
|
||||
width: 300, 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',
|
||||
}}
|
||||
>
|
||||
{/* Screen — locked to a real phone's aspect ratio (~9:19.5) */}
|
||||
<div style={{
|
||||
borderRadius: 32, overflow: 'hidden', background: '#ffffff',
|
||||
position: 'relative', aspectRatio: '9 / 19.5',
|
||||
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: 32, 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) {
|
||||
.spa-phone-shell { width: 280px !important; --pm-scale: calc(260 / 390); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import AnimatedDarkBg from './AnimatedDarkBg';
|
||||
@@ -13,15 +13,6 @@ 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;
|
||||
@@ -54,7 +45,15 @@ const IMAGE_PATHS = [
|
||||
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 [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(MOBILE_QUERY);
|
||||
setIsMobile(mql.matches);
|
||||
const update = () => setIsMobile(mql.matches);
|
||||
mql.addEventListener('change', update);
|
||||
return () => mql.removeEventListener('change', update);
|
||||
}, []);
|
||||
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const pinRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
Reference in New Issue
Block a user