diff --git a/app/components/NavigationProgressProvider.tsx b/app/components/NavigationProgressProvider.tsx new file mode 100644 index 0000000..416a3b0 --- /dev/null +++ b/app/components/NavigationProgressProvider.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import PageLoadingBar from "./PageLoadingBar"; +import type { ReactNode } from "react"; + +export default function NavigationProgressProvider({ + children, +}: { + children: ReactNode; +}) { + const [showProgress, setShowProgress] = useState(false); + const router = useRouter(); + + useEffect(() => { + const originalPush = router.push; + const originalReplace = router.replace; + + const handleNavigationStart = () => { + setShowProgress(true); + }; + + const wrappedPush = (href: string, options?: any) => { + handleNavigationStart(); + return originalPush.call(router, href, options); + }; + + const wrappedReplace = (href: string, options?: any) => { + handleNavigationStart(); + return originalReplace.call(router, href, options); + }; + + router.push = wrappedPush; + router.replace = wrappedReplace; + + const handlePopState = () => { + handleNavigationStart(); + }; + + window.addEventListener("popstate", handlePopState); + + const timeoutId = setTimeout(() => { + setShowProgress(false); + }, 5000); + + return () => { + window.removeEventListener("popstate", handlePopState); + clearTimeout(timeoutId); + router.push = originalPush; + router.replace = originalReplace; + }; + }, [router]); + + return ( + <> + {showProgress && } + {children} + + ); +} diff --git a/app/components/PageLoadingBar.tsx b/app/components/PageLoadingBar.tsx new file mode 100644 index 0000000..cd164fd --- /dev/null +++ b/app/components/PageLoadingBar.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function PageLoadingBar() { + const [progress, setProgress] = useState(0); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + setIsVisible(true); + setProgress(10); + + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 90) return prev; + const increment = Math.random() * (15 - 5) + 5; + return Math.min(prev + increment, 90); + }); + }, 300); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (progress >= 90) { + const timeout = setTimeout(() => { + setProgress(100); + setTimeout(() => setIsVisible(false), 300); + }, 500); + return () => clearTimeout(timeout); + } + }, [progress]); + + if (!isVisible) return null; + + return ( +
+
+
+
+ + {progress < 100 && ( +
+ )} +
+ ); +} diff --git a/app/components/Providers.tsx b/app/components/Providers.tsx index b117062..5dcc3cc 100644 --- a/app/components/Providers.tsx +++ b/app/components/Providers.tsx @@ -1,7 +1,18 @@ "use client"; import { SessionProvider } from "next-auth/react"; +import NavigationProgressProvider from "./NavigationProgressProvider"; +import type { ReactNode } from "react"; -export default function Providers({ children }: { children: React.ReactNode }) { - return {children}; +/** + * Client-side context providers: + * - SessionProvider: Required for useSession() in client components + * - NavigationProgressProvider: Shows a loading bar between page transitions + */ +export default function Providers({ children }: { children: ReactNode }) { + return ( + + {children} + + ); }