diff --git a/public/images/mobile_hero.png b/public/images/mobile_hero.png new file mode 100644 index 0000000..af0ff50 Binary files /dev/null and b/public/images/mobile_hero.png differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 1c88091..fec624e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import Lenis from 'lenis'; import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation'; import Link from 'next/link'; +import NextImage from 'next/image'; import ProblemsShift from '@/components/home/ProblemsShift'; import WhatWeDo from '@/components/home/WhatWeDo'; import ProductsSection from '@/components/home/ProductsSection'; @@ -18,9 +19,22 @@ if (typeof window !== 'undefined') { gsap.registerPlugin(ScrollTrigger); } +const DESKTOP_QUERY = '(min-width: 1024px)'; +const subscribeDesktopQuery = (callback: () => void) => { + const mql = window.matchMedia(DESKTOP_QUERY); + mql.addEventListener('change', callback); + return () => mql.removeEventListener('change', callback); +}; +const getDesktopSnapshot = () => window.matchMedia(DESKTOP_QUERY).matches; +// Server has no viewport, so it always renders the desktop hero; the client's +// first render must match that to avoid a hydration mismatch. +const getDesktopServerSnapshot = () => true; + export default function IndexPage() { const [imagesLoaded, setImagesLoaded] = useState(false); const [loadProgress, setLoadProgress] = useState(0); + // Desktop pinned canvas hero vs. mobile static hero. + const isDesktop = useSyncExternalStore(subscribeDesktopQuery, getDesktopSnapshot, getDesktopServerSnapshot); const containerRef = useRef(null); const heroRef = useRef(null); @@ -242,7 +256,7 @@ export default function IndexPage() { /* ── Lenis + GSAP scrub (frame seq + welcome text + hero text) ── */ useIsomorphicLayoutEffect(() => { - if (!imagesLoaded) return; + if (!imagesLoaded || !isDesktop) return; const lenis = new Lenis({ duration: 1.2, @@ -313,7 +327,7 @@ export default function IndexPage() { gsap.ticker.remove(updateTicker); if (typeof cleanupReveal === 'function') cleanupReveal(); }; - }, [imagesLoaded, drawFrame]); + }, [imagesLoaded, isDesktop, drawFrame]); /* ── render ───────────────────────────────────────────────────── */ return ( @@ -404,9 +418,51 @@ export default function IndexPage() { + {/* ══════════════════════════════════════════ + MOBILE HERO — static image below text content + ══════════════════════════════════════════ */} + {!isDesktop && ( +
+
+

+ Turn Every
+ Walk In Into a
+ Loyal Customer +

+

+ Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn coins through games and daily activities, then redeem them at offline stores near them. +

+
+ + Book a Demo + + + + + + Get Started + +
+
+ + +
+ )} + {/* ══════════════════════════════════════════ HERO — pinned, drives canvas frame scrub ══════════════════════════════════════════ */} + {isDesktop && (
+ )} {/* Other sections */}