181 lines
7.7 KiB
TypeScript
181 lines
7.7 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
|
import { MapPin, ArrowUpRight } from 'lucide-react';
|
|
import Image from 'next/image';
|
|
import { usePathname } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import dynamic from 'next/dynamic';
|
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
|
import { Location, projectLocations } from '@/lib/map-data';
|
|
import { standorteLocations } from '@/lib/standorte-data';
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
|
|
const InteractiveMapPins = dynamic(() => import('./InteractiveMapPins'), {
|
|
ssr: false,
|
|
});
|
|
|
|
interface Stat {
|
|
value: string;
|
|
suffix?: string;
|
|
label: string;
|
|
href?: string;
|
|
}
|
|
|
|
interface InteractiveGermanyMapProps {
|
|
badge?: string;
|
|
title?: React.ReactNode;
|
|
description?: string;
|
|
stats?: Stat[];
|
|
locations?: Location[];
|
|
isHero?: boolean;
|
|
hideStandorte?: boolean;
|
|
}
|
|
|
|
|
|
|
|
export function InteractiveGermanyMap({
|
|
badge,
|
|
title,
|
|
description,
|
|
stats,
|
|
locations = projectLocations,
|
|
isHero = false,
|
|
hideStandorte = false
|
|
}: InteractiveGermanyMapProps) {
|
|
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
|
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const handleMouseEnter = useCallback((loc: Location) => {
|
|
if (hoverTimeoutRef.current) {
|
|
clearTimeout(hoverTimeoutRef.current);
|
|
hoverTimeoutRef.current = null;
|
|
}
|
|
setActiveLocation(loc);
|
|
}, []);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
const timeout = setTimeout(() => {
|
|
setActiveLocation(null);
|
|
}, 200);
|
|
hoverTimeoutRef.current = timeout;
|
|
}, []);
|
|
|
|
const pathname = usePathname();
|
|
const locale = useLocale();
|
|
const t = useTranslations('InteractiveGermanyMap');
|
|
const tStandard = useTranslations('StandardPage');
|
|
|
|
const finalStats = stats || [
|
|
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
|
|
{ value: '3', suffix: '', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte', href: `/${locale}/standorte` },
|
|
];
|
|
|
|
const finalBadge = badge || tStandard('badge');
|
|
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
|
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
|
|
|
const finalLocations = useMemo(() => {
|
|
if (hideStandorte) return locations;
|
|
const base = [...locations];
|
|
standorteLocations.forEach(sl => {
|
|
if (!base.some(l => l.id === sl.id)) {
|
|
base.push(sl as Location);
|
|
}
|
|
});
|
|
return base;
|
|
}, [locations, hideStandorte]);
|
|
|
|
const mapBackground = useMemo(() => (
|
|
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
|
|
<img
|
|
src="/germany-map.svg"
|
|
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
|
className="w-full h-full object-cover"
|
|
fetchPriority="high"
|
|
decoding="async"
|
|
/>
|
|
</div>
|
|
), [locale]);
|
|
|
|
return (
|
|
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
|
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
|
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
|
|
|
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
|
|
<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 overflow-hidden" />
|
|
|
|
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
|
|
|
|
<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>{finalBadge}</span>
|
|
</div>
|
|
|
|
{isHero ? (
|
|
<h1 className="font-heading text-3xl md: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 text-balance">
|
|
{finalTitle}
|
|
</h1>
|
|
) : (
|
|
<h3 className="font-heading text-3xl md: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 text-balance">
|
|
{finalTitle}
|
|
</h3>
|
|
)}
|
|
|
|
<p className="text-white/60 text-lg mb-12 leading-relaxed">
|
|
{finalDescription}
|
|
</p>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{finalStats.map((stat, i) => {
|
|
const StatCard = () => (
|
|
<div className={`bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group h-full ${stat.href ? 'cursor-pointer' : ''}`}>
|
|
<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 flex flex-col h-full justify-between">
|
|
<div className="text-3xl md: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 flex items-center ${stat.href ? 'group-hover:text-primary transition-colors' : ''}`}>
|
|
{stat.label}
|
|
{stat.href && <ArrowUpRight className="w-4 h-4 ml-1 opacity-0 -translate-y-1 translate-x-1 group-hover:opacity-100 group-hover:translate-y-0 group-hover:translate-x-0 transition-all duration-300" />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return stat.href ? (
|
|
<Link key={i} href={stat.href} className="block group/link hover:-translate-y-1 transition-transform duration-300">
|
|
<StatCard />
|
|
</Link>
|
|
) : (
|
|
<div key={i}>
|
|
<StatCard />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
|
|
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
|
|
|
{mapBackground}
|
|
|
|
<InteractiveMapPins
|
|
locations={finalLocations}
|
|
locale={locale}
|
|
t={t}
|
|
activeLocation={activeLocation}
|
|
handleMouseEnter={handleMouseEnter}
|
|
handleMouseLeave={handleMouseLeave}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|