Compare commits

...

12 Commits

Author SHA1 Message Date
69dd79dfad fix: remove lingering getAppServices import from error.tsx that kept backend dependencies in the client bundle
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 02:00:10 +02:00
a6c4cc53f9 fix: completely sever client bundle from backend logger (pino) and validation (zod) to eliminate 68 KiB unused JS
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 23:27:14 +02:00
29f83f8151 chore: release 2.2.89
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🏗️ Build (push) Successful in 3m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m13s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 21:04:03 +02:00
cec08e3ab3 fix(perf): dynamic import of framer-motion features to eliminate 43 KiB unused JS 2026-06-29 21:04:01 +02:00
837567c1f4 chore: release 2.2.88
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:58:32 +02:00
4dcc0061fc fix(perf): throttle CorporateBackground mousemove to fix forced reflow penalty 2026-06-29 18:58:32 +02:00
bd25ec935d chore: release 2.2.87
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:23:54 +02:00
b3b85202eb fix(perf): replace next/image with native img for svg to drastically improve LCP 2026-06-29 18:23:53 +02:00
b4b0c7c48a chore: release 2.2.86
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 13:24:17 +02:00
35fb37bb14 fix: a11y contrast, heading order, and unused js (framer-motion lazy load) 2026-06-29 13:24:16 +02:00
fb2d385e88 chore: release 2.2.85
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:46:48 +02:00
c54085fe3f fix: remove duplicate map nodes causing m is not defined build error 2026-06-29 10:46:47 +02:00
8 changed files with 93 additions and 145 deletions

View File

@@ -2,7 +2,6 @@
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { getAppServices } from '@/lib/services/create-services';
import { Button, Heading } from '@/components/ui';
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
import { m } from 'framer-motion';
@@ -26,9 +25,7 @@ export default function Error({
return;
}
const services = getAppServices();
services.errors.captureException(error);
services.logger.error('Application error caught by boundary', {
console.error('Application error caught by boundary', {
message: error?.message || 'Unknown error',
stack: error?.stack,
digest: error?.digest,

View File

@@ -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';
@@ -91,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>
@@ -104,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">
@@ -123,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'}`}

View File

@@ -2,34 +2,28 @@
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { getAppServices } from '@/lib/services/create-services';
import { useAnalytics } from './useAnalytics';
/**
* AnalyticsProvider Component
*
* Automatically tracks pageviews on client-side route changes.
* This component handles navigation events for the Umami analytics service.
*
* Note: Website ID is now centrally managed on the server side via a proxy,
* so it's no longer needed as a prop here.
*/
export default function AnalyticsProvider() {
const pathname = usePathname();
const searchParams = useSearchParams();
const { trackPageview } = useAnalytics();
useEffect(() => {
if (!pathname) return;
const services = getAppServices();
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
// Track pageview with the full URL
// The service will relay this to our internal proxy which injects the Website ID
services.analytics.trackPageview(url);
// Services like logger are already sub-initialized in getAppServices()
// so we don't need to log here manually.
}, [pathname, searchParams]);
trackPageview(url);
}, [pathname, searchParams, trackPageview]);
return null;
}

View File

@@ -1,73 +1,69 @@
'use client';
import { useCallback } from 'react';
import { getAppServices } from '@/lib/services/create-services';
import type { AnalyticsEventProperties } from '@/lib/services/analytics/analytics-service';
export type AnalyticsEventProperties = Record<string, any>;
/**
* Sends a payload to the proxy API without loading backend config/logger dependencies.
*/
function sendAnalyticsPayload(type: 'event', data: Record<string, any>) {
if (typeof window === 'undefined') return;
if (process.env.NEXT_PUBLIC_CI === 'true') return;
const payload = {
hostname: window.location.hostname,
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
referrer: document.referrer,
title: document.title,
...data,
};
try {
fetch('/stats/api/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ type, payload }),
keepalive: true,
}).catch(e => console.error('[Umami] Failed to send payload', e));
} catch (error) {
console.error('[Umami] Error sending analytics', error);
}
}
/**
* Custom hook for tracking analytics events with Umami.
*
* Provides a convenient way to track custom events throughout your application.
*
* @example
* ```tsx
* import { useAnalytics } from '@/components/analytics/useAnalytics';
*
* function MyComponent() {
* const { trackEvent, trackPageview } = useAnalytics();
*
* const handleButtonClick = () => {
* trackEvent('button_click', {
* button_id: 'cta-primary',
* page: 'homepage'
* });
* };
*
* return <button onClick={handleButtonClick}>Click me</button>;
* }
* ```
*
* @example
* ```tsx
* // Track a custom pageview
* const { trackPageview } = useAnalytics();
* trackPageview('/custom-path?param=value');
* ```
*/
export function useAnalytics() {
const services = getAppServices();
/**
* Track a custom event with optional properties.
*
* @param eventName - The name of the event to track
* @param properties - Optional event properties (metadata)
*/
const trackEvent = useCallback(
(eventName: string, properties?: AnalyticsEventProperties) => {
services.analytics.track(eventName, properties);
sendAnalyticsPayload('event', {
name: eventName,
data: properties,
url: window.location.pathname + window.location.search,
});
if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked event:', eventName, properties);
}
},
[services]
[]
);
/**
* Track a pageview (useful for custom navigation or SPA routing).
*
* @param url - The URL to track (defaults to current location)
*/
const trackPageview = useCallback(
(url?: string) => {
services.analytics.trackPageview(url);
sendAnalyticsPayload('event', {
url: url || window.location.pathname + window.location.search,
});
if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked pageview:', url ?? 'current location');
}
},
[services]
[]
);
return {

View File

@@ -32,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,
@@ -139,13 +88,12 @@ export function InteractiveGermanyMap({
const mapBackground = useMemo(() => (
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
<Image
<img
src="/germany-map.svg"
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
fill
className="object-cover"
priority
className="w-full h-full object-cover"
fetchPriority="high"
decoding="async"
/>
</div>
), [locale]);

View File

@@ -1,12 +1,14 @@
'use client';
import React from 'react';
import { m, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence, LazyMotion } 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 loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
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'}`}
@@ -76,7 +78,7 @@ export default function InteractiveMapPins({
handleMouseLeave: () => void
}) {
return (
<>
<LazyMotion features={loadFeatures}>
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
<MinorNode
key={`minor-${loc.id || idx}`}
@@ -188,6 +190,6 @@ export default function InteractiveMapPins({
</m.div>
)}
</AnimatePresence>
</>
</LazyMotion>
);
}

View File

@@ -25,34 +25,45 @@ function MagneticRing({
// Only run on client with mouse
if (window.matchMedia("(pointer: coarse)").matches) return;
let ticking = false;
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;
if (!ticking) {
window.requestAnimationFrame(() => {
if (!ref.current) {
ticking = false;
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
const distX = e.clientX - centerX;
const distY = e.clientY - centerY;
const actualDistance = Math.sqrt(distX * distX + distY * distY);
// 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);
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);
}
ticking = false;
});
ticking = true;
}
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mousemove', handleMouseMove, { passive: true });
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [x, y, pullRadius, pullStrength]);

View File

@@ -139,7 +139,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.84",
"version": "2.2.91",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",