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 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:20:10 +02:00
co-authored by Claude Sonnet 4.6
parent a7be6b7c79
commit 240a42b2b7
5 changed files with 67 additions and 9 deletions
+3 -1
View File
@@ -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<HTMLDivElement>();
const [markets, setMarkets] = useState<string[]>(["SE", "NO", "DK", "FI"]);
const [type, setType] = useState<EntityType>("Bolag");
const [fieldIds, setFieldIds] = useState<string[]>(["lei", "financials", "contact"]);
@@ -121,7 +123,7 @@ export default function ConfiguratorPage() {
<main id="main-content" className="min-h-screen bg-background text-foreground">
<OrderStepper active={1} />
<div className="mx-auto max-w-[80rem] px-6 py-16">
<div ref={revealRef} className="mx-auto max-w-[80rem] px-6 py-16">
{/* Editorial masthead — oversized index as graphic texture */}
<div className="reveal relative border-b border-border pb-10">
<span
+3 -2
View File
@@ -228,6 +228,7 @@ export default function DatasetDetailPage() {
function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k: TabKey) => void }) {
const t = useT();
const revealRef = useReveal<HTMLDivElement>();
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 (
<>
<div ref={revealRef}>
{/* breadcrumb */}
<div className="reveal flex items-center gap-2 border-b border-border py-4 font-mono text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
<Link href="/#katalog" className="underline-offset-4 transition-colors hover:text-foreground hover:underline">
@@ -368,7 +369,7 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
</div>
</aside>
</div>
</>
</div>
);
}
+5 -1
View File
@@ -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
</a>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
<LocaleProvider>{children}</LocaleProvider>
<LocaleProvider>
<RouteReveal />
{children}
</LocaleProvider>
</ThemeProvider>
<Toaster position="bottom-right" toastOptions={{ classNames: { toast: "font-mono text-xs tracking-[0.06em] rounded-none border border-border bg-background text-foreground", title: "font-mono", description: "font-mono text-muted-foreground" } }} />
<Analytics />
+35
View File
@@ -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<HTMLElement>(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;
}
+21 -5
View File
@@ -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<T extends HTMLElement = HTMLElement>() {
const ref = useRef<T>(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);
}
}),