Files
e-tib.com/components/providers/TransitionProvider.tsx

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;
}