331 lines
17 KiB
TypeScript
331 lines
17 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { TransitionLink } from '@/components/ui/TransitionLink';
|
|
import Image from 'next/image';
|
|
import { usePathname } from 'next/navigation';
|
|
import { motion, useScroll, useTransform, AnimatePresence } from 'framer-motion';
|
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
|
|
export interface NavLink {
|
|
label: string;
|
|
url: string;
|
|
children?: NavLink[];
|
|
isGroupLabel?: boolean;
|
|
}
|
|
|
|
interface HeaderProps {
|
|
navLinks: NavLink[];
|
|
}
|
|
|
|
export function Header({ navLinks }: HeaderProps) {
|
|
const [isScrolled, setIsScrolled] = React.useState(false);
|
|
const [forceSolid, setForceSolid] = React.useState(false);
|
|
const [headerVariant, setHeaderVariant] = React.useState<'capsule' | 'standard'>('capsule');
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
|
const pathname = usePathname() || '/';
|
|
|
|
// Scroll bound shine effect (continuous looping as user scrolls)
|
|
const { scrollY } = useScroll();
|
|
const shineX = useTransform(scrollY, (y) => {
|
|
const adjustedY = Math.max(0, y - 20); // Start after the solid mode kicks in
|
|
const range = 800; // 1 full sweep per 800px scrolled
|
|
const progress = (adjustedY % range) / range;
|
|
return `${-150 + progress * 300}%`;
|
|
});
|
|
|
|
// Determine current locale from pathname
|
|
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
|
|
|
|
React.useEffect(() => {
|
|
const handleScroll = () => {
|
|
setIsScrolled(window.scrollY > 20);
|
|
};
|
|
window.addEventListener('scroll', handleScroll);
|
|
|
|
// Check for forced solid header (used by error pages, legal pages without hero, etc.)
|
|
const checkHeaderState = () => {
|
|
setForceSolid(document.body.getAttribute('data-force-solid-header') === 'true');
|
|
setHeaderVariant((document.body.getAttribute('data-header-variant') as 'capsule' | 'standard') || 'capsule');
|
|
};
|
|
checkHeaderState();
|
|
|
|
// Set up observer for client-side navigation changes
|
|
const observer = new MutationObserver(checkHeaderState);
|
|
observer.observe(document.body, { attributes: true, attributeFilter: ['data-force-solid-header', 'data-header-variant'] });
|
|
|
|
// Re-check when pathname changes
|
|
checkHeaderState();
|
|
setIsMobileMenuOpen(false);
|
|
|
|
return () => {
|
|
window.removeEventListener('scroll', handleScroll);
|
|
observer.disconnect();
|
|
};
|
|
}, [pathname]);
|
|
|
|
|
|
const isSolidMode = isScrolled || forceSolid || headerVariant === 'standard';
|
|
const isStandard = headerVariant === 'standard';
|
|
|
|
return (
|
|
<>
|
|
<header
|
|
className={`fixed top-0 left-0 right-0 z-50 flex justify-center transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
|
isStandard
|
|
? 'bg-[#050B14]/95 backdrop-blur-xl border-b border-white/10'
|
|
: (isSolidMode ? 'pt-4 px-4 sm:px-6' : 'pt-0 px-0')
|
|
}`}
|
|
>
|
|
<div
|
|
className={`relative w-full max-w-7xl mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
|
|
isStandard
|
|
? 'py-4 px-6 md:px-8'
|
|
: isSolidMode
|
|
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-8 md:px-10'
|
|
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
|
|
}`}
|
|
>
|
|
{/* Scroll bound shine effect */}
|
|
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden rounded-full">
|
|
<motion.div
|
|
style={{ x: shineX }}
|
|
className={`absolute top-0 h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-20deg] mix-blend-overlay transition-opacity duration-300 ${isSolidMode ? 'opacity-100' : 'opacity-0'}`}
|
|
/>
|
|
</div>
|
|
|
|
<TransitionLink href={`/${currentLocale}`} className={`relative flex items-center transition-all duration-500 ease-out z-50 group ${isSolidMode ? 'h-8 md:h-9 w-28 md:w-36' : 'h-10 md:h-12 w-36 md:w-48'}`}>
|
|
<Image
|
|
src={isSolidMode ? "/assets/logo.png" : "/assets/logo-white.png"}
|
|
alt="E-TIB Gruppe"
|
|
fill
|
|
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
|
|
priority
|
|
fetchPriority="high"
|
|
sizes="192px"
|
|
/>
|
|
{/* Masked sweep overlay on hover */}
|
|
<div
|
|
className="absolute inset-0 z-10 pointer-events-none transition-transform duration-500 origin-left group-hover:scale-105"
|
|
style={{
|
|
WebkitMaskImage: `url(${isSolidMode ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
|
WebkitMaskSize: 'contain',
|
|
WebkitMaskRepeat: 'no-repeat',
|
|
WebkitMaskPosition: 'left center',
|
|
maskImage: `url(${isSolidMode ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
|
maskSize: 'contain',
|
|
maskRepeat: 'no-repeat',
|
|
maskPosition: 'left center'
|
|
}}
|
|
>
|
|
<div className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg] group-hover:left-[150%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)]" />
|
|
</div>
|
|
</TransitionLink>
|
|
|
|
<div className="flex items-center gap-2 md:gap-4 ml-auto md:ml-0">
|
|
<nav className="relative z-10 hidden md:flex items-center gap-8 lg:gap-10">
|
|
{navLinks && navLinks.length > 0 && navLinks.map((link) => (
|
|
<NavItem
|
|
key={link.url}
|
|
link={link}
|
|
currentLocale={currentLocale}
|
|
pathname={pathname}
|
|
isSolidMode={isSolidMode}
|
|
/>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Right side elements (Language Switcher always visible, Contact button desktop only) */}
|
|
<div className="flex items-center gap-2 md:gap-4 lg:gap-6">
|
|
<div className={`hidden md:block h-5 w-px transition-colors duration-300 ${isSolidMode ? 'bg-neutral-200' : 'bg-white/20'}`} />
|
|
|
|
{/* Quick Actions (Mobile Only) */}
|
|
<div className="md:hidden flex items-center gap-2">
|
|
<a
|
|
href="tel:+492572946760"
|
|
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
|
|
isSolidMode
|
|
? 'bg-primary text-white shadow-md shadow-primary/20 hover:-translate-y-0.5'
|
|
: 'bg-white text-primary shadow-md hover:-translate-y-0.5'
|
|
}`}
|
|
aria-label="Anrufen"
|
|
>
|
|
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
</svg>
|
|
</a>
|
|
<a
|
|
href="mailto:info@e-tib.com"
|
|
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
|
|
isSolidMode
|
|
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
|
|
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
|
|
}`}
|
|
aria-label="E-Mail"
|
|
>
|
|
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
</a>
|
|
<a
|
|
href="https://maps.google.com/?q=E-TIB+GmbH,+Greven"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
|
|
isSolidMode
|
|
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
|
|
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
|
|
}`}
|
|
aria-label="Route"
|
|
>
|
|
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
|
|
{/* Language Switcher visible on desktop only */}
|
|
<div className="z-50 relative hidden md:flex items-center">
|
|
<LanguageSwitcher isSolidMode={isSolidMode} />
|
|
</div>
|
|
|
|
{/* Contact Button Desktop Only */}
|
|
<TransitionLink
|
|
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
|
className={`relative hidden md:inline-flex ml-2 px-7 py-2.5 rounded-full font-bold text-xs lg:text-sm uppercase tracking-widest transition-all duration-500 overflow-hidden group/cta isolate select-none ${
|
|
isSolidMode
|
|
? 'bg-primary text-white shadow-lg shadow-primary/20 hover:shadow-primary/40 hover:-translate-y-0.5 border border-transparent'
|
|
: 'bg-white/10 text-white backdrop-blur-md border border-white/30 hover:bg-white/20 hover:-translate-y-0.5 hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]'
|
|
}`}
|
|
>
|
|
<span className="relative z-10 transition-transform duration-300 group-hover/cta:scale-105 inline-block">{currentLocale === 'de' ? 'Kontakt' : 'Contact'}</span>
|
|
{/* Sleek shine sweep effect */}
|
|
<span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" />
|
|
</TransitionLink>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink, currentLocale: string, pathname: string, isSolidMode: boolean }) {
|
|
const [isHovered, setIsHovered] = React.useState(false);
|
|
|
|
// Close dropdown on navigation
|
|
React.useEffect(() => {
|
|
setIsHovered(false);
|
|
}, [pathname]);
|
|
|
|
// Close dropdown on scroll
|
|
React.useEffect(() => {
|
|
const handleScroll = () => {
|
|
if (isHovered) setIsHovered(false);
|
|
};
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, [isHovered]);
|
|
|
|
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
|
|
? `/${currentLocale}${link.url}`
|
|
: link.url;
|
|
|
|
const isActive = pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`));
|
|
|
|
return (
|
|
<div
|
|
className="relative group py-2"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<TransitionLink
|
|
href={mappedUrl}
|
|
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group flex items-center gap-1.5 select-none ${
|
|
isSolidMode
|
|
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
|
|
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
|
|
}`}
|
|
>
|
|
{link.label}
|
|
{link.children && (
|
|
<svg className={`w-3.5 h-3.5 transition-transform duration-300 ${isHovered ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
)}
|
|
|
|
{/* Framer Motion Active Indicator */}
|
|
{isActive && !link.children && (
|
|
<motion.div
|
|
layoutId="header-active-indicator"
|
|
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
|
|
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
|
/>
|
|
)}
|
|
|
|
{/* Subtle hover underline */}
|
|
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
|
|
</TransitionLink>
|
|
|
|
<AnimatePresence>
|
|
{isHovered && link.children && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
|
|
exit={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
|
|
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
style={{ perspective: 1000, transformOrigin: 'top center' }}
|
|
className="absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50"
|
|
>
|
|
<div className={`p-2.5 rounded-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] border ${isSolidMode ? 'bg-white/95 border-neutral-200/60' : 'bg-[#050B14]/95 border-white/[0.08] shadow-[0_0_40px_rgba(0,0,0,0.4)]'} backdrop-blur-2xl flex flex-col gap-1 relative overflow-hidden`}>
|
|
{/* Top glowing accent line */}
|
|
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-70" />
|
|
|
|
{link.children.map((child, idx) => {
|
|
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
|
|
? `/${currentLocale}${child.url}`
|
|
: child.url;
|
|
const isChildActive = pathname === childUrl;
|
|
|
|
if (child.isGroupLabel) {
|
|
return (
|
|
<div key={`group-${idx}`} className={`mt-2 mb-1 px-4 text-[11px] font-bold uppercase tracking-wider ${isSolidMode ? 'text-neutral-400' : 'text-neutral-500'}`}>
|
|
{child.label}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TransitionLink
|
|
key={child.url}
|
|
href={childUrl}
|
|
onClick={() => setIsHovered(false)}
|
|
className={`group/dropitem relative block px-4 py-3.5 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
|
isSolidMode
|
|
? isChildActive ? 'bg-primary/10 text-primary' : 'text-neutral-600 hover:text-primary hover:bg-neutral-50/80'
|
|
: isChildActive ? 'bg-white/10 text-white' : 'text-neutral-300 hover:text-white hover:bg-white/[0.06]'
|
|
}`}
|
|
>
|
|
<div className="relative z-10 flex items-center justify-between">
|
|
<span className="relative">
|
|
{child.label}
|
|
<span className={`absolute -bottom-0.5 left-0 w-0 h-0.5 rounded-full transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover/dropitem:w-full ${isSolidMode ? 'bg-primary/40' : 'bg-primary/60'} ${isChildActive ? 'opacity-0' : 'opacity-100'}`} />
|
|
</span>
|
|
<svg className={`w-4 h-4 transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] -translate-x-3 opacity-0 group-hover/dropitem:translate-x-0 group-hover/dropitem:opacity-100 ${isSolidMode ? 'text-primary' : 'text-white'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</TransitionLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|
|
|