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
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
'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>
|
|
);
|
|
}
|