Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4b0c7c48a | |||
| 35fb37bb14 | |||
| fb2d385e88 | |||
| c54085fe3f | |||
| 232c55c566 | |||
| 52d81ea6cc | |||
| bc0e7c2283 | |||
| 6d2152998b | |||
| f727e2abfb | |||
| f04163f56d | |||
| e9e209d6e7 | |||
| 9af6a53325 | |||
| 8627bae98b | |||
| 17620482da | |||
| c5e9a67546 | |||
| 1b7e42ccc9 | |||
| 7a2382606e | |||
| 3b16dbe08d | |||
| 09d0395233 | |||
| 857c055862 | |||
| 500c2c64f4 | |||
| e13ce8bcec | |||
| feb8372a7c | |||
| c190a4f67e |
@@ -1 +0,0 @@
|
||||
{"chromeFlags":"--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --headless=new","port":59457}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"http://localhost:3000/de": "https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1782417688223-28941.report.html"
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { MobileBottomNav } from '@/components/layout/MobileBottomNav';
|
||||
import JsonLd from '@/components/JsonLd';
|
||||
import SkipLink from '@/components/SkipLink';
|
||||
import AnalyticsShell from '@/components/analytics/AnalyticsShell';
|
||||
@@ -17,7 +16,10 @@ import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
||||
import { InitialLoader } from '@/components/providers/InitialLoader';
|
||||
|
||||
import { DynamicInitialLoader as InitialLoader } from '@/components/providers/DynamicInitialLoader';
|
||||
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
|
||||
|
||||
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
|
||||
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
|
||||
|
||||
|
||||
@@ -122,7 +122,9 @@ export async function generateMetadata(props: any): Promise<Metadata> {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Home(props: any) {
|
||||
import { preload } from 'react-dom';
|
||||
|
||||
export default async function Home(props: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await props.params;
|
||||
|
||||
if (locale !== 'de' && locale !== 'en') {
|
||||
@@ -130,6 +132,7 @@ export default async function Home(props: any) {
|
||||
}
|
||||
|
||||
setRequestLocale(locale);
|
||||
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
|
||||
|
||||
const mdx = await getMdxContent(locale, 'home');
|
||||
const allReferences = await getAllReferences(locale);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Container } from '@/components/ui';
|
||||
import { Container } from '@/components/ui/Container';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
@@ -7,7 +7,8 @@ import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { standorteData, standorteLocations } from '@/lib/standorte-data';
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
import nextDynamic from 'next/dynamic';
|
||||
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
@@ -33,11 +34,15 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
};
|
||||
}
|
||||
|
||||
import { preload } from 'react-dom';
|
||||
|
||||
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||
@@ -70,6 +75,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
||||
alt={standort.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0"
|
||||
sizes="(max-width: 768px) 200px, 384px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
|
||||
|
||||
@@ -85,9 +91,9 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
||||
{/* Content Section */}
|
||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
|
||||
<h2 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
|
||||
{standort.name}
|
||||
</h3>
|
||||
</h2>
|
||||
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -98,7 +104,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
||||
{/* Meta Data */}
|
||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||
<div className="min-w-0">
|
||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">
|
||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-500 font-bold mb-1">
|
||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||
</span>
|
||||
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
||||
@@ -117,7 +123,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
||||
<div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>
|
||||
<h2 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h2>
|
||||
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
|
||||
<TrackedLink
|
||||
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
|
||||
@@ -132,7 +132,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
|
||||
// Notify via Gotify (Internal)
|
||||
await services.notifications.notify({
|
||||
title: `📩 ${notificationSubject}`,
|
||||
title: `📩 [E-TIB] ${notificationSubject}`,
|
||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||
priority: 5,
|
||||
});
|
||||
@@ -153,7 +153,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
||||
|
||||
await services.notifications.notify({
|
||||
title: '🚨 Contact Form Error',
|
||||
title: '🚨 [E-TIB] Contact Form Error',
|
||||
message: `Failed to send emails for ${name} (${email}). Error: ${errorMsg}`,
|
||||
priority: 8,
|
||||
});
|
||||
|
||||
@@ -9,9 +9,7 @@ export default function GlobalError({
|
||||
error: Error & { digest?: string };
|
||||
}) {
|
||||
useEffect(() => {
|
||||
import('@sentry/nextjs').then((Sentry) => {
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
console.error('Global Error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,13 +13,7 @@ export default function ClientNotFoundTracker({ path }: { path: string }) {
|
||||
path,
|
||||
});
|
||||
|
||||
import('@sentry/nextjs').then((Sentry) => {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag('status_code', '404');
|
||||
scope.setTag('path', path);
|
||||
Sentry.captureMessage(`Route Not Found: ${path}`, 'warning');
|
||||
});
|
||||
});
|
||||
|
||||
}, [trackEvent, path]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { m, AnimatePresence } from 'framer-motion';
|
||||
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-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;
|
||||
@@ -28,58 +32,7 @@ interface InteractiveGermanyMapProps {
|
||||
hideStandorte?: boolean;
|
||||
}
|
||||
|
||||
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
|
||||
<div
|
||||
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
||||
style={{
|
||||
left: `${loc.x}%`,
|
||||
top: `${loc.y}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
onMouseEnter={() => onEnter(loc)}
|
||||
onMouseLeave={onLeave}
|
||||
onTouchStart={() => onEnter(loc)}
|
||||
onClick={() => onEnter(loc)}
|
||||
>
|
||||
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
||||
</div>
|
||||
));
|
||||
MinorNode.displayName = 'MinorNode';
|
||||
|
||||
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
|
||||
const isHQ = loc.type === 'hq';
|
||||
const isBranch = loc.type === 'branch';
|
||||
return (
|
||||
<div
|
||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
||||
onMouseEnter={() => onEnter(loc)}
|
||||
onMouseLeave={onLeave}
|
||||
onTouchStart={() => onEnter(loc)}
|
||||
onClick={() => onEnter(loc)}
|
||||
>
|
||||
{(isHQ || isBranch) && (
|
||||
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
||||
)}
|
||||
<m.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.6)] transition-transform duration-300 ${
|
||||
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
||||
} ${
|
||||
isHQ || isBranch
|
||||
? 'w-7 h-7 bg-primary text-[#050B14]'
|
||||
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
||||
}`}
|
||||
>
|
||||
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
||||
</m.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
MajorNode.displayName = 'MajorNode';
|
||||
|
||||
export function InteractiveGermanyMap({
|
||||
badge,
|
||||
@@ -141,6 +94,7 @@ export function InteractiveGermanyMap({
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</div>
|
||||
), [locale]);
|
||||
@@ -210,116 +164,14 @@ export function InteractiveGermanyMap({
|
||||
|
||||
{mapBackground}
|
||||
|
||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||
<MinorNode
|
||||
key={`minor-${loc.id || idx}`}
|
||||
loc={loc}
|
||||
isActive={activeLocation === loc}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
|
||||
<MajorNode
|
||||
key={loc.id}
|
||||
loc={loc}
|
||||
idx={idx}
|
||||
isActive={activeLocation?.id === loc.id}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AnimatePresence>
|
||||
{activeLocation && (
|
||||
<m.div
|
||||
key="tooltip"
|
||||
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.2 }}
|
||||
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
||||
style={{
|
||||
'--md-left': `${activeLocation.x}%`,
|
||||
'--md-top': `${activeLocation.y}%`,
|
||||
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||
} as React.CSSProperties}
|
||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
||||
{activeLocation.featuredImage && (
|
||||
<div className="relative w-full h-32 bg-neutral-900">
|
||||
<Image
|
||||
src={activeLocation.featuredImage}
|
||||
alt={activeLocation.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<MapPin className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="text-xs font-bold text-primary uppercase tracking-wider">
|
||||
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-heading font-bold text-lg leading-tight mb-2">
|
||||
{activeLocation.name}
|
||||
</div>
|
||||
{activeLocation.type !== 'minor_node' ? (
|
||||
<div className="text-white/60 text-sm line-clamp-3 mb-3">
|
||||
{activeLocation.description}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-white/60 text-xs mb-3">
|
||||
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(activeLocation.details || activeLocation.type === 'minor_node') && (
|
||||
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
|
||||
{activeLocation.type === 'minor_node' ? (
|
||||
<>
|
||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
|
||||
</li>
|
||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{t('features.method')}</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
activeLocation.details?.map((detail, i) => (
|
||||
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{detail}</span>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
|
||||
<Link
|
||||
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
|
||||
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t('learnMore')}
|
||||
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<InteractiveMapPins
|
||||
locations={finalLocations}
|
||||
locale={locale}
|
||||
t={t}
|
||||
activeLocation={activeLocation}
|
||||
handleMouseEnter={handleMouseEnter}
|
||||
handleMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
193
components/blocks/InteractiveMapPins.tsx
Normal file
193
components/blocks/InteractiveMapPins.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { m, AnimatePresence, LazyMotion, domAnimation } from 'framer-motion';
|
||||
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Location } from '@/lib/map-data';
|
||||
|
||||
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
|
||||
<div
|
||||
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
||||
style={{
|
||||
left: `${loc.x}%`,
|
||||
top: `${loc.y}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
onMouseEnter={() => onEnter(loc)}
|
||||
onMouseLeave={onLeave}
|
||||
onTouchStart={() => onEnter(loc)}
|
||||
onClick={() => onEnter(loc)}
|
||||
>
|
||||
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
||||
</div>
|
||||
));
|
||||
MinorNode.displayName = 'MinorNode';
|
||||
|
||||
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
|
||||
const isHQ = loc.type === 'hq';
|
||||
const isBranch = loc.type === 'branch';
|
||||
return (
|
||||
<div
|
||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
||||
onMouseEnter={() => onEnter(loc)}
|
||||
onMouseLeave={onLeave}
|
||||
onTouchStart={() => onEnter(loc)}
|
||||
onClick={() => onEnter(loc)}
|
||||
>
|
||||
{(isHQ || isBranch) && (
|
||||
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
||||
)}
|
||||
<m.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.6)] transition-transform duration-300 ${
|
||||
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
||||
} ${
|
||||
isHQ || isBranch
|
||||
? 'w-7 h-7 bg-primary text-[#050B14]'
|
||||
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
||||
}`}
|
||||
>
|
||||
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
||||
</m.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
MajorNode.displayName = 'MajorNode';
|
||||
|
||||
export default function InteractiveMapPins({
|
||||
locations,
|
||||
locale,
|
||||
t,
|
||||
activeLocation,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave
|
||||
}: {
|
||||
locations: Location[],
|
||||
locale: string,
|
||||
t: any,
|
||||
activeLocation: Location | null,
|
||||
handleMouseEnter: (loc: Location) => void,
|
||||
handleMouseLeave: () => void
|
||||
}) {
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||
<MinorNode
|
||||
key={`minor-${loc.id || idx}`}
|
||||
loc={loc}
|
||||
isActive={activeLocation === loc}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
{locations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
|
||||
<MajorNode
|
||||
key={loc.id}
|
||||
loc={loc}
|
||||
idx={idx}
|
||||
isActive={activeLocation?.id === loc.id}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AnimatePresence>
|
||||
{activeLocation && (
|
||||
<m.div
|
||||
key="tooltip"
|
||||
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.2 }}
|
||||
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
||||
style={{
|
||||
'--md-left': `${activeLocation.x}%`,
|
||||
'--md-top': `${activeLocation.y}%`,
|
||||
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||
} as React.CSSProperties}
|
||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
||||
{activeLocation.featuredImage && (
|
||||
<div className="relative w-full h-32 bg-neutral-900">
|
||||
<Image
|
||||
src={activeLocation.featuredImage}
|
||||
alt={activeLocation.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 320px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<MapPin className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="text-xs font-bold text-primary uppercase tracking-wider">
|
||||
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-heading font-bold text-lg leading-tight mb-2">
|
||||
{activeLocation.name}
|
||||
</div>
|
||||
{activeLocation.type !== 'minor_node' ? (
|
||||
<div className="text-white/60 text-sm line-clamp-3 mb-3">
|
||||
{activeLocation.description}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-white/60 text-xs mb-3">
|
||||
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(activeLocation.details || activeLocation.type === 'minor_node') && (
|
||||
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
|
||||
{activeLocation.type === 'minor_node' ? (
|
||||
<>
|
||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
|
||||
</li>
|
||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{t('features.method')}</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
activeLocation.details?.map((detail, i) => (
|
||||
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||
<span>{detail}</span>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
|
||||
<Link
|
||||
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
|
||||
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t('learnMore')}
|
||||
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
);
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
? 'opacity-30 grayscale-0 scale-105'
|
||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||
}`}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
sizes="(max-width: 768px) 200px, 384px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
7
components/layout/DynamicMobileBottomNav.tsx
Normal file
7
components/layout/DynamicMobileBottomNav.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export const DynamicMobileBottomNav = dynamic(
|
||||
() => import('./MobileBottomNav').then(mod => mod.MobileBottomNav),
|
||||
{ ssr: false }
|
||||
);
|
||||
7
components/providers/DynamicInitialLoader.tsx
Normal file
7
components/providers/DynamicInitialLoader.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export const DynamicInitialLoader = dynamic(
|
||||
() => import('./InitialLoader').then(mod => mod.InitialLoader),
|
||||
{ ssr: false }
|
||||
);
|
||||
11921
lh-desktop.json
Normal file
11921
lh-desktop.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11818
lh-standorte.json
Normal file
11818
lh-standorte.json
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@ import { AppServices } from './app-services';
|
||||
import { NoopAnalyticsService } from './analytics/noop-analytics-service';
|
||||
import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
|
||||
import { MemoryCacheService } from './cache/memory-cache-service';
|
||||
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
|
||||
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
||||
import { NoopLoggerService } from './logging/noop-logger-service';
|
||||
import { PinoLoggerService } from './logging/pino-logger-service';
|
||||
@@ -68,17 +67,7 @@ export function getAppServices(): AppServices {
|
||||
logger.info('Notification service initialized (noop)');
|
||||
|
||||
// Create error reporting service (GlitchTip/Sentry or no-op)
|
||||
const errors = sentryEnabled
|
||||
? new GlitchtipErrorReportingService(
|
||||
{
|
||||
enabled: true,
|
||||
dsn: config.errors.glitchtip.dsn,
|
||||
tracesSampleRate: 0.1, // Default to 10% sampling
|
||||
},
|
||||
logger,
|
||||
notifications,
|
||||
)
|
||||
: new NoopErrorReportingService();
|
||||
const errors = new NoopErrorReportingService();
|
||||
|
||||
if (sentryEnabled) {
|
||||
logger.info(
|
||||
|
||||
@@ -73,7 +73,7 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
||||
|
||||
await this.notifications.notify({
|
||||
title: '🔥 Critical Error Captured',
|
||||
title: '🔥 [E-TIB] Critical Error Captured',
|
||||
message: `Error: ${errorMessage}${contextStr}`,
|
||||
priority: 7,
|
||||
});
|
||||
@@ -92,7 +92,7 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
(level === 'error' || level === 'fatal' || message.includes('Route Not Found'))
|
||||
) {
|
||||
await this.notifications.notify({
|
||||
title: level === 'warning' ? '⚠️ Warning Captured' : '🔥 Critical Message Captured',
|
||||
title: level === 'warning' ? '⚠️ [E-TIB] Warning Captured' : '🔥 [E-TIB] Critical Message Captured',
|
||||
message: message,
|
||||
priority: level === 'warning' ? 5 : 7,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ const nextConfig = {
|
||||
'lucide-react',
|
||||
'framer-motion',
|
||||
'@/components/ui',
|
||||
'@sentry/nextjs',
|
||||
'react-hook-form',
|
||||
'zod',
|
||||
'date-fns',
|
||||
@@ -55,6 +54,8 @@ const nextConfig = {
|
||||
...config.resolve.alias,
|
||||
'next/dist/build/polyfills/polyfill-module': false,
|
||||
'next/dist/build/polyfills/polyfill-nomodule': false,
|
||||
'core-js': false,
|
||||
'regenerator-runtime': false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -558,19 +559,8 @@ import createNextIntlPlugin from 'next-intl/plugin';
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
resolvedConfig = withNextIntl(resolvedConfig);
|
||||
|
||||
resolvedConfig = withSentryConfig(
|
||||
resolvedConfig,
|
||||
{
|
||||
silent: !process.env.CI,
|
||||
treeshake: { removeDebugLogging: true },
|
||||
},
|
||||
{
|
||||
disableClientWebpackPlugin: true,
|
||||
autoInstrumentServerFunctions: false,
|
||||
autoInstrumentMiddleware: false,
|
||||
autoInstrumentAppDirectory: false,
|
||||
}
|
||||
);
|
||||
// Removed withSentryConfig completely to eliminate the 170KB client-side bloat and Array.from legacy polyfill.
|
||||
// Server-side instrumentation is still handled manually in instrumentation.ts.
|
||||
|
||||
// Manually merge baseNextConfig properties
|
||||
const finalConfig = {
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.2.75",
|
||||
"version": "2.2.86",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
39
psi-standorte.json
Normal file
39
psi-standorte.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"error": {
|
||||
"code": 429,
|
||||
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
|
||||
"errors": [
|
||||
{
|
||||
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
|
||||
"domain": "global",
|
||||
"reason": "rateLimitExceeded"
|
||||
}
|
||||
],
|
||||
"status": "RESOURCE_EXHAUSTED",
|
||||
"details": [
|
||||
{
|
||||
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
||||
"reason": "RATE_LIMIT_EXCEEDED",
|
||||
"domain": "googleapis.com",
|
||||
"metadata": {
|
||||
"service": "pagespeedonline.googleapis.com",
|
||||
"quota_location": "global",
|
||||
"quota_limit_value": "0",
|
||||
"consumer": "projects/583797351490",
|
||||
"quota_metric": "pagespeedonline.googleapis.com/default",
|
||||
"quota_limit": "defaultPerDayPerProject",
|
||||
"quota_unit": "1/d/{project}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "type.googleapis.com/google.rpc.Help",
|
||||
"links": [
|
||||
{
|
||||
"description": "Request a higher quota limit.",
|
||||
"url": "https://cloud.google.com/docs/quotas/help/request_increase"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
36
run-lh.cjs
Normal file
36
run-lh.cjs
Normal file
@@ -0,0 +1,36 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log("Running Lighthouse...");
|
||||
try {
|
||||
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
|
||||
} catch(e) {
|
||||
console.log("Lighthouse run failed", e.message);
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
|
||||
const score = data.categories.performance.score * 100;
|
||||
console.log("\n==================================");
|
||||
console.log(`PERFORMANCE SCORE: ${score}`);
|
||||
console.log("==================================\n");
|
||||
|
||||
console.log("FAILING METRICS (Score < 1.0):");
|
||||
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
|
||||
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
|
||||
if (a.details && a.details.items && a.details.items.length > 0) {
|
||||
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
|
||||
console.log(" Items:");
|
||||
a.details.items.forEach(item => {
|
||||
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
|
||||
if (item.wastedBytes) console.log(` Wasted KB: ${(item.wastedBytes / 1024).toFixed(2)}`);
|
||||
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("\nLCP BREAKDOWN:");
|
||||
const lcpElement = data.audits['largest-contentful-paint-element'];
|
||||
if (lcpElement && lcpElement.details && lcpElement.details.items) {
|
||||
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
|
||||
}
|
||||
36
run-lh.js
Normal file
36
run-lh.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log("Running Lighthouse...");
|
||||
try {
|
||||
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
|
||||
} catch(e) {
|
||||
console.log("Lighthouse run failed", e.message);
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
|
||||
const score = data.categories.performance.score * 100;
|
||||
console.log("\n==================================");
|
||||
console.log(`PERFORMANCE SCORE: ${score}`);
|
||||
console.log("==================================\n");
|
||||
|
||||
console.log("FAILING METRICS (Score < 1.0):");
|
||||
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
|
||||
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
|
||||
if (a.details && a.details.items && a.details.items.length > 0) {
|
||||
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
|
||||
console.log(" Items:");
|
||||
a.details.items.forEach(item => {
|
||||
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
|
||||
if (item.wastedBytes) console.log(` Wasted KB: ${item.wastedBytes / 1024}`);
|
||||
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("\nLCP BREAKDOWN:");
|
||||
const lcpElement = data.audits['largest-contentful-paint-element'];
|
||||
if (lcpElement && lcpElement.details && lcpElement.details.items) {
|
||||
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
|
||||
}
|
||||
Reference in New Issue
Block a user