Files
gunshiz 6cc0945e2c
CI / Verify (push) Successful in 1m17s
CI / Build immutable images and deploy (push) Successful in 2m14s
style : Less H for page progress and vistor less width
2026-09-11 19:44:08 +07:00

108 lines
3.4 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) => {
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)
}, [])
// 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)
const fallback = window.setTimeout(() => {
window.clearInterval(checkInterval)
setNavigationKey((key) => key + 1)
}, 10_000)
return () => {
window.clearInterval(checkInterval)
window.clearTimeout(fallback)
}
}, [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 />
}