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:
BIN
public/images/mobile_hero.png
Normal file
BIN
public/images/mobile_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user