Compare commits

...
Author SHA1 Message Date
mmintel a9f16b49cc fix(performance): disable inlineCss to reduce HTML size from 557KB to 162KB and fix FCP fluctuations
Build & Deploy / 🔍 Prepare (push) Successful in 57s
Build & Deploy / 🧪 QA (push) Successful in 1m18s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 12:26:05 +02:00
mmintel 21cf0725ab release: v2.4.5
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 10:29:32 +02:00
mmintel 2c162f5314 perf: completely fix non-composited animations, eliminate shutter unused JS, and aggressively compress images 2026-07-02 10:29:31 +02:00
mmintel be3b2e6a7b release: v2.4.4
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m18s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-01 18:23:50 +02:00
mmintel f2e59fa52f perf: fix sizes attribute scaling bug to prevent mobile devices from downloading 1920w desktop images 2026-07-01 18:23:50 +02:00
mmintel 16d494689f release: v2.4.3
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m15s
Build & Deploy / 🏗️ Build (push) Successful in 2m26s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 52s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 18:21:25 +02:00
mmintel d24b8a8694 perf: prevent bandwidth theft from non-critical preloads and strip InitialLoader chunk 2026-07-01 18:21:25 +02:00
mmintel 8d42946711 release: v2.4.2
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 18:11:33 +02:00
mmintel b15217b9be content: update Hochspannung wording per WhatsApp screenshot 2026-07-01 18:11:33 +02:00
mmintel b138025380 release: v2.4.1
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 17:19:41 +02:00
mmintel 18d2ace03b perf: compress hero image and defer page transition shutter
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
2026-07-01 17:19:35 +02:00
mmintel 89ed1faa6b release: v2.4.0
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🧪 QA (push) Successful in 1m15s
Build & Deploy / 🏗️ Build (push) Successful in 2m7s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-07-01 15:30:55 +02:00
mmintel 7307e812a4 release: v2.2.96 2026-07-01 15:30:49 +02:00
mmintel 20d6194a7e perf: fix massive LCP render delay and TTFB on Home page
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
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-07-01 15:30:28 +02:00
mmintel 86e2fd7097 2.2.95
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
Build & Deploy / 🏗️ Build (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 13:35:58 +02:00
mmintel bf8073f5bf perf: optimize LCP images and enable AVIF
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m2s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 13:35:50 +02:00
mmintel e8adb87b02 fix(lcp): revert DynamicFramerMotion wrapper to prevent React 18 Suspense DOM deletion during hydration
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Failing after 48s
2026-07-01 12:10:15 +02:00
mmintel 067e34c19b fix(lcp): revert unoptimized=true on HeroVideo to restore Next.js image optimization for massive CMS images
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 42s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 12:07:18 +02:00
mmintel e6017e7436 perf(images): remove AVIF format to prevent massive CPU spikes and TTFB delays during on-the-fly Next.js image optimization
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 02:24:38 +02:00
mmintel 1955bfcd1b perf(lcp): bypass Next.js image optimization for hero poster and use static WebP to eliminate server-side TTFB block
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m51s
Build & Deploy / 🏗️ Build (push) Successful in 3m29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-01 01:15:18 +02:00
mmintel 73886406a4 perf: completely lazy load framer-motion core via DynamicFramerMotion wrapper
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 23:05:31 +02:00
mmintel b2701d6bcd perf(server): aggressively cache OG image assets in memory to prevent synchronous file I/O from blocking the Node event loop
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 23:02:12 +02:00
mmintel 4f46825fb4 perf: fix 3.8s LCP delays by removing dynamic hero and heavy css filters
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🏗️ Build (push) Successful in 3m21s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 11:47:23 +02:00
mmintel c63766711d chore: release 2.2.92
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 10:19:29 +02:00
mmintel c738001f9a perf: eliminate feedback widget chunk via dead-code elimination
Build & Deploy / 🔍 Prepare (push) Successful in 1m13s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 10:09:45 +02:00
mmintel 69dd79dfad fix: remove lingering getAppServices import from error.tsx that kept backend dependencies in the client bundle
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
mmintel a6c4cc53f9 fix: completely sever client bundle from backend logger (pino) and validation (zod) to eliminate 68 KiB unused JS
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
mmintel 29f83f8151 chore: release 2.2.89
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
mmintel cec08e3ab3 fix(perf): dynamic import of framer-motion features to eliminate 43 KiB unused JS 2026-06-29 21:04:01 +02:00
mmintel 837567c1f4 chore: release 2.2.88
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
mmintel 4dcc0061fc fix(perf): throttle CorporateBackground mousemove to fix forced reflow penalty 2026-06-29 18:58:32 +02:00
mmintel bd25ec935d chore: release 2.2.87
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
mmintel b3b85202eb fix(perf): replace next/image with native img for svg to drastically improve LCP 2026-06-29 18:23:53 +02:00
mmintel b4b0c7c48a chore: release 2.2.86
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
mmintel 35fb37bb14 fix: a11y contrast, heading order, and unused js (framer-motion lazy load) 2026-06-29 13:24:16 +02:00
mmintel fb2d385e88 chore: release 2.2.85
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
mmintel c54085fe3f fix: remove duplicate map nodes causing m is not defined build error 2026-06-29 10:46:47 +02:00
mmintel 232c55c566 chore: release 2.2.84
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🧪 QA (push) Failing after 1m38s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:42:04 +02:00
mmintel 52d81ea6cc perf: completely eliminate mobile lighthouse penalties via ssr: false map pins and 200px sizes 2026-06-29 10:42:04 +02:00
mmintel bc0e7c2283 chore: release 2.2.83
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:19:44 +02:00
mmintel 6d2152998b perf: revert InteractiveGermanyMap to dynamic import and use ReactDOM.preload for LCP 2026-06-29 10:19:44 +02:00
mmintel f727e2abfb chore: release 2.2.82
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-28 16:37:01 +02:00
mmintel f04163f56d perf: explicit 384px sizes to eliminate lighthouse image delivery penalties 2026-06-28 16:37:01 +02:00
mmintel e9e209d6e7 chore: release 2.2.81
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-27 22:56:54 +02:00
mmintel 9af6a53325 perf: optimize Image component sizes and hoist LCP preload tags 2026-06-27 22:56:52 +02:00
mmintel 8627bae98b chore: release 2.2.80
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 2m43s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-27 21:35:44 +02:00
mmintel 17620482da build: ruthlessly strip core-js polyfills to fix Lighthouse Legacy JS audit
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
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-27 21:34:34 +02:00
mmintel c5e9a67546 2.2.79
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-27 10:22:37 +02:00
mmintel 1b7e42ccc9 fix(performance): completely eradicate sentry from client payload to achieve 100/100 LH score
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔍 Prepare (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
2026-06-27 10:22:30 +02:00
mmintel 7a2382606e chore: release 2.2.78
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 43s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-27 08:30:42 +02:00
mmintel 3b16dbe08d perf: completely remove withSentryConfig from next.config.mjs to drop 170KB unused JS and legacy Array.from polyfills from the client bundle 2026-06-27 08:30:42 +02:00
mmintel 09d0395233 refactor: remove obsolete Lighthouse report files and associated artifacts 2026-06-27 08:17:49 +02:00
mmintel 857c055862 chore(notifications): add [E-TIB] prefix to Gotify messages
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
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-27 08:16:07 +02:00
mmintel 500c2c64f4 chore: release 2.2.77
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🏗️ Build (push) Successful in 3m42s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m15s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-27 08:09:37 +02:00
mmintel e13ce8bcec fix: resolve ssr:false in Server Component build error by creating Client Component wrappers 2026-06-27 08:09:26 +02:00
mmintel feb8372a7c chore: release 2.2.76
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Failing after 2m28s
2026-06-27 08:01:31 +02:00
mmintel c190a4f67e perf: dynamically import InitialLoader and MobileBottomNav to remove framer-motion from main JS bundle 2026-06-27 08:01:27 +02:00
mmintel 6717b32d44 chore: release 2.2.75
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-27 07:20:55 +02:00
mmintel f1732917bc perf: enable experimental.inlineCss to eliminate 380ms render-blocking CSS waterfall 2026-06-27 07:20:50 +02:00
mmintel 1f666d1b34 chore: release 2.2.74
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m35s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-27 06:14:39 +02:00
mmintel ab42590206 test: fix gatekeeper bypass logic in pagespeed-sitemap.ts by passing TARGET_URL 2026-06-27 06:14:33 +02:00
mmintel 7d4ff03806 chore: release 2.2.73
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🏗️ Build (push) Successful in 3m34s
Build & Deploy / 🧪 QA (push) Successful in 1m52s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m15s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-26 21:22:51 +02:00
mmintel 26ed3e3f1f fix(perf): remove animate-fade-in from main and force sync decode on logo to eliminate 2s Element Render Delay and align LCP with FCP 2026-06-26 21:22:38 +02:00
mmintel eaf40dec7c chore: release 2.2.72
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Successful in 1m52s
Build & Deploy / 🏗️ Build (push) Successful in 3m34s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-26 19:22:17 +02:00
mmintel d56ce045c3 fix(perf): replace framer-motion with pure css animations in HeroSection to eliminate JS-induced 4.5s LCP delay 2026-06-26 19:22:17 +02:00
mmintel 586b6cdee8 chore: release 2.2.71
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 3m40s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m17s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-26 13:00:33 +02:00
mmintel f1b8ee1188 fix(perf): completely disable Sentry client-side auto-instrumentation to save 556KB of JS
Build & Deploy / 🔍 Prepare (push) Successful in 1m18s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-26 12:24:07 +02:00
mmintel 88cc214ddb fix(ci): use correct auth cookie name for e-tib gatekeeper bypass
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m29s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-26 11:21:24 +02:00
mmintel 25facb995f chore: bump version for fix
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m32s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-26 10:22:03 +02:00
mmintel 7b01924055 fix: revert optimizeCss to fix LCP/FCP regression caused by critters with Tailwind v4 2026-06-26 10:21:57 +02:00
mmintel 474672b2a1 chore: release 2.2.69 (PageSpeed optimizations)
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m19s
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-06-26 09:53:12 +02:00
mmintel 7e552bbea5 chore: release 2.2.68
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-25 22:40:40 +02:00
mmintel 21886a693c fix: resolve yaml syntax error in deploy.yml and invalid next.config 2026-06-25 22:40:38 +02:00
mmintel 45944cbfa7 chore: release 2.2.67
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 22:32:19 +02:00
mmintel 761c9c956f chore: release 2.2.65 2026-06-25 22:32:14 +02:00
mmintel e18559b83c fix: enforce deployment priorities and completely strip client sentry bundle 2026-06-25 22:32:14 +02:00
mmintel 818ccc8fae chore: release 2.2.64
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m17s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 22:08:40 +02:00
mmintel b64b2ccf25 fix: pipeline execution, deployment priority, and bundle optimizations
Build & Deploy / 🔍 Prepare (push) Successful in 1m33s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 22:08:26 +02:00
mmintel e219b3b818 fix: apply GIT_HTTP_VERSION workaround globally for all jobs
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 18:25:28 +02:00
77 changed files with 43600 additions and 41651 deletions
+24 -2
View File
@@ -20,6 +20,7 @@ concurrency:
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
GIT_HTTP_VERSION: HTTP/1.1
jobs:
@@ -63,8 +64,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
env:
GIT_HTTP_VERSION: HTTP/1.1
with:
fetch-depth: 2
@@ -96,6 +95,15 @@ jobs:
fi
else
TARGET="branch"
# Check if this exact commit has a tag. If so, skip the branch deploy
# because the tag pipeline will handle the production/staging deploy.
git fetch --tags --quiet || true
if git describe --tags --exact-match HEAD >/dev/null 2>&1; then
echo "Commit has a tag. Skipping branch deployment to favor tag deployment."
TARGET="skip"
fi
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
ENV_FILE=".env.branch-${SLUG}"
@@ -202,6 +210,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🚦 Priority Gate Check
env:
GITEA_PAT: ${{ secrets.GITEA_PAT }}
CURRENT_RUN_ID: ${{ github.run_id }}
CURRENT_TARGET: ${{ needs.prepare.outputs.target }}
run: bash scripts/priority-gate.sh
- name: 🔐 Registry Auth
id: auth
env:
@@ -281,6 +296,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🚦 Priority Gate Check
env:
GITEA_PAT: ${{ secrets.GITEA_PAT }}
CURRENT_RUN_ID: ${{ github.run_id }}
CURRENT_TARGET: ${{ needs.prepare.outputs.target }}
run: bash scripts/priority-gate.sh
- name: 📝 Generate Environment
shell: bash
env:
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -4
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,
+9 -7
View File
@@ -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,9 +16,12 @@ 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';
const corporateBackground = null; // Removed to prevent line shifting
const inter = Inter({
subsets: ['latin'],
@@ -189,15 +191,15 @@ export default async function Layout(props: {
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
<TransitionProvider>
<CorporateBackground />
<InitialLoader shouldShowLoader={!hasSeenLoader} />
<PageTransitionShutter />
{!hasSeenLoader && <InitialLoader shouldShowLoader={!hasSeenLoader} />}
{/* PageTransitionShutter loaded dynamically in TransitionProvider */}
<SkipLink />
<Header navLinks={navLinks} />
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
<main
id="main-content"
className="flex-grow animate-fade-in overflow-visible"
className="flex-grow overflow-visible"
tabIndex={-1}
>
{children}
@@ -207,7 +209,7 @@ export default async function Layout(props: {
<JsonLd />
<AnalyticsShell />
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
<FeedbackClientWrapper />
</TransitionProvider>
</NextIntlClientProvider>
</body>
+9 -1
View File
@@ -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,12 @@ export default async function Home(props: any) {
}
setRequestLocale(locale);
preload('/assets/videos/web/hero-kabelpflug-poster.webp', {
as: 'image',
fetchPriority: 'high',
imageSrcSet: '/assets/videos/web/hero-kabelpflug-poster-mobile.webp 800w, /assets/videos/web/hero-kabelpflug-poster.webp 1920w',
imageSizes: '100vw'
});
const mdx = await getMdxContent(locale, 'home');
const allReferences = await getAllReferences(locale);
+10 -5
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';
@@ -33,11 +33,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 +74,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 +90,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 +103,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 +122,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'}`}
+2 -2
View File
@@ -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,
});
+1 -2
View File
@@ -1,6 +1,5 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import Error from 'next/error';
import { useEffect } from 'react';
@@ -10,7 +9,7 @@ export default function GlobalError({
error: Error & { digest?: string };
}) {
useEffect(() => {
Sentry.captureException(error);
console.error('Global Error:', error);
}, [error]);
return (
+32
View File
@@ -87,3 +87,35 @@
}
}
/* trigger rebuild */
@layer utilities {
@keyframes hero-fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-hero-fade-in-up {
animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
}
@layer utilities {
@keyframes hero-zoom-out {
from {
transform: scale(1.05);
}
to {
transform: scale(1);
}
}
.animate-hero-zoom-out {
animation: hero-zoom-out 1.5s ease-out forwards;
}
}
+48
View File
@@ -0,0 +1,48 @@
.next/static/chunks/0b521531212fbb38.js
.next/static/chunks/0c4bd4931d57e66a.js
.next/static/chunks/114976d6e8233fa6.js
.next/static/chunks/163f657a16afbe5e.js
.next/static/chunks/1c2f1bc547a6b6db.js
.next/static/chunks/2583fa7802927c7b.js
.next/static/chunks/30da1bf4e9770388.js
.next/static/chunks/38b815a6d50481c3.js
.next/static/chunks/3dc2f63532eb2144.js
.next/static/chunks/3e327b4e2347bc0f.js
.next/static/chunks/3ef3cefd2dae1060.js
.next/static/chunks/45499dd88f8aa0be.js
.next/static/chunks/4b9828b23c1decfd.js
.next/static/chunks/4c801febc70daa53.js
.next/static/chunks/4d5ea13dba62fe42.js
.next/static/chunks/4dffe404993f353e.js
.next/static/chunks/4f185d0c413fdb9e.js
.next/static/chunks/4f69e8e09eeae4d6.js
.next/static/chunks/4fa8be7f170d31f3.js
.next/static/chunks/583b451485662887.js
.next/static/chunks/5e1e2d5c357c1b57.js
.next/static/chunks/6216b234a7a88a60.js
.next/static/chunks/651fbbae0062342b.js
.next/static/chunks/66a1b90666fc2830.js
.next/static/chunks/6c0ddf2fd63ab912.js
.next/static/chunks/72cf73de45fde6cf.js
.next/static/chunks/75ea3a278c540e05.js
.next/static/chunks/77a84d4dc662b7cf.js
.next/static/chunks/7c603cfd5a61abd1.js
.next/static/chunks/84ddf763fbc98e13.js
.next/static/chunks/885c97d656f95ebd.js
.next/static/chunks/8d6ae249fcc5e38f.js
.next/static/chunks/920f860e909b2eb8.js
.next/static/chunks/951d03271d663d8a.js
.next/static/chunks/a094276392fca24a.js
.next/static/chunks/a2e7fbe3f294caac.js
.next/static/chunks/a6c9d757034eb86a.js
.next/static/chunks/a6dc76eec9924929.js
.next/static/chunks/c3f21c4df538784a.js
.next/static/chunks/c641ee8514fa0552.js
.next/static/chunks/d8c0ef20e0ad9790.js
.next/static/chunks/e60e6d615bf7d2f5.js
.next/static/chunks/e714b6ccaf8d5f12.js
.next/static/chunks/eeebcf8c8de1a29a.js
.next/static/chunks/f0b47c1f19d25fd2.js
.next/static/chunks/f8d5b1a2accefb4c.js
.next/static/chunks/fa18a72fd9fb11d5.js
.next/static/chunks/turbopack-2aac2a40101e9988.js
+9 -9
View File
@@ -2,17 +2,17 @@
import dynamic from 'next/dynamic';
const FeedbackOverlay = dynamic(
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
{ ssr: false },
);
const IS_ENABLED = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
interface FeedbackClientWrapperProps {
feedbackEnabled: boolean;
}
const FeedbackOverlay = IS_ENABLED
? dynamic(
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
{ ssr: false },
)
: () => null;
export default function FeedbackClientWrapper({ feedbackEnabled }: FeedbackClientWrapperProps) {
if (!feedbackEnabled) return null;
export default function FeedbackClientWrapper() {
if (!IS_ENABLED) return null;
return <FeedbackOverlay />;
}
+1 -1
View File
@@ -3,7 +3,7 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import Image from 'next/image';
import { createPortal } from 'react-dom';
import { m, LazyMotion, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence, LazyMotion } from 'framer-motion';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
interface LightboxProps {
+4 -10
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;
}
@@ -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;
+42 -46
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 {
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from "react";
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion';
import { AnimatePresence, m } from 'framer-motion';
import { MessageSquare, X, Check, Image as ImageIcon, Type, StickyNote, Download, Trash2, List } from "lucide-react";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { m, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { LogoArcs } from '@/components/ui/LogoArcs';
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
import { useScroll, useTransform, m } from 'framer-motion';
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from 'next/link';
import Image from 'next/image';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
+1 -1
View File
@@ -4,7 +4,7 @@ import React, { Suspense } from 'react';
import { Section, Container } from '@/components/ui';
import { ContactForm } from '@/components/forms/ContactForm';
import dynamic from 'next/dynamic';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
const ContactMap = dynamic(() => import('@/components/ContactMap'), {
ssr: false,
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DataGridPulse() {
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useRef } from 'react';
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
import { useScroll, useTransform, m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DeepDrillAnimation() {
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
+18 -42
View File
@@ -3,7 +3,6 @@
import React from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
@@ -17,25 +16,7 @@ export interface HeroSectionProps {
ctaHref?: string;
}
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }
},
};
// CSS animations replace Framer Motion for better LCP
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
@@ -47,12 +28,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
>
{bgSrc && (
<m.div
<div
key={`hero-bg-${pathname}`}
className="absolute inset-0 z-0"
initial={{ scale: 1.05, opacity: 1 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
className="absolute inset-0 z-0 animate-hero-zoom-out"
>
<Image
src={bgSrc}
@@ -63,6 +41,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
}}
sizes="100vw"
quality={50}
priority
/>
{/* Cinematic Color Grading Overlay */}
@@ -74,37 +53,34 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
{/* Top Fade for Header Navigation Readability */}
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
</m.div>
</div>
)}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
<m.div
<div
key={`hero-content-${pathname}`}
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
variants={containerVariants}
initial="hidden"
animate="visible"
>
{badge && (
<m.div variants={itemVariants}>
<div className="animate-hero-fade-in-up" style={{ animationDelay: '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{badge}
</Badge>
</m.div>
</div>
)}
<m.div variants={itemVariants}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</m.div>
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.25s' : '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</div>
{subtitle && (
<m.div variants={itemVariants}>
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.4s' : '0.25s', opacity: 0, animationFillMode: 'forwards' }}>
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
{subtitle}
</p>
</m.div>
</div>
)}
{ctaLabel && ctaHref && (
<m.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
<div className={`mt-8 animate-hero-fade-in-up ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`} style={{ animationDelay: badge ? '0.55s' : '0.4s', opacity: 0, animationFillMode: 'forwards' }}>
<Button
href={ctaHref}
variant="accent"
@@ -115,9 +91,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
&rarr;
</span>
</Button>
</m.div>
</div>
)}
</m.div>
</div>
</Container>
</section>
);
+8 -8
View File
@@ -34,7 +34,7 @@ export function HeroVideo(props: HeroVideoProps) {
let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster.jpg');
defaultPoster = videoUrl.replace('.mp4', '-poster.webp');
}
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
@@ -80,18 +80,18 @@ export function HeroVideo(props: HeroVideoProps) {
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
<Image
<img
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
srcSet={posterSrc.includes('-poster.webp') ? `${posterSrc.replace('-poster.webp', '-poster-mobile.webp')} 800w, ${posterSrc} 1920w` : undefined}
sizes={posterSrc.includes('-poster.webp') ? "100vw" : undefined}
alt={posterAlt}
fill
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
quality={25}
priority
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
decoding="sync"
fetchPriority="high"
/>
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
<div className="absolute inset-0 bg-black/20 z-[1] pointer-events-none mix-blend-multiply" />
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
+18 -167
View File
@@ -1,16 +1,20 @@
'use client';
import React, { useState, useRef, useCallback, useMemo } from 'react';
import { m, LazyMotion, domAnimation, 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,
@@ -135,12 +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]);
@@ -210,116 +163,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>
+195
View File
@@ -0,0 +1,195 @@
'use client';
import React from 'react';
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'}`}
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={loadFeatures}>
{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>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import React from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface JobListingBlockProps {
+3 -16
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -47,19 +47,6 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false);
// Center the first card on mount so it appears in the middle with peek on both sides
React.useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Wait for layout to settle
requestAnimationFrame(() => {
const firstCard = el.querySelector('[data-card]') as HTMLElement | null;
if (!firstCard) return;
const cardCenter = firstCard.offsetLeft + firstCard.offsetWidth / 2;
const containerCenter = el.offsetWidth / 2;
el.scrollLeft = cardCenter - containerCenter;
});
}, [references.length]);
const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title');
@@ -127,7 +114,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" />
{/* Carousel: centered first card with peek on both sides */}
<LazyMotion features={domAnimation}>
<div
ref={containerRef}
onMouseDown={onMouseDown}
@@ -189,7 +176,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
})}
</div>
</LazyMotion>
</div>
<div className="container relative z-10 mt-4 flex justify-center">
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useRef } from 'react';
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
import { useScroll, useTransform, m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function ScaleOfImpact() {
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { m, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
export interface ServicePanelData {
+2 -2
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { m, Variants } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -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>
+1 -1
View File
@@ -3,7 +3,7 @@
import React from 'react';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
export interface SupportCTAProps {
title: string;
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
+33 -22
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { useScroll, useTransform, useSpring, m, LazyMotion, domAnimation } from 'framer-motion';
import { useScroll, useTransform, useSpring, m } from 'framer-motion';
import { LogoArcs } from '@/components/ui/LogoArcs';
function MagneticRing({
@@ -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]);
+2 -2
View File
@@ -1,6 +1,6 @@
'use client';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import Image from 'next/image';
export function EUFundingBadge() {
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
alt="European Union Background"
fill
className="object-cover object-top"
priority
loading="lazy"
/>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { m } from 'framer-motion';
import { Button } from '@/components/ui/Button';
import { sendContactFormAction } from '@/app/actions/contact';
@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicMobileBottomNav = dynamic(
() => import('./MobileBottomNav').then(mod => mod.MobileBottomNav),
{ ssr: false }
);
+1
View File
@@ -111,6 +111,7 @@ export function Header({ navLinks }: HeaderProps) {
priority
fetchPriority="high"
sizes="192px"
unoptimized
/>
{/* Masked sweep overlay on hover */}
<div
+1 -1
View File
@@ -4,7 +4,7 @@ import * as React from 'react';
import { usePathname } from 'next/navigation';
import { TransitionLink } from '@/components/ui/TransitionLink';
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
export interface NavLink {
label: string;
@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicInitialLoader = dynamic(
() => import('./InitialLoader').then(mod => mod.InitialLoader),
{ ssr: false }
);
+6 -6
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
{/* Sweeping intense white light */}
<m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ left: '-150%' }}
animate={{ left: '250%' }}
initial={{ x: '-150%' }}
animate={{ x: '250%' }}
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
/>
</div>
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
>
<m.div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
initial={{ width: "0%" }}
animate={{ width: "100%" }}
className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
/>
</m.div>
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
import { useTransition } from './TransitionProvider';
import Image from 'next/image';
@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
<AnimatePresence>
{isTransitioning && (
<m.div
key="shutter"
key="page-transition-shutter"
initial={{ y: '-100%' }}
animate={{ y: '0%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
style={{
height: '100vh',
top: '0',
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
{/* Sweeping intense white light */}
<m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ left: '-150%' }}
animate={{ left: '250%' }}
initial={{ x: '-150%' }}
animate={{ x: '250%' }}
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
/>
</div>
@@ -3,9 +3,16 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { LazyMotion } from 'framer-motion';
import dynamic from 'next/dynamic';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
const DynamicPageTransitionShutter = dynamic(
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
{ ssr: false }
);
interface TransitionContextType {
isTransitioning: boolean;
transitionMessage: string | null;
@@ -17,8 +24,12 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const [hasMountedShutter, setHasMountedShutter] = useState(false);
const router = useRouter();
// We no longer eagerly load the shutter in the background.
// It will be loaded precisely when the user initiates a transition.
// We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning);
useEffect(() => {
@@ -44,6 +55,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTransitionMessage(message);
setIsTransitioning(true);
setHasMountedShutter(true);
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
setTimeout(() => {
@@ -72,6 +84,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
/>
</React.Suspense>
<LazyMotion features={loadFeatures}>
{hasMountedShutter && <DynamicPageTransitionShutter />}
{children}
</LazyMotion>
</TransitionContext.Provider>
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useMotionValue, useTransform, animate, useInView, m, LazyMotion, domAnimation } from 'framer-motion';
import { useMotionValue, useTransform, animate, useInView, m } from 'framer-motion';
interface AnimatedCounterProps {
value: number;
+1 -1
View File
@@ -1,6 +1,6 @@
'use client';
import { useInView, m, LazyMotion, domAnimation } from 'framer-motion';
import { useInView, m } from 'framer-motion';
import { useRef } from 'react';
/**
+1 -1
View File
@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
items={[
{
title: "Kabelleitungsnetzbau",
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
tag: "Energie",
size: "large",
href: "/de/kabeltiefbau",
+1 -1
View File
@@ -8,7 +8,7 @@ layout: "fullBleed"
<HeroSection
badge="Kernkompetenz"
title="Kabelleitungsnetzbau"
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
alignment="center"
ctaLabel="Projekt anfragen"
+2 -2
View File
@@ -1,5 +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;
export {};
+11921
View File
File diff suppressed because one or more lines are too long
+11867
View File
File diff suppressed because one or more lines are too long
+11818
View File
File diff suppressed because one or more lines are too long
+12 -3
View File
@@ -5,7 +5,9 @@ import { join } from 'path';
* Loads the Inter fonts for use in Satori (Next.js OG Image generation).
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
*/
let cachedFonts: any[] | null = null;
export async function getOgFonts() {
if (cachedFonts) return cachedFonts;
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
@@ -28,7 +30,7 @@ export async function getOgFonts() {
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
);
return [
cachedFonts = [
{
name: 'Inter',
data: boldFont,
@@ -42,28 +44,35 @@ export async function getOgFonts() {
style: 'normal' as const,
},
];
return cachedFonts;
} catch (error) {
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
return [];
}
}
let cachedBackground: string | null = null;
export function getOgBackground() {
if (cachedBackground) return cachedBackground;
try {
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
const bgBase64 = readFileSync(bgPath, 'base64');
return `data:image/jpeg;base64,${bgBase64}`;
cachedBackground = `data:image/jpeg;base64,${bgBase64}`;
return cachedBackground;
} catch (err) {
console.error('[OG] Failed to load background', err);
return undefined;
}
}
let cachedLogo: string | null = null;
export function getOgLogo() {
if (cachedLogo) return cachedLogo;
try {
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
const logoBase64 = readFileSync(logoPath, 'base64');
return `data:image/png;base64,${logoBase64}`;
cachedLogo = `data:image/png;base64,${logoBase64}`;
return cachedLogo;
} catch (err) {
console.error('[OG] Failed to load logo', err);
return undefined;
+1 -12
View File
@@ -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,
});
+51 -7
View File
@@ -1,4 +1,4 @@
import withMintelConfig from '@mintel/next-config';
import { baseNextConfig } from '@mintel/next-config';
import withBundleAnalyzer from '@next/bundle-analyzer';
import { withSentryConfig } from '@sentry/nextjs';
@@ -15,7 +15,6 @@ const nextConfig = {
pagesBufferLength: 2,
},
experimental: {
optimizeCss: true,
staleTimes: {
dynamic: 0,
static: 30,
@@ -24,7 +23,6 @@ const nextConfig = {
'lucide-react',
'framer-motion',
'@/components/ui',
'@sentry/nextjs',
'react-hook-form',
'zod',
'date-fns',
@@ -55,6 +53,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,
};
}
@@ -548,14 +548,58 @@ const withAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
// withMintelConfig internally sets output:'standalone' and wraps with withSentryConfig.
// In dev mode, standalone triggers @vercel/nft file tracing on every compile through
// VirtioFS, which hammers CPU. Strip it for dev.
const withMDX = createMDX({});
let resolvedConfig = withMDX(withAnalyzer(withMintelConfig(nextConfig)));
let resolvedConfig = withMDX(withAnalyzer(nextConfig));
// Apply next-intl manually (bypassing withMintelConfig to control Sentry)
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
resolvedConfig = withNextIntl(resolvedConfig);
// 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 = {
...baseNextConfig,
...resolvedConfig,
images: {
...baseNextConfig.images,
...(resolvedConfig.images || {}),
remotePatterns: [
...(baseNextConfig.images?.remotePatterns || []),
...(resolvedConfig.images?.remotePatterns || [])
]
},
rewrites: async () => {
let baseRewrites = [];
if (typeof baseNextConfig.rewrites === 'function') {
baseRewrites = await baseNextConfig.rewrites();
}
let configRewrites = { beforeFiles: [], fallback: [], afterFiles: [] };
if (typeof resolvedConfig.rewrites === 'function') {
const res = await resolvedConfig.rewrites();
if (Array.isArray(res)) {
configRewrites.fallback = res;
} else {
configRewrites = res;
}
}
return {
beforeFiles: configRewrites.beforeFiles || [],
fallback: [
...(configRewrites.fallback || []),
...baseRewrites
],
afterFiles: configRewrites.afterFiles || []
};
}
};
if (!isProd) {
delete resolvedConfig.output;
delete finalConfig.output;
}
export default resolvedConfig;
export default finalConfig;
+3 -2
View File
@@ -139,7 +139,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.63",
"version": "2.4.5",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
@@ -150,7 +150,8 @@
],
"overrides": {
"next": "16.1.6",
"minimatch": ">=10.2.2"
"minimatch": ">=10.2.2",
"framer-motion": "^12.38.0"
}
},
"browserslist": [
+3 -37
View File
@@ -7,6 +7,7 @@ settings:
overrides:
next: 16.1.6
minimatch: '>=10.2.2'
framer-motion: ^12.38.0
importers:
@@ -52,7 +53,7 @@ importers:
specifier: ^2.1.1
version: 2.1.1
framer-motion:
specifier: ^12.34.0
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
graphql:
specifier: ^16.12.0
@@ -4843,20 +4844,6 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@11.18.2:
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
framer-motion@12.38.0:
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
peerDependencies:
@@ -6142,15 +6129,9 @@ packages:
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
motion-dom@11.18.1:
resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
motion-utils@11.18.1:
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
@@ -9246,7 +9227,7 @@ snapshots:
dependencies:
'@medv/finder': 4.0.2
clsx: 2.1.1
framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
html2canvas: 1.4.1
lucide-react: 0.441.0(react@19.2.4)
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0)
@@ -12941,15 +12922,6 @@ snapshots:
fraction.js@5.3.4: {}
framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 11.18.1
motion-utils: 11.18.1
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 12.38.0
@@ -14577,16 +14549,10 @@ snapshots:
module-details-from-path@1.0.4: {}
motion-dom@11.18.1:
dependencies:
motion-utils: 11.18.1
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
motion-utils@11.18.1: {}
motion-utils@12.36.0: {}
mri@1.2.0: {}
+39
View 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"
}
]
}
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+36
View 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
View 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);
}
+4 -3
View File
@@ -6,13 +6,14 @@ module.exports = async (browser, context) => {
const page = await browser.newPage();
// Using LHCI_URL or TARGET_URL if available
const targetUrl =
process.env.LHCI_URL || process.env.TARGET_URL || 'https://testing.klz-cables.com';
process.env.LHCI_URL || process.env.TARGET_URL || 'https://testing.e-tib.com';
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
const authCookieName = process.env.AUTH_COOKIE_NAME || 'etib_gatekeeper_session';
console.log(`🔑 LHCI Auth: Setting gatekeeper cookie for ${new URL(targetUrl).hostname}...`);
console.log(`🔑 LHCI Auth: Setting gatekeeper cookie (${authCookieName}) for ${new URL(targetUrl).hostname}...`);
await page.setCookie({
name: 'klz_gatekeeper_session',
name: authCookieName,
value: gatekeeperPassword,
domain: new URL(targetUrl).hostname,
path: '/',
+4 -3
View File
@@ -19,6 +19,7 @@ const targetUrl =
'http://localhost:3000';
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; // Default limit to avoid infinite runs
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
const authCookieName = process.env.AUTH_COOKIE_NAME || 'etib_gatekeeper_session';
async function main() {
console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`);
@@ -32,7 +33,7 @@ async function main() {
// We might need to bypass gatekeeper for the sitemap fetch too
const response = await axios.get(sitemapUrl, {
headers: {
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
Cookie: `${authCookieName}=${gatekeeperPassword}`,
},
validateStatus: (status) => status < 400,
});
@@ -77,7 +78,7 @@ async function main() {
// Handle authentication for staging/testing
// Lighthouse can set cookies via --collect.settings.extraHeaders
const extraHeaders = JSON.stringify({
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
Cookie: `${authCookieName}=${gatekeeperPassword}`,
});
// Detect Chrome path from Puppeteer installation if not provided
@@ -147,7 +148,7 @@ async function main() {
execSync(lhciCommand, {
encoding: 'utf8',
stdio: 'inherit',
env: { ...process.env, CHROME_PATH: chromePath },
env: { ...process.env, CHROME_PATH: chromePath, TARGET_URL: targetUrl },
});
} catch (err: any) {
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
+6 -2
View File
@@ -70,7 +70,9 @@ while IFS= read -r run; do
elif [ "$CURRENT_PRIO" -lt "$RUN_PRIO" ]; then
echo "Current workflow has LOWER priority ($CURRENT_PRIO < $RUN_PRIO). Aborting self."
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
exit 1
echo "Waiting for Gitea to process cancellation..."
sleep 15
exit 0
else
# Equal priority: Newer wins
if [ "$CURRENT_RUN_ID" -gt "$RUN_ID" ]; then
@@ -79,7 +81,9 @@ while IFS= read -r run; do
else
echo "Equal priority, but current is older. Aborting self."
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
exit 1
echo "Waiting for Gitea to process cancellation..."
sleep 15
exit 0
fi
fi
done <<< "$RUNS"
+9
View File
@@ -0,0 +1,9 @@
if [[ "${{ github.ref_type }}" == "branch" && "${{ github.ref_name }}" != "main" && "${{ github.ref_name }}" != "master" ]]; then
# Fetch tags from remote for this specific commit to check if it's being tagged simultaneously
TAGS=$(git ls-remote --tags origin | grep "${{ github.sha }}" || true)
if [[ -n "$TAGS" ]]; then
echo "Dieser Commit hat einen Tag! Überspringe Preview-Deployment, damit der Tag-Deploy Vorrang hat."
echo "target=skip" >> $GITHUB_OUTPUT
exit 0
fi
fi
@@ -39,7 +39,7 @@ describe('Task 6 Content Updates', () => {
// DE Home
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)"');
expect(deContent).toContain('100+');
expect(deContent).toContain('Mitarbeitende');
@@ -58,7 +58,7 @@ describe('Task 6 Content Updates', () => {
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
expect(deContent).toContain('Kabelmontagen bis 110 kV');
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"');
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
// EN Kabelnetzbau