Files
atridotdad/src/components/NavigationBar.tsx
2025-07-22 16:48:33 -06:00

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>
);
}