feat(mobile): optimize mobile layout, navigation, and spacing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 2m49s
Build & Deploy / 🏗️ Build (push) Successful in 4m54s
Build & Deploy / 🚀 Deploy (push) Successful in 1m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 2s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 2m49s
Build & Deploy / 🏗️ Build (push) Successful in 4m54s
Build & Deploy / 🚀 Deploy (push) Successful in 1m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Implement MobileBottomNav for native thumb-optimized usage - Simplify Header overlay on mobile - Add dynamic max() padding to ReferencesSlider for mobile view - Condense vertical spacing in Footer for mobile devices - Update layout.tsx to pad main content for bottom nav
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
export interface NavLink {
|
||||
label: string;
|
||||
@@ -20,7 +18,6 @@ interface HeaderProps {
|
||||
|
||||
export function Header({ navLinks }: HeaderProps) {
|
||||
const [isScrolled, setIsScrolled] = React.useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
const [forceSolid, setForceSolid] = React.useState(false);
|
||||
const pathname = usePathname() || '/';
|
||||
|
||||
@@ -36,13 +33,6 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
// Determine current locale from pathname
|
||||
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
|
||||
|
||||
// Helper to switch languages
|
||||
const getSwitchedUrl = (newLocale: string) => {
|
||||
if (!pathname) return `/${newLocale}`;
|
||||
const pathWithoutLocale = pathname.replace(/^\/(en|de)/, '');
|
||||
return `/${newLocale}${pathWithoutLocale === '' ? '' : pathWithoutLocale}`;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
@@ -68,10 +58,7 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
// Close mobile menu on route change
|
||||
React.useEffect(() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
}, [pathname]);
|
||||
// Check for force solid initially
|
||||
|
||||
const isSolidMode = isScrolled || forceSolid;
|
||||
|
||||
@@ -98,9 +85,9 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TransitionLink href={`/${currentLocale}`} className={`relative flex items-center transition-all duration-500 ease-out z-50 group ${isSolidMode ? 'h-9 w-36' : 'h-12 w-48'}`}>
|
||||
<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 || isMobileMenuOpen) ? "/assets/logo.png" : "/assets/logo-white.png"}
|
||||
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"
|
||||
@@ -111,11 +98,11 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
<div
|
||||
className="absolute inset-0 z-10 pointer-events-none transition-transform duration-500 origin-left group-hover:scale-105"
|
||||
style={{
|
||||
WebkitMaskImage: `url(${(isSolidMode || isMobileMenuOpen) ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
||||
WebkitMaskImage: `url(${isSolidMode ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
||||
WebkitMaskSize: 'contain',
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
WebkitMaskPosition: 'left center',
|
||||
maskImage: `url(${(isSolidMode || isMobileMenuOpen) ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
||||
maskImage: `url(${isSolidMode ? "/assets/logo.png" : "/assets/logo-white.png"})`,
|
||||
maskSize: 'contain',
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'left center'
|
||||
@@ -125,7 +112,8 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
</div>
|
||||
</TransitionLink>
|
||||
|
||||
<nav className="relative z-10 hidden md:flex items-center gap-8 lg:gap-10">
|
||||
<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) => {
|
||||
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${link.url}`
|
||||
@@ -160,93 +148,34 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
);
|
||||
})}
|
||||
|
||||
<div className={`h-5 w-px transition-colors duration-300 ${isSolidMode ? 'bg-neutral-200' : 'bg-white/20'}`} />
|
||||
</nav>
|
||||
|
||||
<LanguageSwitcher isSolidMode={isSolidMode} />
|
||||
|
||||
<TransitionLink
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className={`relative 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 ${
|
||||
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>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Nav Button */}
|
||||
<button
|
||||
className="md:hidden z-50 p-2 relative group"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<div className={`absolute inset-0 rounded-full transition-all duration-300 ${isSolidMode ? 'bg-neutral-100 group-hover:bg-neutral-200' : 'bg-white/10 group-hover:bg-white/20'}`} />
|
||||
<svg className="relative z-10 transition-transform duration-300 group-hover:scale-110" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={(isSolidMode || isMobileMenuOpen) ? "currentColor" : "white"} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
{isMobileMenuOpen ? (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" className="origin-center animate-in fade-in zoom-in duration-300" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" className="origin-center animate-in fade-in zoom-in duration-300" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="3" x2="21" y1="6" y2="6" className="transition-all duration-300 group-hover:translate-x-1" />
|
||||
<line x1="3" x2="21" y1="12" y2="12" />
|
||||
<line x1="3" x2="21" y1="18" y2="18" className="transition-all duration-300 group-hover:-translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
{/* 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'}`} />
|
||||
|
||||
{/* Language Switcher visible on all devices */}
|
||||
<div className="z-50 relative 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 ${
|
||||
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>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-40 bg-white md:hidden pt-32 pb-8 px-6 overflow-y-auto"
|
||||
>
|
||||
<nav className="flex flex-col gap-6">
|
||||
{navLinks && navLinks.length > 0 && navLinks.map((link) => {
|
||||
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${link.url}`
|
||||
: link.url;
|
||||
|
||||
return (
|
||||
<TransitionLink
|
||||
key={link.url}
|
||||
href={mappedUrl}
|
||||
className="text-2xl font-heading font-bold text-neutral-dark border-b border-neutral-100 pb-4"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</TransitionLink>
|
||||
);
|
||||
})}
|
||||
|
||||
<TransitionLink
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className="text-2xl font-heading font-bold text-primary border-b border-neutral-100 pb-4"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{currentLocale === 'de' ? 'Kontakt' : 'Contact'}
|
||||
</TransitionLink>
|
||||
|
||||
<div className="mt-8 pt-6">
|
||||
<p className="text-text-secondary text-sm font-bold uppercase tracking-widest mb-4">Sprache wählen</p>
|
||||
<LanguageSwitcher mobile={true} />
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user