212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslations, useLocale } from 'next-intl';
|
|
import {
|
|
Menu,
|
|
X,
|
|
Home,
|
|
User,
|
|
Briefcase,
|
|
BookOpen,
|
|
Phone,
|
|
Cog,
|
|
} from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { usePathname } from 'next/navigation';
|
|
import { NavLink } from './NavLink';
|
|
import LanguageSwitcher from './LanguageSwitcher';
|
|
import { ContactInfo } from './ContactInfo';
|
|
import { throttle } from '@/utils/throttle';
|
|
|
|
const Header = () => {
|
|
const t = useTranslations('navigation');
|
|
const locale = useLocale();
|
|
const pathname = usePathname();
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
|
|
// Close menu on route change
|
|
useEffect(() => {
|
|
setIsMenuOpen(false);
|
|
}, [pathname]);
|
|
|
|
// Scroll handler
|
|
useEffect(() => {
|
|
const handleScroll = throttle(() => {
|
|
setScrolled(window.scrollY > 0);
|
|
}, 100);
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
// Prevent body scroll when menu is open
|
|
useEffect(() => {
|
|
if (isMenuOpen) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = '';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
}, [isMenuOpen]);
|
|
|
|
const navigationLinks = [
|
|
{
|
|
path: `/${locale}`,
|
|
label: t('main.home'),
|
|
icon: <Home className="h-5 w-5" />,
|
|
},
|
|
{
|
|
path: `/${locale}/about`,
|
|
label: t('main.about'),
|
|
icon: <User className="h-5 w-5" />,
|
|
},
|
|
{
|
|
path: `/${locale}/leistungen`,
|
|
label: t('main.services'),
|
|
icon: <Cog className="h-5 w-5" />,
|
|
},
|
|
{
|
|
path: `/${locale}/portfolio`,
|
|
label: t('main.portfolio'),
|
|
icon: <Briefcase className="h-5 w-5" />,
|
|
},
|
|
{
|
|
path: `/${locale}/blog`,
|
|
label: t('main.blog'),
|
|
icon: <BookOpen className="h-5 w-5" />,
|
|
},
|
|
{
|
|
path: `/${locale}/contact`,
|
|
label: t('main.contact'),
|
|
icon: <Phone className="h-5 w-5" />,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* Navigation */}
|
|
<nav
|
|
className={`
|
|
fixed w-full z-40 transition-all duration-300
|
|
bg-zinc-800/50 backdrop-blur-sm border-b border-zinc-700/30
|
|
${scrolled
|
|
? 'bg-zinc-800/70 backdrop-blur-md shadow-lg shadow-zinc-900/30'
|
|
: ''
|
|
}
|
|
`}
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center h-16">
|
|
{/* Logo */}
|
|
<Link href={`/${locale}`} className="flex items-center space-x-2 group shrink-0">
|
|
<Image
|
|
src="/header-logo.svg"
|
|
alt="Damjan Savić Logo"
|
|
className="h-8 w-auto transition-transform duration-200 hover:scale-105"
|
|
width={120}
|
|
height={32}
|
|
priority
|
|
/>
|
|
</Link>
|
|
|
|
{/* Desktop Navigation */}
|
|
<div className="hidden md:flex items-center gap-2">
|
|
{navigationLinks.map(({ path, label, icon }) => (
|
|
<NavLink
|
|
key={path}
|
|
href={path}
|
|
icon={icon}
|
|
label={label}
|
|
className="text-sm"
|
|
/>
|
|
))}
|
|
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
className="md:hidden relative z-50 p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95"
|
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
aria-expanded={isMenuOpen}
|
|
aria-controls="mobile-menu"
|
|
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
|
|
>
|
|
{isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Mobile Menu Backdrop */}
|
|
<div
|
|
className={`fixed inset-0 bg-black/50 md:hidden z-30 transition-opacity duration-200 ${
|
|
isMenuOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
}`}
|
|
onClick={() => setIsMenuOpen(false)}
|
|
/>
|
|
|
|
{/* Mobile Sidebar */}
|
|
<div
|
|
id="mobile-menu"
|
|
className={`fixed right-0 top-0 h-full w-80 bg-zinc-900/95 backdrop-blur-md shadow-xl md:hidden z-50 border-l border-zinc-800 transition-transform duration-300 ease-out ${
|
|
isMenuOpen ? 'translate-x-0' : 'translate-x-full'
|
|
}`}
|
|
>
|
|
{/* Sidebar Header */}
|
|
<div className="flex items-center justify-between px-4 h-16 border-b border-zinc-800">
|
|
<span className="text-white font-semibold">Menu</span>
|
|
<button
|
|
onClick={() => setIsMenuOpen(false)}
|
|
aria-label="Close sidebar"
|
|
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95"
|
|
>
|
|
<X className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Scrollable Sidebar Content */}
|
|
<div className="h-[calc(100vh-4rem)] overflow-y-auto">
|
|
<div className="flex flex-col h-full">
|
|
{/* Contact Info */}
|
|
<ContactInfo />
|
|
|
|
{/* Navigation Links */}
|
|
<div className="py-4 px-4">
|
|
{navigationLinks.map(({ path, label, icon }) => (
|
|
<NavLink
|
|
key={path}
|
|
href={path}
|
|
icon={icon}
|
|
label={label}
|
|
onClick={() => setIsMenuOpen(false)}
|
|
className="flex items-center w-full mb-1"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Language Switcher */}
|
|
<div className="px-4 py-3 border-t border-zinc-800">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-zinc-400">
|
|
Sprache ändern
|
|
</span>
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Header;
|