diff --git a/parfum-shop/src/App.jsx b/parfum-shop/src/App.jsx
index efb750b..a5e944a 100644
--- a/parfum-shop/src/App.jsx
+++ b/parfum-shop/src/App.jsx
@@ -14,6 +14,7 @@ import ScrollToTop from "./components/ScrollToTop";
import ShopDrawer from "./components/ShopDrawer";
import CartToast from "./components/CartToast";
import { ProductTransitionProvider } from "./components/ProductTransition";
+import { PageTransitionProvider } from "./transitions/PageTransition";
import useLenisSmoothScroll from "./hooks/useLenisSmoothScroll";
import useScrollTextReveal from "./hooks/useScrollTextReveal";
import useButtonInteractions from "./hooks/useButtonInteractions";
@@ -59,29 +60,31 @@ function App() {
return (
-
+
+
-
- Zum Inhalt springen
-
+
+ Zum Inhalt springen
+
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
-
-
-
- {showSupportChatbot && }
+
+
+
+ {showSupportChatbot && }
+
);
diff --git a/parfum-shop/src/hooks/useScrollTextReveal.js b/parfum-shop/src/hooks/useScrollTextReveal.js
index 78ed513..812a37e 100644
--- a/parfum-shop/src/hooks/useScrollTextReveal.js
+++ b/parfum-shop/src/hooks/useScrollTextReveal.js
@@ -1,6 +1,7 @@
import { useLayoutEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
+import { isPageTransitionActive } from "../transitions/PageTransition";
let pluginsRegistered = false;
@@ -65,6 +66,115 @@ const restoreRevealWords = (element) => {
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 = "") {
useLayoutEffect(() => {
const scope = scopeRef.current;
@@ -79,96 +189,27 @@ function useScrollTextReveal(scopeRef, dependencyKey = "") {
registerGsap();
- const preparedElements = [];
- const ctx = gsap.context(() => {
- const groups = gsap.utils.toArray("[data-reveal-group]");
+ let result = null;
+ let handleReady = null;
- groups.forEach((group) => {
- const items = Array.from(group.querySelectorAll("[data-reveal]")).filter(
- (item) => item.closest("[data-reveal-group]") === group
- );
-
- 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 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);
+ if (isPageTransitionActive()) {
+ hideRevealItems(scope);
+ handleReady = () => {
+ result = setupReveals(scope);
+ };
+ window.addEventListener("page-transition-ready", handleReady, { once: true });
+ } else {
+ result = setupReveals(scope);
+ }
return () => {
- ctx.revert();
- preparedElements.forEach((element) => restoreRevealWords(element));
+ if (handleReady) {
+ window.removeEventListener("page-transition-ready", handleReady);
+ }
+ if (result) {
+ result.ctx.revert();
+ result.preparedElements.forEach((element) => restoreRevealWords(element));
+ }
};
}, [scopeRef, dependencyKey]);
}
diff --git a/parfum-shop/src/pages/AboutPage.jsx b/parfum-shop/src/pages/AboutPage.jsx
index c4fb5ac..b73f19b 100644
--- a/parfum-shop/src/pages/AboutPage.jsx
+++ b/parfum-shop/src/pages/AboutPage.jsx
@@ -55,7 +55,7 @@ function AboutPage() {
-
+
Unser Ansatz
diff --git a/parfum-shop/src/pages/DiscoverySetPage.jsx b/parfum-shop/src/pages/DiscoverySetPage.jsx
index 7e2453d..8ced7e4 100644
--- a/parfum-shop/src/pages/DiscoverySetPage.jsx
+++ b/parfum-shop/src/pages/DiscoverySetPage.jsx
@@ -88,9 +88,9 @@ function DiscoveryOrderPanel({ onBuy, panelRef }) {
function DiscoveryHero({ onBuy, panelRef }) {
return (
-
+
-
+
Discovery Set
Der Einstieg
@@ -101,7 +101,7 @@ function DiscoveryHero({ onBuy, panelRef }) {
-
+
+
setItemRef(element, index)}
>
- {perfume.id}
+ {perfume.id}
-
+
{perfume.name}
diff --git a/parfum-shop/src/pages/ImpressumPage.jsx b/parfum-shop/src/pages/ImpressumPage.jsx
index 182dc92..49d558c 100644
--- a/parfum-shop/src/pages/ImpressumPage.jsx
+++ b/parfum-shop/src/pages/ImpressumPage.jsx
@@ -85,7 +85,7 @@ function ImpressumPage() {
-
+
{FACTS.map((fact) => (
{fact.label}
diff --git a/parfum-shop/src/pages/LandingPage.jsx b/parfum-shop/src/pages/LandingPage.jsx
index 581ee19..4e7e7d7 100644
--- a/parfum-shop/src/pages/LandingPage.jsx
+++ b/parfum-shop/src/pages/LandingPage.jsx
@@ -539,7 +539,7 @@ function LandingPage() {
className="discovery-section"
id="testen"
data-reveal-group
- data-reveal-start="top 82%"
+ data-reveal-start="top 74%"
>
diff --git a/parfum-shop/src/pages/SmallBatchPage.jsx b/parfum-shop/src/pages/SmallBatchPage.jsx
index 7080178..c9e5d62 100644
--- a/parfum-shop/src/pages/SmallBatchPage.jsx
+++ b/parfum-shop/src/pages/SmallBatchPage.jsx
@@ -140,7 +140,7 @@ function SmallBatchPage() {
{state.releases.map((release) => (
diff --git a/parfum-shop/src/pages/SupportPage.jsx b/parfum-shop/src/pages/SupportPage.jsx
index e77b844..7aad7dc 100644
--- a/parfum-shop/src/pages/SupportPage.jsx
+++ b/parfum-shop/src/pages/SupportPage.jsx
@@ -83,7 +83,7 @@ function SupportPage() {
-
+
{TOPICS.map((topic) => (
{topic.label}
diff --git a/parfum-shop/src/transitions/PageTransition.css b/parfum-shop/src/transitions/PageTransition.css
new file mode 100644
index 0000000..21bb644
--- /dev/null
+++ b/parfum-shop/src/transitions/PageTransition.css
@@ -0,0 +1 @@
+/* Page transition styles removed — fade is handled entirely by GSAP inline. */
diff --git a/parfum-shop/src/transitions/PageTransition.jsx b/parfum-shop/src/transitions/PageTransition.jsx
new file mode 100644
index 0000000..4e1e606
--- /dev/null
+++ b/parfum-shop/src/transitions/PageTransition.jsx
@@ -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 (
+
+ {children}
+
+ );
+}