123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import { useComputed, useSignal } from "@preact/signals";
|
|
import { useEffect } from "preact/hooks";
|
|
import { navigationItems } from "../config/data";
|
|
import type { LucideIcon } from "../types";
|
|
|
|
interface NavigationBarProps {
|
|
currentPath: string;
|
|
}
|
|
|
|
export default function NavigationBar({ currentPath }: NavigationBarProps) {
|
|
const isScrolling = useSignal(false);
|
|
const prevScrollPos = useSignal(0);
|
|
const currentClientPath = useSignal(currentPath);
|
|
|
|
const isVisible = useComputed(() => {
|
|
if (prevScrollPos.value < 50) return true;
|
|
|
|
const currentPos = typeof window !== "undefined" ? globalThis.scrollY : 0;
|
|
return prevScrollPos.value > currentPos;
|
|
});
|
|
|
|
// Filter out disabled navigation items
|
|
const enabledNavigationItems = navigationItems.filter(
|
|
(item) => item.enabled !== false,
|
|
);
|
|
|
|
// Update client path when location changes
|
|
useEffect(() => {
|
|
const updatePath = () => {
|
|
if (typeof window !== "undefined") {
|
|
currentClientPath.value = window.location.pathname;
|
|
}
|
|
};
|
|
|
|
// Set initial path
|
|
updatePath();
|
|
|
|
// Listen for Astro's view transition events
|
|
const handleAstroNavigation = () => {
|
|
updatePath();
|
|
};
|
|
|
|
// Listen for astro:page-load event which fires after navigation completes
|
|
document.addEventListener("astro:page-load", handleAstroNavigation);
|
|
|
|
// Also listen for astro:after-swap as a backup
|
|
document.addEventListener("astro:after-swap", handleAstroNavigation);
|
|
|
|
// Listen for regular navigation events as fallback
|
|
window.addEventListener("popstate", updatePath);
|
|
|
|
return () => {
|
|
document.removeEventListener("astro:page-load", handleAstroNavigation);
|
|
document.removeEventListener("astro:after-swap", handleAstroNavigation);
|
|
window.removeEventListener("popstate", updatePath);
|
|
};
|
|
}, []);
|
|
|
|
// Use the client path
|
|
const activePath = currentClientPath.value;
|
|
|
|
// Normalize path
|
|
const normalizedPath =
|
|
activePath.endsWith("/") && activePath.length > 1
|
|
? activePath.slice(0, -1)
|
|
: activePath;
|
|
|
|
useEffect(() => {
|
|
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const handleScroll = () => {
|
|
isScrolling.value = true;
|
|
prevScrollPos.value = globalThis.scrollY;
|
|
|
|
if (scrollTimer) clearTimeout(scrollTimer);
|
|
|
|
scrollTimer = setTimeout(() => {
|
|
isScrolling.value = false;
|
|
}, 200);
|
|
};
|
|
|
|
globalThis.addEventListener("scroll", handleScroll);
|
|
|
|
return () => {
|
|
globalThis.removeEventListener("scroll", handleScroll);
|
|
if (scrollTimer) clearTimeout(scrollTimer);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
class={`fixed bottom-3 sm:bottom-4 left-1/2 transform -translate-x-1/2 z-20 transition-all duration-300 ${
|
|
isScrolling.value ? "opacity-30" : "opacity-100"
|
|
} ${isVisible.value ? "translate-y-0" : "translate-y-20"}`}
|
|
>
|
|
<div class="overflow-visible">
|
|
<ul class="menu menu-horizontal bg-base-200 rounded-box p-1.5 sm:p-2 flex flex-nowrap whitespace-nowrap">
|
|
{enabledNavigationItems.map((item) => {
|
|
const Icon = item.icon as LucideIcon;
|
|
const isActive = item.isActive
|
|
? item.isActive(normalizedPath)
|
|
: normalizedPath === item.path;
|
|
|
|
return (
|
|
<li key={item.id} class="mx-0.5 sm:mx-1">
|
|
<a
|
|
href={item.path}
|
|
class={`tooltip tooltip-top min-h-[44px] min-w-[44px] inline-flex items-center justify-center ${isActive ? "menu-active" : ""}`}
|
|
aria-label={item.tooltip}
|
|
data-tip={item.tooltip}
|
|
>
|
|
<Icon size={18} class="sm:w-5 sm:h-5" />
|
|
<span class="sr-only">{item.name}</span>
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|