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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user