246 lines
7.0 KiB
Plaintext
246 lines
7.0 KiB
Plaintext
---
|
|
import ThemeToggle from './ThemeToggle.astro';
|
|
|
|
import directus from '../../lib/directus';
|
|
import { readSingleton } from '@directus/sdk';
|
|
|
|
const global = await directus.request(readSingleton('global'));
|
|
|
|
const navItems = [
|
|
{ text: 'Home', href: '/' },
|
|
{ text: 'Blog', href: '/blog' },
|
|
{ text: 'Topics', href: '/topics' },
|
|
{ text: 'About', href: '/about' },
|
|
{ text: 'RSS', href: 'rss.xml' },
|
|
];
|
|
|
|
const pathname = new URL(Astro.request.url).pathname;
|
|
const currentPath = pathname.slice(1);
|
|
---
|
|
|
|
<header
|
|
class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
|
>
|
|
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
|
<!-- Logo -->
|
|
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
|
|
|
|
<!-- Desktop navigation -->
|
|
<nav class="hidden items-center space-x-6 sm:flex">
|
|
{
|
|
navItems.map((item) => {
|
|
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
|
return (
|
|
<a
|
|
href={item.href}
|
|
class={`text-sm font-medium ${
|
|
isActive
|
|
? 'text-zinc-900 dark:text-white'
|
|
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
}`}
|
|
>
|
|
{item.text}
|
|
</a>
|
|
);
|
|
})
|
|
}
|
|
<ThemeToggle />
|
|
</nav>
|
|
|
|
<!-- Mobile menu button -->
|
|
<button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="h-6 w-6 text-zinc-900 dark:text-white"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Mobile menu overlay -->
|
|
<div
|
|
id="mobile-menu"
|
|
class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
|
|
>
|
|
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
|
|
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a>
|
|
<button
|
|
id="close-menu-button"
|
|
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
|
|
aria-label="Close menu"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="h-6 w-6"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
|
|
{
|
|
navItems.map((item, index) => {
|
|
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
|
return (
|
|
<a
|
|
href={item.href}
|
|
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
|
isActive
|
|
? 'text-zinc-900 dark:text-white'
|
|
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
}`}
|
|
style={`transition-delay: ${index * 0.05}s;`}
|
|
>
|
|
{item.text}
|
|
</a>
|
|
);
|
|
})
|
|
}
|
|
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
|
|
<ThemeToggle />
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Spacer to prevent content from hiding behind fixed header -->
|
|
<div class="h-16"></div>
|
|
|
|
<script>
|
|
// Mobile menu toggle with animations
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
|
const closeMenuButton = document.getElementById('close-menu-button');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
const navItems = document.querySelectorAll('.mobile-nav-item');
|
|
|
|
// Open menu with animations
|
|
mobileMenuButton?.addEventListener('click', () => {
|
|
if (!mobileMenu) return;
|
|
|
|
// Prevent body scrolling
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Show menu with fade in
|
|
mobileMenu.classList.remove('pointer-events-none');
|
|
mobileMenu.classList.add('pointer-events-auto');
|
|
|
|
// Animate opacity
|
|
setTimeout(() => {
|
|
mobileMenu.style.opacity = '1';
|
|
|
|
// Animate each nav item with staggered delay
|
|
navItems.forEach((item) => {
|
|
setTimeout(() => {
|
|
item.classList.remove('opacity-0', 'translate-y-4');
|
|
}, 150);
|
|
});
|
|
}, 50);
|
|
});
|
|
|
|
// Close menu with animations
|
|
const closeMenu = () => {
|
|
if (!mobileMenu) return;
|
|
|
|
// Fade out nav items first
|
|
navItems.forEach((item) => {
|
|
item.classList.add('opacity-0', 'translate-y-4');
|
|
});
|
|
|
|
// Then fade out the menu
|
|
setTimeout(() => {
|
|
mobileMenu.style.opacity = '0';
|
|
|
|
// After animation completes, hide menu and restore scrolling
|
|
setTimeout(() => {
|
|
mobileMenu.classList.remove('pointer-events-auto');
|
|
mobileMenu.classList.add('pointer-events-none');
|
|
document.body.style.overflow = '';
|
|
}, 300);
|
|
}, 100);
|
|
};
|
|
|
|
// Close button event
|
|
closeMenuButton?.addEventListener('click', closeMenu);
|
|
|
|
// Close menu when clicking a link
|
|
const mobileLinks = mobileMenu?.querySelectorAll('a');
|
|
mobileLinks?.forEach((link) => {
|
|
link.addEventListener('click', closeMenu);
|
|
});
|
|
|
|
// Close menu on escape key
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
// Add smooth animation to header on scroll
|
|
const header = document.querySelector('header');
|
|
let lastScrollY = window.scrollY;
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (!header) return;
|
|
|
|
const currentScrollY = window.scrollY;
|
|
|
|
// Add shadow on scroll
|
|
if (currentScrollY > 10) {
|
|
header.classList.add('shadow-sm');
|
|
} else {
|
|
header.classList.remove('shadow-sm');
|
|
}
|
|
|
|
// Update last scroll position
|
|
lastScrollY = currentScrollY;
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
/* Smooth animations for mobile navigation */
|
|
.mobile-nav-item {
|
|
transition:
|
|
opacity 0.5s ease,
|
|
transform 0.5s ease,
|
|
color 0.3s ease;
|
|
}
|
|
|
|
/* Header transition */
|
|
header {
|
|
transition:
|
|
box-shadow 0.3s ease,
|
|
transform 0.3s ease,
|
|
background-color 0.3s ease;
|
|
}
|
|
|
|
/* Mobile menu button hover effect */
|
|
#mobile-menu-button {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
#mobile-menu-button:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
/* Mobile menu transition */
|
|
#mobile-menu {
|
|
transition: opacity 0.3s ease;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
</style>
|