updated mobile widget
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useSyncExternalStore } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -15,15 +15,6 @@ const NAV_LINKS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const MOBILE_QUERY = '(max-width: 1023px)';
|
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() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -87,7 +78,15 @@ export default function Header() {
|
|||||||
href === '/' ? pathname === '/' : pathname.startsWith(href);
|
href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||||
|
|
||||||
// Mobile keeps the original plain white header — the transparent hero overlay is desktop-only.
|
// 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
|
// 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).
|
// show through transparently until the user scrolls (desktop only).
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import { PhoneMockupImage } from './PhoneMockup';
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
|
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
@@ -77,14 +76,76 @@ export default function SpendAnywhereSection() {
|
|||||||
</div>
|
</div>
|
||||||
</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%' }}>
|
<div ref={phoneRef} style={{ display: 'flex', justifyContent: 'center', opacity: 0, width: '100%' }}>
|
||||||
<PhoneMockupImage
|
<div
|
||||||
src="/assets/For_people/02_spend-cropped.png"
|
className="spa-phone-shell"
|
||||||
alt="LytsUp Spend screen showing redeem-at categories"
|
style={{
|
||||||
floatClassName="spa-phone-shell"
|
width: 300, borderRadius: 42, padding: 10,
|
||||||
aspectRatio={1516 / 3040}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||||
import AnimatedDarkBg from './AnimatedDarkBg';
|
import AnimatedDarkBg from './AnimatedDarkBg';
|
||||||
@@ -13,15 +13,6 @@ if (typeof window !== 'undefined') {
|
|||||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
const MOBILE_QUERY = '(max-width: 768px)';
|
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 {
|
interface Step {
|
||||||
step: string;
|
step: string;
|
||||||
@@ -54,7 +45,15 @@ const IMAGE_PATHS = [
|
|||||||
export default function TheLoop() {
|
export default function TheLoop() {
|
||||||
// Only counts uses state — everything scroll-driven is ref-mutated
|
// Only counts uses state — everything scroll-driven is ref-mutated
|
||||||
const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0));
|
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 sectionRef = useRef<HTMLDivElement>(null);
|
||||||
const pinRef = useRef<HTMLDivElement>(null);
|
const pinRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user