add page transitions
This commit is contained in:
parent
dd841dc6d6
commit
5278df56f6
@ -14,6 +14,7 @@ import ScrollToTop from "./components/ScrollToTop";
|
|||||||
import ShopDrawer from "./components/ShopDrawer";
|
import ShopDrawer from "./components/ShopDrawer";
|
||||||
import CartToast from "./components/CartToast";
|
import CartToast from "./components/CartToast";
|
||||||
import { ProductTransitionProvider } from "./components/ProductTransition";
|
import { ProductTransitionProvider } from "./components/ProductTransition";
|
||||||
|
import { PageTransitionProvider } from "./transitions/PageTransition";
|
||||||
import useLenisSmoothScroll from "./hooks/useLenisSmoothScroll";
|
import useLenisSmoothScroll from "./hooks/useLenisSmoothScroll";
|
||||||
import useScrollTextReveal from "./hooks/useScrollTextReveal";
|
import useScrollTextReveal from "./hooks/useScrollTextReveal";
|
||||||
import useButtonInteractions from "./hooks/useButtonInteractions";
|
import useButtonInteractions from "./hooks/useButtonInteractions";
|
||||||
@ -59,29 +60,31 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider value={{ theme, isLight, toggleTheme }}>
|
<ThemeProvider value={{ theme, isLight, toggleTheme }}>
|
||||||
<ProductTransitionProvider>
|
<ProductTransitionProvider>
|
||||||
<ScrollToTop />
|
<PageTransitionProvider>
|
||||||
|
<ScrollToTop />
|
||||||
|
|
||||||
<a href="#main-content" className="skip-link">
|
<a href="#main-content" className="skip-link">
|
||||||
Zum Inhalt springen
|
Zum Inhalt springen
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div ref={routeContentRef} data-route-content>
|
<div ref={routeContentRef} data-route-content>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LandingPage />} />
|
<Route path="/" element={<LandingPage />} />
|
||||||
<Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} />
|
<Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} />
|
||||||
<Route path="/about" element={<AboutPage />} />
|
<Route path="/about" element={<AboutPage />} />
|
||||||
<Route path="/impressum" element={<ImpressumPage />} />
|
<Route path="/impressum" element={<ImpressumPage />} />
|
||||||
<Route path="/datenschutz" element={<DatenschutzPage />} />
|
<Route path="/datenschutz" element={<DatenschutzPage />} />
|
||||||
<Route path="/support" element={<SupportPage />} />
|
<Route path="/support" element={<SupportPage />} />
|
||||||
<Route path="/discovery-set" element={<DiscoverySetPage />} />
|
<Route path="/discovery-set" element={<DiscoverySetPage />} />
|
||||||
<Route path="/small-batch" element={<SmallBatchPage />} />
|
<Route path="/small-batch" element={<SmallBatchPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ShopDrawer />
|
<ShopDrawer />
|
||||||
<CartToast />
|
<CartToast />
|
||||||
<Footer flushTop={shouldFlushFooter} />
|
<Footer flushTop={shouldFlushFooter} />
|
||||||
{showSupportChatbot && <SupportChatbot />}
|
{showSupportChatbot && <SupportChatbot />}
|
||||||
|
</PageTransitionProvider>
|
||||||
</ProductTransitionProvider>
|
</ProductTransitionProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect } from "react";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
import { isPageTransitionActive } from "../transitions/PageTransition";
|
||||||
|
|
||||||
let pluginsRegistered = false;
|
let pluginsRegistered = false;
|
||||||
|
|
||||||
@ -65,6 +66,115 @@ const restoreRevealWords = (element) => {
|
|||||||
delete element.dataset.revealOriginalHtml;
|
delete element.dataset.revealOriginalHtml;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function collectRevealItems(scope) {
|
||||||
|
const groups = gsap.utils.toArray("[data-reveal-group]", scope);
|
||||||
|
return groups.map((group) => ({
|
||||||
|
group,
|
||||||
|
items: Array.from(group.querySelectorAll("[data-reveal]")).filter(
|
||||||
|
(item) => item.closest("[data-reveal-group]") === group
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRevealItems(scope) {
|
||||||
|
collectRevealItems(scope).forEach(({ items }) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.dataset.reveal === "lines") {
|
||||||
|
gsap.set(item, { autoAlpha: 0 });
|
||||||
|
} else {
|
||||||
|
gsap.set(item, { y: 36, autoAlpha: 0, force3D: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupReveals(scope) {
|
||||||
|
const preparedElements = [];
|
||||||
|
const ctx = gsap.context(() => {
|
||||||
|
const entries = collectRevealItems(scope);
|
||||||
|
|
||||||
|
entries.forEach(({ group, items }) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.dataset.reveal === "lines") {
|
||||||
|
gsap.set(item, { autoAlpha: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gsap.set(item, {
|
||||||
|
y: 36,
|
||||||
|
autoAlpha: 0,
|
||||||
|
force3D: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ScrollTrigger.create({
|
||||||
|
trigger: group,
|
||||||
|
start: group.dataset.revealStart || "top 78%",
|
||||||
|
once: true,
|
||||||
|
onEnter: () => {
|
||||||
|
const timeline = gsap.timeline();
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const position = index === 0 ? 0 : "<0.16";
|
||||||
|
|
||||||
|
if (item.dataset.reveal === "lines") {
|
||||||
|
const words = createRevealWords(item);
|
||||||
|
|
||||||
|
if (words.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedElements.push(item);
|
||||||
|
gsap.set(item, { autoAlpha: 1 });
|
||||||
|
gsap.set(words, {
|
||||||
|
yPercent: 115,
|
||||||
|
rotate: 2.2,
|
||||||
|
transformOrigin: "0% 100%",
|
||||||
|
force3D: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
timeline.to(
|
||||||
|
words,
|
||||||
|
{
|
||||||
|
yPercent: 0,
|
||||||
|
rotate: 0,
|
||||||
|
duration: 1.08,
|
||||||
|
stagger: 0.065,
|
||||||
|
ease: "power4.out",
|
||||||
|
clearProps: "transform",
|
||||||
|
},
|
||||||
|
position
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.to(
|
||||||
|
item,
|
||||||
|
{
|
||||||
|
y: 0,
|
||||||
|
autoAlpha: 1,
|
||||||
|
duration: 1.02,
|
||||||
|
ease: "power4.out",
|
||||||
|
clearProps: "transform,opacity,visibility",
|
||||||
|
},
|
||||||
|
position
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ScrollTrigger.refresh();
|
||||||
|
}, scope);
|
||||||
|
|
||||||
|
return { ctx, preparedElements };
|
||||||
|
}
|
||||||
|
|
||||||
function useScrollTextReveal(scopeRef, dependencyKey = "") {
|
function useScrollTextReveal(scopeRef, dependencyKey = "") {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const scope = scopeRef.current;
|
const scope = scopeRef.current;
|
||||||
@ -79,96 +189,27 @@ function useScrollTextReveal(scopeRef, dependencyKey = "") {
|
|||||||
|
|
||||||
registerGsap();
|
registerGsap();
|
||||||
|
|
||||||
const preparedElements = [];
|
let result = null;
|
||||||
const ctx = gsap.context(() => {
|
let handleReady = null;
|
||||||
const groups = gsap.utils.toArray("[data-reveal-group]");
|
|
||||||
|
|
||||||
groups.forEach((group) => {
|
if (isPageTransitionActive()) {
|
||||||
const items = Array.from(group.querySelectorAll("[data-reveal]")).filter(
|
hideRevealItems(scope);
|
||||||
(item) => item.closest("[data-reveal-group]") === group
|
handleReady = () => {
|
||||||
);
|
result = setupReveals(scope);
|
||||||
|
};
|
||||||
if (items.length === 0) {
|
window.addEventListener("page-transition-ready", handleReady, { once: true });
|
||||||
return;
|
} else {
|
||||||
}
|
result = setupReveals(scope);
|
||||||
|
}
|
||||||
items.forEach((item) => {
|
|
||||||
if (item.dataset.reveal === "lines") {
|
|
||||||
gsap.set(item, { autoAlpha: 0 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gsap.set(item, {
|
|
||||||
y: 36,
|
|
||||||
autoAlpha: 0,
|
|
||||||
force3D: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ScrollTrigger.create({
|
|
||||||
trigger: group,
|
|
||||||
start: group.dataset.revealStart || "top 86%",
|
|
||||||
once: true,
|
|
||||||
onEnter: () => {
|
|
||||||
const timeline = gsap.timeline();
|
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
const position = index === 0 ? 0 : "<0.16";
|
|
||||||
|
|
||||||
if (item.dataset.reveal === "lines") {
|
|
||||||
const words = createRevealWords(item);
|
|
||||||
|
|
||||||
if (words.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preparedElements.push(item);
|
|
||||||
gsap.set(item, { autoAlpha: 1 });
|
|
||||||
gsap.set(words, {
|
|
||||||
yPercent: 115,
|
|
||||||
rotate: 2.2,
|
|
||||||
transformOrigin: "0% 100%",
|
|
||||||
force3D: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
timeline.to(
|
|
||||||
words,
|
|
||||||
{
|
|
||||||
yPercent: 0,
|
|
||||||
rotate: 0,
|
|
||||||
duration: 1.08,
|
|
||||||
stagger: 0.065,
|
|
||||||
ease: "power4.out",
|
|
||||||
clearProps: "transform",
|
|
||||||
},
|
|
||||||
position
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.to(
|
|
||||||
item,
|
|
||||||
{
|
|
||||||
y: 0,
|
|
||||||
autoAlpha: 1,
|
|
||||||
duration: 1.02,
|
|
||||||
ease: "power4.out",
|
|
||||||
clearProps: "transform,opacity,visibility",
|
|
||||||
},
|
|
||||||
position
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ScrollTrigger.refresh();
|
|
||||||
}, scope);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ctx.revert();
|
if (handleReady) {
|
||||||
preparedElements.forEach((element) => restoreRevealWords(element));
|
window.removeEventListener("page-transition-ready", handleReady);
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
result.ctx.revert();
|
||||||
|
result.preparedElements.forEach((element) => restoreRevealWords(element));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [scopeRef, dependencyKey]);
|
}, [scopeRef, dependencyKey]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ function AboutPage() {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="about-pillars" data-reveal-group data-reveal-start="top 86%">
|
<section className="about-pillars" data-reveal-group data-reveal-start="top 78%">
|
||||||
<header className="about-section-head">
|
<header className="about-section-head">
|
||||||
<span className="about-eyebrow" data-reveal="fade">
|
<span className="about-eyebrow" data-reveal="fade">
|
||||||
Unser Ansatz
|
Unser Ansatz
|
||||||
|
|||||||
@ -88,9 +88,9 @@ function DiscoveryOrderPanel({ onBuy, panelRef }) {
|
|||||||
|
|
||||||
function DiscoveryHero({ onBuy, panelRef }) {
|
function DiscoveryHero({ onBuy, panelRef }) {
|
||||||
return (
|
return (
|
||||||
<section className="discovery-hero">
|
<section className="discovery-hero" data-reveal-group data-reveal-start="top 90%">
|
||||||
<div className="discovery-hero-stage">
|
<div className="discovery-hero-stage">
|
||||||
<div className="discovery-hero-copy">
|
<div className="discovery-hero-copy" data-reveal="fade">
|
||||||
<span className="discovery-kicker">Discovery Set</span>
|
<span className="discovery-kicker">Discovery Set</span>
|
||||||
<h1>Der Einstieg</h1>
|
<h1>Der Einstieg</h1>
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ function DiscoveryHero({ onBuy, panelRef }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="discovery-hero-visual">
|
<figure className="discovery-hero-visual" data-reveal="fade">
|
||||||
<img
|
<img
|
||||||
src={DISCOVERY_SET_IMAGE}
|
src={DISCOVERY_SET_IMAGE}
|
||||||
alt="Atmos Discovery Set"
|
alt="Atmos Discovery Set"
|
||||||
@ -283,7 +283,7 @@ function DiscoveryIncludedSection() {
|
|||||||
const isActive = activeIndex === index;
|
const isActive = activeIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="discovery-scroll-list-item" key={perfume.id}>
|
<li className="discovery-scroll-list-item" key={perfume.id} data-reveal-group data-reveal-start="top 84%">
|
||||||
<article
|
<article
|
||||||
className={`discovery-scroll-item${
|
className={`discovery-scroll-item${
|
||||||
isActive ? " discovery-scroll-item--active" : ""
|
isActive ? " discovery-scroll-item--active" : ""
|
||||||
@ -291,9 +291,9 @@ function DiscoveryIncludedSection() {
|
|||||||
aria-current={isActive ? "true" : undefined}
|
aria-current={isActive ? "true" : undefined}
|
||||||
ref={(element) => setItemRef(element, index)}
|
ref={(element) => setItemRef(element, index)}
|
||||||
>
|
>
|
||||||
<span className="discovery-product-index">{perfume.id}</span>
|
<span className="discovery-product-index" data-reveal="fade">{perfume.id}</span>
|
||||||
|
|
||||||
<div className="discovery-scroll-copy">
|
<div className="discovery-scroll-copy" data-reveal="fade">
|
||||||
<h3>{perfume.name}</h3>
|
<h3>{perfume.name}</h3>
|
||||||
<div className="discovery-scroll-line" aria-hidden="true" />
|
<div className="discovery-scroll-line" aria-hidden="true" />
|
||||||
<div className="discovery-scroll-mobile-detail">
|
<div className="discovery-scroll-mobile-detail">
|
||||||
|
|||||||
@ -85,7 +85,7 @@ function ImpressumPage() {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="legal-fact-grid" data-reveal-group data-reveal-start="top 88%">
|
<section className="legal-fact-grid" data-reveal-group data-reveal-start="top 80%">
|
||||||
{FACTS.map((fact) => (
|
{FACTS.map((fact) => (
|
||||||
<article className="legal-fact" key={fact.label} data-reveal="fade">
|
<article className="legal-fact" key={fact.label} data-reveal="fade">
|
||||||
<span className="legal-eyebrow">{fact.label}</span>
|
<span className="legal-eyebrow">{fact.label}</span>
|
||||||
|
|||||||
@ -539,7 +539,7 @@ function LandingPage() {
|
|||||||
className="discovery-section"
|
className="discovery-section"
|
||||||
id="testen"
|
id="testen"
|
||||||
data-reveal-group
|
data-reveal-group
|
||||||
data-reveal-start="top 82%"
|
data-reveal-start="top 74%"
|
||||||
>
|
>
|
||||||
<div className="discovery-copy">
|
<div className="discovery-copy">
|
||||||
<h2 data-reveal="lines">
|
<h2 data-reveal="lines">
|
||||||
|
|||||||
@ -140,7 +140,7 @@ function SmallBatchPage() {
|
|||||||
<section
|
<section
|
||||||
className="release-grid"
|
className="release-grid"
|
||||||
data-reveal-group
|
data-reveal-group
|
||||||
data-reveal-start="top 88%"
|
data-reveal-start="top 80%"
|
||||||
>
|
>
|
||||||
{state.releases.map((release) => (
|
{state.releases.map((release) => (
|
||||||
<article className="release-card" key={release.name} data-reveal="fade">
|
<article className="release-card" key={release.name} data-reveal="fade">
|
||||||
|
|||||||
@ -83,7 +83,7 @@ function SupportPage() {
|
|||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="support-topics" data-reveal-group data-reveal-start="top 88%">
|
<section className="support-topics" data-reveal-group data-reveal-start="top 80%">
|
||||||
{TOPICS.map((topic) => (
|
{TOPICS.map((topic) => (
|
||||||
<article className="support-topic" key={topic.label} data-reveal="fade">
|
<article className="support-topic" key={topic.label} data-reveal="fade">
|
||||||
<span className="support-eyebrow">{topic.label}</span>
|
<span className="support-eyebrow">{topic.label}</span>
|
||||||
|
|||||||
1
parfum-shop/src/transitions/PageTransition.css
Normal file
1
parfum-shop/src/transitions/PageTransition.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* Page transition styles removed — fade is handled entirely by GSAP inline. */
|
||||||
119
parfum-shop/src/transitions/PageTransition.jsx
Normal file
119
parfum-shop/src/transitions/PageTransition.jsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { createContext, useCallback, useContext, useLayoutEffect, useRef } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
import { useProductTransition } from "./ProductTransitionContext";
|
||||||
|
|
||||||
|
const PageTransitionContext = createContext({ navigateWithTransition: () => {} });
|
||||||
|
|
||||||
|
export const usePageTransition = () => useContext(PageTransitionContext);
|
||||||
|
|
||||||
|
let transitionActive = false;
|
||||||
|
export const isPageTransitionActive = () => transitionActive;
|
||||||
|
|
||||||
|
const prefersReducedMotion = () =>
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
|
const isInternalLink = (anchor) => {
|
||||||
|
if (!anchor || anchor.target === "_blank") return false;
|
||||||
|
const href = anchor.getAttribute("href");
|
||||||
|
if (!href || href.startsWith("http") || href.startsWith("mailto:") || href.startsWith("#")) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageTransitionProvider({ children }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { phase: productPhase } = useProductTransition();
|
||||||
|
const tlRef = useRef(null);
|
||||||
|
const isTransitioning = useRef(false);
|
||||||
|
|
||||||
|
const navigateWithTransition = useCallback(
|
||||||
|
(to) => {
|
||||||
|
if (isTransitioning.current) return;
|
||||||
|
if (productPhase !== "idle") {
|
||||||
|
navigate(to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prefersReducedMotion()) {
|
||||||
|
navigate(to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeContent = document.querySelector("[data-route-content]");
|
||||||
|
if (!routeContent) {
|
||||||
|
navigate(to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTransitioning.current = true;
|
||||||
|
transitionActive = true;
|
||||||
|
tlRef.current?.kill();
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
navigate(to);
|
||||||
|
window.scrollTo({ top: 0, behavior: "instant" });
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
gsap.fromTo(
|
||||||
|
routeContent,
|
||||||
|
{ autoAlpha: 0 },
|
||||||
|
{
|
||||||
|
autoAlpha: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "power2.out",
|
||||||
|
onComplete: () => {
|
||||||
|
isTransitioning.current = false;
|
||||||
|
transitionActive = false;
|
||||||
|
gsap.set(routeContent, { clearProps: "opacity,visibility" });
|
||||||
|
window.dispatchEvent(new CustomEvent("page-transition-ready"));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
tl.to(routeContent, {
|
||||||
|
autoAlpha: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.in",
|
||||||
|
});
|
||||||
|
|
||||||
|
tlRef.current = tl;
|
||||||
|
},
|
||||||
|
[navigate, productPhase]
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const handleClick = (event) => {
|
||||||
|
if (event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
|
||||||
|
const anchor = event.target.closest("a[href]");
|
||||||
|
if (!anchor || !isInternalLink(anchor)) return;
|
||||||
|
|
||||||
|
if (anchor.querySelector("[data-product-transition-source]")) return;
|
||||||
|
|
||||||
|
const href = anchor.getAttribute("href");
|
||||||
|
const resolved = new URL(href, window.location.origin);
|
||||||
|
if (resolved.pathname === location.pathname) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
navigateWithTransition(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", handleClick, { capture: true });
|
||||||
|
return () => document.removeEventListener("click", handleClick, { capture: true });
|
||||||
|
}, [location.pathname, navigateWithTransition]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTransitionContext.Provider value={{ navigateWithTransition }}>
|
||||||
|
{children}
|
||||||
|
</PageTransitionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user