feat: Add professional portfolio images and Hero redesign
- Add 15 optimized WebP images for portfolio (21-53KB each) - Redesign Hero section with full-width background image - Add SSR support for Hero component - Update About section with new portrait image - Update Contact page with professional headshot - Add images to Services/Leistungen page - Add image optimization script (sharp) - Update translations for gallery section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
'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';
|
||||
|
||||
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 = () => {
|
||||
setScrolled(window.scrollY > 0);
|
||||
};
|
||||
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;
|
||||
Reference in New Issue
Block a user