Compare commits

..

14 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
43 changed files with 1579 additions and 176 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

@@ -18,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'],
@@ -175,9 +176,10 @@ 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 />

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

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

@@ -32,7 +32,7 @@ export function Footer({ companyInfo }: FooterProps) {
<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-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">
<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"
@@ -59,11 +59,11 @@ export function Footer({ companyInfo }: FooterProps) {
</div>
</div>
<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">
<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>
@@ -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>
@@ -114,7 +114,7 @@ export function Footer({ companyInfo }: FooterProps) {
{ 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>

View File

@@ -71,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'
@@ -125,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')
@@ -162,7 +162,7 @@ export function Header({ navLinks }: HeaderProps) {
{/* 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 ${
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)]'

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

@@ -72,7 +72,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
<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 ${
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'
}`}
>

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');
}
}