Add static mobile hero, replacing the canvas frame-sequence background

Below the lg breakpoint, render a static image below the hero text instead of the desktop's pinned scroll-scrubbed canvas animation. Use useSyncExternalStore to pick the variant so server and client agree on first render, avoiding a hydration mismatch, and gate GSAP's ScrollTrigger pin so it never attaches to the hidden desktop hero on mobile.
This commit is contained in:
R-Bharathraj
2026-07-01 11:44:12 +05:30
parent 0498f2cd0e
commit 7a15ee2b52
2 changed files with 60 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -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<HTMLDivElement | null>(null);
const heroRef = useRef<HTMLDivElement | null>(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() {
</div>
</div>
{/* ══════════════════════════════════════════
MOBILE HERO — static image below text content
══════════════════════════════════════════ */}
{!isDesktop && (
<section
className="loyaly-home relative bg-white overflow-hidden"
style={{ margin: '0 20px 20px', borderRadius: '25px' }}
>
<div className="px-6 sm:px-8 pt-12 pb-8 flex flex-col">
<h1 className="text-[#171512] hero-heading text-3xl sm:text-4xl font-extrabold tracking-tight leading-[1.12] mb-6 select-none">
Turn Every <br />
Walk In Into a <br />
<span className="hero-gradient-text">Loyal Customer</span>
</h1>
<p className="text-[#4f463a] hero-description text-sm sm:text-base mb-8 leading-relaxed select-none">
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.
</p>
<div className="flex flex-wrap gap-4">
<Link href="/contacts" className="flex items-center gap-3 bg-[#f4be28] hover:bg-[#ffd95a] hero-btn-primary text-[#171512] px-6 py-3 rounded-xl font-bold text-base transition-all duration-300 shadow-lg shadow-yellow-500/30 hover:shadow-yellow-500/50 cursor-pointer no-underline">
Book a Demo
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.6} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
<Link href="/contacts" className="flex items-center border-2 border-[#f4be28] hover:bg-[#f4be28]/15 hero-btn-secondary text-[#f4be28] px-6 py-3 rounded-xl font-bold text-base transition-all duration-300 cursor-pointer no-underline">
Get Started
</Link>
</div>
</div>
<NextImage
src="/images/mobile_hero.png"
alt="Loyaly.AI"
width={792}
height={1271}
sizes="100vw"
className="w-full h-auto block"
/>
</section>
)}
{/* ══════════════════════════════════════════
HERO — pinned, drives canvas frame scrub
══════════════════════════════════════════ */}
{isDesktop && (
<main
ref={containerRef}
className="loyaly-home relative bg-white overflow-hidden"
@@ -519,6 +575,7 @@ export default function IndexPage() {
</div>
</div>
</main>
)}
{/* Other sections */}
<ProblemsShift isLoaded={imagesLoaded} />