Compare commits
6 Commits
feature/pr
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
| 55d633a33c | |||
| 1023ef978e | |||
| 2a1f6864c9 | |||
| fa9fb60dec | |||
| 3abe4f419f | |||
| ba3a711d5b |
@@ -65,7 +65,7 @@ jobs:
|
||||
TARGET="testing"
|
||||
IMAGE_TAG="main-${SHORT_SHA}"
|
||||
ENV_FILE=".env.testing"
|
||||
TRAEFIK_HOST="test.${DOMAIN}, testing.${DOMAIN}"
|
||||
TRAEFIK_HOST="test.${DOMAIN}"
|
||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
TARGET="production"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { MobileBottomNav } from '@/components/layout/MobileBottomNav';
|
||||
import JsonLd from '@/components/JsonLd';
|
||||
import SkipLink from '@/components/SkipLink';
|
||||
import AnalyticsShell from '@/components/analytics/AnalyticsShell';
|
||||
@@ -181,10 +182,11 @@ export default async function Layout(props: {
|
||||
<PageTransitionShutter />
|
||||
<SkipLink />
|
||||
<Header navLinks={navLinks} />
|
||||
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
||||
|
||||
<main
|
||||
id="main-content"
|
||||
className="flex-grow animate-fade-in overflow-visible"
|
||||
className="flex-grow animate-fade-in overflow-visible pb-[90px] md:pb-0"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -101,7 +101,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseMove={onMouseMove}
|
||||
className={`flex gap-6 overflow-x-auto pb-12 pt-4 px-[calc((100vw-min(100%,1280px))/2)] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
|
||||
className={`flex gap-6 overflow-x-auto pb-12 pt-4 px-[max(1rem,calc((100vw-1280px)/2))] md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
|
||||
>
|
||||
{references.map((ref, i) => {
|
||||
const imgSrc = (ref.image && typeof ref.image === 'object' && ref.image.url)
|
||||
|
||||
@@ -18,7 +18,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<footer className="bg-[#050B14] text-neutral-light py-16 relative overflow-hidden border-t border-white/5">
|
||||
<footer className="bg-[#050B14] text-neutral-light py-10 md:py-16 relative overflow-hidden border-t border-white/5">
|
||||
{/* Subtle background tech grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||
|
||||
@@ -26,13 +26,13 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/40 to-transparent" />
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-[200px] bg-primary/20 blur-[100px] rounded-full pointer-events-none opacity-50" />
|
||||
|
||||
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-6 lg:gap-8 mb-12">
|
||||
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-8 md:mb-12">
|
||||
{/* Brand Column Bento */}
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
<TransitionLink href={`/${locale}`} className="relative block h-14 w-48 mb-6 group/logo">
|
||||
<div className="space-y-4 md:space-y-6 relative z-10">
|
||||
<TransitionLink href={`/${locale}`} className="relative block h-12 md:h-14 w-40 md:w-48 mb-4 md:mb-6 group/logo">
|
||||
<Image
|
||||
src="/assets/logo-white.png"
|
||||
alt="E-TIB Gruppe"
|
||||
@@ -48,7 +48,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</div>
|
||||
|
||||
{companyInfo && (
|
||||
<div className="text-white/70 text-sm space-y-4 mt-12 relative z-10">
|
||||
<div className="text-white/70 text-sm space-y-3 md:space-y-4 mt-8 md:mt-12 relative z-10">
|
||||
<div className="flex gap-4 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.05] transition-colors">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
@@ -58,7 +58,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<p className="whitespace-pre-line font-medium text-white/90">{companyInfo.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 gap-3 md:gap-4">
|
||||
<a href={`mailto:${companyInfo.contactEmail}`} className="flex flex-col gap-2 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact">
|
||||
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">E-Mail</p>
|
||||
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactEmail}</p>
|
||||
@@ -73,16 +73,16 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</div>
|
||||
|
||||
{/* Navigation Bento Grid */}
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6 lg:gap-8">
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-4 md:gap-6 lg:gap-8">
|
||||
|
||||
{/* Company Column */}
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="absolute inset-0 bg-gradient-to-bl from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-8 text-white">
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
|
||||
{locale === 'de' ? 'Unternehmen' : 'Company'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
|
||||
</h4>
|
||||
<ul className="space-y-4 relative z-10 flex-grow">
|
||||
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/kompetenzen` },
|
||||
{ label: locale === 'de' ? 'Über uns' : 'About us', href: `/${locale}/ueber-uns` },
|
||||
@@ -101,13 +101,13 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</div>
|
||||
|
||||
{/* Legal Column */}
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-8 text-white">
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
|
||||
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
|
||||
</h4>
|
||||
<ul className="space-y-4 relative z-10 flex-grow">
|
||||
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/impressum` },
|
||||
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/datenschutz` },
|
||||
@@ -123,7 +123,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</ul>
|
||||
|
||||
{/* Built Info inside Legal box for compact layout */}
|
||||
<div className="mt-12 pt-6 border-t border-white/5 text-white/30 text-xs">
|
||||
<div className="mt-8 pt-4 md:mt-12 md:pt-6 border-t border-white/5 text-white/30 text-xs">
|
||||
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-white/60 transition-colors flex items-center gap-2">
|
||||
<span>Designed & Engineered by Mintel</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17l9.2-9.2M17 17V7H7"/></svg>
|
||||
@@ -135,7 +135,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</div>
|
||||
|
||||
<div className="container relative z-10">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6 py-6 px-8 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-md">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 md:gap-6 py-4 px-5 md:py-6 md:px-8 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-md text-center md:text-left">
|
||||
<p className="text-white/50 text-sm font-medium">© {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
|
||||
<div className="flex items-center gap-6">
|
||||
<LanguageSwitcher isSolidMode={false} />
|
||||
@@ -143,7 +143,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</div>
|
||||
|
||||
{/* Quality Badges */}
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-x-8 gap-y-4 text-white/30 text-xs font-medium uppercase tracking-wider">
|
||||
<div className="mt-4 md:mt-6 flex flex-wrap justify-center gap-x-4 gap-y-3 md:gap-x-8 md:gap-y-4 text-white/30 text-[10px] md:text-xs font-medium uppercase tracking-wider px-2">
|
||||
<div className="flex items-center gap-2"><ShieldCheck className="w-4 h-4 text-white/40" /><span>SSL Secured</span></div>
|
||||
<div className="flex items-center gap-2"><Leaf className="w-4 h-4 text-primary/60" /><span>Green Hosting</span></div>
|
||||
<div className="flex items-center gap-2"><Lock className="w-4 h-4 text-white/40" /><span>DSGVO Compliant</span></div>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
101
components/layout/MobileBottomNav.tsx
Normal file
101
components/layout/MobileBottomNav.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||
import { Home, Briefcase, Users, Mail } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export interface NavLink {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface MobileBottomNavProps {
|
||||
navLinks: NavLink[];
|
||||
currentLocale: string;
|
||||
}
|
||||
|
||||
export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProps) {
|
||||
const pathname = usePathname() || '/';
|
||||
|
||||
// Construct our bottom nav items based on the passed links
|
||||
const items = React.useMemo(() => {
|
||||
const homeUrl = `/${currentLocale}`;
|
||||
|
||||
// Find the specific links from the provided navLinks (which might be translated)
|
||||
// We do a simple match based on index or keywords if needed, but since we know the order:
|
||||
// 0: Kompetenzen, 1: Über uns, 2: Karriere, 3: Messen
|
||||
const competenciesUrl = navLinks[0]?.url || `/${currentLocale}/kompetenzen`;
|
||||
const aboutUrl = navLinks[1]?.url || `/${currentLocale}/ueber-uns`;
|
||||
const contactUrl = `/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`;
|
||||
|
||||
return [
|
||||
{
|
||||
label: currentLocale === 'de' ? 'Start' : 'Home',
|
||||
url: homeUrl,
|
||||
icon: <Home className="w-6 h-6 mb-1" strokeWidth={2} />,
|
||||
exactMatch: true
|
||||
},
|
||||
{
|
||||
label: navLinks[0]?.label || (currentLocale === 'de' ? 'Kompetenzen' : 'Competencies'),
|
||||
url: competenciesUrl,
|
||||
icon: <Briefcase className="w-6 h-6 mb-1" strokeWidth={2} />
|
||||
},
|
||||
{
|
||||
label: navLinks[1]?.label || (currentLocale === 'de' ? 'Über uns' : 'About Us'),
|
||||
url: aboutUrl,
|
||||
icon: <Users className="w-6 h-6 mb-1" strokeWidth={2} />
|
||||
},
|
||||
{
|
||||
label: currentLocale === 'de' ? 'Kontakt' : 'Contact',
|
||||
url: contactUrl,
|
||||
icon: <Mail className="w-6 h-6 mb-1" strokeWidth={2} />
|
||||
}
|
||||
];
|
||||
}, [currentLocale, navLinks]);
|
||||
|
||||
return (
|
||||
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-6 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
|
||||
<nav className="flex justify-around items-center max-w-md mx-auto">
|
||||
{items.map((item) => {
|
||||
// Determine if active
|
||||
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${item.url}`
|
||||
: item.url;
|
||||
|
||||
const isActive = item.exactMatch
|
||||
? pathname === mappedUrl
|
||||
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
|
||||
|
||||
return (
|
||||
<TransitionLink
|
||||
key={item.label}
|
||||
href={mappedUrl}
|
||||
className={`relative flex flex-col items-center justify-center w-full min-h-[44px] py-1 transition-colors duration-300 ${
|
||||
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
{/* Active Icon Background */}
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="mobile-nav-active-bg"
|
||||
className="absolute inset-0 bg-primary/10 rounded-full scale-150"
|
||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Icon wrapper to ensure it sits above the background */}
|
||||
<div className="relative z-10 transition-transform duration-300 active:scale-90">
|
||||
{item.icon}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold mt-0.5 tracking-wide z-10">{item.label}</span>
|
||||
</TransitionLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Karriere"
|
||||
title="Zukunft bauen"
|
||||
description="Suchen Sie eine neue Herausforderung in einem zukunftssicheren Markt? Bei E-TIB arbeiten wir täglich daran, Deutschland zu vernetzen und die Energiewende voranzutreiben."
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="Offene Stellen"
|
||||
linkHref="#stellen"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Unsere Leistungen"
|
||||
title="Alles aus einer Hand"
|
||||
description="Als Full-Service-Partner im Bereich Kabelleitungstiefbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
linkText="Kontakt aufnehmen"
|
||||
linkHref="/de/kontakt"
|
||||
secondaryCtaLabel="Referenzen ansehen"
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Events"
|
||||
title="Treffen Sie uns\nvor Ort"
|
||||
description="Kommen Sie mit unseren Experten ins Gespräch. Wir sind auf den wichtigsten Branchenmessen vertreten und freuen uns auf den persönlichen Austausch mit Ihnen."
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
videoUrl="/assets/videos/web/hero-messe.mp4"
|
||||
linkText="Zu den Terminen"
|
||||
linkHref="#termine"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Careers"
|
||||
title="Build the Future\nwith Us"
|
||||
description="We are looking for motivated talents who want to shape the digital and energy infrastructure of tomorrow. Join the E-TIB team."
|
||||
videoUrl="/assets/videos/web/hero-bahnkreuzung.mp4"
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="View Open Positions"
|
||||
linkHref="#jobs"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Competencies"
|
||||
title="Everything from a\nsingle source"
|
||||
description="From the first planning sketch to the final documentation – we are your reliable partner for complex infrastructure projects."
|
||||
videoUrl="/assets/videos/web/hero-bahnkreuzung.mp4"
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
linkText="Get in Touch"
|
||||
linkHref="/en/kontakt"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Events"
|
||||
title="Meet us\nin person"
|
||||
description="Get in touch with our experts. We are represented at the most important industry trade fairs and look forward to speaking with you personally."
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
videoUrl="/assets/videos/web/hero-messe.mp4"
|
||||
linkText="View dates"
|
||||
linkHref="#dates"
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-etib-ratelimit,etib-forward,etib-compress}"
|
||||
|
||||
# Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-e-tib.com}`) || Host(`staging.${TRAEFIK_HOST:-e-tib.com}`) || Host(`testing.${TRAEFIK_HOST:-e-tib.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|assets|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-e-tib.com}`) || Host(`staging.${TRAEFIK_HOST:-e-tib.com}`) || Host(`testing.${TRAEFIK_HOST:-e-tib.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
||||
@@ -58,9 +58,10 @@ services:
|
||||
|
||||
etib-gatekeeper:
|
||||
profiles: [ "gatekeeper" ]
|
||||
image: registry.infra.mintel.me/mintel/gatekeeper:testing
|
||||
image: registry.infra.mintel.me/mintel/gatekeeper:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
infra:
|
||||
aliases:
|
||||
- ${PROJECT_NAME:-klz}-gatekeeper
|
||||
@@ -68,6 +69,8 @@ services:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
PORT: 3000
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
NODE_ENV: production
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.${PROJECT_NAME:-klz}-gatekeeper-svc.loadbalancer.server.port=3000"
|
||||
|
||||
@@ -32,10 +32,7 @@ async function run() {
|
||||
for (const p of pages) {
|
||||
console.log(`📄 Checking ${p}...`);
|
||||
const res = await page.goto(`${targetUrl}${p}`, { waitUntil: 'domcontentloaded' });
|
||||
const status = res?.status();
|
||||
if (status !== 200 && status !== 304 && status !== 307 && status !== 308) {
|
||||
throw new Error(`Page ${p} returned ${status}`);
|
||||
}
|
||||
if (res?.status() !== 200) throw new Error(`Page ${p} returned ${res?.status()}`);
|
||||
|
||||
// Check for broken images (using in-page fetch to preserve session and avoid heavy navigation)
|
||||
const images = await page.$$eval('img', (imgs) => imgs.map((img) => img.src).filter(Boolean));
|
||||
|
||||
Reference in New Issue
Block a user