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:
2026-01-19 23:33:28 +01:00
co-authored by Claude Opus 4.5
parent b1ec7b4d61
commit 87c7ebc5e3
75 changed files with 10271 additions and 1314 deletions
+210
View File
@@ -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;