updated mobile widget

This commit is contained in:
R-Bharathraj
2026-07-15 15:13:10 +05:30
parent 85e0f6eb63
commit b627126713
3 changed files with 89 additions and 30 deletions

View File

@@ -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).

View File

@@ -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>

View File

@@ -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);