Compare commits

..

23 Commits

Author SHA1 Message Date
0191fdb770 fix: resolve reveal flickering, adjust grid header UI, update Guben image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:49:59 +02:00
afaa5e038c fix: resolve TS dynamic collision and remove anomalous map entries
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:35:14 +02:00
e0d6ea0d6c perf: implement KLZ blueprint optimizations (LCP, Payload, Caching)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 20:32:08 +02:00
34839ad5a5 fix: update team titles and location names
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 20:23:15 +02:00
a8d6a45aad chore: bump version to trigger SSL retry
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 19:17:04 +02:00
6de8ad8516 fix(content): correct drilling distance for Jevenstedt reference
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 17:06:20 +02:00
023fef5343 chore: trigger pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m19s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 10:24:35 +02:00
7181dafca2 fix(ui): enforce scroll lock on documentElement when flyout is open
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 20m1s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 09:32:05 +02:00
8b6d2da821 feat(header): add mail and map action buttons for mobile
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-20 09:31:48 +02:00
17736860d6 feat(header): replace mobile language switcher with phone button
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:18:59 +02:00
d67acdbcc1 fix(referenzen): link slider to overview anchors and add section ids
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 00:18:12 +02:00
05b517731b feat(ui): refine mobile bottom nav, add drag-to-close flyout, custom icons
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:09:11 +02:00
67696b6afa chore: release v2.2.13-rc.58
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:46:49 +02:00
24472990e5 2.2.13-rc.47 2026-06-19 23:46:36 +02:00
53a885acdb fix(ui): resolve unclosed Link tag in ReferencesSlider and MobileBottomNav type errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-19 23:42:35 +02:00
9afff09ed7 feat(ui): add buttery smooth scroll reveal animations and disable annotator
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m16s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 23:29:51 +02:00
ce5acc5558 refactor(footer): merge navigation and about us links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:26:54 +02:00
5027bea666 fix(ui): resolve annotator overlap, slider images, subcompany rounding and copy updates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 23:25:29 +02:00
fcfb567991 fix: remove pb-[90px] from main container which caused white gap above footer on mobile
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:57:52 +02:00
efb9bdbb3f feat: modernize mobile bottom nav with 2030 design and fix z-index
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:39:13 +02:00
11e7937e79 fix: remaining mobile paddings for CTA boxes
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:36:30 +02:00
41158697fd fix: mobile layout and visual polishing based on review
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:34:53 +02:00
d00a4e29ff Merge branch 'feature/annotator-integration' into main
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:03:13 +02:00
37 changed files with 409 additions and 374 deletions

View File

@@ -229,7 +229,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
{/* Support Section */}
<Container>
<div className="mt-24 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>

View File

@@ -150,7 +150,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
{/* Contact Info */}
<div className="lg:col-span-5 space-y-6 md:space-y-12">
<div className="lg:col-span-5 order-2 lg:order-1 space-y-6 md:space-y-12">
<div className="animate-fade-in">
<Heading level={3} subtitle={t('info.subtitle')} className="mb-6 md:mb-8">
{t('info.howToReachUs')}
@@ -249,7 +249,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
</div>
{/* Contact Form */}
<div className="lg:col-span-7">
<div className="lg:col-span-7 order-1 lg:order-2">
<Suspense
fallback={
<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>

View File

@@ -186,8 +186,6 @@ export default async function Layout(props: {
data-scroll-behavior="smooth"
>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="preconnect" href="https://img.infra.mintel.me" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
@@ -206,7 +204,7 @@ export default async function Layout(props: {
<main
id="main-content"
className="flex-grow animate-fade-in overflow-visible pb-[90px] md:pb-0"
className="flex-grow animate-fade-in overflow-visible"
tabIndex={-1}
>
{children}
@@ -215,7 +213,8 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{process.env.TARGET !== 'production' && <AnnotatorClientWrapper />}
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider>
</NextIntlClientProvider>

View File

@@ -6,21 +6,22 @@ import { MDXRemote } from 'next-mdx-remote/rsc';
export const dynamic = 'force-dynamic';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
import { HeroSection } from '@/components/blocks/HeroSection';
import nextDynamic from 'next/dynamic';
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
const HeroSection = nextDynamic(() => import('@/components/blocks/HeroSection').then(mod => mod.HeroSection), { ssr: true });
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading';
import { AnimatedCounter } from '@/components/ui';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact';
const AnimatedCounter = nextDynamic(() => import('@/components/ui').then(mod => mod.AnimatedCounter));
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
const ScaleOfImpact = nextDynamic(() => import('@/components/blocks/ScaleOfImpact').then(mod => mod.ScaleOfImpact));
import { getAllReferences } from '@/lib/references';
const mdxComponents = (references: any[]) => ({

View File

@@ -3,19 +3,41 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { getAllReferences } from '@/lib/references';
import TrackedLink from '@/components/analytics/TrackedLink';
import { MapPin, Calendar, Briefcase } from 'lucide-react';
import { MapPin, Calendar, Briefcase, Zap, Activity, ShieldCheck, Wrench, CheckCircle2 } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { SITE_URL } from '@/lib/schema';
import Image from 'next/image';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { HeroSection } from '@/components/blocks/HeroSection';
import { defaultLocations, minorLocations } from '@/lib/map-data';
import { MDXRemote } from 'next-mdx-remote/rsc';
const CustomLi = ({ children, ...props }: any) => {
let text = '';
if (typeof children === 'string') text = children;
else if (Array.isArray(children)) text = children.map((c: any) => typeof c === 'string' ? c : '').join(' ');
else if (children?.props?.children) text = children.props.children;
const lowerText = String(text).toLowerCase();
let Icon = CheckCircle2;
if (lowerText.includes('kabel')) Icon = Zap;
else if (lowerText.includes('bohrung')) Icon = Activity;
else if (lowerText.includes('rohr')) Icon = ShieldCheck;
else if (lowerText.includes('montage')) Icon = Wrench;
else if (lowerText.includes('vlf') || lowerText.includes('otdr')) Icon = Zap;
return (
<li className="flex items-start gap-3 p-3 bg-neutral-50/50 border border-neutral-100 rounded-xl" {...props}>
<Icon className="w-4 h-4 text-primary shrink-0 mt-[2px]" />
<span className="text-sm font-medium text-neutral-700 leading-tight">{children}</span>
</li>
);
};
const mdxComponents = {
ul: (props: any) => <ul className="list-disc pl-5 mb-6 text-neutral-600 text-sm space-y-2" {...props} />,
li: (props: any) => <li className="pl-1" {...props} />,
ul: (props: any) => <ul className="grid grid-cols-1 gap-2 mb-6" {...props} />,
li: CustomLi,
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
};
@@ -80,7 +102,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
});
return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
<div className="flex flex-col min-h-screen bg-neutral-50 pb-8 md:pb-24">
{/* Map Section */}
<InteractiveGermanyMap
isHero={true}
@@ -100,7 +122,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{references.map((ref) => (
<div
key={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block"
id={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block scroll-mt-32"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
@@ -165,7 +188,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="mt-8 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>

View File

@@ -158,7 +158,7 @@ export default async function StandortDetail(props: { params: Promise<{ locale:
{/* Support Section */}
<Container>
<div className="mt-12 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>

View File

@@ -114,7 +114,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import React from 'react';
import { cn } from '@/components/ui';
interface RevealProps {
@@ -10,40 +10,12 @@ interface RevealProps {
delay?: number;
}
export default function Reveal({ children, className, threshold = 0.1, delay = 0 }: RevealProps) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(entry.target);
}
},
{ threshold }
);
const currentRef = ref.current;
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [threshold]);
export default function Reveal({ children, className }: RevealProps) {
// Reveal animation disabled due to flickering issues on mobile/hydration
return (
<div
ref={ref}
className={cn('reveal-on-scroll', isVisible && 'is-visible', className)}
style={{ transitionDelay: `${delay}ms` }}
>
<div className={cn(className)}>
{children}
</div>
);
}

View File

@@ -239,30 +239,31 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
return (
<div className="annotator-ui-ignore">
{/* 1. Global Toolbar */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] w-max max-w-[95vw]">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-1 sm:gap-2 overflow-x-auto hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<div className="fixed bottom-36 md:bottom-24 right-4 z-[10000] flex flex-col items-end gap-2">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-1.5 rounded-2xl shadow-2xl flex flex-col items-center gap-1">
<button
onClick={() => setIsActive(!isActive)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-medium",
"p-2.5 rounded-xl transition-all",
isActive
? "bg-blue-500 text-white shadow-lg shadow-blue-500/20"
: "text-white/70 hover:text-white hover:bg-white/10"
)}
title={isActive ? "Modus beenden" : "Korrekturen erfassen"}
>
{isActive ? <X size={18} /> : <MessageSquare size={18} />}
{isActive ? "Modus beenden" : "Korrekturen erfassen"}
{isActive ? <X size={20} /> : <MessageSquare size={20} />}
</button>
<div className="w-px h-6 bg-white/10 mx-1" />
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={() => setShowList(!showList)}
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
className="p-2.5 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
title="Liste anzeigen"
>
<List size={20} />
{annotations.length > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 text-[10px] flex items-center justify-center rounded-full text-white font-bold border-2 border-[#1a1a1a]">
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 text-[9px] flex items-center justify-center rounded-full text-white font-bold border border-[#1a1a1a]">
{annotations.length}
</span>
)}
@@ -270,13 +271,13 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
{annotations.length > 0 && (
<>
<div className="w-px h-6 bg-white/10 mx-1" />
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={exportJSON}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all font-medium"
className="p-2.5 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all"
title="JSON Export"
>
<Download size={18} />
JSON Export
<Download size={20} />
</button>
</>
)}

View File

@@ -30,8 +30,8 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } },
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {

View File

@@ -93,8 +93,8 @@ export function CertificatesBlock({ badge, title, description, certificates = de
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } },
hidden: { opacity: 0, y: 40 },
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
if (!isMounted) {
@@ -125,8 +125,8 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
>
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>

View File

@@ -118,8 +118,8 @@ export function CompanyTimeline({
key={i}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7, ease: "easeOut" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="relative flex flex-col md:flex-row items-center w-full group"
>
{/* Timeline Dot with Pulse */}

View File

@@ -86,10 +86,6 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
<Button href={ctaHref} variant="ghost" className="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
{ctaLabel}
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
@@ -109,10 +105,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
return (
<motion.div
key={idx}
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
@@ -127,8 +123,8 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<HoverShineOverlay shineColor="via-white/30" />
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
<h4 className="font-bold text-xl md:text-2xl relative z-10 leading-snug">{item.title}</h4>
{item.description && <p className="text-white/60 text-sm mt-3 relative z-10">{item.description}</p>}
<h4 className="font-heading font-extrabold text-2xl md:text-3xl relative z-10 leading-snug">{item.title}</h4>
{item.description && <p className="text-white/80 text-base md:text-lg mt-3 relative z-10 font-medium">{item.description}</p>}
</motion.div>
);
}
@@ -136,10 +132,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
return (
<motion.div
key={idx}
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
initial={{ opacity: 0, y: 40, scale: 0.98 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
@@ -159,7 +155,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
sizes={isLarge ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 100vw, 25vw"}
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90 transition-opacity duration-500 z-0" />
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/60 to-black/30 opacity-100 transition-opacity duration-500 z-0 group-hover:from-black/80 group-hover:via-black/50 group-hover:to-black/20" />
</>
)}
@@ -177,11 +173,11 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
{item.tag}
</span>
)}
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-2xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
<h4 className="font-heading font-extrabold mb-2 text-2xl md:text-3xl">
{item.title}
</h4>
{item.description && (
<p className={`text-white/80 ${isLarge ? 'text-lg md:text-xl max-w-sm' : 'text-sm'}`}>
<p className="text-white/90 text-sm md:text-base max-w-md">
{item.description}
</p>
)}

View File

@@ -49,7 +49,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
>
<motion.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</motion.h2>
<motion.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
@@ -104,7 +104,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
variants={formVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
>
<Suspense
fallback={

View File

@@ -45,7 +45,7 @@ export function GrowthChart() {
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${kabelWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 1.5, delay: 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-primary rounded-full"
/>
@@ -61,7 +61,7 @@ export function GrowthChart() {
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${offenerWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 1.5, delay: 0.15 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-300 rounded-full"
/>
@@ -77,7 +77,7 @@ export function GrowthChart() {
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${hddWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 1.5, delay: 0.2 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-dark rounded-full"
/>

View File

@@ -92,7 +92,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div>
)}
<motion.div variants={itemVariants}>
<Heading level={1} variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</motion.div>
@@ -104,7 +104,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div>
)}
{ctaLabel && ctaHref && (
<motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex justify-center' : ''}`}>
<motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
<Button
href={ctaHref}
variant="accent"

View File

@@ -2,7 +2,6 @@
import { Button } from '@/components/ui/Button';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
@@ -81,11 +80,9 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
<div className="container relative z-[4] text-center px-4">
<motion.div
<div
key={`hero-content-${pathname}`}
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
>
<h1
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
@@ -96,7 +93,7 @@ export function HeroVideo(props: HeroVideoProps) {
{subtitle}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-4 md:gap-6">
<Button
href={ctaHref}
variant="primary"
@@ -114,7 +111,7 @@ export function HeroVideo(props: HeroVideoProps) {
{secondaryCtaLabel}
</Button>
</div>
</motion.div>
</div>
</div>
</div>
);

View File

@@ -37,11 +37,11 @@ const containerVariants = {
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
hidden: { opacity: 0, y: 40 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] as const },
},
};
@@ -75,7 +75,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
>
@@ -86,7 +86,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, amount: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{fairs.map((messe, idx) => {
@@ -180,7 +180,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.h2
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
>
@@ -192,7 +192,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, amount: 0.1 }}
className="grid gap-6"
>
{jobs.map((job: any) => (
@@ -221,7 +221,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, amount: 0.1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center"
>

View File

@@ -94,7 +94,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
return (
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
@@ -115,11 +115,11 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none 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'}`}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-6 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)
? ref.image.url
const imgSrc = ref.image
? (typeof ref.image === 'string' ? ref.image : ref.image.url)
: fallbacks[i % fallbacks.length];
return (
@@ -127,13 +127,19 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
key={ref.id}
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-start group pointer-events-auto"
>
<div
<Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
}
}}
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
>
<Image
draggable={false}
@@ -153,7 +159,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
</span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</div>
</Link>
</motion.div>
);
})}

View File

@@ -38,7 +38,7 @@ export function ScaleOfImpact() {
{/* Foreground Content */}
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
<h2 className="text-2xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight leading-tight">
{locale === 'en' ? (
<>An infrastructure <br className="md:hidden"/> that connects the country.</>
) : (

View File

@@ -39,7 +39,7 @@ const containerVariants: Variants = {
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] } },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {
@@ -137,7 +137,7 @@ export function ServiceDetailGrid({
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
>
{problemStatement && (

View File

@@ -68,7 +68,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})) || defaultCompanies;
return (
<section id="unternehmen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<section id="unternehmen" className="pt-12 pb-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4">
{(badge || title) && (
@@ -228,7 +228,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
</>
);
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full rounded-2xl select-none ${
isCurrent
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'

View File

@@ -42,10 +42,10 @@ export function TeamGrid({ members }: TeamGridProps) {
{members.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
{/* Card Banner */}

View File

@@ -108,8 +108,13 @@ export function Footer({ companyInfo }: FooterProps) {
{locale === 'de' ? 'Navigation' : 'Navigation'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 mb-8">
<ul className="grid grid-cols-2 gap-x-8 gap-y-3 mb-8">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -123,28 +128,6 @@ export function Footer({ companyInfo }: FooterProps) {
</li>
))}
</ul>
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-4 text-white">
<TransitionLink href={`/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}`} className="hover:text-primary transition-colors cursor-pointer inline-block">
{locale === 'de' ? 'Über uns' : 'About us'}
</TransitionLink>
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 border-l border-white/10 pl-4 ml-1">
{[
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
{/* Mobile View (Accordions) */}
@@ -157,6 +140,11 @@ export function Footer({ companyInfo }: FooterProps) {
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -171,32 +159,6 @@ export function Footer({ companyInfo }: FooterProps) {
</ul>
</div>
</details>
<div className="h-px bg-white/10 w-full"></div>
<details className="group/acc">
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
{locale === 'de' ? 'Über uns' : 'About us'}
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
</summary>
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
</details>
</div>
</div>

View File

@@ -138,8 +138,54 @@ export function Header({ navLinks }: HeaderProps) {
<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">
{/* 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>
@@ -157,81 +203,10 @@ export function Header({ navLinks }: HeaderProps) {
<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>
{/* Hamburger Menu (Mobile Only) */}
<button
className={`md:hidden ml-2 p-2 transition-colors focus:outline-none ${isSolidMode && !isMobileMenuOpen ? 'text-text-primary hover:text-primary' : 'text-white hover:text-primary z-[60] relative'}`}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle Menu"
>
<div className="w-6 flex flex-col gap-[6px] relative items-center justify-center">
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? 'rotate-45 translate-y-[8px]' : ''}`} />
<span className={`w-full h-[2px] rounded-full bg-current transition-all duration-300 ease-in-out ${isMobileMenuOpen ? 'opacity-0 translate-x-3' : 'opacity-100'}`} />
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? '-rotate-45 -translate-y-[8px]' : ''}`} />
</div>
</button>
</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.3 }}
className="fixed inset-0 z-[40] bg-[#050B14]/95 backdrop-blur-xl md:hidden pt-24 pb-24 px-6 overflow-y-auto"
>
<nav className="flex flex-col gap-6 mt-8">
{navLinks && navLinks.length > 0 && navLinks.map((link) => {
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 key={link.url} className="flex flex-col">
<TransitionLink
href={mappedUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-2xl font-bold uppercase tracking-widest transition-colors ${isActive ? 'text-primary' : 'text-white'}`}
>
{link.label}
</TransitionLink>
{link.children && (
<div className="flex flex-col gap-3 mt-3 pl-4 border-l-2 border-white/10">
{link.children.map((child, idx) => {
if (child.isGroupLabel) {
return <div key={idx} className="text-sm font-bold text-neutral-500 uppercase tracking-widest mt-2">{child.label}</div>;
}
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
? `/${currentLocale}${child.url}`
: child.url;
const isChildActive = pathname === childUrl;
return (
<TransitionLink
key={child.url}
href={childUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-lg font-medium transition-colors ${isChildActive ? 'text-primary' : 'text-white/70'}`}
>
{child.label}
</TransitionLink>
);
})}
</div>
)}
</div>
);
})}
</nav>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@@ -3,12 +3,13 @@
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';
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
export interface NavLink {
label: string;
url: string;
children?: NavLink[];
}
interface MobileBottomNavProps {
@@ -18,84 +19,213 @@ interface MobileBottomNavProps {
export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProps) {
const pathname = usePathname() || '/';
const [isFlyoutOpen, setIsFlyoutOpen] = React.useState(false);
// Scroll lock when flyout is open
React.useEffect(() => {
if (isFlyoutOpen) {
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
};
}, [isFlyoutOpen]);
// 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 referencesUrl = navLinks[2]?.url || `/${currentLocale}/referenzen`;
const contactUrl = `/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`;
return [
{
id: 'home',
label: currentLocale === 'de' ? 'Start' : 'Home',
url: homeUrl,
icon: <Home className="w-6 h-6 mb-1" strokeWidth={2} />,
icon: <Home className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />,
exactMatch: true
},
{
id: 'kompetenzen',
label: navLinks[0]?.label || (currentLocale === 'de' ? 'Kompetenzen' : 'Competencies'),
url: competenciesUrl,
icon: <Briefcase className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Layers className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
label: navLinks[1]?.label || (currentLocale === 'de' ? 'Über uns' : 'About Us'),
url: aboutUrl,
icon: <Users className="w-6 h-6 mb-1" strokeWidth={2} />
id: 'referenzen',
label: navLinks[2]?.label || (currentLocale === 'de' ? 'Referenzen' : 'References'),
url: referencesUrl,
icon: <Star className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'kontakt',
label: currentLocale === 'de' ? 'Kontakt' : 'Contact',
url: contactUrl,
icon: <Mail className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Mail className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'menu',
label: currentLocale === 'de' ? 'Mehr' : 'Menu',
url: '#',
icon: <Menu className="w-[24px] h-[24px] md:w-6 md:h-6" strokeWidth={2.5} />,
isFlyoutTrigger: true
}
];
}, [currentLocale, navLinks]);
return (
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-2 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}/`)));
const flyoutLinks = React.useMemo(() => {
const aboutNode = navLinks[1];
const karriereNode = navLinks[3];
const messenNode = navLinks[4];
let links: {label: string, url: string}[] = [];
if (aboutNode?.children) {
links.push(...aboutNode.children.map(c => ({ label: c.label, url: c.url })));
}
if (karriereNode) links.push({ label: karriereNode.label, url: karriereNode.url });
if (messenNode) links.push({ label: messenNode.label, url: messenNode.url });
return links;
}, [navLinks]);
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 select-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
}`}
>
<div className="relative flex flex-col items-center justify-center">
{/* Active Icon Background */}
return (
<>
<div className="md:hidden fixed bottom-0 left-0 right-0 z-[9999] px-2 pb-[calc(env(safe-area-inset-bottom)+12px)] pt-4 pointer-events-none flex justify-center">
<nav className="flex justify-between items-center bg-white/75 backdrop-blur-2xl border border-white/60 shadow-[0_8px_32px_rgba(0,0,0,0.12)] rounded-3xl p-1.5 pointer-events-auto w-full max-w-[480px]">
{items.map((item) => {
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
: item.url;
let isActive = false;
if (item.isFlyoutTrigger) {
isActive = isFlyoutOpen;
} else {
isActive = item.exactMatch
? pathname === mappedUrl
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
}
const innerContent = (
<>
{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 }}
className="absolute inset-0 bg-primary/10 rounded-2xl shadow-sm border border-primary/10"
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
/>
)}
{/* Icon wrapper to ensure it sits above the background */}
<div className="relative z-10 transition-transform duration-300 active:scale-90">
{item.icon}
<div className="relative flex flex-col items-center justify-center z-10 w-full h-full">
<motion.div
whileTap={{ scale: 0.8 }}
animate={{ y: isActive ? -2 : 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{item.icon}
</motion.div>
<motion.span
animate={{ opacity: isActive ? 1 : 0.7, y: isActive ? 0 : 2 }}
className="text-[9px] font-bold mt-1 tracking-wide"
>
{item.label}
</motion.span>
</div>
</>
);
if (item.isFlyoutTrigger) {
return (
<button
key={item.label}
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none w-full focus:outline-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
{innerContent}
</button>
);
}
return (
<TransitionLink
key={item.label}
href={mappedUrl}
onClick={() => setIsFlyoutOpen(false)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
{innerContent}
</TransitionLink>
);
})}
</nav>
</div>
<AnimatePresence>
{isFlyoutOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[9997] md:hidden"
onClick={() => setIsFlyoutOpen(false)}
/>
<motion.div
initial={{ y: '100%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '100%', opacity: 0 }}
transition={{ type: 'spring', stiffness: 350, damping: 30 }}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.2}
onDragEnd={(e, info) => {
if (info.offset.y > 50 || info.velocity.y > 100) {
setIsFlyoutOpen(false);
}
}}
className="fixed bottom-0 left-0 right-0 z-[9998] bg-white rounded-t-[32px] pt-4 pb-[calc(env(safe-area-inset-bottom)+7rem)] px-6 md:hidden shadow-[0_-20px_40px_rgba(0,0,0,0.1)] border-t border-neutral-100"
>
<div className="w-16 h-1.5 bg-neutral-200 rounded-full mx-auto mb-6 cursor-grab active:cursor-grabbing" />
<h3 className="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-4 px-2">{currentLocale === 'de' ? 'Über uns & Mehr' : 'About us & More'}</h3>
<div className="flex flex-col gap-1">
{flyoutLinks.map((link, i) => {
const childUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
? `/${currentLocale}${link.url}`
: link.url;
const isChildActive = pathname === childUrl;
return (
<TransitionLink
key={i}
href={childUrl}
onClick={() => setIsFlyoutOpen(false)}
className={`px-4 py-4 rounded-2xl text-[15px] font-semibold transition-all flex items-center justify-between ${
isChildActive ? 'bg-primary/10 text-primary' : 'text-neutral-700 hover:bg-neutral-50 active:bg-neutral-100'
}`}
>
{link.label}
<svg className={`w-4 h-4 ${isChildActive ? 'text-primary' : 'text-neutral-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</TransitionLink>
);
})}
</div>
<span className="text-[10px] font-semibold mt-0.5 tracking-wide z-10">{item.label}</span>
</TransitionLink>
);
})}
</nav>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}

View File

@@ -1,4 +1,6 @@
import React, { ReactNode } from 'react';
'use client';
import React, { ReactNode, useState, useEffect, useRef } from 'react';
interface TooltipProps {
children: ReactNode;
@@ -6,10 +8,33 @@ interface TooltipProps {
}
export function Tooltip({ children, content }: TooltipProps) {
const [isActive, setIsActive] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsActive(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, []);
return (
<div className="group relative flex items-center justify-center">
<div
ref={containerRef}
className="group relative flex items-center justify-center cursor-help"
onClick={() => setIsActive(!isActive)}
onMouseEnter={() => setIsActive(true)}
onMouseLeave={() => setIsActive(false)}
>
{children}
<div className="absolute bottom-full mb-3 invisible opacity-0 translate-y-2 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 w-max max-w-[250px] px-4 py-2.5 text-[11px] md:text-xs text-white bg-neutral-900/95 backdrop-blur-md rounded-xl shadow-2xl border border-white/10 transition-all duration-300 pointer-events-none z-50 text-center leading-relaxed font-normal normal-case tracking-normal">
<div className={`absolute bottom-full mb-3 w-max max-w-[250px] px-4 py-2.5 text-[11px] md:text-xs text-white bg-neutral-900/95 backdrop-blur-md rounded-xl shadow-2xl border border-white/10 transition-all duration-300 z-50 text-center leading-relaxed font-normal normal-case tracking-normal ${isActive ? 'visible opacity-100 translate-y-0 pointer-events-auto' : 'invisible opacity-0 translate-y-2 pointer-events-none'}`}>
{content}
{/* Triangle Arrow */}
<div className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-neutral-900/95" />

View File

@@ -79,8 +79,8 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeitende</div>
</div>
<div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Kunden</div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">200+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Projekte</div>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
- 500 m Kabelgraben ausheben und verfüllen
- 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160
- 3.200 m Horizontalspülbohrung d 160
- 12.000 m Horizontalspülbohrung d 160
- 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV
- 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV
- 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV

View File

@@ -42,7 +42,7 @@ layout: "fullBleed"
{
id: "martin-polan",
name: "Martin Polan",
position: "Bauleiter",
position: "Bauleitung",
email: "m.polan@e-tib.com",
phone: "+49 171 3868167",
image: "/assets/photos/team/martin.jpg"

View File

@@ -79,8 +79,8 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Employees</div>
</div>
<div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Clients</div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">200+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Projects</div>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
- Excavation and backfilling of 500 m cable trench
- 4,500 m plowing of 3 x PE protective pipe d 160
- 3,200 m HDD directional drilling d 160
- 12,000 m HDD directional drilling d 160
- 8,500 m laying of HV cable 1,000 mm² 64/110 kV
- 11 sets of connection joints 1,000 mm² 64/110 kV
- 2 sets of outdoor terminations 1,000 mm² 64/110 kV

View File

@@ -42,7 +42,7 @@ layout: "fullBleed"
{
id: "martin-polan",
name: "Martin Polan",
position: "Site Manager",
position: "Site Management",
email: "m.polan@e-tib.com",
phone: "+49 171 3868167",
image: "/assets/photos/team/martin.jpg"

View File

@@ -189,62 +189,6 @@ export const minorLocations: Location[] = [
y: 83.33,
description: 'fiber',
},
{
id: 'hdd-29436-meter-29436',
name: 'HDD 29436 Meter',
type: 'minor_node',
x: 46.89,
y: 41.34,
description: 'power',
},
{
id: 'hdd-28680-meter-28680',
name: 'HDD 28680 Meter',
type: 'minor_node',
x: 44.84,
y: 43.61,
description: 'power',
},
{
id: 'hdd-44655-meter-44655',
name: 'HDD 44655 Meter',
type: 'minor_node',
x: 49.36,
y: 48.57,
description: 'power',
},
{
id: 'kabelverlegung-76727-meter-76727',
name: 'Kabelverlegung 76727 Meter',
type: 'minor_node',
x: 45.57,
y: 49.95,
description: 'power',
},
{
id: 'offener-tiefbau-21288-meter-21288',
name: 'Offener Tiefbau 21288 Meter',
type: 'minor_node',
x: 40.15,
y: 49.41,
description: 'power',
},
{
id: 'offener-tiefbau-30773-meter-30773',
name: 'Offener Tiefbau 30773 Meter',
type: 'minor_node',
x: 41.74,
y: 43.78,
description: 'power',
},
{
id: 'offener-tiefbau-34815-meter-34815',
name: 'Offener Tiefbau 34815 Meter',
type: 'minor_node',
x: 43.91,
y: 42.14,
description: 'power',
},
{
id: 'pv-anlage-01909-grosshartau-01909',
name: 'PV-Anlage, 01909 Großhartau',

View File

@@ -35,7 +35,7 @@ export const standorteData: StandortData[] = [
{
id: 'guben',
name: 'E-TIB GmbH',
shortName: 'Guben (Hauptsitz)',
shortName: 'Standort Brandenburg (Hauptsitz)',
type: 'hq',
description: {
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
@@ -67,7 +67,7 @@ export const standorteData: StandortData[] = [
'Central project management'
]
},
image: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg',
image: '/assets/photos/DSC02114.jpg',
gallery: [
'/assets/photos/DSC02114.jpg',
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-45.jpg'
@@ -117,7 +117,7 @@ export const standorteData: StandortData[] = [
{
id: 'buelstedt',
name: 'E-TIB Ingenieurgesellschaft mbH',
shortName: 'Bülstedt (Ingenieurgesellschaft)',
shortName: 'Standort Niedersachsen',
type: 'branch',
description: {
de: 'Unsere Ingenieurgesellschaft in Bülstedt ist spezialisiert auf die hochpräzise Planung, Vermessung und Projektierung von Energie- und Telekommunikationsnetzen. Hier verschmelzen technische Expertise und modernes Datenmanagement.',

View File

@@ -15,6 +15,10 @@ const nextConfig = {
pagesBufferLength: 2,
},
experimental: {
staleTimes: {
dynamic: 0,
static: 30,
},
optimizePackageImports: [
'lucide-react',
'framer-motion',

View File

@@ -138,7 +138,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.13-rc.46",
"version": "2.2.14",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",