diff --git a/app/layout.tsx b/app/layout.tsx index f49c460..93f83cf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -69,6 +69,7 @@ export const metadata: Metadata = { import { TooltipProvider } from "@/components/ui/tooltip"; import { Toaster } from "sonner"; import { ThemeProvider } from "@/components/theme-provider"; +import { PageProgress } from "@/components/page-progress"; export default function RootLayout({ children, @@ -82,6 +83,7 @@ export default function RootLayout({ suppressHydrationWarning > + { + const handleClick = (e: MouseEvent) => { + const target = (e.target as HTMLElement).closest("a") + if (!target) return + + const href = target.getAttribute("href") + if (!href) return + + // Check if it's an internal link + const isInternal = href.startsWith("/") || href.startsWith(window.location.origin) + const isSamePage = href === window.location.pathname || href.startsWith(window.location.pathname + "#") || href === window.location.href + + // Only show progress for actual page transitions + if (isInternal && !isSamePage && target.getAttribute("target") !== "_blank") { + setIsVisible(true) + setProgress(15) + + // Simulate progress while waiting for route change + // Uses a decaying approach so it slows down as it gets closer to 95% + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 95) { + clearInterval(interval) + return prev + } + return prev + (95 - prev) * 0.1 + }) + }, 100) + + // Store interval so we can clear it on route change + ;(window as any).__page_progress_interval = interval + } + } + + document.addEventListener("click", handleClick) + return () => document.removeEventListener("click", handleClick) + }, []) + + // Finish progress on route change + useEffect(() => { + if (typeof window !== "undefined" && (window as any).__page_progress_interval) { + clearInterval((window as any).__page_progress_interval) + } + + if (isVisible) { + setProgress(100) + const timeout = setTimeout(() => { + setIsVisible(false) + setTimeout(() => setProgress(0), 300) // Reset after hiding + }, 400) + return () => clearTimeout(timeout) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname, searchParams]) + + if (!isVisible && progress === 0) return null + + return ( +
+ +
+ ) +} + +export function PageProgress() { + return ( + + + + ) +} diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..777e627 --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import { Progress as ProgressPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress }