diff --git a/public/images/stock/deals-retail.jpg b/public/images/stock/deals-retail.jpg deleted file mode 100644 index 158d081..0000000 Binary files a/public/images/stock/deals-retail.jpg and /dev/null differ diff --git a/public/images/stock/deals-retail.png b/public/images/stock/deals-retail.png new file mode 100644 index 0000000..63eca7c Binary files /dev/null and b/public/images/stock/deals-retail.png differ diff --git a/public/images/stock/physical retail.png b/public/images/stock/physical retail.png new file mode 100644 index 0000000..45a756b Binary files /dev/null and b/public/images/stock/physical retail.png differ diff --git a/public/images/stock/spend-anywhere.png b/public/images/stock/spend-anywhere.png new file mode 100644 index 0000000..fb32e85 Binary files /dev/null and b/public/images/stock/spend-anywhere.png differ diff --git a/src/app/about_us/page.tsx b/src/app/about_us/page.tsx index da01861..dc50e88 100644 --- a/src/app/about_us/page.tsx +++ b/src/app/about_us/page.tsx @@ -36,7 +36,7 @@ export default function AboutUsPage() { minHeight: '100vh', background: 'radial-gradient(ellipse 120% 90% at 22% 0%, #3a2b5e 0%, #251c40 38%, #1a1530 100%)', display: 'flex', alignItems: 'center', padding: '120px 8% 80px', - margin: '0 20px 20px', borderRadius: '0 0 32px 32px', + margin: '20px', borderRadius: '32px', position: 'relative', overflow: 'hidden', }}> {/* Background photo */} diff --git a/src/app/blog_main/page.tsx b/src/app/blog_main/page.tsx index ce2ddb7..ea9ceca 100644 --- a/src/app/blog_main/page.tsx +++ b/src/app/blog_main/page.tsx @@ -67,7 +67,7 @@ export default function BlogMainPage() { {/* ── HERO ── */}
diff --git a/src/app/blog_single_page/page.tsx b/src/app/blog_single_page/page.tsx index 7b20854..87cf836 100644 --- a/src/app/blog_single_page/page.tsx +++ b/src/app/blog_single_page/page.tsx @@ -43,7 +43,7 @@ export default function BlogSinglePage() { {/* Hero */}
{/* Background photo */} diff --git a/src/app/contacts/page.tsx b/src/app/contacts/page.tsx index 070f2be..b07c162 100644 --- a/src/app/contacts/page.tsx +++ b/src/app/contacts/page.tsx @@ -15,7 +15,7 @@ export default function ContactsPage() { {/* ── HERO ── */}
diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx index cdd7f81..d1ed0bb 100644 --- a/src/app/faq/page.tsx +++ b/src/app/faq/page.tsx @@ -51,7 +51,7 @@ export default function FAQPage() { {/* Hero */}
diff --git a/src/app/globals.css b/src/app/globals.css index c66a97c..ac7f61d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,6 +14,7 @@ html, body { margin: 0; padding: 0; + background: #ffffff; } html.lenis, html.lenis body { @@ -246,11 +247,6 @@ body.is-home-page.scroll-up .sticky-active { z-index: 999999999 !important; } -/* Ensure page loader covers everything during initial render */ -.page-loader-container { - z-index: 1000000000 !important; -} - /* Respect user preference for reduced motion */ @media (prefers-reduced-motion: reduce) { /* Disable utility animation classes used across the project */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c3aca24..a14ce4a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import Header from '@/components/Header'; import Footer from '@/components/Footer'; import GetInTouch from '@/components/GetInTouch'; import BodyClassManager from '@/components/BodyClassManager'; +import Preloader from '@/components/Preloader'; export const metadata: Metadata = { title: 'Loyaly.AI | AI Powered Retail Intelligence', @@ -46,6 +47,7 @@ export default function RootLayout({ +
{children} diff --git a/src/app/page.tsx b/src/app/page.tsx index fec624e..23c78ce 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,8 @@ 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 ProblemSection from '@/components/home/ProblemSection'; +import ShiftSection from '@/components/home/ShiftSection'; import WhatWeDo from '@/components/home/WhatWeDo'; import ProductsSection from '@/components/home/ProductsSection'; import TheLoop from '@/components/home/TheLoop'; @@ -32,7 +33,6 @@ 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); @@ -40,7 +40,6 @@ export default function IndexPage() { const heroRef = useRef(null); const canvasRef = useRef(null); const overlayRef = useRef(null); - const preloaderRef = useRef(null); const welcomeRef = useRef(null); const imagesRef = useRef([]); @@ -110,13 +109,15 @@ export default function IndexPage() { drawFrame(currentFrameIndexRef.current); }, [drawFrame]); - /* ── lock scroll until preloader exits ───────────────────────── */ + /* ── lock scroll on desktop until frames are ready to avoid the + pinned ScrollTrigger engaging mid-scroll and snapping the page ─ */ useEffect(() => { + if (imagesLoaded || !isDesktop) return; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; - }, []); + }, [imagesLoaded, isDesktop]); - /* ── load frame 0 immediately (canvas visible behind preloader) ─ */ + /* ── load frame 0 immediately (canvas visible on first paint) ─── */ useEffect(() => { const firstImg = new Image(); firstImg.onload = () => { @@ -144,16 +145,14 @@ export default function IndexPage() { const images: HTMLImageElement[] = []; - // Use RAF-throttled progress updates to avoid 192 rapid re-renders + // Use RAF-throttled updates to avoid 192 rapid re-renders let rafId = 0; - const scheduleUpdate = (pct: number, done: boolean) => { + const scheduleUpdate = (done: boolean) => { + if (!done) return; cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { - setLoadProgress(pct); - if (done) { - imagesRef.current = images; - setImagesLoaded(true); - } + imagesRef.current = images; + setImagesLoaded(true); }); }; @@ -162,9 +161,8 @@ export default function IndexPage() { const paddedIndex = String(i).padStart(3, '0'); const onLoad = () => { loadedCountRef.current++; - const pct = Math.min(100, Math.round((loadedCountRef.current / frameCount) * 100)); const done = loadedCountRef.current >= frameCount; - scheduleUpdate(pct, done); + scheduleUpdate(done); }; img.onload = onLoad; img.onerror = onLoad; @@ -195,30 +193,6 @@ export default function IndexPage() { return () => { clearTimeout(t1); clearTimeout(t2); }; }, [imagesLoaded, handleResize]); - /* ── animate preloader out when all frames are ready ─────────── */ - useEffect(() => { - if (!imagesLoaded) return; - const preloader = preloaderRef.current; - if (!preloader) return; - - const tl = gsap.timeline({ - delay: 0.2, - onComplete: () => { - preloader.style.display = 'none'; - document.body.style.overflow = ''; - }, - }); - - tl.to('.pl-number', { opacity: 0, y: -50, duration: 0.55, ease: 'power3.in' }, 0) - .to('.pl-pct', { opacity: 0, duration: 0.4, ease: 'power2.in' }, 0.05) - .to('.pl-label', { opacity: 0, duration: 0.4, ease: 'power2.in' }, 0.1) - .to('.pl-bar', { scaleX: 1, duration: 0.25, ease: 'power3.out' }, 0) - .to(preloader, { yPercent: -100, duration: 1.05, ease: 'expo.inOut' }, 0.4); - - // Kill the timeline on unmount so stale onComplete never runs after navigation - return () => { tl.kill(); }; - }, [imagesLoaded]); - /* ── homepage header show/hide on scroll direction ───────────── */ useEffect(() => { document.body.classList.add('is-home-page', 'scroll-down'); @@ -332,99 +306,12 @@ export default function IndexPage() { /* ── render ───────────────────────────────────────────────────── */ return ( <> - {/* ══════════════════════════════════════════ - PRELOADER - Fixed overlay — slides up when images done - ══════════════════════════════════════════ */} -
- {/* Background: frame_000 blurred + dark overlay */} -
-
- - {/* Noise grain */} -
- - {/* Brand wordmark */} -
- Loyaly -
- - {/* Big percentage counter */} -
- - {loadProgress} - - % -
- - {/* Sub-label */} -
- Loading experience -
- - {/* Progress bar (full width, bottom) */} -
-
-
-
- {/* ══════════════════════════════════════════ MOBILE HERO — static image below text content ══════════════════════════════════════════ */} - {!isDesktop && (

@@ -457,20 +344,19 @@ export default function IndexPage() { className="w-full h-auto block" />

- )} {/* ══════════════════════════════════════════ HERO — pinned, drives canvas frame scrub ══════════════════════════════════════════ */} - {isDesktop && (
{/* Canvas */} @@ -575,10 +461,10 @@ export default function IndexPage() {
- )} {/* Other sections */} - + + diff --git a/src/app/solutions_page/page.tsx b/src/app/solutions_page/page.tsx index dc4395a..af3fcc8 100644 --- a/src/app/solutions_page/page.tsx +++ b/src/app/solutions_page/page.tsx @@ -52,7 +52,7 @@ export default function SolutionsPage() { minHeight: '92vh', background: 'radial-gradient(ellipse 120% 90% at 70% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)', display: 'flex', alignItems: 'center', padding: '120px 8% 100px', - margin: '0 20px 20px', borderRadius: '0 0 32px 32px', + margin: '20px', borderRadius: '32px', position: 'relative', overflow: 'hidden', }}> {/* Background photo */} diff --git a/src/app/use_cases/page.tsx b/src/app/use_cases/page.tsx index db189ae..0da64bf 100644 --- a/src/app/use_cases/page.tsx +++ b/src/app/use_cases/page.tsx @@ -91,7 +91,7 @@ export default function ForBusinessPage() { minHeight: '100vh', background: 'radial-gradient(ellipse 120% 90% at 30% 0%, #2c2350 0%, #211c3e 38%, #181429 100%)', display: 'flex', alignItems: 'center', padding: '120px 8% 100px', position: 'relative', overflow: 'hidden', - margin: '0 20px 20px', borderRadius: '0 0 32px 32px', + margin: '20px', borderRadius: '32px', }}> {/* Background photo */} @@ -264,27 +264,33 @@ export default function ForBusinessPage() {
{/* ── WHO IT'S FOR ── */} -
- -
Who It's For
-

- Built for every format of physical retail. -

-
-
- {WHO.map((w, i) => ( - -
-
{w.icon}
-
{w.label}
-
-
- ))} +
+ {/* Background photo */} + +
+ +
+ +
Who It's For
+

+ Built for every format of physical retail. +

+
+
+ {WHO.map((w, i) => ( + +
+
{w.icon}
+
{w.label}
+
+
+ ))} +
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9f432f7..a8a4e4c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useSyncExternalStore } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; @@ -14,22 +14,26 @@ const NAV_LINKS = [ { label: 'Meet the Crew', href: '/about_us' }, ]; +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(); - const [loading, setLoading] = useState(true); const [mobileOpen, setMobileOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const mobileMenuRef = useRef(null); - // Hide loader after brief mount delay - useEffect(() => { - const t = setTimeout(() => setLoading(false), 400); - return () => clearTimeout(t); - }, []); - // Detect scroll to show/hide shadow on header useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 300); + const onScroll = () => setScrolled(window.scrollY > 50); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); @@ -60,28 +64,15 @@ export default function Header() { const isActive = (href: string) => 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); + + // 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). + const transparentHeader = !scrolled && !isMobileViewport; + return (
- {/* Page pre-loader */} - {loading && ( - - )} - {/* Mobile menu overlay */} {mobileOpen && (
- {/* Spacer only needed when header is fixed */} - {scrolled &&