118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
|
import { LazyMotion } from 'framer-motion';
|
|
import dynamic from 'next/dynamic';
|
|
|
|
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
|
|
|
|
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
|
|
const DynamicPageTransitionShutter = dynamic(
|
|
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
|
|
{ ssr: false }
|
|
);
|
|
|
|
interface TransitionContextType {
|
|
isTransitioning: boolean;
|
|
transitionMessage: string | null;
|
|
startTransition: (href: string, message?: string | null) => void;
|
|
}
|
|
|
|
const TransitionContext = createContext<TransitionContextType | undefined>(undefined);
|
|
|
|
export function TransitionProvider({ children }: { children: ReactNode }) {
|
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
|
const [hasMountedShutter, setHasMountedShutter] = useState(false);
|
|
const router = useRouter();
|
|
|
|
// We no longer eagerly load the shutter in the background.
|
|
// It will be loaded precisely when the user initiates a transition.
|
|
|
|
// We use a ref to track transition state without triggering the route change effect when it turns true
|
|
const isTransitioningRef = React.useRef(isTransitioning);
|
|
useEffect(() => {
|
|
isTransitioningRef.current = isTransitioning;
|
|
}, [isTransitioning]);
|
|
|
|
const startTransition = useCallback((href: string, message: string | null = null) => {
|
|
// Wenn wir bereits navigieren, nichts tun
|
|
if (isTransitioning) return;
|
|
|
|
// Normalize paths to avoid false mismatches (e.g. /de vs /de/)
|
|
const currentPath = typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, '') || '/' : '';
|
|
const targetPath = href.split('?')[0].replace(/\/$/, '') || '/';
|
|
const currentSearch = typeof window !== 'undefined' ? window.location.search : '';
|
|
const targetSearch = href.includes('?') ? '?' + href.split('?')[1] : '';
|
|
|
|
// Aktuelle URL prüfen, falls es dieselbe ist, nicht navigieren
|
|
if (currentPath === targetPath && currentSearch === targetSearch) {
|
|
// Optional: Wir könnten weich nach oben scrollen
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
return;
|
|
}
|
|
|
|
setTransitionMessage(message);
|
|
setIsTransitioning(true);
|
|
setHasMountedShutter(true);
|
|
|
|
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
|
setTimeout(() => {
|
|
router.push(href);
|
|
|
|
// Fallback: If for some reason the route doesn't change after 60 seconds (useful for slow dev servers), open the shutter anyway
|
|
setTimeout(() => {
|
|
setIsTransitioning((current) => {
|
|
if (current) {
|
|
setTimeout(() => setTransitionMessage(null), 300);
|
|
return false;
|
|
}
|
|
return current;
|
|
});
|
|
}, 60000);
|
|
}, 700);
|
|
}, [isTransitioning, router]);
|
|
|
|
return (
|
|
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
|
|
<React.Suspense fallback={null}>
|
|
<TransitionRouteWatcher
|
|
isTransitioningRef={isTransitioningRef}
|
|
setIsTransitioning={setIsTransitioning}
|
|
setTransitionMessage={setTransitionMessage}
|
|
/>
|
|
</React.Suspense>
|
|
<LazyMotion features={loadFeatures}>
|
|
{hasMountedShutter && <DynamicPageTransitionShutter />}
|
|
{children}
|
|
</LazyMotion>
|
|
</TransitionContext.Provider>
|
|
);
|
|
}
|
|
|
|
function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
|
|
useEffect(() => {
|
|
if (isTransitioningRef.current) {
|
|
const timer = setTimeout(() => {
|
|
setIsTransitioning(false);
|
|
setTimeout(() => setTransitionMessage(null), 300);
|
|
}, 100);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]);
|
|
|
|
return null;
|
|
}
|
|
|
|
export function useTransition() {
|
|
const context = useContext(TransitionContext);
|
|
if (!context) {
|
|
throw new Error('useTransition must be used within a TransitionProvider');
|
|
}
|
|
return context;
|
|
}
|