129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { Progress } from "@/components/ui/progress"
|
|
|
|
function PageProgressContent() {
|
|
const [progress, setProgress] = useState(0)
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
const [navigationKey, setNavigationKey] = useState(0)
|
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
const navigationUrlRef = useRef<string | null>(null)
|
|
|
|
// Start progress on click
|
|
useEffect(() => {
|
|
const handleClick = (e: MouseEvent) => {
|
|
if (e.defaultPrevented) return
|
|
|
|
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") {
|
|
navigationUrlRef.current = window.location.href
|
|
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)
|
|
intervalRef.current = interval
|
|
}
|
|
}
|
|
|
|
document.addEventListener("click", handleClick)
|
|
return () => document.removeEventListener("click", handleClick)
|
|
}, [])
|
|
|
|
// A canceled native beforeunload prompt leaves the current document alive.
|
|
// The timeout only gets a chance to run when the user chooses to stay.
|
|
useEffect(() => {
|
|
const handleBeforeUnload = () => {
|
|
const navigationUrl = navigationUrlRef.current
|
|
if (!navigationUrl) return
|
|
|
|
window.setTimeout(() => {
|
|
if (window.location.href !== navigationUrl) return
|
|
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current)
|
|
intervalRef.current = null
|
|
}
|
|
navigationUrlRef.current = null
|
|
setIsVisible(false)
|
|
setProgress(0)
|
|
}, 0)
|
|
}
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
|
return () => window.removeEventListener("beforeunload", handleBeforeUnload)
|
|
}, [])
|
|
|
|
// Detect completion without reading Next.js URL hooks during prerendering.
|
|
useEffect(() => {
|
|
if (!isVisible || !navigationUrlRef.current) return
|
|
const initialUrl = navigationUrlRef.current
|
|
const checkInterval = window.setInterval(() => {
|
|
if (window.location.href !== initialUrl) {
|
|
window.clearInterval(checkInterval)
|
|
setNavigationKey((key) => key + 1)
|
|
}
|
|
}, 50)
|
|
return () => {
|
|
window.clearInterval(checkInterval)
|
|
}
|
|
}, [isVisible])
|
|
|
|
// Finish progress on route change
|
|
useEffect(() => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current)
|
|
intervalRef.current = null
|
|
}
|
|
|
|
if (isVisible) {
|
|
const finishTimeout = setTimeout(() => setProgress(100), 0)
|
|
const timeout = setTimeout(() => {
|
|
setIsVisible(false)
|
|
setTimeout(() => setProgress(0), 900) // Reset after hiding
|
|
navigationUrlRef.current = null
|
|
}, 400)
|
|
return () => {
|
|
clearTimeout(finishTimeout)
|
|
clearTimeout(timeout)
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [navigationKey])
|
|
|
|
if (!isVisible && progress === 0) return null
|
|
|
|
return (
|
|
<div
|
|
className="fixed top-0 left-0 w-full z-50 pointer-events-none transition-opacity duration-300"
|
|
style={{ opacity: isVisible ? 1 : 0 }}
|
|
>
|
|
<Progress value={progress} className="h-0.5 w-full rounded-none bg-transparent" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PageProgress() {
|
|
return <PageProgressContent />
|
|
}
|