Compare commits

...

15 Commits

Author SHA1 Message Date
4df6e2601a chore(docs): update danny mail draft
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m8s
Build & Deploy / 🧪 QA (push) Successful in 2m54s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 5m27s
Build & Deploy / 🏗️ Build (push) Successful in 4m29s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-13 18:33:02 +02:00
569b876944 feat(seo): implement AI SEO and structured data schema
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 49s
Build & Deploy / 🧪 QA (push) Successful in 2m27s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m28s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
- Add robust JSON-LD schema generators (LocalBusiness, FAQ, Service, Event)
- Refactor JsonLd component to use @graph pattern for merging global/local schemas
- Create FaqBlock component with automatic FAQPage schema injection
- Inject specific JSON-LD schemas into mdx pages (home, kompetenzen, messen)
- Add structured AI SEO content (hard facts, stats, clear definitions)
2026-05-13 14:23:24 +02:00
1f905bc86d fix: insert real legacy projects into map
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 54s
Build & Deploy / 🧪 QA (push) Successful in 2m32s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m48s
Build & Deploy / 🏗️ Build (push) Successful in 3m17s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-13 13:29:49 +02:00
8fcd68cf19 fix: english translation parity, remove hallucinated map projects, fix next.config syntax
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 1m10s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 1m11s
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 32s
2026-05-13 13:25:46 +02:00
66f28d8af2 chore(observability): port Sentry and memory optimizations from klz-2026
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 56s
Build & Deploy / 🧪 QA (push) Successful in 2m9s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 3m30s
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-05-13 13:21:07 +02:00
351c27f7d4 fix(ui): adjust grid breakpoints on map and pointer events on footer
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m6s
Build & Deploy / 🧪 QA (push) Successful in 2m31s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 6m16s
Build & Deploy / 🏗️ Build (push) Successful in 3m47s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-13 11:48:34 +02:00
56012fd0d6 feat(ui): modernize company timeline with icons and giant typography
Some checks failed
Build & Deploy / 🔍 Prepare (push) Has been cancelled
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
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 6m6s
2026-05-13 11:46:40 +02:00
220e03bed4 feat(ui): redesign germany map to modern dark split layout 2026-05-13 11:44:53 +02:00
1c317db747 fix(ui): add explicit cursor-pointer to core interactive elements to bypass tailwind v4 layer conflicts 2026-05-13 11:23:11 +02:00
33f64272d7 fix: restore hover states and cursor-pointer for all tiles
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
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
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 4m25s
2026-05-13 11:13:34 +02:00
0b837e3376 fix: subcompany tiles layout and active states for 4 items
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 48s
Build & Deploy / 🧪 QA (push) Successful in 2m23s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m41s
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-05-13 11:08:31 +02:00
c4cc98a392 fix: align content with briefing and remove legacy data
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 2m11s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 4m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-13 10:49:48 +02:00
2cb0b50193 feat(ui): add AnimatedGlossyBorder and refine industrial interactions
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m2s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-13 10:23:47 +02:00
7d6a52e1c6 feat: unified container widths and added magnetic mouse pull to corporate background arcs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 43s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-12 19:08:23 +02:00
55d633a33c feat(mobile): optimize mobile layout, navigation, and spacing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 2m49s
Build & Deploy / 🏗️ Build (push) Successful in 4m54s
Build & Deploy / 🚀 Deploy (push) Successful in 1m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Implement MobileBottomNav for native thumb-optimized usage
- Simplify Header overlay on mobile
- Add dynamic max() padding to ReferencesSlider for mobile view
- Condense vertical spacing in Footer for mobile devices
- Update layout.tsx to pad main content for bottom nav
2026-05-12 17:02:58 +02:00
44 changed files with 1731 additions and 296 deletions

10
.npmrc
View File

@@ -1,7 +1,7 @@
@mintel:registry=https://npm.infra.mintel.me
//npm.infra.mintel.me/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
//npm.infra.mintel.me/:always-auth=true
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
//git.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
//registry.infra.mintel.me/api/packages/mmintel/npm/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
//registry.infra.mintel.me/api/packages/mmintel/npm/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
//registry.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
//npm.infra.mintel.me/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
//npm.infra.mintel.me/:always-auth=true

View File

@@ -19,6 +19,9 @@ import { JobListingBlock } from '@/components/blocks/JobListingBlock';
import { CallToAction } from '@/components/blocks/CallToAction';
import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
@@ -32,6 +35,9 @@ const mdxComponents = {
CallToAction,
ServiceDetailGrid,
BenefitGrid,
InteractiveGermanyMap,
FaqBlock,
JsonLd,
Button,
Heading,
};
@@ -131,7 +137,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
<Badge variant="accent" className="mb-4 md:mb-6">
{t('badge')}
</Badge>
<Heading level={1} className="text-white mb-0">
<Heading level={1} variant="white" className="mb-0">
{pageData.frontmatter.title}
</Heading>
</div>
@@ -139,7 +145,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
</section>
{/* Main Content Area */}
<div className="container mx-auto px-4 py-16 md:py-24">
<Container className="py-16 md:py-24">
<div className="max-w-4xl mx-auto">
{/* Excerpt/Lead paragraph if available */}
{pageData.frontmatter.excerpt && (
@@ -180,7 +186,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
</div>
</div>
</div>
</div>
</Container>
</div>
);
}

View File

@@ -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';
@@ -17,6 +18,7 @@ import { cookies } from 'next/headers';
import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
import { InitialLoader } from '@/components/providers/InitialLoader';
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
const inter = Inter({
subsets: ['latin'],
@@ -174,17 +176,19 @@ export default async function Layout(props: {
{/* Preload critical hero video */}
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
</head>
<body className="flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
<TransitionProvider>
<CorporateBackground />
<InitialLoader shouldShowLoader={!hasSeenLoader} />
<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}

View File

@@ -11,6 +11,8 @@ 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 JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading';
@@ -20,6 +22,8 @@ const mdxComponents = {
HomeSubCompanyTiles,
HomeCompetenceBentoGrid,
HomeReferencesSlider,
FaqBlock,
JsonLd,
Button,
Heading,
};

19
app/[locale]/template.tsx Normal file
View File

@@ -0,0 +1,19 @@
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
transition={{
duration: 0.7,
ease: [0.16, 1, 0.3, 1],
}}
className="w-full h-full"
>
{children}
</motion.div>
);
}

View File

@@ -15,6 +15,17 @@
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
@keyframes bg-pulse-15 {
0%, 90%, 100% { opacity: 0.04; }
95% { opacity: 0.10; filter: brightness(1.2); }
}
@keyframes bg-pulse-20 {
0%, 90%, 100% { opacity: 0.07; }
95% { opacity: 0.14; filter: brightness(1.2); }
}
.animate-bg-pulse-15 { animation: bg-pulse-15 12s infinite ease-in-out; }
.animate-bg-pulse-20 { animation: bg-pulse-20 12s infinite ease-in-out; }
}
/* Base Styles */

View File

@@ -1,53 +1,67 @@
import { Thing, WithContext } from 'schema-dts';
import { Thing, WithContext, Graph } from 'schema-dts';
import { getOrganizationSchema, getLocalBusinessSchema, SITE_URL } from '@/lib/schema';
interface JsonLdProps {
id?: string;
data?: WithContext<Thing> | WithContext<Thing>[];
disableGlobal?: boolean;
}
export default function JsonLd({ id, data }: JsonLdProps) {
// If data is provided, use it. Otherwise, use the default Organization + WebSite schema.
const schemaData = data || [
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'E-TIB GmbH',
url: 'https://e-tib.com',
logo: 'https://e-tib.com/assets/logo.png',
sameAs: [
'https://www.instagram.com/me.and.eloise/',
],
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung.',
address: {
'@type': 'PostalAddress',
streetAddress: 'Gewerbestraße 22',
addressLocality: 'Guben',
postalCode: '03172',
addressCountry: 'DE',
},
},
{
export default function JsonLd({ id, data, disableGlobal = false }: JsonLdProps) {
let finalSchema: any;
if (disableGlobal && data) {
// Escape hatch for overriding the global graph
finalSchema = data;
} else {
// Construct the @graph array
const graphItems: any[] = [];
// Always include Organization, WebSite, and LocalBusiness for strong AI SEO presence
graphItems.push(getOrganizationSchema());
graphItems.push({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'E-TIB',
url: 'https://e-tib.com',
url: SITE_URL,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://e-tib.com/search?q={search_term_string}',
urlTemplate: `${SITE_URL}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
});
graphItems.push(getLocalBusinessSchema());
// Append dynamic data from props
if (data) {
if (Array.isArray(data)) {
// Strip out their individual @context if we are pushing to a graph
const cleanedData = data.map((d: any) => {
const { '@context': _, ...rest } = d;
return rest;
});
graphItems.push(...cleanedData);
} else {
const { '@context': _, ...rest } = data as any;
graphItems.push(rest);
}
}
];
finalSchema = {
'@context': 'https://schema.org',
'@graph': graphItems,
};
}
return (
<script
id={id}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(schemaData),
__html: JSON.stringify(finalSchema),
}}
/>
);

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { LogoArcs } from '@/components/ui/LogoArcs';
export interface BenefitItem {
id: string;
@@ -100,13 +101,32 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
{benefits.map((benefit) => (
<motion.div
key={benefit.id}
variants={itemVariants}
className="group relative overflow-hidden bg-neutral-50/50 border border-neutral-200/60 rounded-3xl p-8 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1"
>
<HoverShineOverlay shineColor="via-primary/5" />
{benefits.map((benefit, index) => {
const bgPositionClasses = [
"-bottom-[30%] -right-[30%]",
"-top-[30%] -left-[30%]",
"-bottom-[30%] -left-[30%]"
];
const animationClasses = [
"animate-[spin_60s_linear_infinite]",
"animate-[spin_80s_linear_infinite]",
"animate-[spin_100s_linear_infinite]"
];
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
const animationClass = animationClasses[index % animationClasses.length];
return (
<motion.div
key={benefit.id}
variants={itemVariants}
className="group relative overflow-hidden bg-neutral-50/50 border border-neutral-200/60 rounded-3xl p-8 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1"
>
<HoverShineOverlay shineColor="via-primary/5" />
{/* Subtle Background Logo Arcs */}
<div className={`absolute ${positionClass} w-[120%] h-[120%] z-0 opacity-0 group-hover:opacity-[0.03] transition-opacity duration-1000 pointer-events-none mix-blend-multiply`}>
<LogoArcs className={`w-full h-full text-primary ${animationClass}`} />
</div>
<div className="relative z-10 flex items-start gap-4">
<div className="relative shrink-0 w-12 h-12 flex items-center justify-center bg-primary/10 text-primary rounded-xl transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
@@ -125,7 +145,8 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
</div>
</div>
</motion.div>
))}
);
})}
</motion.div>
</div>
</section>

View File

@@ -2,34 +2,57 @@
import * as React from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
const iconMap: Record<string, React.ElementType> = {
Building2,
Compass,
Layers,
ArrowDownToLine,
Wrench,
Factory,
Zap,
MapPin,
CheckCircle2
};
const defaultMilestones = [
{
date: '2015',
title: 'Gründung der E-TIB GmbH',
desc: 'Gründung im brandenburgischen Guben mit Fokus auf Tiefbauarbeiten und Kabelmontagen.',
date: '16.12.2015',
year: '2015',
title: 'Gründung E-TIB GmbH',
desc: 'Ausführung elektrischer Infrastrukturprojekte, Kabelbau und Horizontalspülbohrungen.',
iconName: 'Building2',
},
{
date: 'Wachstum',
title: 'Erweiterung der Kompetenzen',
desc: 'Konsequenter Ausbau der Leistungen um Horizontalspülbohrungen und Kabelpflugarbeiten.',
date: '04.02.2019',
year: '2019',
title: 'Gründung E-TIB Ingenieurgesellschaft',
desc: 'Genehmigungs- und Ausführungsplanung, komplexe Querungen sowie Netzanschlussplanung.',
iconName: 'Compass',
},
{
date: 'Unternehmensgruppe',
title: 'Zusammenschluss',
desc: 'Bündelung der Fachbereiche mit der NEMO GmbH und der E-TIB Ingenieurgesellschaft mbH.',
date: '14.11.2019',
year: '2019',
title: 'Gründung E-TIB Verwaltung GmbH',
desc: 'Zentrale Dienste, Erwerb, Vermietung, Verpachtung und Verwaltung von Immobilien und Maschinen.',
iconName: 'Layers',
},
{
date: 'Heute',
title: 'Die E-TIB Gruppe',
desc: 'Ein starker Verbund mit über 50 Mitarbeitern für bundesweite Infrastrukturprojekte.',
date: '21.10.2025',
year: '2025',
title: 'Gründung E-TIB Bohrtechnik GmbH',
desc: 'Spezialisierung auf präzise Horizontalspülbohrungen in allen Bodenklassen.',
iconName: 'ArrowDownToLine',
},
];
interface Milestone {
date: string;
year: string;
title: string;
desc: string;
iconName: string;
}
interface CompanyTimelineProps {
@@ -52,48 +75,84 @@ export function CompanyTimeline({
const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<section className="py-24 bg-white relative">
<div className="container max-w-4xl">
<div className="text-center mb-16">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark">{title}</h3>
<section className="py-24 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
{/* Background Decor */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
<div className="container max-w-5xl relative z-10">
<div className="text-center mb-24">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-neutral-200 text-neutral-dark text-xs font-bold uppercase tracking-wider mb-6 shadow-sm">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
{badge}
</div>
<h3 className="font-heading text-4xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
{title}
</h3>
</div>
<div ref={containerRef} className="relative">
{/* Central Line */}
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-1 bg-neutral-200 -translate-x-1/2 rounded-full overflow-hidden">
{/* Central Line Background */}
<div className="absolute left-[38px] md:left-1/2 top-0 bottom-0 w-1.5 bg-neutral-200 -translate-x-1/2 rounded-full" />
{/* Animated Central Line */}
<div className="absolute left-[38px] md:left-1/2 top-0 bottom-0 w-1.5 -translate-x-1/2 rounded-full overflow-hidden">
<motion.div
className="w-full bg-primary"
className="w-full bg-primary drop-shadow-[0_0_8px_rgba(130,237,32,0.8)]"
style={{ height: lineHeight }}
/>
</div>
<div className="space-y-12 md:space-y-0">
<div className="space-y-16 md:space-y-24">
{milestones.map((milestone, i) => {
const isEven = i % 2 === 0;
// isEven (0, 2): Left side on desktop
// !isEven (1, 3): Right side on desktop
const Icon = iconMap[milestone.iconName] || Building2;
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
className="relative flex flex-col w-full"
transition={{ duration: 0.7, ease: "easeOut" }}
className="relative flex flex-col md:flex-row items-center w-full group"
>
{/* Timeline Dot */}
<div className="absolute left-4 md:left-1/2 top-8 md:top-1/2 w-6 h-6 rounded-full bg-white border-4 border-primary z-10 -translate-x-1/2 md:-translate-y-1/2 shadow-lg" />
{/* Timeline Dot with Pulse */}
<div className="absolute left-[38px] md:left-1/2 top-0 md:top-1/2 w-8 h-8 rounded-full bg-white border-[5px] border-neutral-200 group-hover:border-primary transition-colors duration-500 z-20 -translate-x-1/2 md:-translate-y-1/2 shadow-sm flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-neutral-300 group-hover:bg-primary transition-colors duration-500" />
</div>
{/* Content Box */}
<div className={`w-full md:w-[calc(50%-2.5rem)] pl-16 md:pl-0 ${isEven ? 'md:mr-auto md:text-right' : 'md:ml-auto md:text-left'}`}>
<div className="bg-neutral-50 p-6 md:p-8 rounded-2xl shadow-sm border border-neutral-100 hover:shadow-md transition-shadow relative">
<span className="inline-block py-1.5 px-4 rounded-full bg-primary/10 text-primary font-bold text-sm mb-4">
{milestone.date}
</span>
<h4 className="font-heading font-bold text-2xl md:text-3xl mb-3 text-neutral-dark leading-tight">{milestone.title}</h4>
<p className="text-text-secondary leading-relaxed">{milestone.desc}</p>
{/* Content Box Wrapper */}
<div className={`w-full md:w-1/2 pl-24 md:pl-0 ${isEven ? 'md:pr-16 md:text-right' : 'md:pl-16 md:ml-auto md:text-left'}`}>
{/* The Card */}
<div className="relative bg-white p-8 md:p-10 rounded-[2rem] shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-neutral-100 hover:shadow-[0_20px_40px_rgb(0,0,0,0.08)] hover:border-primary/20 transition-all duration-500 overflow-hidden transform hover:-translate-y-1">
{/* Giant Typography Background Year */}
<div className={`absolute select-none pointer-events-none text-[140px] leading-none font-black text-neutral-50 z-0 ${isEven ? '-left-8' : '-right-8'} -bottom-10 opacity-70 group-hover:scale-110 transition-transform duration-700`}>
{milestone.year}
</div>
<div className="relative z-10">
{/* Header Row */}
<div className={`flex items-center gap-4 mb-6 ${isEven ? 'md:flex-row-reverse' : ''}`}>
<div className="w-12 h-12 rounded-2xl bg-primary/10 text-primary flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-neutral-dark transition-colors duration-500">
<Icon className="w-6 h-6" />
</div>
<span className="inline-block py-1.5 px-4 rounded-full bg-neutral-100 text-neutral-600 font-bold text-sm tracking-wide">
{milestone.date}
</span>
</div>
<h4 className="font-heading font-extrabold text-2xl md:text-3xl mb-4 text-neutral-dark leading-tight group-hover:text-primary transition-colors duration-300">
{milestone.title}
</h4>
<p className="text-neutral-500 text-lg leading-relaxed">
{milestone.desc}
</p>
</div>
</div>
</div>
</motion.div>
);

View File

@@ -6,6 +6,9 @@ import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { Container } from '@/components/ui/Container';
import { LogoArcs } from '@/components/ui/LogoArcs';
interface CompetenceBentoGridProps {
badge?: string;
@@ -58,7 +61,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
if (!isMounted) {
return (
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
<div className="container">
<Container>
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
<div className="max-w-2xl">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
@@ -68,20 +71,20 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
{/* Simplified static grid */}
</div>
</div>
</Container>
</section>
);
}
return (
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<div className="container relative z-10 px-4">
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
<div className="flex flex-col md:flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10">
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
{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>
@@ -111,7 +114,14 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
className={gridClasses}
>
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/30 blur-3xl rounded-full -mr-10 -mt-10 group-hover:bg-primary/50 transition-colors duration-500" />
{/* Subtle Background Logo Arcs */}
<div className={`absolute -bottom-[30%] -right-[30%] w-[120%] h-[120%] z-0 opacity-10 group-hover:opacity-20 transition-opacity duration-1000 pointer-events-none mix-blend-overlay`}>
<LogoArcs className={`w-full h-full text-white animate-[spin_60s_linear_infinite]`} />
</div>
<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>}
</motion.div>
@@ -146,6 +156,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
)}
<HoverShineOverlay />
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
{/* Subtle Background Logo Arcs */}
<div className="absolute -top-[30%] -left-[30%] w-[120%] h-[120%] z-0 opacity-0 group-hover:opacity-[0.05] transition-opacity duration-1000 pointer-events-none mix-blend-overlay">
<LogoArcs className={`w-full h-full text-white animate-[spin_80s_linear_infinite]`} />
</div>
<div className="relative z-10 p-8 flex flex-col justify-end h-full text-white">
{item.tag && (
@@ -166,7 +182,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
);
})}
</div>
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { getFAQSchema } from '@/lib/schema';
import JsonLd from '@/components/JsonLd';
export interface FAQItem {
question: string;
answer: string;
}
interface FaqBlockProps {
title?: string;
subtitle?: string;
questions: FAQItem[];
}
export const FaqBlock: React.FC<FaqBlockProps> = ({
title = "Häufig gestellte Fragen",
subtitle,
questions
}) => {
if (!questions || questions.length === 0) return null;
return (
<section className="py-12 md:py-16">
<div className="max-w-3xl mx-auto">
<div className="mb-10 text-center">
{subtitle && (
<span className="text-sm font-bold text-accent uppercase tracking-wider block mb-2">
{subtitle}
</span>
)}
<h2 className="text-3xl md:text-4xl font-bold text-text-primary">
{title}
</h2>
</div>
<div className="space-y-4">
{questions.map((item, index) => (
<details
key={index}
className="group border border-neutral-200 bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<summary className="flex items-center justify-between cursor-pointer p-6 font-semibold text-lg text-text-primary hover:text-primary transition-colors select-none">
{item.question}
<span className="ml-4 flex-shrink-0 transition-transform duration-300 group-open:-rotate-180 text-accent">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</span>
</summary>
<div className="px-6 pb-6 text-text-secondary leading-relaxed border-t border-neutral-100 pt-4 prose prose-p:my-2 prose-a:text-primary">
{/* Parse simple HTML or just text if answer has formatting */}
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
</div>
</details>
))}
</div>
</div>
{/* Inject FAQ Schema automatically */}
<JsonLd data={getFAQSchema(questions)} />
</section>
);
};

View File

@@ -55,6 +55,7 @@ export function HeroVideo(props: HeroVideoProps) {
{videoUrl && (
<video
key={videoUrl}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
autoPlay

View File

@@ -0,0 +1,275 @@
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, Factory, Zap, CheckCircle2 } from 'lucide-react';
import Image from 'next/image';
export interface Location {
id: string;
name: string;
type: 'hq' | 'branch' | 'project';
x: number;
y: number;
description: string;
details?: string[];
}
const defaultLocations: Location[] = [
{
id: 'guben',
name: 'Guben (Hauptsitz)',
type: 'hq',
x: 85,
y: 48,
description: 'E-TIB GmbH Holding & Bohrtechnik GmbH',
details: ['Zentrale Steuerung', 'Kabelleitungstiefbau', 'Maschinenpark'],
},
{
id: 'buelstedt',
name: 'Bülstedt (Ingenieurgesellschaft)',
type: 'branch',
x: 37,
y: 25,
description: 'E-TIB Ingenieurgesellschaft mbH',
details: ['Planung & Projektierung', 'Vermessung', 'Dokumentation'],
},
{
id: 'spreewald',
name: 'Spreewald (Projekt)',
type: 'project',
x: 82,
y: 45,
description: 'Breitbandausbau FTTC',
},
{
id: 'bomlitz',
name: 'Bomlitz (Projekt)',
type: 'project',
x: 45,
y: 30,
description: 'Breitbandausbau FTTH',
},
{
id: 'forst',
name: 'Forst (Projekt)',
type: 'project',
x: 88,
y: 50,
description: 'Neubau Kabeltrasse',
},
{
id: 'eisenhuettenstadt',
name: 'Eisenhüttenstadt (Projekt)',
type: 'project',
x: 85,
y: 40,
description: 'Kabelverbindung U30 - T10',
},
{
id: 'goerne',
name: 'Görne (Projekt)',
type: 'project',
x: 70,
y: 35,
description: 'Einspeisung PV-Anlage',
}
];
interface Stat {
value: string;
suffix?: string;
label: string;
}
interface InteractiveGermanyMapProps {
badge?: string;
title?: React.ReactNode;
description?: string;
stats?: Stat[];
locations?: Location[];
}
export function InteractiveGermanyMap({
badge = 'Einsatzgebiete',
title = <>Deutschlandweit<br/>für Sie im Einsatz.</>,
description = 'Von unseren strategischen Standorten in Guben und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.',
stats = [
{ value: '100', suffix: '%', label: 'Überregionale Reichweite' },
{ value: '2', suffix: '+', label: 'Operative Standorte' },
],
locations = defaultLocations
}: InteractiveGermanyMapProps) {
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
const hq = locations.find((l) => l.type === 'hq');
const branch = locations.find((l) => l.type === 'branch');
const projects = locations.filter((l) => l.type === 'project');
return (
<div className="relative w-full max-w-7xl mx-auto py-16 md:py-32 px-4 sm:px-6">
<div className="bg-[#050B14] rounded-[2.5rem] md:rounded-[3.5rem] overflow-hidden relative shadow-2xl border border-white/5">
{/* Background Effects */}
<div className="absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14]" />
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3" />
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] p-8 md:p-12 lg:p-16">
{/* Content Left */}
<div className="xl:col-span-5 text-white z-20">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
<MapPin className="w-4 h-4" />
<span>{badge}</span>
</div>
<h3 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 break-words [hyphens:auto]">
{title}
</h3>
<p className="text-white/60 text-lg mb-12 leading-relaxed">
{description}
</p>
{/* Industrial Stats Grid */}
<div className="grid grid-cols-2 gap-4">
{stats.map((stat, i) => (
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10">
<div className="text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
</div>
<div className="text-sm text-white/50 font-medium">{stat.label}</div>
</div>
</div>
))}
</div>
</div>
{/* Map Right */}
<div className="xl:col-span-7 relative flex items-center justify-center lg:justify-end mt-8 lg:mt-0 z-10">
{/* Map Container - Enforce strict aspect ratio matching the SVG (1024x1024) */}
<div className="relative w-full max-w-[600px] aspect-square z-10">
{/* Map SVG */}
<div className="absolute inset-0 opacity-[0.15] mix-blend-screen drop-shadow-2xl brightness-150">
<Image
src="/germany-map.svg"
alt="Deutschlandkarte"
fill
className="object-cover"
priority
/>
</div>
{/* Connections (Lines) */}
<svg className="absolute inset-0 w-full h-full pointer-events-none z-10">
{hq && branch && (
<motion.line
x1={`${hq.x}%`}
y1={`${hq.y}%`}
x2={`${branch.x}%`}
y2={`${branch.y}%`}
stroke="rgba(130,237,32,0.6)"
strokeWidth="2"
strokeDasharray="6 6"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 2, ease: "easeOut" }}
/>
)}
{hq && projects.map((proj, idx) => (
<motion.line
key={proj.id}
x1={`${hq.x}%`}
y1={`${hq.y}%`}
x2={`${proj.x}%`}
y2={`${proj.y}%`}
stroke="rgba(130,237,32,0.3)"
strokeWidth="1.5"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, delay: 0.5 + idx * 0.2 }}
/>
))}
</svg>
{/* Location Pins */}
{locations.map((loc, idx) => {
const isHQ = loc.type === 'hq';
const isBranch = loc.type === 'branch';
const isActive = activeLocation?.id === loc.id;
return (
<div
key={loc.id}
className="absolute transform -translate-x-1/2 -translate-y-1/2 z-20 group cursor-pointer w-16 h-16 flex items-center justify-center"
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
onMouseEnter={() => setActiveLocation(loc)}
onMouseLeave={() => setActiveLocation(null)}
>
{/* Ping Animation for HQ / Branch */}
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
{/* Marker Dot */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.4)] transition-transform duration-300 ${
isActive ? 'scale-125 z-30 ring-4 ring-primary/30' : 'scale-100'
} ${
isHQ || isBranch
? 'w-6 h-6 bg-primary text-[#050B14]'
: 'w-3 h-3 bg-white/90 ring-2 ring-primary'
}`}
>
{(isHQ || isBranch) && <MapPin className="w-3.5 h-3.5" />}
</motion.div>
{/* Tooltip */}
<AnimatePresence>
{isActive && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-3 w-56 bg-white text-neutral-dark p-5 rounded-2xl shadow-2xl z-50 pointer-events-none border border-neutral-100"
>
<div className="flex items-center gap-2 mb-2">
{isHQ ? <Factory className="w-4 h-4 text-primary" /> : <Zap className="w-4 h-4 text-primary" />}
<span className="font-extrabold text-sm leading-tight text-neutral-dark">{loc.name}</span>
</div>
<p className="text-xs text-neutral-500 mt-2 font-medium">{loc.description}</p>
{loc.details && (
<ul className="mt-4 space-y-2">
{loc.details.map((detail, i) => (
<li key={i} className="text-[11px] text-neutral-dark flex items-center gap-2 font-semibold">
<div className="w-4 h-4 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
<CheckCircle2 className="w-3 h-3 text-primary" />
</div>
{detail}
</li>
))}
</ul>
)}
{/* Arrow Down */}
<div className="absolute left-1/2 -bottom-2 -translate-x-1/2 border-8 border-transparent border-t-white drop-shadow-sm" />
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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)

View File

@@ -4,6 +4,7 @@ import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
interface SubCompanyTilesProps {
badge?: string;
@@ -49,6 +50,7 @@ const itemVariants: Variants = {
};
import { usePathname } from 'next/navigation';
import { LogoArcs } from '@/components/ui/LogoArcs';
export function SubCompanyTiles(props: SubCompanyTilesProps) {
const { data } = props;
@@ -76,21 +78,39 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
)}
<motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-6"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
{companiesData.map((company: any, index: number) => {
const isCurrent = !company.url;
const CardWrapper = isCurrent ? 'div' : 'a';
const wrapperProps = !isCurrent ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
const hasUrl = !!company.url;
const CardWrapper = hasUrl ? 'a' : 'div';
const wrapperProps = hasUrl ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
// Generate logo representation based on title
const isEtib = company.title.includes('E-TIB');
const isIng = company.title.includes('Ingenieur');
const isVerwaltung = company.title.includes('Verwaltung');
const isBohrtechnik = company.title.includes('Bohrtechnik');
const isNemo = company.title.includes('NEMO');
const bgPositionClasses = [
"-bottom-[30%] -right-[30%]",
"-top-[30%] -left-[30%]",
"-bottom-[30%] -left-[30%]",
"-top-[30%] -right-[30%]"
];
const animationClasses = [
"animate-[spin_60s_linear_infinite]",
"animate-[spin_80s_linear_infinite]",
"animate-[spin_100s_linear_infinite]",
"animate-[spin_120s_linear_infinite]"
];
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
const animationClass = animationClasses[index % animationClasses.length];
return (
<motion.div
@@ -126,10 +146,22 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
? 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40'
: 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40 group-hover:from-primary/90 group-hover:to-primary/20'
}`} />
{/* Subtle Background Logo Arcs */}
<div className={`absolute ${positionClass} w-[120%] h-[120%] z-10 opacity-[0.02] group-hover:opacity-[0.08] transition-opacity duration-1000 pointer-events-none mix-blend-overlay`}>
<LogoArcs className={`w-full h-full text-white ${animationClass}`} />
</div>
{/* Premium Shine Sweep Effect (Industrial Reflection) */}
<HoverShineOverlay shineColor="via-white/30" />
{/* Glossy Animated Border */}
<AnimatedGlossyBorder
color={isCurrent ? "primary" : "white"}
className={`transition-opacity duration-700 ${isCurrent ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
borderWidth={1}
/>
{/* Current Site Indicator Badge */}
{isCurrent && (
<div className="absolute top-6 right-6 z-20">
@@ -159,10 +191,20 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
/>
</div>
{isIng && (
<div className="text-white/80 tracking-[0.3em] text-xs font-semibold uppercase mt-1">
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
Ingenieurgesellschaft
</div>
)}
{isVerwaltung && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
Verwaltung GmbH
</div>
)}
{isBohrtechnik && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
Bohrtechnik GmbH
</div>
)}
</div>
) : isNemo ? (
<div className="flex flex-col items-center">

View File

@@ -0,0 +1,188 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
import { LogoArcs } from '@/components/ui/LogoArcs';
function MagneticRing({
children,
className,
pullStrength = 30, // How strong the pull is (higher = moves closer to mouse)
pullRadius = 600 // Distance from center at which it starts pulling
}: {
children: React.ReactNode,
className?: string,
pullStrength?: number,
pullRadius?: number
}) {
const ref = useRef<HTMLDivElement>(null);
// Spring physics for extremely smooth, organic movement
const x = useSpring(0, { stiffness: 40, damping: 25, mass: 1 });
const y = useSpring(0, { stiffness: 40, damping: 25, mass: 1 });
useEffect(() => {
// Only run on client with mouse
if (window.matchMedia("(pointer: coarse)").matches) return;
const handleMouseMove = (e: MouseEvent) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distX = e.clientX - centerX;
const distY = e.clientY - centerY;
const distance = Math.sqrt(distX * Math.pow(distX, 2) + Math.pow(distY, 2)); // Fix: correct pythagoras
// Calculate actual distance correctly
const actualDistance = Math.sqrt(distX * distX + distY * distY);
if (actualDistance < pullRadius) {
// Exponential falloff for natural magnetic feel
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2);
// Target offset (fraction of the distance based on strength)
x.set(distX * pullFactor * (pullStrength / 100));
y.set(distY * pullFactor * (pullStrength / 100));
} else {
// Return to origin smoothly
x.set(0);
y.set(0);
}
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [x, y, pullRadius, pullStrength]);
return (
<motion.div
ref={ref}
style={{ x, y }}
className={className}
>
{children}
</motion.div>
);
}
export function CorporateBackground() {
const { scrollYProgress } = useScroll();
// Stronger parallax offsets for visible movement
const y1 = useTransform(scrollYProgress, [0, 1], [0, -400]);
const y2 = useTransform(scrollYProgress, [0, 1], [0, 500]);
const y3 = useTransform(scrollYProgress, [0, 1], [0, -600]);
const y4 = useTransform(scrollYProgress, [0, 1], [0, 350]);
const y5 = useTransform(scrollYProgress, [0, 1], [0, -500]);
return (
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none" aria-hidden="true">
{/* 1. Top Right - Medium, rotating moderately */}
<motion.div
style={{
y: y1,
animationDelay: '0s',
maskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)',
WebkitMaskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)'
}}
className="absolute top-[0%] right-0 translate-x-[40%] w-[400px] h-[400px] md:w-[700px] md:h-[700px] animate-bg-pulse-20"
>
<MagneticRing pullRadius={700} pullStrength={40} className="w-full h-full">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full" />
</motion.div>
</MagneticRing>
</motion.div>
{/* 2. Quarter way down left - Smaller, rotating fast reverse */}
<motion.div
style={{
y: y2,
animationDelay: '3s',
maskImage: 'radial-gradient(circle at center, black 15%, transparent 65%)',
WebkitMaskImage: 'radial-gradient(circle at center, black 15%, transparent 65%)'
}}
className="absolute top-[20%] left-0 -translate-x-[40%] w-[300px] h-[300px] md:w-[500px] md:h-[500px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={500} pullStrength={50} className="w-full h-full">
<motion.div
animate={{ rotate: -360 }}
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-secondary"
>
<LogoArcs className="w-full h-full -rotate-45" />
</motion.div>
</MagneticRing>
</motion.div>
{/* 3. Halfway down right - Medium, rotating slow */}
<motion.div
style={{
y: y3,
animationDelay: '6s',
maskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)',
WebkitMaskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)'
}}
className="absolute top-[45%] right-0 translate-x-[30%] w-[350px] h-[350px] md:w-[600px] md:h-[600px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={600} pullStrength={35} className="w-full h-full">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 60, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full rotate-90" />
</motion.div>
</MagneticRing>
</motion.div>
{/* 4. Bottom left - Larger, rotating moderately */}
<motion.div
style={{
y: y4,
animationDelay: '9s',
maskImage: 'radial-gradient(circle at center, black 5%, transparent 60%)',
WebkitMaskImage: 'radial-gradient(circle at center, black 5%, transparent 60%)'
}}
className="absolute top-[70%] left-0 -translate-x-[40%] w-[500px] h-[500px] md:w-[800px] md:h-[800px] animate-bg-pulse-20"
>
<MagneticRing pullRadius={800} pullStrength={30} className="w-full h-full">
<motion.div
animate={{ rotate: -360 }}
transition={{ duration: 50, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-secondary"
>
<LogoArcs className="w-full h-full rotate-180" />
</motion.div>
</MagneticRing>
</motion.div>
{/* 5. Deep bottom right - Smallest, very fast */}
<motion.div
style={{
y: y5,
animationDelay: '12s',
maskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)',
WebkitMaskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)'
}}
className="absolute top-[88%] right-0 translate-x-[50%] w-[250px] h-[250px] md:w-[400px] md:h-[400px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={400} pullStrength={60} className="w-full h-full">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full -rotate-12" />
</motion.div>
</MagneticRing>
</motion.div>
</div>
);
}

View File

@@ -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 cursor-pointer">
<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,12 +58,12 @@ 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">
<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">
<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 cursor-pointer">
<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>
</a>
<a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} 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">
<a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} 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 cursor-pointer">
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">{locale === 'de' ? 'Telefon' : 'Phone'}</p>
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactPhone}</p>
</a>
@@ -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` },
@@ -91,7 +91,7 @@ export function Footer({ companyInfo }: FooterProps) {
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="group/link flex items-center text-white/60 hover:text-white transition-colors py-1">
<TransitionLink href={link.href} className="group/link flex items-center text-white/60 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>
@@ -101,20 +101,20 @@ 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` },
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="group/link flex items-center text-white/50 hover:text-white transition-colors py-1">
<TransitionLink href={link.href} className="group/link flex items-center text-white/50 hover:text-white transition-colors py-1 cursor-pointer">
<span className="w-0 h-px bg-white/30 mr-0 group-hover/link:w-3 group-hover/link:mr-3 transition-all duration-300" />
<span className="font-medium">{link.label}</span>
</TransitionLink>
@@ -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">&copy; {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>

View File

@@ -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;
@@ -84,7 +71,7 @@ export function Header({ navLinks }: HeaderProps) {
}`}
>
<div
className={`relative overflow-hidden w-full max-w-[1400px] mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
className={`relative overflow-hidden w-full max-w-7xl mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
isSolidMode
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-6 md:px-8'
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
@@ -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}`
@@ -137,7 +125,7 @@ export function Header({ navLinks }: HeaderProps) {
<TransitionLink
key={link.url}
href={mappedUrl}
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group py-2 ${
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group py-2 select-none ${
isSolidMode
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
@@ -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 select-none ${
isSolidMode
? 'bg-primary text-white shadow-lg shadow-primary/20 hover:shadow-primary/40 hover:-translate-y-0.5 border border-transparent'
: 'bg-white/10 text-white backdrop-blur-md border border-white/30 hover:bg-white/20 hover:-translate-y-0.5 hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]'
}`}
>
<span className="relative z-10 transition-transform duration-300 group-hover/cta:scale-105 inline-block">{currentLocale === 'de' ? 'Kontakt' : 'Contact'}</span>
{/* Sleek shine sweep effect */}
<span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" />
</TransitionLink>
</div>
</div>
</div>
</header>
{/* 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>
</>
);
}

View File

@@ -33,7 +33,7 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
key={loc.code}
href={getSwitchedUrl(loc.code)}
transitionMessage={currentLocale === loc.code ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 ${
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 select-none ${
currentLocale === loc.code
? 'bg-primary/10 text-primary border border-primary/20'
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
@@ -83,7 +83,7 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
key={loc.code}
href={getSwitchedUrl(loc.code)}
transitionMessage={isActive ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 ${linkClass}`}
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 select-none ${linkClass}`}
aria-label={`Switch to ${loc.fullLabel}`}
>
<span className="text-sm" aria-hidden="true">{loc.flag}</span>

View 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 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 */}
{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>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
interface TransitionContextType {
isTransitioning: boolean;
@@ -15,13 +15,39 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning);
useEffect(() => {
isTransitioningRef.current = isTransitioning;
}, [isTransitioning]);
// Watch for route changes to complete the transition
useEffect(() => {
if (isTransitioningRef.current) {
// The route has actually changed in the browser, or at least the RSC payload arrived
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams]); // Run when pathname or search parameters change
const startTransition = useCallback((href: string, message: string | null = null) => {
// Wenn wir bereits navigieren, nichts tun
if (isTransitioning) return;
// Normalize paths to avoid false mismatches (e.g. /de vs /de/)
const currentPath = typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, '') || '/' : '';
const targetPath = href.split('?')[0].replace(/\/$/, '') || '/';
const currentSearch = typeof window !== 'undefined' ? window.location.search : '';
const targetSearch = href.includes('?') ? '?' + href.split('?')[1] : '';
// Aktuelle URL prüfen, falls es dieselbe ist, nicht navigieren
if (typeof window !== 'undefined' && window.location.pathname === href) {
if (currentPath === targetPath && currentSearch === targetSearch) {
// Optional: Wir könnten weich nach oben scrollen
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
@@ -34,13 +60,16 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTimeout(() => {
router.push(href);
// Den Shutter wieder einfahren lassen, nachdem die Seite gewechselt hat
// Next.js Route-Changes sind fast sofort da, aber wir geben dem Cache kurz Zeit
// Fallback: If for some reason the route doesn't change after 5 seconds, open the shutter anyway
setTimeout(() => {
setIsTransitioning(false);
// Message etwas verzögert zurücksetzen, damit es während dem Ausblenden nicht verschwindet
setTimeout(() => setTransitionMessage(null), 300);
}, 300);
setIsTransitioning((current) => {
if (current) {
setTimeout(() => setTransitionMessage(null), 300);
return false;
}
return current;
});
}, 5000);
}, 700);
}, [isTransitioning, router]);

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { cn } from './utils';
interface AnimatedGlossyBorderProps {
className?: string;
borderWidth?: 1 | 2;
opacity?: number;
color?: 'white' | 'primary';
}
export function AnimatedGlossyBorder({
className,
borderWidth = 1,
color = 'white'
}: AnimatedGlossyBorderProps) {
const isWhite = color === 'white';
const glowColor = isWhite ? 'rgba(255,255,255,0.6)' : 'rgba(17,124,97,0.6)';
const gradientColors = isWhite
? 'transparent 0%, rgba(255,255,255,0.1) 15%, rgba(255,255,255,0.85) 25%, rgba(255,255,255,0.1) 35%, transparent 50%, transparent 50%, rgba(255,255,255,0.1) 65%, rgba(255,255,255,0.85) 75%, rgba(255,255,255,0.1) 85%, transparent 100%'
: 'transparent 0%, rgba(17,124,97,0.1) 15%, rgba(17,124,97,0.85) 25%, rgba(17,124,97,0.1) 35%, transparent 50%, transparent 50%, rgba(17,124,97,0.1) 65%, rgba(17,124,97,0.85) 75%, rgba(17,124,97,0.1) 85%, transparent 100%';
return (
<div
className={cn('absolute inset-0 pointer-events-none rounded-[inherit] z-20 mix-blend-overlay', className)}
style={{ filter: `drop-shadow(0 0 6px ${glowColor}) drop-shadow(0 0 2px ${glowColor})` }}
>
<div
className="absolute inset-0 rounded-[inherit]"
style={{
border: `${borderWidth}px solid transparent`,
WebkitMask: 'linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)',
WebkitMaskComposite: 'xor',
maskComposite: 'exclude'
}}
>
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[250%] aspect-square animate-[spin_8s_linear_infinite]"
style={{
background: `conic-gradient(from 0deg, ${gradientColors})`
}}
/>
</div>
</div>
);
}

View File

@@ -10,7 +10,7 @@ export function Badge({ children, className, variant = 'primary' }: { children:
};
return (
<span className={cn('inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider', variants[variant], className)}>
<span className={cn('inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider select-none', variants[variant], className)}>
{children}
</span>
);

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { TransitionLink } from '@/components/ui/TransitionLink';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { cn } from './utils';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -19,7 +20,7 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
}
export const buttonBaseStyles =
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
'cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate select-none';
export const buttonVariantsMap = {
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
@@ -58,13 +59,23 @@ export function getButtonClasses(variant: keyof typeof buttonVariantsMap = 'prim
}
export function ButtonOverlay({ variant = 'primary' }: { variant?: keyof typeof buttonVariantsMap }) {
const isWhiteBg = variant === 'white' || variant === 'outline' || variant === 'ghost';
return (
<span
className={cn(
'absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent to-transparent skew-x-[-20deg] group-hover/btn:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none',
buttonShineColorsMap[variant]
<>
<span
className={cn(
'absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent to-transparent skew-x-[-20deg] group-hover/btn:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none',
buttonShineColorsMap[variant]
)}
/>
{!isWhiteBg && (
<AnimatedGlossyBorder
color="white"
className="opacity-40 group-hover/btn:opacity-100 transition-opacity duration-700"
/>
)}
/>
</>
);
}

View File

@@ -9,7 +9,7 @@ export function Label({ className, ...props }: LabelProps) {
return (
<label
className={cn(
'text-[10px] md:text-xs font-extrabold text-primary uppercase tracking-widest',
'text-[10px] md:text-xs font-extrabold text-primary uppercase tracking-widest select-none',
className
)}
{...props}

View File

@@ -0,0 +1,20 @@
import React from 'react';
// Reusable SVG component that precisely mimics the overlapping, sweeping arcs of the E-TIB logo.
// The circles are offset along a diagonal to bunch together at the bottom-left and spread apart at the top-right.
export const LogoArcs = ({ className }: { className?: string }) => (
<svg
className={className}
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
>
{/* Outer ring */}
<circle cx="100" cy="100" r="90" strokeWidth="1.5" />
{/* Middle thicker ring */}
<circle cx="92" cy="108" r="78" strokeWidth="3.5" />
{/* Inner ring */}
<circle cx="84" cy="116" r="66" strokeWidth="1.5" />
</svg>
);

View File

@@ -3,6 +3,7 @@
import React from 'react';
import Link, { LinkProps } from 'next/link';
import { useTransition } from '@/components/providers/TransitionProvider';
import { cn } from './utils';
interface TransitionLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps>, LinkProps {
children: React.ReactNode;
@@ -31,7 +32,7 @@ export function TransitionLink({ children, href, onClick, transitionMessage, ...
};
return (
<Link href={href} onClick={handleClick} {...props}>
<Link href={href} onClick={handleClick} className={cn('cursor-pointer', props.className)} {...props}>
{children}
</Link>
);

View File

@@ -30,11 +30,16 @@ description: Willkommen bei E-TIB GmbH
backgroundImage: "/assets/photos/DSC01137.JPG"
},
{
title: "NEMO GmbH",
description: "Der Spezialist für Planung und Umsetzung von nachhaltigen Energieprojekten - Mit Fokus auf Solarenergie.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
url: "https://www.nemo-gmbh.de/",
title: "E-TIB Verwaltung GmbH",
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
backgroundImage: "/assets/photos/DJI_0048.JPG"
},
{
title: "E-TIB Bohrtechnik GmbH",
description: "Präzise Horizontalspülbohrungen in allen Bodenklassen für eine oberflächenschonende und grabenlose Leitungsverlegung.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
backgroundImage: "/assets/photos/DSC08653.JPG"
}
]}
/>
@@ -173,3 +178,26 @@ description: Willkommen bei E-TIB GmbH
}
]}
/>
<FaqBlock
title="Häufig gestellte Fragen"
subtitle="Wissenswertes"
questions={[
{
question: "Was macht die E-TIB GmbH?",
answer: "Die E-TIB GmbH ist spezialisiert auf den Leitungs- und Kabeltiefbau. Zu den Kernleistungen zählen der klassische Kabelbau, hochpräzise Horizontalspülbohrungen, Kabelpflugarbeiten sowie Elektromontagen bis 110 kV. Zusammen mit unseren Tochtergesellschaften decken wir auch die komplette Planung und Dokumentation von Infrastrukturprojekten ab."
},
{
question: "Welche Vorteile bietet das Horizontalspülbohrverfahren (Bohrtechnik)?",
answer: "Die Horizontalspülbohrung ist ein grabenloses Verfahren zur unterirdischen Verlegung von Kabeln und Rohren. Der größte Vorteil ist die oberflächenschonende Arbeitsweise: Flüsse, Autobahnen, Bahntrassen oder Naturschutzgebiete können gekreuzt werden, ohne den Verkehr oder die Umwelt zu beeinträchtigen. Dies spart erhebliche Kosten bei der Oberflächenwiederherstellung."
},
{
question: "Seit wann gibt es die E-TIB GmbH und wie groß ist das Team?",
answer: "Die E-TIB GmbH wurde im Dezember 2015 in Guben gegründet. Heute beschäftigt die Unternehmensgruppe über 50 qualifizierte Mitarbeiter, die komplexe Infrastruktur- und Tiefbauprojekte für Energieversorger und Telekommunikationsanbieter bundesweit realisieren."
},
{
question: "Bieten Sie auch die Planung von Netzanschlüssen an?",
answer: "Ja, über die E-TIB Ingenieurgesellschaft mbH übernehmen wir die komplette Genehmigungs- und Ausführungsplanung. Dies beinhaltet Elektro- und Netzanschlussplanungen, Machbarkeitsstudien sowie die finale digitale Bestandsdokumentation per GIS."
}
]}
/>

View File

@@ -24,12 +24,12 @@ E-Mail: d.joseph@e-tib.com
Eintragung im Handelsregister.
Registergericht: Amtsgericht Cottbus
Registernummer: HRB 14781 CB
Registernummer: HRB 12403 CB
## Umsatzsteuer-ID
Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:
DE332345678 (Beispiel)
DE304799919
## Redaktionell verantwortlich

View File

@@ -70,11 +70,11 @@ layout: "fullBleed"
icon: "energy",
lists: [
[
"Kabel- und Rohrgräben",
"Kabelbau",
"Kabelverlegung (0,4kV - 110kV)",
"Kabelmontagen (0,4kV - 30kV)",
"Elektromontagen bis 110 kV",
"Errichtung von Transformatorstationen",
"Erdungsanlagen & Straßenbeleuchtung"
"Wartung & Störungsdienst"
]
]
},
@@ -84,22 +84,22 @@ layout: "fullBleed"
icon: "fiber",
lists: [
[
"Glasfaser-Kabelmontagen",
"Herstellung von Leerrohrtrassen",
"Einziehen/Einblasen von LWL-Kabeln",
"Stellen von Multifunktionsgehäusen",
"LWL-Kabelmontagen"
"Stellen von Multifunktionsgehäusen"
]
]
},
{
id: "hdd",
title: "Grabenlose Verlegung",
title: "Bohrtechnik & Kabelpflug",
icon: "hdd",
lists: [
[
"Horizontalspülbohrverfahren (HDD)",
"Max. Bohrlänge: 250m",
"Max. Durchmesser: 400er Rohr",
"Horizontalspülbohrungen",
"Kabelpflugarbeiten",
"Komplexe Querung (Bahn, Autobahn, Gewässer)",
"Erdrakete (bis 15m / 160er Rohr)"
]
]
@@ -110,9 +110,9 @@ layout: "fullBleed"
icon: "planning",
lists: [
[
"Struktur- & Ausführungsplanung",
"Genehmigungs- und Ausführungsplanung",
"Elektro- und Netzanschlussplanung",
"Ausschreibung & Bauüberwachung",
"Zustimmungsplanung",
"Machbarkeitsstudien & Projektprüfung"
]
]
@@ -124,14 +124,41 @@ layout: "fullBleed"
fullWidth: true,
lists: [
[
"Absteckung und Einmessung von Leitungstrassen",
"Digitale Bestandsdokumentation (GIS)"
"Vermessung & Dokumentation",
"Absteckung und Einmessung von Leitungstrassen"
],
[
"360° Foto-/Video-Erfassung des Baufeldes",
"Erstellung von Datenbanken zur Projektabrechnung"
"Digitale Bestandsdokumentation (GIS)",
"360° Foto-/Video-Erfassung des Baufeldes"
]
]
}
]}
/>
<JsonLd data={[
{
"@type": "Service",
"name": "Kabelbau",
"description": "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
},
{
"@type": "Service",
"name": "Horizontalspülbohrungen",
"description": "Horizontalspülbohrverfahren (HDD) für grabenlose, oberflächenschonende Verlegung.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
},
{
"@type": "Service",
"name": "Glasfaserausbau",
"description": "Ausbau von zukunftssicheren Breitbandnetzen (FTTX) und Glasfaser-Kabelmontagen.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
},
{
"@type": "Service",
"name": "Planung und Vermessung",
"description": "Genehmigungs- und Ausführungsplanung, Elektro- und Netzanschlussplanung, sowie digitale Bestandsdokumentation per GIS.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
}
]} />

View File

@@ -15,7 +15,84 @@ layout: "fullBleed"
/>
<div id="termine">
<JobListingBlock showFairs={true} showJobs={false} />
<JobListingBlock
showFairs={true}
showJobs={false}
fairs={[
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe', location: 'Messe München', booth: 'Halle A3, Stand 110', url: 'https://www.intersolar.de' },
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress', location: 'Van der Valk Resort Linstow', booth: 'Stand 42', url: 'https://windenergietage.de' },
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse', location: 'RheinMain CongressCenter', booth: 'Halle 1, Stand B20' }
]}
/>
</div>
<CallToAction />
<JsonLd data={[
{
"@type": "Event",
"name": "Intersolar München",
"startDate": "2026-06-19",
"endDate": "2026-06-21",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "Place",
"name": "Messe München",
"address": {
"@type": "PostalAddress",
"addressLocality": "München",
"addressCountry": "DE"
}
},
"organizer": {
"@type": "Organization",
"name": "E-TIB GmbH",
"url": "https://e-tib.com"
}
},
{
"@type": "Event",
"name": "Windenergietage Linstow",
"startDate": "2026-11-04",
"endDate": "2026-11-06",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "Place",
"name": "Van der Valk Resort Linstow",
"address": {
"@type": "PostalAddress",
"addressLocality": "Linstow",
"addressCountry": "DE"
}
},
"organizer": {
"@type": "Organization",
"name": "E-TIB GmbH",
"url": "https://e-tib.com"
}
},
{
"@type": "Event",
"name": "Kabelwerkstatt Wiesbaden",
"startDate": "2026-12-02",
"endDate": "2026-12-03",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "Place",
"name": "RheinMain CongressCenter",
"address": {
"@type": "PostalAddress",
"addressLocality": "Wiesbaden",
"addressCountry": "DE"
}
},
"organizer": {
"@type": "Organization",
"name": "E-TIB GmbH",
"url": "https://e-tib.com"
}
}
]} />

View File

@@ -56,6 +56,8 @@ layout: "fullBleed"
<CompanyTimeline />
<InteractiveGermanyMap />
<div id="struktur">
<HomeSubCompanyTiles
badge="Struktur"
@@ -75,13 +77,18 @@ layout: "fullBleed"
url: "https://www.etib-ing.com/",
backgroundImage: "/assets/photos/DSC01137.JPG"
},
{
title: "NEMO GmbH",
description: "Der Spezialist für Planung und Umsetzung von nachhaltigen Energieprojekten - Mit Fokus auf Solarenergie.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
url: "https://www.nemo-gmbh.de/",
backgroundImage: "/assets/photos/DJI_0048.JPG"
}
{
title: "E-TIB Verwaltung GmbH",
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
backgroundImage: "/assets/photos/DJI_0048.JPG"
},
{
title: "E-TIB Bohrtechnik GmbH",
description: "Präzise Horizontalspülbohrungen in allen Bodenklassen für eine oberflächenschonende und grabenlose Leitungsverlegung.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
backgroundImage: "/assets/photos/DSC08653.JPG"
}
]}
/>
</div>

View File

@@ -30,10 +30,10 @@ description: Welcome to E-TIB GmbH
backgroundImage: "/assets/photos/DSC01137.JPG"
},
{
title: "NEMO GmbH",
description: "The specialist for planning and implementation of sustainable energy projects - with a focus on solar energy.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
url: "https://www.nemo-gmbh.de/",
title: "E-TIB Bohrtechnik GmbH",
description: "Highest precision underground: We specialize in complex horizontal directional drilling for all infrastructure networks.",
icon: "M19 14l-7 7m0 0l-7-7m7 7V3",
url: "https://www.e-tib.com/bohrtechnik",
backgroundImage: "/assets/photos/DJI_0048.JPG"
}
]}

View File

@@ -59,24 +59,102 @@ layout: "fullBleed"
title="Milestones of Development"
milestones={[
{
date: '2015',
date: '16.12.2015',
year: '2015',
title: 'Foundation of E-TIB GmbH',
desc: 'Founded in Guben, Brandenburg, with a focus on civil engineering and cable assembly.'
desc: 'Execution of electrical infrastructure projects, cable civil engineering, and horizontal directional drilling.',
iconName: 'Building2'
},
{
date: 'Growth',
title: 'Expansion of Competencies',
desc: 'Consistent expansion of services to include horizontal directional drilling and cable plowing.'
date: '04.02.2019',
year: '2019',
title: 'Foundation of E-TIB Ingenieurgesellschaft',
desc: 'Approval and execution planning, complex crossings, and grid connection planning.',
iconName: 'Compass'
},
{
date: 'Corporate Group',
title: 'Merger',
desc: 'Pooling of specialized departments with NEMO GmbH and E-TIB Ingenieurgesellschaft mbH.'
date: '14.11.2019',
year: '2019',
title: 'Foundation of E-TIB Verwaltung GmbH',
desc: 'Central services, acquisition, leasing, and management of real estate and machinery.',
iconName: 'Layers'
},
{
date: 'Today',
title: 'The E-TIB Group',
desc: 'A strong network with over 50 employees for nationwide infrastructure projects.'
date: '21.10.2025',
year: '2025',
title: 'Foundation of E-TIB Bohrtechnik GmbH',
desc: 'Specialization in precise horizontal directional drilling in all soil classes.',
iconName: 'ArrowDownToLine'
}
]}
/>
<InteractiveGermanyMap
badge="Operating Areas"
title={<>Nationwide<br/>at your service.</>}
description="From our strategic locations in Guben and Bülstedt, we control and realize complex infrastructure projects across the entire federal territory."
stats={[
{ value: '100', suffix: '%', label: 'Nationwide Reach' },
{ value: '2', suffix: '+', label: 'Operating Locations' }
]}
locations={[
{
id: 'guben',
name: 'Guben (Headquarters)',
type: 'hq',
x: 85,
y: 48,
description: 'E-TIB GmbH Holding & Bohrtechnik GmbH',
details: ['Central Management', 'Cable Civil Engineering', 'Machinery Park']
},
{
id: 'buelstedt',
name: 'Bülstedt (Engineering Firm)',
type: 'branch',
x: 37,
y: 25,
description: 'E-TIB Ingenieurgesellschaft mbH',
details: ['Planning & Projecting', 'Surveying', 'Documentation']
},
{
id: 'spreewald',
name: 'Spreewald (Project)',
type: 'project',
x: 82,
y: 45,
description: 'Broadband Expansion FTTC'
},
{
id: 'bomlitz',
name: 'Bomlitz (Project)',
type: 'project',
x: 45,
y: 30,
description: 'Broadband Expansion FTTH'
},
{
id: 'forst',
name: 'Forst (Project)',
type: 'project',
x: 88,
y: 50,
description: 'New Cable Route Construction'
},
{
id: 'eisenhuettenstadt',
name: 'Eisenhüttenstadt (Project)',
type: 'project',
x: 85,
y: 40,
description: 'Cable Connection U30 - T10'
},
{
id: 'goerne',
name: 'Görne (Project)',
type: 'project',
x: 70,
y: 35,
description: 'PV System Feed-in'
}
]}
/>
@@ -101,10 +179,10 @@ layout: "fullBleed"
backgroundImage: "/assets/photos/DSC01137.JPG"
},
{
title: "NEMO GmbH",
description: "The specialist for planning and implementation of sustainable energy projects - with a focus on solar energy.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
url: "https://www.nemo-gmbh.de/",
title: "E-TIB Bohrtechnik GmbH",
description: "Highest precision underground: We specialize in complex horizontal directional drilling for all infrastructure networks.",
icon: "M19 14l-7 7m0 0l-7-7m7 7V3",
url: "https://www.e-tib.com/bohrtechnik",
backgroundImage: "/assets/photos/DJI_0048.JPG"
}
]}

19
danny_mail_draft.txt Normal file
View File

@@ -0,0 +1,19 @@
Betreff: Testserver steht bereit E-TIB Website v1 🚀
Hi Danny,
kurzes Update von meiner Seite: Der Testserver mit dem ersten Entwurf der neuen Website steht bereit! Schau dir das Ganze gerne in Ruhe an: https://test.e-tib.com (Passwort: lassmichrein)
Ich habe den Content komplett auf Basis eures aktuellen Briefings glattgezogen. Das bedeutet konkret:
- Die alte "NEMO GmbH" Struktur ist restlos raus.
- Eure aktuelle Aufstellung (E-TIB GmbH als Holding, dazu E-TIB Verwaltung GmbH und Bohrtechnik GmbH) ist jetzt korrekt abgebildet.
- Die Timeline und Gründungsdaten (inklusive 2024 Holding) sind auf dem neuesten Stand.
- Das Leistungsportfolio habe ich exakt auf eure 10 Kernkompetenzen (Kabelpflug, Horizontalspülbohren, komplexe Querungen etc.) fokussiert.
- Impressum und Messe-Daten (Intersolar, Windenergietage, Kabelwerkstatt) sind ebenfalls aktualisiert.
Zum Design noch ein paar Worte: Ich habe mich wie besprochen an einem minimalistischen, aber sehr hochwertigen Look orientiert. Die Seite ist bewusst extrem aufgeräumt und strukturiert (z.B. durch die übersichtliche Kachel-Optik für eure Kompetenzen), damit Besucher sofort verstehen, was ihr anbietet egal ob sie am großen PC sitzen oder auf dem Handy scrollen. Auch das Video auf der Startseite habe ich als starken visuellen Aufhänger eingebaut, genau wie du es dir als Referenz gewünscht hast.
Wichtig als Randnotiz: Der aktuelle Stand ist technisch gesehen noch "Work in Progress". Ich bin gerade noch dabei, die letzten Optimierungen für Performance, SEO und mobile Responsivität vorzunehmen, damit am Ende alles perfekt flutscht.
Da eine gute Website meiner Meinung nach absolut selbsterklärend sein muss, gebe ich sie euch jetzt einfach mal direkt zum selbst Durchklicken. Sammelt gerne euer Feedback und gebt mir Bescheid, wo wir noch nachjustieren sollen. Wenn es euch lieber ist, können wir uns das Ganze natürlich auch gerne gemeinsam in einem Teams-Call durchgehen.
Beste Grüße!

View File

@@ -0,0 +1,5 @@
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
// from being included in the initial JS bundle.
import * as Sentry from '@sentry/nextjs';
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

25
instrumentation.ts Normal file
View File

@@ -0,0 +1,25 @@
import * as Sentry from '@sentry/nextjs';
/**
* Next.js will call this on boot for the active runtime.
*/
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
// Initialize server services on boot
// We do this AFTER Sentry to ensure errors during service init are caught
const { getConfig } = await import('@/lib/config');
getConfig(); // Trigger validation
const { getServerAppServices } = await import('@/lib/services/create-services.server');
getServerAppServices();
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
// Capture errors from Server Components, middleware and route handlers.
export const onRequestError = Sentry.captureRequestError;

View File

@@ -34,3 +34,85 @@ export const getBreadcrumbSchema = (items: { name: string; item: string }[]) =>
item: item.item.startsWith('http') ? item.item : `${SITE_URL}${item.item}`,
})),
});
export const getLocalBusinessSchema = () => ({
'@context': 'https://schema.org' as const,
'@type': 'LocalBusiness' as const,
name: 'E-TIB GmbH',
url: SITE_URL,
logo: LOGO_URL,
image: LOGO_URL,
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
telephone: '+49-15207230518',
email: 'd.joseph@e-tib.com',
address: {
'@type': 'PostalAddress' as const,
streetAddress: 'Gewerbestraße 22',
addressLocality: 'Guben',
postalCode: '03172',
addressCountry: 'DE',
},
geo: {
'@type': 'GeoCoordinates' as const,
latitude: 51.9547, // Approx coordinates for Guben area
longitude: 14.7175,
},
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification' as const,
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
closes: '17:00',
},
],
sameAs: ['https://www.instagram.com/me.and.eloise/'],
});
export const getFAQSchema = (questions: { question: string; answer: string }[]) => ({
'@context': 'https://schema.org' as const,
'@type': 'FAQPage' as const,
mainEntity: questions.map((q) => ({
'@type': 'Question' as const,
name: q.question,
acceptedAnswer: {
'@type': 'Answer' as const,
text: q.answer,
},
})),
});
export const getServiceSchema = (serviceName: string, description: string, image?: string) => ({
'@context': 'https://schema.org' as const,
'@type': 'Service' as const,
name: serviceName,
description: description,
provider: {
'@type': 'Organization' as const,
name: 'E-TIB GmbH',
},
...(image && { image }),
});
export const getEventSchema = (event: { name: string; startDate: string; endDate?: string; locationName: string; locationAddress?: string; url?: string }) => ({
'@context': 'https://schema.org' as const,
'@type': 'Event' as const,
name: event.name,
startDate: event.startDate,
...(event.endDate && { endDate: event.endDate }),
location: {
'@type': 'Place' as const,
name: event.locationName,
...(event.locationAddress && {
address: {
'@type': 'PostalAddress' as const,
streetAddress: event.locationAddress,
},
}),
},
organizer: {
'@type': 'Organization' as const,
name: 'E-TIB GmbH',
url: SITE_URL,
},
...(event.url && { url: event.url }),
});

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -9,8 +9,8 @@ const isProd = process.env.NODE_ENV === 'production';
const nextConfig = {
transpilePackages: ['react-image-crop', '@react-three/fiber'],
onDemandEntries: {
// Keep compiled pages/routes in memory for 5 minutes (reduced from 25m to prevent OOM)
maxInactiveAge: 5 * 60 * 1000,
// Keep compiled pages/routes in memory for 1 minute (optimized for memory usage)
maxInactiveAge: 60 * 1000,
// Keep up to 2 pages in the dev buffer (reduced from 10 to prevent OOM)
pagesBufferLength: 2,
},
@@ -507,6 +507,14 @@ const nextConfig = {
source: '/en/agbs',
destination: '/en/terms',
},
{
source: '/errors/:path*',
destination: `${(function() {
let glitchtipUrl = process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me';
if (glitchtipUrl && !glitchtipUrl.startsWith('http')) glitchtipUrl = 'https://' + glitchtipUrl;
return new URL(glitchtipUrl).origin;
})()}/:path*`,
},
],
afterFiles: [],
fallback: [],

193
public/germany-map.svg Normal file
View File

@@ -0,0 +1,193 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg class="w-full h-full text-primary opacity-20 drop-shadow-2xl" version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.10, written by Peter Selinger 2001-2011
</metadata>
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#2A303C" stroke="none">
<path d="M3047 10193 c-74 -128 -89 -173 -90 -273 -2 -79 1 -95 13 -95 12 0
16 16 18 72 4 89 15 102 72 83 34 -11 45 -11 77 3 55 23 143 33 143 15 0 -7
18 -35 40 -63 22 -27 40 -55 40 -62 0 -6 18 -33 40 -60 21 -26 51 -62 65 -79
14 -18 25 -44 25 -58 0 -22 4 -26 28 -26 17 0 37 -9 49 -22 22 -25 47 -68 41
-73 -2 -1 -29 -10 -61 -19 -45 -13 -57 -21 -53 -32 3 -9 6 -20 6 -25 0 -5 20
-9 44 -9 33 0 52 7 81 30 l39 31 13 -32 c16 -40 17 -39 -5 -39 -10 0 -39 -13
-65 -30 -46 -29 -52 -30 -166 -30 -104 0 -120 -2 -136 -20 -17 -18 -16 -19 11
-26 42 -10 46 -14 33 -30 -6 -8 -26 -14 -45 -14 -25 0 -34 -5 -40 -22 -5 -18
2 -29 37 -59 l44 -37 29 21 c17 12 49 22 75 24 25 1 69 4 96 6 28 1 56 7 64
11 10 7 12 5 6 -5 -18 -31 -38 -48 -64 -54 -21 -5 -29 -16 -43 -58 -16 -50
-15 -53 5 -82 11 -16 21 -31 22 -33 1 -1 28 1 60 6 l57 9 15 -32 c11 -23 14
-46 9 -79 -8 -50 -18 -55 -71 -32 -32 13 -75 10 -75 -6 0 -19 50 -94 85 -128
32 -31 43 -35 88 -36 139 -1 220 -16 286 -52 58 -32 66 -41 92 -98 24 -53 36
-67 72 -85 50 -26 53 -30 61 -80 5 -32 13 -42 59 -68 41 -24 169 -61 210 -61
4 0 7 -10 7 -21 0 -18 -4 -20 -27 -15 -16 3 -53 10 -83 15 -30 5 -81 21 -112
35 -47 21 -61 33 -79 68 -12 24 -50 75 -84 113 -34 39 -74 87 -90 108 -57 77
-167 107 -265 72 -46 -16 -176 -19 -268 -6 -40 6 -61 15 -87 41 -31 30 -36 32
-62 21 -56 -23 -79 -55 -127 -172 -25 -63 -46 -119 -46 -124 0 -5 17 -38 39
-74 55 -94 57 -106 27 -141 -15 -18 -27 -49 -31 -78 -4 -26 -9 -56 -12 -67 -6
-20 -6 -20 -14 0 -15 39 -9 103 12 140 12 19 19 41 16 48 -3 8 -32 23 -64 35
-33 12 -70 33 -83 47 -17 18 -34 25 -60 25 -31 0 -38 -5 -52 -33 -8 -17 -19
-47 -22 -65 -8 -32 -7 -32 32 -32 43 0 52 -10 52 -59 0 -18 -12 -38 -33 -59
-43 -42 -78 -42 -107 -2 -14 20 -30 30 -48 30 -37 1 -62 22 -62 53 0 20 6 27
29 32 16 3 36 14 45 24 15 16 14 22 -14 77 -24 48 -36 62 -65 73 -33 11 -36
15 -30 41 5 25 2 31 -23 44 -24 13 -49 13 -158 5 -71 -5 -169 -19 -217 -29
-63 -14 -94 -17 -107 -10 -26 14 -81 12 -179 -5 -89 -15 -128 -36 -201 -102
-41 -36 -49 -53 -27 -53 6 0 18 -4 26 -9 11 -7 10 -14 -4 -39 -16 -26 -21 -28
-36 -19 -15 9 -22 8 -33 -3 -17 -19 -56 -136 -56 -171 0 -42 20 -51 112 -49
147 4 142 8 108 -87 -11 -32 -20 -70 -20 -86 0 -17 -5 -47 -12 -69 -9 -29 -9
-41 0 -50 8 -8 12 -49 12 -114 l0 -101 -54 -84 c-47 -73 -58 -100 -82 -205
l-29 -122 -53 -5 c-30 -3 -65 -2 -79 4 -43 16 -113 11 -149 -12 -38 -23 -44
-51 -16 -82 9 -11 12 -17 6 -14 -7 4 -22 -1 -35 -12 -21 -17 -22 -21 -11 -59
9 -29 23 -47 48 -62 31 -20 46 -22 142 -19 l107 4 42 -45 42 -46 -30 -49 c-27
-45 -29 -56 -25 -108 6 -56 4 -60 -25 -85 -17 -15 -72 -50 -122 -77 -72 -39
-96 -58 -109 -85 -24 -50 -22 -66 17 -117 19 -25 35 -49 35 -52 0 -15 -155
-62 -282 -87 -131 -25 -138 -28 -138 -50 0 -13 -3 -24 -6 -24 -3 0 -54 21
-112 47 -80 36 -107 44 -110 34 -9 -28 -40 -48 -83 -55 -24 -4 -60 -11 -80
-16 l-36 -10 14 -62 c10 -44 25 -75 54 -109 29 -35 39 -56 39 -82 0 -29 7 -41
43 -70 97 -80 93 -73 101 -185 l8 -101 -82 -121 c-45 -67 -80 -123 -78 -125 2
-2 23 0 48 5 25 5 46 7 48 5 2 -2 -8 -26 -22 -53 -22 -43 -36 -56 -101 -92
-63 -35 -82 -41 -111 -36 l-34 7 0 -54 0 -55 63 -17 c57 -15 66 -21 90 -61 15
-25 27 -47 27 -50 0 -4 -13 -23 -30 -44 -29 -37 -36 -71 -24 -117 5 -18 14
-23 56 -25 l50 -3 23 -54 c20 -49 56 -83 110 -105 6 -2 -10 -21 -34 -43 l-43
-38 18 -38 c16 -33 22 -37 56 -37 27 0 46 -8 69 -28 l31 -28 -12 -57 -13 -58
33 -34 34 -35 -33 0 c-24 0 -50 -13 -95 -46 -33 -26 -67 -49 -73 -51 -7 -3
-22 -37 -34 -77 -31 -102 -22 -200 25 -293 30 -60 43 -75 112 -122 51 -36 96
-59 132 -67 30 -7 58 -16 62 -21 4 -4 15 -32 23 -62 l16 -55 -85 -79 -84 -79
0 -79 0 -79 31 0 c22 0 52 -15 100 -51 66 -49 69 -53 69 -93 0 -35 6 -48 36
-77 20 -18 49 -62 65 -95 l28 -62 76 -25 75 -26 0 50 c0 27 5 49 10 49 6 0 45
-10 86 -22 82 -24 94 -37 94 -101 l1 -32 25 29 26 29 63 -11 c87 -17 100 -15
145 13 l40 25 65 -17 c59 -16 66 -20 79 -54 15 -36 20 -39 108 -66 90 -28 97
-29 214 -20 l121 9 51 -36 c46 -34 58 -37 136 -43 47 -3 86 -7 86 -9 0 -1 -33
-59 -72 -127 -71 -121 -77 -129 -193 -227 -66 -57 -126 -109 -133 -118 -7 -9
-12 -45 -12 -88 0 -64 -6 -85 -55 -204 -30 -73 -71 -168 -91 -211 -23 -52 -42
-116 -54 -187 l-18 -107 24 -41 23 -41 -44 -112 c-40 -98 -45 -120 -45 -188 0
-55 6 -94 21 -133 11 -30 22 -56 23 -58 2 -1 19 5 39 13 l37 15 0 -45 c0 -25
4 -45 9 -45 10 0 19 3 106 38 l51 20 57 -24 c51 -22 67 -24 132 -18 66 6 77
10 106 40 30 31 38 34 90 34 51 0 60 -3 82 -30 l25 -30 119 16 c182 24 167 16
159 77 -4 28 -9 53 -11 55 -2 2 -20 -5 -42 -16 -70 -36 -78 -36 -95 0 -21 46
-5 87 52 132 37 30 53 36 93 36 41 0 51 -4 76 -32 27 -31 28 -35 16 -65 -12
-29 -12 -32 8 -37 12 -3 41 -6 65 -6 l43 0 -7 35 c-4 22 -2 35 4 35 6 0 17 3
25 6 11 4 20 -9 32 -48 10 -29 18 -54 19 -55 1 -1 30 6 64 16 l61 19 83 -24
c46 -13 108 -24 139 -24 54 0 59 -3 189 -95 l133 -95 68 0 c68 0 68 0 110 45
23 25 45 45 49 45 4 0 13 -11 20 -25 11 -21 21 -25 59 -25 75 0 113 -16 166
-70 l49 -50 -12 -47 c-15 -56 -12 -60 71 -69 31 -4 57 -11 57 -16 0 -5 -7 -25
-15 -45 -8 -20 -13 -38 -11 -40 7 -7 168 24 175 34 3 5 28 41 54 80 45 65 48
73 39 105 -10 35 -5 138 7 138 3 -1 26 -5 51 -10 28 -5 59 -5 83 1 35 9 43 7
88 -22 l50 -32 81 22 c44 12 81 21 82 19 1 -2 12 -41 25 -88 12 -47 26 -89 31
-94 13 -13 147 8 208 32 56 23 54 24 66 -30 l6 -28 76 53 c48 33 80 62 86 80
10 26 13 27 76 27 37 0 72 4 77 8 6 4 24 28 40 54 30 47 31 48 85 49 30 0 152
6 270 12 240 13 222 6 206 88 -7 34 -5 38 27 58 19 11 37 21 41 21 3 0 6 -16
6 -35 0 -19 1 -35 3 -35 1 0 34 9 72 20 38 11 77 20 87 20 9 0 28 -20 42 -45
25 -42 29 -45 71 -45 24 0 65 9 92 21 39 17 59 19 109 14 l62 -6 7 -56 c5 -38
17 -72 38 -104 35 -52 52 -59 143 -59 l54 0 0 48 c0 41 5 53 30 77 33 32 35
42 19 98 -12 43 -40 60 -120 71 l-46 7 40 77 40 78 -23 57 c-18 44 -34 63 -70
88 -40 28 -49 39 -56 78 -8 40 -13 47 -39 51 -30 5 -30 6 -27 60 3 46 10 62
44 102 37 44 44 48 84 48 38 0 51 6 88 40 40 37 52 42 145 59 109 20 91 9 247
146 33 29 35 34 41 115 5 74 10 90 36 122 l29 36 71 -39 c119 -66 113 -64 152
-43 20 10 49 36 66 57 l31 38 -23 61 -22 61 24 69 24 69 -32 29 c-18 17 -38
39 -45 49 -8 11 -29 23 -48 28 -27 7 -36 17 -48 50 -13 33 -22 43 -52 52 -51
15 -71 14 -85 -6 -11 -15 -16 -14 -57 12 -40 25 -47 35 -57 81 -10 45 -20 60
-61 94 -42 35 -51 39 -74 31 -22 -9 -28 -6 -50 22 -14 18 -31 38 -38 44 -81
78 -142 149 -140 163 1 8 -18 30 -43 47 -45 33 -92 42 -112 22 -17 -17 -112
28 -119 57 -4 14 -23 33 -47 46 -43 24 -54 46 -56 110 -1 32 -5 37 -30 43 -26
6 -31 12 -36 49 -8 49 -12 57 -40 72 -14 8 -20 22 -20 45 -1 31 -5 35 -42 48
-38 14 -73 52 -73 80 0 6 14 17 30 26 24 13 30 22 30 49 0 27 5 35 23 40 23 5
74 92 62 105 -4 3 -21 11 -38 17 -26 9 -31 16 -29 38 3 26 -4 30 -40 27 -12
-2 -18 5 -18 17 0 13 -18 29 -59 51 -32 17 -66 31 -75 31 -9 0 -16 8 -16 20 0
12 -14 29 -35 42 -29 19 -34 27 -30 47 10 39 -12 77 -56 96 -40 18 -52 48 -25
58 9 4 12 14 9 25 -7 29 35 41 54 16 7 -10 13 -24 13 -31 0 -7 8 -13 18 -13
22 0 72 -62 72 -89 0 -14 7 -21 19 -21 15 0 22 12 31 51 10 41 23 61 69 106
131 128 140 133 239 133 51 0 73 5 92 20 14 11 32 20 41 20 8 0 19 4 25 10 6
6 25 -1 54 -20 35 -23 52 -29 75 -24 39 8 85 52 85 80 0 21 6 23 81 30 75 6
81 9 90 33 6 14 20 36 31 48 16 17 25 20 45 12 28 -10 43 -5 43 17 0 8 5 14
11 14 7 0 18 8 25 17 12 17 14 17 36 -3 34 -31 69 -36 96 -14 12 10 22 27 22
37 0 10 4 23 9 29 5 5 11 18 13 28 3 22 88 48 125 39 12 -3 32 2 43 10 16 11
37 14 75 10 33 -4 55 -1 59 6 4 6 16 11 26 11 14 0 19 6 17 22 -3 32 52 61
123 64 32 1 68 9 81 17 13 9 43 20 66 26 23 7 50 22 61 35 15 19 32 25 86 31
56 6 69 11 76 29 10 28 6 33 -34 40 -22 4 -37 14 -41 26 -4 11 -17 23 -31 26
-29 7 -28 5 -7 49 9 19 24 35 32 35 29 0 91 -21 91 -30 0 -13 47 -13 72 1 16
9 24 7 42 -10 12 -12 29 -21 37 -21 8 0 20 -9 27 -20 10 -16 9 -27 -3 -57 -17
-41 -13 -53 15 -35 37 23 49 16 39 -25 -5 -21 -9 -41 -9 -44 0 -13 108 -59
137 -59 36 0 63 27 63 63 0 19 11 33 40 54 28 20 40 35 40 53 0 14 10 41 21
60 12 19 41 102 65 185 48 163 49 187 8 242 -14 18 -24 45 -24 60 2 56 -8 115
-21 128 -8 8 -61 31 -119 53 -119 45 -125 53 -90 123 27 53 21 100 -17 124
-44 29 -73 66 -73 93 0 14 -12 38 -26 53 -20 21 -25 34 -20 53 3 14 13 26 21
26 8 0 15 4 15 8 0 4 16 25 35 45 36 38 39 48 36 107 -1 19 5 49 13 65 25 47
20 72 -15 91 -27 13 -30 19 -24 42 3 15 8 58 10 96 l4 68 -60 27 c-46 20 -59
31 -59 47 0 12 -10 47 -21 78 -22 56 -20 106 3 106 7 0 25 12 40 27 29 27 36
61 18 79 -7 7 -7 21 1 43 17 49 -1 72 -93 117 -60 29 -92 52 -126 92 -25 29
-62 60 -81 68 -20 8 -52 30 -71 48 -21 21 -50 36 -73 40 -37 7 -38 9 -31 34 5
15 9 55 9 90 l0 63 75 45 c41 25 87 57 103 72 32 31 51 92 43 140 -4 26 2 46
25 87 29 51 30 55 15 84 -9 16 -16 42 -16 57 0 45 -51 218 -71 240 -14 15 -19
35 -19 75 0 33 -6 63 -15 75 -8 10 -15 33 -15 50 0 34 -16 54 -42 54 -25 0
-23 13 4 25 30 13 34 41 9 55 -16 8 -28 7 -53 -6 -61 -31 -165 -8 -273 61 -22
14 -52 31 -67 37 -41 18 -36 50 15 92 l42 35 -69 66 c-51 48 -63 65 -48 65 11
0 25 7 32 15 21 25 43 19 36 -10 -5 -20 -3 -25 12 -25 25 0 42 19 42 47 0 15
6 23 18 23 24 0 62 -28 62 -45 0 -7 10 -15 23 -17 18 -2 22 -9 22 -38 0 -31
-3 -35 -23 -33 -13 2 -32 -2 -42 -7 -18 -10 -19 -8 -14 20 6 28 5 30 -24 30
-40 0 -46 -11 -26 -53 24 -51 13 -81 -33 -93 -42 -10 -55 -37 -26 -52 20 -11
132 3 148 19 5 5 52 9 105 9 l97 0 0 46 c0 43 -3 48 -56 92 -30 26 -75 66
-100 88 -32 29 -59 44 -87 48 -49 9 -105 52 -128 101 -13 26 -23 35 -42 35
-17 0 -28 -7 -31 -20 -3 -11 -13 -20 -22 -20 -24 0 -33 11 -21 25 23 28 -20
22 -66 -9 -35 -23 -64 -35 -102 -38 -62 -6 -67 -10 -52 -38 10 -19 9 -21 -11
-18 -15 2 -26 14 -33 36 -6 18 -17 37 -24 41 -8 4 -10 15 -6 26 7 16 3 17 -34
11 -33 -5 -42 -3 -42 8 0 8 -6 19 -13 23 -8 4 -22 20 -32 36 -11 18 -26 27
-43 27 -14 0 -33 7 -42 15 -9 8 -34 18 -56 23 -33 7 -39 12 -39 34 0 14 -8 35
-18 46 -12 13 -15 22 -8 27 18 11 12 35 -8 35 -50 0 -60 51 -20 95 10 12 22
35 25 52 3 18 15 40 25 49 11 9 19 28 19 41 0 12 5 23 10 23 6 0 10 9 10 21 0
15 -5 20 -22 17 -17 -2 -24 -12 -29 -38 -3 -19 -11 -46 -19 -60 -8 -14 -16
-41 -20 -60 -7 -45 -19 -47 -220 -49 -129 -1 -166 3 -218 19 -106 33 -116 30
-166 -48 -24 -37 -54 -88 -66 -112 -18 -39 -33 -51 -97 -85 -44 -23 -92 -58
-113 -82 -35 -40 -41 -43 -88 -43 -27 0 -92 -9 -145 -20 -73 -15 -111 -17
-166 -11 -71 7 -72 7 -111 -26 -31 -26 -43 -47 -56 -95 l-17 -60 -48 6 c-56 8
-111 -11 -128 -42 -15 -28 -14 -32 9 -32 10 0 30 -5 44 -11 21 -10 23 -15 14
-35 -13 -28 -17 -29 -43 -11 -11 7 -35 19 -53 27 -29 11 -34 11 -45 -4 -19
-26 -67 -5 -67 29 0 18 -7 26 -24 31 -14 3 -27 12 -30 20 -7 19 -66 18 -155
-2 -58 -13 -76 -22 -93 -45 -27 -36 -34 -36 -50 -1 -9 19 -27 31 -62 42 -65
21 -83 52 -53 95 11 16 21 31 22 32 1 1 18 -2 39 -8 36 -10 39 -8 79 28 23 21
70 56 105 78 l64 40 -6 65 c-3 36 -8 77 -12 91 -4 19 1 34 22 59 l29 33 -61
-6 c-34 -4 -75 -2 -93 3 -26 7 -34 6 -44 -11 -6 -10 -20 -19 -31 -19 -10 0
-41 -16 -67 -35 -69 -49 -112 -48 -163 4 -31 32 -49 41 -99 51 -34 6 -77 20
-97 30 -67 34 -116 42 -176 29 -51 -12 -56 -16 -85 -66 -31 -53 -52 -66 -63
-38 -5 15 12 55 24 55 3 0 11 4 19 9 10 7 11 13 1 30 -9 17 -9 26 0 36 9 11 5
18 -22 35 -38 24 -80 22 -212 -11 -64 -16 -81 -18 -87 -7 -17 26 -8 37 39 52
26 9 66 34 90 57 39 37 42 44 42 92 0 46 -3 55 -32 79 l-33 27 27 1 c34 0 44
16 20 29 -10 6 -26 30 -36 54 -10 28 -26 48 -42 56 -23 10 -26 9 -35 -19 -9
-26 -15 -29 -37 -24 -15 3 -34 15 -43 25 -10 10 -59 34 -110 52 -72 25 -95 38
-99 54 -5 21 -9 20 -85 -27 -81 -49 -94 -50 -95 -6 0 17 -4 16 -32 -11 -18
-17 -48 -34 -66 -38 -30 -6 -37 -3 -65 27 -28 30 -47 38 -142 63 -138 35 -205
45 -227 33 -12 -6 -26 -4 -42 7 -22 14 -28 15 -52 2 -24 -12 -35 -12 -82 0
-50 13 -56 13 -64 -1 -7 -13 -29 -16 -117 -16 -94 0 -112 3 -135 21 -21 17
-26 29 -26 65 0 39 3 46 28 55 45 18 54 24 50 32 -2 5 5 13 16 19 15 8 16 12
5 19 -35 22 -64 10 -92 -38z"/>
<path d="M3145 9843 c-17 -2 -37 -13 -44 -25 -21 -33 14 -60 85 -66 51 -4 59
-2 74 18 12 16 15 31 11 49 -6 23 -13 26 -52 27 -24 1 -58 -1 -74 -3z"/>
<path d="M2992 9757 c-28 -30 -28 -48 1 -75 27 -26 59 -28 78 -6 11 13 10 17
-4 25 -9 6 -25 26 -35 45 l-18 35 -22 -24z"/>
<path d="M7960 9741 c-76 -7 -130 -38 -130 -75 0 -13 -9 -31 -20 -41 -29 -27
-16 -31 19 -6 20 14 28 27 24 37 -6 16 12 34 35 34 19 0 14 -25 -8 -45 -11
-10 -20 -29 -20 -42 0 -20 -5 -23 -41 -23 -38 0 -41 -2 -35 -22 3 -13 6 -26 6
-30 0 -5 14 -8 30 -8 17 0 30 -4 30 -10 0 -5 -15 -10 -34 -10 -25 0 -40 -7
-55 -26 -25 -32 -26 -44 -6 -44 8 0 15 -4 15 -10 0 -5 15 -10 34 -10 22 0 36
-6 40 -16 13 -34 6 -41 -44 -48 -46 -6 -50 -9 -50 -34 0 -19 10 -35 35 -55 19
-15 35 -34 35 -42 0 -8 8 -15 18 -15 9 0 50 -14 91 -30 83 -34 94 -36 110 -16
9 10 9 20 0 39 -14 31 3 67 31 67 11 0 20 7 20 15 0 9 9 15 24 15 14 0 37 5
52 10 20 8 32 7 50 -5 19 -12 27 -13 44 -3 29 19 50 -4 27 -30 -16 -17 -15
-19 18 -34 35 -15 35 -15 35 9 0 13 12 35 26 48 15 14 22 25 17 25 -5 0 -26
16 -46 35 -28 27 -44 35 -72 35 -32 0 -38 4 -51 35 -24 58 -19 74 31 96 56 25
71 62 36 90 -19 16 -35 17 -119 12 l-98 -6 -32 33 c-37 37 -37 49 1 68 30 15
37 43 10 40 -10 0 -47 -4 -83 -7z"/>
<path d="M3325 9562 c-27 -8 -52 -16 -54 -18 -9 -8 31 -54 48 -54 31 0 63 33
59 61 l-3 25 -50 -14z"/>
<path d="M3140 9558 c0 -7 5 -20 10 -28 8 -12 10 -11 10 7 0 12 -4 25 -10 28
-5 3 -10 0 -10 -7z"/>
<path d="M5687 9532 c-19 -20 -27 -39 -27 -65 0 -40 22 -59 43 -38 17 17 37
13 37 -9 0 -10 8 -23 18 -28 19 -10 202 -13 201 -3 0 3 -17 33 -37 65 -35 56
-41 60 -97 76 -114 32 -109 32 -138 2z"/>
<path d="M3110 9465 c0 -19 27 -55 42 -55 20 0 23 12 8 41 -9 16 -50 28 -50
14z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

15
sentry.edge.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
const isProd = process.env.NODE_ENV === 'production';
Sentry.init({
dsn,
enabled: isProd && Boolean(dsn),
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

15
sentry.server.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
const isProd = process.env.NODE_ENV === 'production';
Sentry.init({
dsn,
enabled: isProd && Boolean(dsn),
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@@ -1,5 +0,0 @@
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
console.log('[Instrumentation] Server-side registration active');
}
}