From 240a42b2b7c3e84bd0ba9a6937b3aaceecf5c08d Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Thu, 25 Jun 2026 19:20:10 +0200 Subject: [PATCH] fix: reveal animations for standalone pages on direct load RouteReveal layout component applies in-view on every route change. useReveal hook falls back to immediate rAF when element is already visible. bestall/page.tsx: added missing useReveal ref. katalog/[id]/page.tsx: DatasetView uses useReveal directly. Co-Authored-By: Claude Sonnet 4.6 --- app/bestall/page.tsx | 4 +++- app/katalog/[id]/page.tsx | 5 +++-- app/layout.tsx | 6 +++++- components/route-reveal.tsx | 35 +++++++++++++++++++++++++++++++++++ hooks/use-reveal.ts | 26 +++++++++++++++++++++----- 5 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 components/route-reveal.tsx diff --git a/app/bestall/page.tsx b/app/bestall/page.tsx index d2daac4..03d46a1 100644 --- a/app/bestall/page.tsx +++ b/app/bestall/page.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { OrderStepper } from "@/components/order-stepper"; import { ProvenanceBadge } from "@/components/provenance-badge"; import { useT } from "@/components/locale-provider"; +import { useReveal } from "@/hooks/use-reveal"; import { createOrder } from "@/lib/order-client"; import { SERVICES, serviceById } from "@/lib/services"; import { @@ -33,6 +34,7 @@ function confidenceColor(v: number) { export default function ConfiguratorPage() { const t = useT(); + const revealRef = useReveal(); const [markets, setMarkets] = useState(["SE", "NO", "DK", "FI"]); const [type, setType] = useState("Bolag"); const [fieldIds, setFieldIds] = useState(["lei", "financials", "contact"]); @@ -121,7 +123,7 @@ export default function ConfiguratorPage() {
-
+
{/* Editorial masthead — oversized index as graphic texture */}
void }) { const t = useT(); + const revealRef = useReveal(); const deposit = Math.round(d.price / 2); const remainder = d.price - deposit; @@ -245,7 +246,7 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k: } return ( - <> +
{/* breadcrumb */}
@@ -368,7 +369,7 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
- +
); } diff --git a/app/layout.tsx b/app/layout.tsx index 94eb407..45197b8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import { Analytics } from "@vercel/analytics/next"; import { Toaster } from "sonner"; import { LocaleProvider } from "@/components/locale-provider"; import { ThemeProvider } from "@/components/theme-provider"; +import { RouteReveal } from "@/components/route-reveal"; import "./globals.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); @@ -54,7 +55,10 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac Skip to content - {children} + + + {children} + diff --git a/components/route-reveal.tsx b/components/route-reveal.tsx new file mode 100644 index 0000000..bdc8edd --- /dev/null +++ b/components/route-reveal.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname } from "next/navigation"; + +const REVEAL_SELECTOR = ".reveal, .reveal-left, .reveal-right, .reveal-scale, .reveal-fade"; + +/** + * On every route change, applies `.in-view` to any reveal element whose + * top edge is already within the viewport. This handles standalone pages + * (like /bestall, /katalog/[id]) that are fully visible on load, where + * IntersectionObserver alone may not fire before the user sees blank content. + */ +export function RouteReveal() { + const pathname = usePathname(); + + useEffect(() => { + const vh = window.innerHeight; + const apply = () => { + document.querySelectorAll(REVEAL_SELECTOR).forEach((el) => { + if (!el.classList.contains("in-view")) { + const { top } = el.getBoundingClientRect(); + if (top < vh * 0.95) el.classList.add("in-view"); + } + }); + }; + // Two passes: immediate + after paint (for async-loaded content). + apply(); + const raf = requestAnimationFrame(apply); + const t = setTimeout(apply, 600); + return () => { cancelAnimationFrame(raf); clearTimeout(t); }; + }, [pathname]); + + return null; +} diff --git a/hooks/use-reveal.ts b/hooks/use-reveal.ts index f0c86bc..7c43bc4 100644 --- a/hooks/use-reveal.ts +++ b/hooks/use-reveal.ts @@ -6,21 +6,37 @@ import { useEffect, useRef } from "react"; * Wire a section so its descendants carrying `.reveal` (+ optional * `.reveal-left|-right|-scale|-fade` and `.delay-N`) animate in when the * section scrolls into view. Matches the keyframes in globals.css. + * + * If the section is already visible on mount (e.g. top-of-page navigation), + * reveals are applied immediately via rAF instead of waiting for scroll. */ export function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; + + const applyReveal = () => { + el + .querySelectorAll( + ".reveal, .reveal-left, .reveal-right, .reveal-scale, .reveal-fade", + ) + .forEach((n) => n.classList.add("in-view")); + }; + + // If the element's top edge is already within the viewport, apply immediately. + const rect = el.getBoundingClientRect(); + if (rect.top < window.innerHeight * 0.9) { + requestAnimationFrame(applyReveal); + return; + } + + // Otherwise use IntersectionObserver for scroll-triggered reveal. const io = new IntersectionObserver( (entries) => entries.forEach((e) => { if (e.isIntersecting) { - el - .querySelectorAll( - ".reveal, .reveal-left, .reveal-right, .reveal-scale, .reveal-fade", - ) - .forEach((n) => n.classList.add("in-view")); + applyReveal(); io.unobserve(el); } }),