Compare commits

..

210 Commits

Author SHA1 Message Date
db6492207d 2.4.13
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 57s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 00:54:35 +02:00
99ae6d90c2 perf(css): remove inlineCss:false to fix 300ms render-blocking delay 2026-07-03 00:54:35 +02:00
7d53ca5a11 2.4.12
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 2m45s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-02 22:15:03 +02:00
0331f3f7ec fix(i18n): add missing Team.gallery translations & perf(hero): force eager LCP image 2026-07-02 22:15:03 +02:00
274d013047 2.4.11
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-07-02 22:10:45 +02:00
f1d0c9c5b2 perf: remove template.tsx wrapper to eliminate LCP hydration delay 2026-07-02 22:10:45 +02:00
c121fe0b93 2.4.10
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m27s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 22:03:38 +02:00
640a9dfc9f perf: remove framer-motion filter blur animations to fix main thread blocking, compress heavy images 2026-07-02 22:03:37 +02:00
460e27c497 chore: bump version to 2.4.9
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m14s
Build & Deploy / 🏗️ Build (push) Successful in 2m39s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-07-02 21:03:51 +02:00
e257f8450b 2.4.8 2026-07-02 21:03:43 +02:00
09a1ac47a7 perf: remove old tailwind config and duplicate globals.css 2026-07-02 21:03:34 +02:00
2241775a7f perf: migrate to native Tailwind v4 @theme and disable inlineCss for optimal LCP 2026-07-02 21:03:22 +02:00
7eb9e0cfe6 fix: resolve invalid package.json literal newline character breaking pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 57s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 18:31:37 +02:00
ab4f4af555 chore: bump version to 2.4.7 for final lighthouse fix
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 QA (push) Failing after 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 16:35:19 +02:00
b6bd2065bb perf: restore inlineCss for optimal LCP score & add missing pdf-renderer dependency 2026-07-02 16:34:24 +02:00
8d69ac2de2 chore(release): v2.4.6
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m43s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-02 15:25:40 +02:00
a9f16b49cc fix(performance): disable inlineCss to reduce HTML size from 557KB to 162KB and fix FCP fluctuations
All checks were successful
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
21cf0725ab release: v2.4.5
All checks were successful
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
2c162f5314 perf: completely fix non-composited animations, eliminate shutter unused JS, and aggressively compress images 2026-07-02 10:29:31 +02:00
be3b2e6a7b release: v2.4.4
All checks were successful
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
f2e59fa52f perf: fix sizes attribute scaling bug to prevent mobile devices from downloading 1920w desktop images 2026-07-01 18:23:50 +02:00
16d494689f release: v2.4.3
All checks were successful
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
d24b8a8694 perf: prevent bandwidth theft from non-critical preloads and strip InitialLoader chunk 2026-07-01 18:21:25 +02:00
8d42946711 release: v2.4.2
All checks were successful
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
b15217b9be content: update Hochspannung wording per WhatsApp screenshot 2026-07-01 18:11:33 +02:00
b138025380 release: v2.4.1
All checks were successful
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
18d2ace03b perf: compress hero image and defer page transition shutter
Some checks failed
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
89ed1faa6b release: v2.4.0
All checks were successful
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
7307e812a4 release: v2.2.96 2026-07-01 15:30:49 +02:00
20d6194a7e perf: fix massive LCP render delay and TTFB on Home page
Some checks failed
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
86e2fd7097 2.2.95
All checks were successful
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
bf8073f5bf perf: optimize LCP images and enable AVIF
All checks were successful
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
e8adb87b02 fix(lcp): revert DynamicFramerMotion wrapper to prevent React 18 Suspense DOM deletion during hydration
Some checks failed
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
067e34c19b fix(lcp): revert unoptimized=true on HeroVideo to restore Next.js image optimization for massive CMS images
Some checks failed
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
e6017e7436 perf(images): remove AVIF format to prevent massive CPU spikes and TTFB delays during on-the-fly Next.js image optimization
All checks were successful
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
1955bfcd1b perf(lcp): bypass Next.js image optimization for hero poster and use static WebP to eliminate server-side TTFB block
All checks were successful
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
73886406a4 perf: completely lazy load framer-motion core via DynamicFramerMotion wrapper
All checks were successful
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
b2701d6bcd perf(server): aggressively cache OG image assets in memory to prevent synchronous file I/O from blocking the Node event loop
All checks were successful
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
4f46825fb4 perf: fix 3.8s LCP delays by removing dynamic hero and heavy css filters
All checks were successful
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
c63766711d chore: release 2.2.92
All checks were successful
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
c738001f9a perf: eliminate feedback widget chunk via dead-code elimination
All checks were successful
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
69dd79dfad fix: remove lingering getAppServices import from error.tsx that kept backend dependencies in the client bundle
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-30 02:00:10 +02:00
a6c4cc53f9 fix: completely sever client bundle from backend logger (pino) and validation (zod) to eliminate 68 KiB unused JS
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 23:27:14 +02:00
29f83f8151 chore: release 2.2.89
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🏗️ Build (push) Successful in 3m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m13s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 21:04:03 +02:00
cec08e3ab3 fix(perf): dynamic import of framer-motion features to eliminate 43 KiB unused JS 2026-06-29 21:04:01 +02:00
837567c1f4 chore: release 2.2.88
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:58:32 +02:00
4dcc0061fc fix(perf): throttle CorporateBackground mousemove to fix forced reflow penalty 2026-06-29 18:58:32 +02:00
bd25ec935d chore: release 2.2.87
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 18:23:54 +02:00
b3b85202eb fix(perf): replace next/image with native img for svg to drastically improve LCP 2026-06-29 18:23:53 +02:00
b4b0c7c48a chore: release 2.2.86
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-29 13:24:17 +02:00
35fb37bb14 fix: a11y contrast, heading order, and unused js (framer-motion lazy load) 2026-06-29 13:24:16 +02:00
fb2d385e88 chore: release 2.2.85
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-29 10:46:48 +02:00
c54085fe3f fix: remove duplicate map nodes causing m is not defined build error 2026-06-29 10:46:47 +02:00
232c55c566 chore: release 2.2.84
Some checks failed
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
52d81ea6cc perf: completely eliminate mobile lighthouse penalties via ssr: false map pins and 200px sizes 2026-06-29 10:42:04 +02:00
bc0e7c2283 chore: release 2.2.83
All checks were successful
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
6d2152998b perf: revert InteractiveGermanyMap to dynamic import and use ReactDOM.preload for LCP 2026-06-29 10:19:44 +02:00
f727e2abfb chore: release 2.2.82
All checks were successful
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
f04163f56d perf: explicit 384px sizes to eliminate lighthouse image delivery penalties 2026-06-28 16:37:01 +02:00
e9e209d6e7 chore: release 2.2.81
All checks were successful
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
9af6a53325 perf: optimize Image component sizes and hoist LCP preload tags 2026-06-27 22:56:52 +02:00
8627bae98b chore: release 2.2.80
All checks were successful
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
17620482da build: ruthlessly strip core-js polyfills to fix Lighthouse Legacy JS audit
Some checks failed
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
c5e9a67546 2.2.79
All checks were successful
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
1b7e42ccc9 fix(performance): completely eradicate sentry from client payload to achieve 100/100 LH score
Some checks failed
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
7a2382606e chore: release 2.2.78
All checks were successful
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
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
09d0395233 refactor: remove obsolete Lighthouse report files and associated artifacts 2026-06-27 08:17:49 +02:00
857c055862 chore(notifications): add [E-TIB] prefix to Gotify messages
Some checks failed
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
500c2c64f4 chore: release 2.2.77
All checks were successful
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
e13ce8bcec fix: resolve ssr:false in Server Component build error by creating Client Component wrappers 2026-06-27 08:09:26 +02:00
feb8372a7c chore: release 2.2.76
Some checks failed
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
c190a4f67e perf: dynamically import InitialLoader and MobileBottomNav to remove framer-motion from main JS bundle 2026-06-27 08:01:27 +02:00
6717b32d44 chore: release 2.2.75
All checks were successful
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
f1732917bc perf: enable experimental.inlineCss to eliminate 380ms render-blocking CSS waterfall 2026-06-27 07:20:50 +02:00
1f666d1b34 chore: release 2.2.74
All checks were successful
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
ab42590206 test: fix gatekeeper bypass logic in pagespeed-sitemap.ts by passing TARGET_URL 2026-06-27 06:14:33 +02:00
7d4ff03806 chore: release 2.2.73
All checks were successful
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
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
eaf40dec7c chore: release 2.2.72
All checks were successful
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
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
586b6cdee8 chore: release 2.2.71
All checks were successful
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
f1b8ee1188 fix(perf): completely disable Sentry client-side auto-instrumentation to save 556KB of JS
All checks were successful
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
88cc214ddb fix(ci): use correct auth cookie name for e-tib gatekeeper bypass
All checks were successful
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
25facb995f chore: bump version for fix
All checks were successful
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
7b01924055 fix: revert optimizeCss to fix LCP/FCP regression caused by critters with Tailwind v4 2026-06-26 10:21:57 +02:00
474672b2a1 chore: release 2.2.69 (PageSpeed optimizations)
All checks were successful
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
7e552bbea5 chore: release 2.2.68
All checks were successful
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
21886a693c fix: resolve yaml syntax error in deploy.yml and invalid next.config 2026-06-25 22:40:38 +02:00
45944cbfa7 chore: release 2.2.67
All checks were successful
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
761c9c956f chore: release 2.2.65 2026-06-25 22:32:14 +02:00
e18559b83c fix: enforce deployment priorities and completely strip client sentry bundle 2026-06-25 22:32:14 +02:00
818ccc8fae chore: release 2.2.64
All checks were successful
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
b64b2ccf25 fix: pipeline execution, deployment priority, and bundle optimizations
All checks were successful
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
e219b3b818 fix: apply GIT_HTTP_VERSION workaround globally for all jobs
All checks were successful
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
88628ee663 fix: remove git trace variables
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 4s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
2026-06-25 17:31:17 +02:00
734dd19725 2.2.63
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m47s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-25 17:24:19 +02:00
3175b99177 chore: force http1.1 for git checkout 2026-06-25 17:24:19 +02:00
1986280520 2.2.62
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 17:23:09 +02:00
d4437f5274 chore: debug checkout action 2026-06-25 17:23:09 +02:00
f09c338d0d 2.2.61
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 27s
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 / 🧪 QA (push) Failing after 13m8s
2026-06-25 17:19:59 +02:00
91d6073a58 2.2.60
Some checks failed
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) Failing after 13m49s
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-25 17:16:36 +02:00
c5f2642d05 fix(ci): prevent hostedtoolcache purge to fix checkout action failure 2026-06-25 17:16:36 +02:00
c2bfdc7e7c 2.2.59
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 28s
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 / 🧪 QA (push) Failing after 14m39s
2026-06-25 17:13:36 +02:00
5625428113 fix(ci): remove aggressive docker prune steps causing deadlock in runner 2026-06-25 17:13:36 +02:00
8f9a37466c 2.2.58
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 0s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Failing after 3s
2026-06-25 17:09:39 +02:00
803ba997df 2.2.57
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 2m12s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-25 17:07:15 +02:00
4985ae0315 chore: trigger pipeline to check for transient failure
Some checks failed
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-06-25 17:06:28 +02:00
5c93a51db9 chore: bump version to 2.2.56
Some checks failed
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) Failing after 20m22s
2026-06-25 15:04:00 +02:00
70a312052c fix: resolve ssr:false build error for dynamic imports in Server Components 2026-06-25 15:03:51 +02:00
0ccc408a8d chore: bump version to 2.2.55
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3h10m20s
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-06-25 13:13:21 +02:00
f9f0970f3f perf: resolve Lighthouse issues, optimize CSS and JS payload 2026-06-25 13:13:15 +02:00
f463e854fd perf(images): set fetchPriority high and aggressive quality 25 for LCP poster
Some checks failed
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-24 21:08:47 +02:00
81dd2c95ed perf(images): aggressively compress reference slider images for lighthouse
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
2026-06-24 18:31:33 +02:00
ecebe63084 perf(build): completely strip polyfills via postinstall script and aggressively compress LCP poster image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
2026-06-24 15:57:14 +02:00
ba4d21195c perf(build): brutally remove next.js internal legacy polyfills via webpack alias
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-24 14:45:06 +02:00
c74c786988 fix(ui): regenerate video posters at 2 second mark to prevent black initial frames
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-24 14:38:46 +02:00
4fd7160967 perf: remove legacy JS polyfills by targeting modern browserslist and fix LCP penalty by rendering hero background immediately visible instead of opacity 0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 4s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
2026-06-24 13:49:03 +02:00
04a2bb0ebc perf: fix massive unused JS payload by replacing synchronous framer-motion imports with LazyMotion (m) in template/error boundaries, revert HeroSection lazy load to save LCP
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
2026-06-24 12:15:59 +02:00
a52c35a224 fix(seo): remove process.env.CI override for baseUrl to prevent etib.localhost leaking into prod canonicals
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-24 12:13:28 +02:00
343b484c6e chore(release): v2.2.46
Some checks failed
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-24 10:48:56 +02:00
a6a8dbd134 perf: fix canonical origin mismatch and reduce unused js
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m23s
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 / 🧪 QA (push) Has been cancelled
2026-06-24 10:46:29 +02:00
8f2509c3fc chore(release): v2.2.45
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m2s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 21:24:23 +02:00
41a3c10cf8 fix(ui): center carousel cards with peek on both sides
Some checks failed
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-23 21:00:18 +02:00
103e4e63ed fix(ui): center references slider title and description
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 3m0s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m2s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 18:54:47 +02:00
34860cab18 fix: resolve lighthouse canonical and apple-touch-icon issues
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m1s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 18:38:35 +02:00
5105f449b5 perf: enable static generation and optimize images for 100/100 lighthouse
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 17:46:41 +02:00
1d07e47e5d fix(carousel): restore center start, fix drag events, remove favicon from manifest, optimize framer-motion 2026-06-23 15:20:56 +02:00
3d48359768 fix(hydration): add suppressHydrationWarning to html and body to prevent extension injection errors
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m47s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-23 15:16:07 +02:00
d9c2ea1ede fix(slider): restore layout to match main and fix drag by disabling native link drag
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-23 14:06:50 +02:00
76798e4704 refactor(ui): replace dragging carousel with centered grid layout
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-23 13:32:43 +02:00
d014c875fc fix(lint): add missing key to LazyMotion in ReferencesSlider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 41s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 12:43:44 +02:00
e4a20b08b9 test: fix Header TDD test to assert css visibility instead of dom unmounting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m38s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 12:33:20 +02:00
7b02750d50 fix(a11y): improve color contrast in mobile navigation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
- Increased background opacity from bg-white/75 to bg-white/95
- Updated text colors to text-neutral-dark to meet 4.5:1 contrast ratio requirement
2026-06-23 12:25:44 +02:00
992a07f54a perf(motion): migrate global framer-motion imports to LazyMotion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m25s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
- Replaced synchronous `motion` imports with `m` and `LazyMotion` across all components
- Removed 1-second `animate-in` delay on LCP element in HeroVideo
- Added AVIF image format support to next.config.mjs
- Fixed ReferencesSlider carousel left padding alignment
- Dropped Total Blocking Time (TBT) to 0ms
2026-06-23 12:18:54 +02:00
490f216502 chore: bump version to 2.2.44
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 09:39:38 +02:00
3674e21fda Perf: Remove 'unoptimized' from HeroVideo Image to enable Next.js responsive WebP optimization for LCP 2026-06-23 09:39:37 +02:00
298b81713c chore: bump version to 2.2.43
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m1s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 02:29:30 +02:00
3b438de94d Perf: Remove optimizeCss (causes render-blocking CSS), compress 94 photos via sharp/mozjpeg (avg ~70% size reduction) 2026-06-23 02:29:30 +02:00
aff47de037 chore: bump version to 2.2.42
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 02:02:40 +02:00
85d366cfe2 Fix: Carousel stays inside container for left alignment, breaks right only via negative margin-right. Fix contrast on mobile bottom nav (neutral-800, 10px). 2026-06-23 02:02:39 +02:00
5dc2a9fd37 chore: bump version to 2.2.41
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 01:04:43 +02:00
57e5431e2a Fix: Remove conflicting blurDataURL from HeroVideo and fix ReferencesSlider syntax error 2026-06-23 01:04:43 +02:00
a13b033b7d chore: bump version to 2.2.40
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Failing after 1m26s
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 2s
2026-06-23 00:51:34 +02:00
93714d2683 Fix: Add unoptimized to hero poster to fix 4.8s LCP delay and restore optimizeCss 2026-06-23 00:51:34 +02:00
b0fae48e65 chore: bump version to 2.2.39
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Failing after 1m31s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-23 00:49:50 +02:00
60391b5a3f Fix: Replace JS padding logic with flawless pure CSS full-bleed container grid calculation 2026-06-23 00:49:49 +02:00
6d4e080d17 chore: bump version to 2.2.38
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 00:27:21 +02:00
2a4c605a77 Fix: Add robust flex spacer for carousel padding, remove optimizeCss which broke LCP, and fix video opacity overlay 2026-06-23 00:27:20 +02:00
da9d1954d6 chore: bump version to 2.2.37
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 00:06:46 +02:00
4fc00098f2 Fix: Perfectly align carousel left padding with text container using JS calculated offset 2026-06-23 00:06:44 +02:00
2df7aece5c chore: bump version to 2.2.36
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
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 / 🏗️ Build (push) Has been cancelled
2026-06-22 23:59:14 +02:00
d65be95e95 Fix: Add base64 placeholder for hero video and squash all massive photo assets down to max 1920px 2026-06-22 23:59:09 +02:00
e83d3c0933 2.2.35
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 23:39:30 +02:00
f69d1977ad fix(ui): resolve lighthouse a11y contrast and css blocking issues 2026-06-22 23:39:30 +02:00
4f73ac109e 2.2.34
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 23:05:24 +02:00
535d6d0588 2.2.34-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 22:40:51 +02:00
c2ce09c106 fix(ui): use video posters for hero and adjust reference list padding 2026-06-22 22:40:49 +02:00
265f93716e 2.2.34-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
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-22 22:35:48 +02:00
66fb4f3939 fix(ui): use minmax for bento grid to prevent overflow 2026-06-22 22:35:43 +02:00
10f5dd9774 chore: release candidate 1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 21:55:14 +02:00
30a177663c perf: use responsive next/image for mdx images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
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 / 🏗️ Build (push) Has been cancelled
2026-06-22 21:20:16 +02:00
8d4f085c58 chore: release v2.2.32
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 18:11:51 +02:00
c0a577cff3 2.2.32-rc.0
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
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-06-22 14:16:39 +02:00
bec2a53d3d 2.2.32
Some checks failed
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-22 14:12:57 +02:00
b4fc3f6a28 fix(og): Remove redundant description on root OG image 2026-06-22 14:12:49 +02:00
3e5d7dbce9 2.2.31
Some checks failed
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-22 14:11:31 +02:00
c4e5b44d5e 2.2.30-rc.8
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 46s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 46s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:47:00 +02:00
0378dde328 feat(og): Standardize OG images across all subpages with logo and background 2026-06-22 13:46:50 +02:00
3202139b1e 2.2.30-rc.7
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Successful in 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:05:39 +02:00
dc5489706d fix(og): Add missing title and description variables 2026-06-22 13:05:33 +02:00
e8b8493740 2.2.30-rc.6
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m40s
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 2s
2026-06-22 12:49:57 +02:00
ef44ebcf4f 2.2.30-rc.5
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 2m55s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:42:55 +02:00
816aef924e 2.2.30-rc.4
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20m43s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 12:05:04 +02:00
1bd9f26356 style(og): improve spacing, truncate text, and switch to flex layout 2026-06-22 12:04:53 +02:00
156ae66dec 2.2.30-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 11:39:23 +02:00
b2d1865f40 fix(og): add missing logo type to OGImageTemplateProps 2026-06-22 11:39:20 +02:00
dd363d0a55 2.2.30-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 11:31:20 +02:00
84f10694bb feat(og): improve design with background image, logo and dynamic geometry 2026-06-22 11:31:19 +02:00
3e2b20ce0e 2.2.30-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
2026-06-22 10:04:17 +02:00
4cd919545e fix(og): add missing Inter fonts for Satori rendering 2026-06-22 10:04:17 +02:00
e5891e8214 2.2.30-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-22 09:12:50 +02:00
4e4e44a5fd feat(og): adapt OG images to E-TIB branding and fix ArrayBuffer font parsing 2026-06-22 09:12:50 +02:00
3e37c37863 fix: useSuspense bailout for TransitionProvider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m18s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 00:23:53 +02:00
40ec588d52 perf: lighthouse 100 optimization and UI cleanup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
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 / 🏗️ Build (push) Has been cancelled
2026-06-22 00:20:35 +02:00
b2c38fca7d refactor: improve deployment cleanup logic using explicit project-based teardown and label-based ghost container removal 2026-06-22 00:12:09 +02:00
1b6f1d9405 2.2.29
All checks were successful
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔍 Prepare (push) Successful in 1m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:44:44 +02:00
6cc9ccdea8 fix: entirely remove auto-play timeout for video to ensure 100 LCP 2026-06-21 19:44:44 +02:00
aafc705f0e 2.2.28
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:31:25 +02:00
691f02a5b8 2.2.24 2026-06-21 19:31:19 +02:00
4b04178f01 chore: retry pipeline 2
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:55:56 +02:00
f76940dc34 chore: retry pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 11m39s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:42:21 +02:00
2653bd2569 fix: restore UMAMI tracking and aggressively defer Hero Video to reach 100 PageSpeed
Some checks failed
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) Failing after 3h3m59s
2026-06-21 13:26:51 +02:00
adbf2855d8 fix(perf): hardcode Next.js optimized image URLs into MDX for massive LCP boost
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:44:01 +02:00
87942dff41 fix(perf): map img tag in MDX to Next.js Image optimizer for massive performance gains
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 12:28:47 +02:00
38bd728393 fix(perf): defer hero video loading and lazy load massive markdown images
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:23:58 +02:00
547e62f9d4 2.2.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 11:40:59 +02:00
27fc34e186 fix(perf): refactor HeroVideo to provide immediate Next.js optimized LCP image behind video 2026-06-21 11:40:54 +02:00
d068d68d7a 2.2.22
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 11:04:55 +02:00
908f2b0b69 fix(lint): fix react-hooks/immutability error in InitialLoader 2026-06-21 11:04:54 +02:00
272b7a397b 2.2.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
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
2026-06-21 10:44:09 +02:00
9edfe24509 fix(analytics, perf): fix umami proxy route redirect and remove blocking 10MB video preload to fix pagespeed 2026-06-21 10:44:08 +02:00
de8f41cc6d chore(release): 2.2.20 - Optimize deployment workflow concurrency to cancel previous tag runs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 09:38:53 +02:00
57cc429b54 chore(release): 2.2.19 - Fix mail sender company details & add gotify secrets
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-21 09:33:32 +02:00
01cf4013df chore(ci): add default values for umami and glitchtip
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:23:33 +02:00
caf3fe8c1e chore(release): 2.2.17 - Performance and LCP optimization
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m32s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:17:11 +02:00
51eac9fb50 perf(LCP): add fetchPriority=high to Header logo 2026-06-21 09:17:10 +02:00
6426512192 UI: simplify reveal animations to prevent flickering and late triggers
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m38s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 3m31s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-21 09:08:44 +02:00
a63ee1b9f5 chore: bump version to 2.2.16
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 22:03:05 +02:00
43667e7d68 2.2.15 2026-06-20 22:02:43 +02:00
245 changed files with 80825 additions and 1677 deletions

View File

@@ -1,5 +1,5 @@
# ============================================================================
# KLZ Cables - Production Environment Configuration
# E-TIB - Production Environment Configuration
# ============================================================================
# This file contains runtime environment variables for the production deployment.
# It should be placed on the production server at: /home/deploy/sites/e-tib.com/.env
@@ -12,7 +12,7 @@ NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://e-tib.com
# Analytics (Umami)
UMAMI_WEBSITE_ID=
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking (GlitchTip/Sentry)
@@ -23,7 +23,7 @@ MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@e-tib.com>
MAIL_FROM=E-TIB GmbH <noreply@e-tib.com>
MAIL_RECIPIENTS=info@e-tib.com
# Varnish Cache Size (optional)

View File

@@ -14,12 +14,13 @@ on:
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/v') && 'production' || github.ref }}
cancel-in-progress: true
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
GIT_HTTP_VERSION: HTTP/1.1
jobs:
@@ -49,9 +50,7 @@ jobs:
echo "=== Tool Cache Usage ==="
du -sh /opt/hostedtoolcache/* || true
du -sh /opt/hostedtoolcache/.[!.]* || true
echo "Purging old tool caches..."
rm -rf /opt/hostedtoolcache/* || true
rm -rf /opt/hostedtoolcache/.[!.]* || true
echo "Purging old tool caches skipped to prevent runner corruption."
echo "=== Host Disk Usage ==="
docker run --rm -v /:/host-root alpine df -h || true
echo "=== Host Inode Usage ==="
@@ -60,23 +59,7 @@ jobs:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
echo "=== Host Docker Info ==="
docker info || true
echo "Pruning host Buildkit builder caches aggressively..."
for container in $(docker ps --filter "name=buildx_buildkit_builder" --format "{{.Names}}"); do
echo "Pruning Buildkit inside container: $container"
docker exec -i "$container" buildctl prune --all || true
done
echo "Removing buildkit builder containers to clear overlay storage..."
containers=$(docker ps -a --filter "name=buildx_buildkit_builder" -q)
if [ -n "$containers" ]; then
docker rm -f $containers || true
fi
echo "=== Truncating host Docker container log files ==="
docker run --rm -v /:/host-root alpine sh -c "truncate -s 0 /host-root/mnt/HC_Volume_104796416/docker/containers/*/*.log" || true
echo "Purging old build layers, unused volumes and images..."
docker builder prune -a -f || true
docker volume prune -f || true
docker system prune -a -f --volumes || true
echo "=== System Disk Usage After Prune ==="
echo "=== System Disk Usage After Check ==="
df -h || true
- name: Checkout repository
@@ -112,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}"
@@ -218,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:
@@ -235,7 +234,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
@@ -282,17 +281,28 @@ jobs:
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }}
# Monitoring
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN || 'https://dcb81958-dbf2-4a3d-b422-875f4672c14b@glitchtip.infra.mintel.me/5' }}
# Gatekeeper
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
# Analytics
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
# Notifications
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
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:
@@ -350,6 +360,10 @@ jobs:
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo ""
echo "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
echo ""
echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME"
@@ -392,10 +406,19 @@ jobs:
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
ssh root@alpha.mintel.me "docker system prune -f"
ssh root@alpha.mintel.me "docker rm -f \$(docker ps -a -q --filter name=${SLUG}-etib-) 2>/dev/null || true"
# ── Deterministic cleanup: stop the EXACT compose project we're about to redeploy ──
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' down --remove-orphans 2>/dev/null || true"
# ── Safety net: kill ANY container whose Traefik host rule matches our target domain ──
# This catches ghost containers from prior deployments that used a different project name
# (e.g. 'e-tibcom' vs 'etib-production' due to Docker Compose defaulting to dir name)
ssh root@alpha.mintel.me "docker ps -q --filter label=traefik.http.routers.${PROJECT_NAME}.rule 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true"
ssh root@alpha.mintel.me "docker ps --format '{{.Names}}' | grep -E '^e-tibcom-' | xargs -r docker rm -f 2>/dev/null || true"
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' up -d --remove-orphans"
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
# ──────────────────────────────────────────────────────────────────────────────

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -20,12 +20,16 @@ export default async function Image({
}
const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse(
<OGImageTemplate
title={pageData.frontmatter.title}
description={pageData.frontmatter.excerpt}
title="Die Experten für Kabelnetzbau"
description={`${pageData.frontmatter.title}`}
label="Information"
image={bg}
logo={logo}
/>,
{
...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema';
export const size = OG_IMAGE_SIZE;
@@ -52,13 +52,16 @@ export default async function Image({
if (featuredImage) {
base64Image = await fetchImageAsBase64(featuredImage);
}
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse(
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
title="Die Experten für Kabelnetzbau"
description={`${post.frontmatter.title}`}
label={post.frontmatter.category || 'Blog'}
image={base64Image || featuredImage}
image={base64Image || featuredImage || bg}
logo={logo}
/>,
{
...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,9 +11,17 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse(
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
<OGImageTemplate
title="Die Experten für Kabelnetzbau"
description={t('title')}
label="Blog"
image={bg}
logo={logo}
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -11,12 +11,22 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
let title = t('meta.title') || t('title');
if (title.startsWith('E-TIB GmbH | ')) {
title = title.substring('E-TIB GmbH | '.length);
}
return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Contact" />,
<OGImageTemplate
title="Die Experten für Kabelnetzbau"
description={title}
label="Contact"
image={bg}
logo={logo}
/>,
{
...OG_IMAGE_SIZE,
fonts,

View File

@@ -57,7 +57,7 @@ export async function generateStaticParams() {
}
export default async function ContactPage({ params }: ContactPageProps) {
console.log('--- RENDERING CONTACT PAGE FOR LOCALE ---');
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Contact' });

View File

@@ -2,10 +2,9 @@
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 { motion } from 'framer-motion';
import { m } from 'framer-motion';
export default function Error({
error,
@@ -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,
@@ -65,7 +62,7 @@ export default function Error({
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center">
{/* The "Broken" Machine Visual */}
<div className="relative mb-12">
<motion.div
<m.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="relative"
@@ -81,7 +78,7 @@ export default function Error({
{/* Warning Overlay */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<motion.div
<m.div
animate={{
opacity: [0.4, 1, 0.4],
scale: [0.95, 1.05, 0.95]
@@ -90,9 +87,9 @@ export default function Error({
className="p-4 rounded-2xl bg-red-500/20 border border-red-500/40 backdrop-blur-md shadow-[0_0_30px_rgba(239,68,68,0.3)]"
>
<ShieldAlert className="w-12 h-12 text-red-500" />
</motion.div>
</m.div>
</div>
</motion.div>
</m.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 w-full items-start">

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';
@@ -10,16 +9,20 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { cookies } from 'next/headers';
import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
import { InitialLoader } from '@/components/providers/InitialLoader';
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
import { DynamicInitialLoader as InitialLoader } from '@/components/providers/DynamicInitialLoader';
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
const corporateBackground = null; // Removed to prevent line shifting
const inter = Inter({
subsets: ['latin'],
display: 'swap',
@@ -32,7 +35,8 @@ export async function generateMetadata(props: {
const params = await props.params;
const { locale } = params;
const baseUrl = process.env.CI ? 'http://etib.localhost' : SITE_URL;
const baseUrl = SITE_URL;
return {
title: {
template: '%s | E-TIB',
@@ -42,10 +46,11 @@ export async function generateMetadata(props: {
metadataBase: new URL(baseUrl),
manifest: '/manifest.webmanifest',
alternates: {
canonical: `${baseUrl}/${locale}`,
canonical: `/${locale}`,
languages: {
de: `${baseUrl}/de`,
en: `${baseUrl}/en`,
'de': `/de`,
'en': `/en`,
'x-default': `/de`,
},
},
};
@@ -60,6 +65,9 @@ export const viewport: Viewport = {
themeColor: '#117c61',
};
export async function generateStaticParams() {
return [{ locale: 'de' }, { locale: 'en' }];
}
export default async function Layout(props: {
children: React.ReactNode;
@@ -148,19 +156,9 @@ export default async function Layout(props: {
const serverServices = getServerAppServices();
try {
const { headers } = await import('next/headers');
const requestHeaders = await headers();
// Disable analytics in CI to prevent console noise/score penalties
if (process.env.NEXT_PUBLIC_CI === 'true') {
// Skip setting server context for analytics in CI
} else if ('setServerContext' in serverServices.analytics) {
(serverServices.analytics as any).setServerContext({
userAgent: requestHeaders.get('user-agent') || undefined,
language: requestHeaders.get('accept-language')?.split(',')[0] || undefined,
referrer: requestHeaders.get('referer') || undefined,
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined,
});
}
// Server-side analytics tracking removed to prevent duplicate/empty events.
@@ -175,36 +173,33 @@ export default async function Layout(props: {
// Read directly from process.env — bypasses all abstraction to guarantee correctness
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
const cookieStore = await cookies();
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
return (
<html
lang={safeLocale}
className={`scroll-smooth overflow-x-hidden ${inter.variable}`}
data-scroll-behavior="smooth"
suppressHydrationWarning
>
<head>
<link rel="preconnect" href="https://img.infra.mintel.me" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
{/* Preload critical hero video */}
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
</head>
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white" suppressHydrationWarning>
<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}
@@ -213,9 +208,8 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
<FeedbackClientWrapper />
</TransitionProvider>
</NextIntlClientProvider>
</body>

View File

@@ -4,7 +4,7 @@ import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Button, Heading } from '@/components/ui';
import { Terminal, Search, Home, Mail, Activity, Cpu } from 'lucide-react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
export default function NotFound() {
@@ -46,7 +46,7 @@ export default function NotFound() {
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center">
{/* 404 Visual */}
<div className="relative mb-12">
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="relative"
@@ -62,7 +62,7 @@ export default function NotFound() {
{/* Search Scan Overlay */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<motion.div
<m.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 1, 0.5]
@@ -71,9 +71,9 @@ export default function NotFound() {
className="p-5 rounded-full bg-primary/20 border border-primary/40 backdrop-blur-md shadow-[0_0_40px_rgba(var(--color-primary),0.3)]"
>
<Search className="w-10 h-10 text-primary" />
</motion.div>
</m.div>
</div>
</motion.div>
</m.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 w-full items-start">

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
@@ -10,17 +10,21 @@ export const runtime = 'nodejs';
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Index.meta' });
const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse(
<OGImageTemplate
title={t('title')}
description={t('description')}
label="Reliable Energy Infrastructure"
title="Die Experten für Kabelnetzbau"
label="Kabelnetzbau & Infrastruktur"
image={bg}
logo={logo}
/>,
{
...OG_IMAGE_SIZE,
fonts,
},
);
}
}

View File

@@ -4,17 +4,16 @@ import { notFound } from 'next/navigation';
import { getMdxContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc';
export const dynamic = 'force-dynamic';
import nextDynamic from 'next/dynamic';
import { getImageProps } from 'next/image';
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
const HeroSection = nextDynamic(() => import('@/components/blocks/HeroSection').then(mod => mod.HeroSection), { ssr: true });
import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
@@ -38,6 +37,77 @@ const mdxComponents = (references: any[]) => ({
AnimatedCounter,
InteractiveGermanyMap,
ScaleOfImpact,
ResponsiveImage: (props: any) => {
// Parse src if it's already a next image proxy
let src = props.src;
if (typeof src === 'string' && src.startsWith('/_next/image')) {
try {
const urlParam = new URLSearchParams(src.split('?')[1]).get('url');
if (urlParam) src = decodeURIComponent(urlParam);
} catch (e) {
// Fallback
}
}
// Use Next.js getImageProps for responsive srcSet generation
const {
props: { srcSet, src: finalSrc, sizes }
} = getImageProps({
src,
alt: props.alt || '',
width: 1920,
height: 1080,
sizes: '(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw',
quality: 80,
});
return (
<img
{...props}
src={finalSrc}
srcSet={srcSet}
sizes={sizes}
loading="lazy"
decoding="async"
/>
);
},
img: (props: any) => {
// Parse src if it's already a next image proxy
let src = props.src;
if (typeof src === 'string' && src.startsWith('/_next/image')) {
try {
const urlParam = new URLSearchParams(src.split('?')[1]).get('url');
if (urlParam) src = decodeURIComponent(urlParam);
} catch (e) {
// Fallback
}
}
// Use Next.js getImageProps for responsive srcSet generation
const {
props: { srcSet, src: finalSrc, sizes }
} = getImageProps({
src,
alt: props.alt || '',
width: 1920,
height: 1080,
sizes: '(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw',
quality: 80,
});
return (
<img
{...props}
src={finalSrc}
srcSet={srcSet}
sizes={sizes}
loading="lazy"
decoding="async"
style={{ width: '100%', height: 'auto', ...props.style }}
/>
);
}
});
export async function generateMetadata(props: any): Promise<Metadata> {
@@ -52,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') {
@@ -60,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);

View File

@@ -28,9 +28,9 @@ const CustomLi = ({ children, ...props }: any) => {
else if (lowerText.includes('vlf') || lowerText.includes('otdr')) Icon = Zap;
return (
<li className="flex items-start gap-3 p-3 bg-neutral-50/50 border border-neutral-100 rounded-xl" {...props}>
<Icon className="w-4 h-4 text-primary shrink-0 mt-[2px]" />
<span className="text-sm font-medium text-neutral-700 leading-tight">{children}</span>
<li className="flex items-start gap-3 pl-4 pr-6 py-3.5 bg-neutral-50/50 border border-neutral-100 rounded-xl" {...props}>
<Icon className="w-5 h-5 text-primary shrink-0 mt-0.5" />
<span className="text-sm font-medium text-neutral-700 leading-snug [&>p]:mb-0">{children}</span>
</li>
);
};

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'}`}

View File

@@ -1,19 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
transition={{
duration: 0.7,
ease: [0.16, 1, 0.3, 1],
}}
className="w-full h-full"
>
{children}
</motion.div>
);
}

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,
});

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 (

View File

@@ -1,89 +0,0 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@config "../tailwind.config.cjs";
/* E-TIB Custom Utilities */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.hide-scrollbar {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none !important; /* Chrome, Safari and Opera */
width: 0 !important;
height: 0 !important;
}
@keyframes bg-pulse-15 {
0%, 90%, 100% { opacity: 0.04; }
95% { opacity: 0.10; filter: brightness(1.2); }
}
@keyframes bg-pulse-20 {
0%, 90%, 100% { opacity: 0.07; }
95% { opacity: 0.14; filter: brightness(1.2); }
}
.animate-bg-pulse-15 { animation: bg-pulse-15 12s infinite ease-in-out; }
.animate-bg-pulse-20 { animation: bg-pulse-20 12s infinite ease-in-out; }
}
/* Base Styles */
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-neutral font-sans text-neutral-dark antialiased;
}
/* Interactive elements pointer */
a,
button,
[role="button"],
input[type="button"],
input[type="submit"],
input[type="reset"],
select,
summary,
label[for] {
cursor: pointer;
}
/* Ensure disabled elements don't show pointer */
button:disabled,
[role="button"]:disabled,
input:disabled,
select:disabled,
button[disabled] {
cursor: not-allowed;
}
/* Custom Modern Scrollbar */
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(14, 122, 92, 0.4);
border-radius: 10px;
border: 4px solid transparent;
background-clip: padding-box;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(14, 122, 92, 0.8);
}
/* Firefox */
html {
scrollbar-color: rgba(14, 122, 92, 0.5) transparent;
scrollbar-width: thin;
}
}
/* trigger rebuild */

View File

@@ -10,11 +10,7 @@ export default function manifest(): MetadataRoute.Manifest {
background_color: '#001a4d',
theme_color: '#001a4d',
icons: [
{
src: '/favicon.ico',
sizes: 'any',
type: 'image/x-icon',
},
{
src: '/logo.png',
sizes: '512x512',

View File

@@ -40,7 +40,10 @@ export async function POST(request: NextRequest) {
website: websiteId,
};
const umamiEndpoint = config.analytics.umami.apiEndpoint;
let umamiEndpoint = config.analytics.umami.apiEndpoint || 'https://analytics.infra.mintel.me';
if (!umamiEndpoint.startsWith('http')) {
umamiEndpoint = `https://${umamiEndpoint}`;
}
// Log the event (internal only)
logger.debug('Forwarding analytics event', {

48
bundle-stats.json Normal file
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

View File

@@ -98,7 +98,7 @@ export default function ContactForm() {
if (status === 'error') {
return (
<Card
className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-destructive/20 shadow-2xl text-center bg-destructive/5 animate-slide-up"
className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-destructive/20 shadow-2xl text-center bg-destructive/5 "
role="alert"
aria-live="assertive"
>
@@ -134,7 +134,7 @@ export default function ContactForm() {
}
return (
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl animate-slide-up">
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl ">
<Heading level={3} subtitle={t('form.subtitle')} className="mb-6 md:mb-10">
{t('form.title')}
</Heading>

View File

@@ -15,7 +15,7 @@ export default function DatasheetDownload({ datasheetPath, className }: Datashee
const { trackEvent } = useAnalytics();
return (
<div className={cn('mt-8 animate-slight-fade-in-from-bottom', className)}>
<div className={cn('mt-8 ', className)}>
<a
href={datasheetPath}
target="_blank"

View File

@@ -15,7 +15,7 @@ export default function ExcelDownload({ excelPath, className }: ExcelDownloadPro
const { trackEvent } = useAnalytics();
return (
<div className={cn('mt-4 animate-slight-fade-in-from-bottom', className)}>
<div className={cn('mt-4 ', className)}>
<a
href={excelPath}
target="_blank"

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 />;
}

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 {
@@ -215,9 +215,9 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
<AnimatePresence mode="wait" initial={false}>
<m.div
key={currentIndex}
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.9, filter: 'blur(10px)' }}
initial={{ opacity: 0, scale: 1.1 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.7, ease: [0.25, 0.46, 0.45, 0.94] }}
className="relative w-full h-full"
>

View File

@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
description?: string;
label?: string;
image?: string;
logo?: string;
mode?: 'dark' | 'light' | 'image';
}
@@ -13,11 +14,11 @@ export function OGImageTemplate({
description,
label,
image,
logo,
mode = 'dark',
}: OGImageTemplateProps) {
const primaryBlue = '#001a4d';
const accentGreen = '#10a379';
const saturatedBlue = '#011dff';
const backgroundDark = '#1a1a1a';
const primaryGreen = '#0e7a5c';
const containerStyle: React.CSSProperties = {
height: '100%',
@@ -26,7 +27,7 @@ export function OGImageTemplate({
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
backgroundColor: mode === 'light' ? '#ffffff' : backgroundDark,
padding: '80px',
position: 'relative',
fontFamily: 'Inter',
@@ -65,7 +66,7 @@ export function OGImageTemplate({
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))',
background: 'linear-gradient(135deg, rgba(26,26,26,0.98) 0%, rgba(26,26,26,0.85) 40%, rgba(14,122,92,0.4) 100%)',
}}
/>
</div>
@@ -75,101 +76,127 @@ export function OGImageTemplate({
<div
style={{
position: 'absolute',
top: '-150px',
right: '-150px',
width: '600px',
height: '600px',
borderRadius: '300px',
backgroundColor: `${accentGreen}15`,
top: '-300px',
right: '-200px',
width: '800px',
height: '1200px',
backgroundColor: `${primaryGreen}1A`,
transform: 'rotate(25deg)',
display: 'flex',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}>
{/* Label / Category */}
{label && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: accentGreen,
textTransform: 'uppercase',
letterSpacing: '0.3em',
marginBottom: '32px',
display: 'flex',
}}
>
{label}
</div>
)}
{/* Title */}
<div
style={{
fontSize: title.length > 40 ? '64px' : '82px',
fontWeight: 700,
color: 'white',
lineHeight: '1.05',
maxWidth: '950px',
marginBottom: '40px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '32px',
color: 'rgba(255,255,255,0.7)',
maxWidth: '850px',
lineHeight: '1.4',
display: 'flex',
fontWeight: 400,
}}
>
{description.length > 160 ? description.substring(0, 157) + '...' : description}
</div>
)}
</div>
{/* Brand Footer */}
{/* Main Content Wrapper */}
<div
style={{
position: 'absolute',
bottom: '80px',
left: '80px',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'space-between',
height: '100%',
width: '100%',
position: 'relative',
zIndex: 10,
}}
>
{/* Top: Text Content */}
<div style={{ display: 'flex', flexDirection: 'column' }}>
{/* Label / Category */}
{label && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: primaryGreen,
textTransform: 'uppercase',
letterSpacing: '0.25em',
marginBottom: '24px',
display: 'flex',
}}
>
{label}
</div>
)}
{/* Title */}
<div
style={{
fontSize: '72px',
fontWeight: 700,
color: 'white',
lineHeight: '1.1',
maxWidth: '900px',
marginBottom: '24px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
</div>
{/* Description */}
{description && (
<div
style={{
fontSize: '36px',
color: 'rgba(255,255,255,0.85)',
maxWidth: '850px',
lineHeight: '1.4',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontWeight: 400,
}}
>
{description}
</div>
)}
</div>
{/* Bottom: Brand Footer */}
<div
style={{
width: '80px',
height: '6px',
backgroundColor: accentGreen,
borderRadius: '3px',
marginRight: '24px',
}}
/>
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
alignItems: 'center',
}}
>
KLZ Cables
{logo ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={logo}
alt="E-TIB GmbH"
height="56"
style={{ marginRight: '24px', objectFit: 'contain' }}
/>
) : (
<div
style={{
width: '80px',
height: '6px',
backgroundColor: primaryGreen,
borderRadius: '3px',
marginRight: '24px',
}}
/>
)}
{!logo && (
<div
style={{
fontSize: '24px',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.15em',
display: 'flex',
}}
>
E-TIB GmbH
</div>
)}
</div>
</div>
{/* Saturated Blue Brand Strip */}
{/* Primary Green Brand Strip */}
<div
style={{
position: 'absolute',
@@ -177,7 +204,7 @@ export function OGImageTemplate({
right: 0,
width: '12px',
height: '100%',
backgroundColor: saturatedBlue,
backgroundColor: primaryGreen,
}}
/>
</div>

View File

@@ -26,7 +26,7 @@ export default function ProductSidebar({
const t = useTranslations('Products');
return (
<aside className={cn('flex flex-col gap-4 animate-slight-fade-in-from-bottom', className)}>
<aside className={cn('flex flex-col gap-4 ', className)}>
{/* Request Quote Form Card */}
<div className="bg-white rounded-3xl border border-neutral-medium shadow-sm transition-all duration-500 hover:shadow-2xl hover:-translate-y-1 overflow-hidden group/card">
<div className="bg-primary p-6 text-white relative overflow-hidden">

View File

@@ -84,7 +84,7 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
if (status === 'success') {
return (
<div
className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center animate-fade-in !mt-0 w-full"
className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center !mt-0 w-full"
role="alert"
aria-live="polite"
>
@@ -126,7 +126,7 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
if (status === 'error') {
return (
<div
className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center animate-fade-in !mt-0 w-full"
className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center !mt-0 w-full"
role="alert"
aria-live="assertive"
>

View File

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

View File

@@ -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;

View File

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

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from "react";
import { motion, AnimatePresence } 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";
@@ -289,7 +289,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
{isActive && (
<div className="fixed inset-0 pointer-events-none z-[9998]">
{hoveredRect && (
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -304,7 +304,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
)}
{selectedRect && (
<motion.div
<m.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="absolute border-2 border-yellow-400 bg-yellow-400/20 rounded-sm"
@@ -324,7 +324,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<AnimatePresence>
{selectedElement && (
<div className="fixed inset-0 flex items-center justify-center z-[10000] bg-black/40 backdrop-blur-sm">
<motion.div
<m.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
@@ -475,7 +475,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<Check size={20} />
Speichern
</button>
</motion.div>
</m.div>
</div>
)}
</AnimatePresence>
@@ -484,14 +484,14 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<AnimatePresence>
{showList && (
<>
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowList(false)}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[10001]"
/>
<motion.div
<m.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
@@ -618,7 +618,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
</button>
</div>
)}
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { m, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { LogoArcs } from '@/components/ui/LogoArcs';
@@ -30,7 +30,7 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
@@ -94,7 +94,7 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
)}
</div>
<motion.div
<m.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
@@ -116,7 +116,7 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
const animationClass = animationClasses[index % animationClasses.length];
return (
<motion.div
<m.div
key={benefit.id}
variants={itemVariants}
className="group relative overflow-hidden bg-neutral-50/50 border border-neutral-200/60 rounded-3xl p-8 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1"
@@ -144,10 +144,10 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
)}
</div>
</div>
</motion.div>
</m.div>
);
})}
</motion.div>
</m.div>
</div>
</section>
);

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } 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';
@@ -93,7 +93,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
};
const itemVariants = {
hidden: { opacity: 0, y: 40 },
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
@@ -122,7 +122,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<Container className="relative z-10">
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
@@ -131,20 +131,20 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</motion.div>
</m.div>
</div>
)}
<motion.div
<m.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
{certificates.map((cert, index) => {
const isIso = cert.type === 'iso';
const Wrapper = cert.pdfUrl ? motion.a : motion.div;
const Wrapper = cert.pdfUrl ? m.a : m.div;
const wrapperProps = cert.pdfUrl ? {
href: encodeURI(cert.pdfUrl),
target: "_blank",
@@ -219,7 +219,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
</Wrapper>
);
})}
</motion.div>
</m.div>
</Container>
</section>
);

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, useScroll, useTransform } 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';
@@ -102,7 +102,7 @@ export function CompanyTimeline({
{/* Animated Central Line */}
<div className="absolute left-[38px] md:left-1/2 top-0 bottom-0 w-1.5 -translate-x-1/2 rounded-full overflow-hidden">
<motion.div
<m.div
className="w-full bg-primary drop-shadow-[0_0_8px_rgba(130,237,32,0.8)]"
style={{ height: lineHeight }}
/>
@@ -114,9 +114,9 @@ export function CompanyTimeline({
const Icon = iconMap[milestone.iconName] || Building2;
return (
<motion.div
<m.div
key={i}
initial={{ opacity: 0, y: 40 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
@@ -159,7 +159,7 @@ export function CompanyTimeline({
</div>
</div>
</motion.div>
</m.div>
);
})}
</div>

View File

@@ -2,7 +2,7 @@
import Link from 'next/link';
import Image from 'next/image';
import { motion } 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';
@@ -70,7 +70,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[minmax(220px,auto)]">
{/* Simplified static grid */}
</div>
</Container>
@@ -88,7 +88,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[minmax(220px,auto)] px-4 md:px-0">
{items.map((item, idx) => {
const isLarge = item.size === 'large';
const isMedium = item.size === 'medium';
@@ -103,9 +103,9 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
if (isAccent) {
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, y: 40 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
@@ -125,14 +125,14 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
<h4 className="font-heading font-extrabold text-2xl md:text-3xl relative z-10 leading-snug">{item.title}</h4>
{item.description && <p className="text-white/80 text-base md:text-lg mt-3 relative z-10 font-medium">{item.description}</p>}
</motion.div>
</m.div>
);
}
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, y: 40, scale: 0.98 }}
initial={{ opacity: 0, y: 20, scale: 0.98 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
@@ -182,7 +182,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
</p>
)}
</div>
</motion.div>
</m.div>
);
})}
</div>

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 { motion } from 'framer-motion';
import { m } from 'framer-motion';
const ContactMap = dynamic(() => import('@/components/ContactMap'), {
ssr: false,
@@ -44,21 +44,21 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<Section className="bg-neutral-light relative overflow-hidden">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
<motion.div
<m.div
className="lg:col-span-5 flex flex-col justify-center"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
<motion.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</motion.h2>
<motion.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
<motion.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
<m.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</m.h2>
<m.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</m.h3>
<m.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
Haben Sie Fragen zu unseren Leistungen oder möchten Sie ein konkretes Projekt besprechen? Rufen Sie uns an oder schreiben Sie uns eine E-Mail.
</motion.p>
</m.p>
<div className="space-y-6">
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
@@ -68,9 +68,9 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<a href="tel:+4935616857733">+49 (0) 3561 / 68577 33</a>
</p>
</div>
</motion.div>
</m.div>
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</div>
@@ -80,9 +80,9 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<a href="mailto:info@e-tib.com">info@e-tib.com</a>
</p>
</div>
</motion.div>
</m.div>
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</div>
@@ -94,17 +94,17 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
Deutschland
</p>
</div>
</motion.div>
</m.div>
</div>
</motion.div>
</m.div>
{showForm && (
<motion.div
<m.div
className="lg:col-span-7 relative"
variants={formVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
<Suspense
fallback={
@@ -113,12 +113,12 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
>
<ContactForm />
</Suspense>
</motion.div>
</m.div>
)}
</div>
</Container>
{showMap && (
<motion.section
<m.section
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
@@ -135,7 +135,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
]}
/>
</Suspense>
</motion.section>
</m.section>
)}
</Section>
);

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DataGridPulse() {
@@ -19,12 +19,12 @@ export function DataGridPulse() {
/>
{/* Pulsing horizontal lines (Data Flow) */}
<motion.div
<m.div
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/30 to-transparent blur-[1px]"
/>
<motion.div
<m.div
animate={{ x: ["100%", "-100%"] }}
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/25 to-transparent blur-[1px]"

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useScroll, useTransform, m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DeepDrillAnimation() {
@@ -33,20 +33,20 @@ export function DeepDrillAnimation() {
{/* The Drill Path */}
<div className="relative w-2 h-96 bg-white/10 rounded-full mb-16">
<motion.div
<m.div
className="absolute top-0 left-0 w-full bg-primary rounded-full"
style={{ height: drillDepth, boxShadow: '0 0 20px rgba(130,237,32,0.6)' }}
>
{/* The Drill Head */}
<div className="absolute -bottom-2 -left-2 w-6 h-6 bg-white rounded-full border-4 border-primary shadow-[0_0_15px_rgba(255,255,255,1)]" />
</motion.div>
</m.div>
</div>
{/* The Reveal Data */}
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center"
>
@@ -56,7 +56,7 @@ export function DeepDrillAnimation() {
<div className="text-sm font-bold uppercase tracking-widest text-primary">
{locale === 'en' ? 'Drilled distance since 2023' : 'Gebohrte Strecke seit 2023'}
</div>
</motion.div>
</m.div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
@@ -42,10 +42,10 @@ export function GrowthChart() {
{/* Kabelverlegung Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-6 overflow-hidden relative">
<motion.div
<m.div
initial={{ width: 0 }}
whileInView={{ width: `${kabelWidth}%` }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-primary rounded-full"
/>
@@ -58,10 +58,10 @@ export function GrowthChart() {
{/* Offener Tiefbau Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-5 overflow-hidden relative">
<motion.div
<m.div
initial={{ width: 0 }}
whileInView={{ width: `${offenerWidth}%` }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.15 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-300 rounded-full"
/>
@@ -74,10 +74,10 @@ export function GrowthChart() {
{/* HDD Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-4 overflow-hidden relative">
<motion.div
<m.div
initial={{ width: 0 }}
whileInView={{ width: `${hddWidth}%` }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.2 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-dark rounded-full"
/>

View File

@@ -3,7 +3,6 @@
import React from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion } 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 && (
<motion.div
<div
key={`hero-bg-${pathname}`}
className="absolute inset-0 z-0"
initial={{ scale: 1.05, opacity: 0 }}
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" />
</motion.div>
</div>
)}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
<motion.div
<div
key={`hero-content-${pathname}`}
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
variants={containerVariants}
initial="hidden"
animate="visible"
>
{badge && (
<motion.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>
</motion.div>
</div>
)}
<motion.div variants={itemVariants}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</motion.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 && (
<motion.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>
</motion.div>
</div>
)}
{ctaLabel && ctaHref && (
<motion.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>
</motion.div>
</div>
)}
</motion.div>
</div>
</Container>
</section>
);

View File

@@ -22,6 +22,8 @@ interface HeroVideoProps {
badge?: string;
}
import placeholders from '@/lib/placeholders.json';
export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
@@ -30,7 +32,12 @@ export function HeroVideo(props: HeroVideoProps) {
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const pathname = usePathname();
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster.webp');
}
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
const posterAlt = props.backgroundImage?.alt || data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
@@ -41,31 +48,63 @@ export function HeroVideo(props: HeroVideoProps) {
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
let interactionTriggered = false;
const handleInteraction = () => {
if (interactionTriggered) return;
interactionTriggered = true;
setVideoSrcLoaded(true);
// Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
// Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true })
);
return () => {
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
}, []);
return (
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{videoUrl ? (
<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}
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
decoding="async"
loading="eager"
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 && (
<video
key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
className={`absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90 transition-opacity duration-1000 ${videoSrcLoaded ? 'opacity-100' : 'opacity-0'}`}
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="auto"
/>
) : (
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
preload="none"
/>
)}
@@ -82,7 +121,7 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="container relative z-[4] text-center px-4">
<div
key={`hero-content-${pathname}`}
className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
className="relative"
>
<h1
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"

View File

@@ -1,16 +1,20 @@
'use client';
import React, { useState, useRef, useCallback, useMemo } from 'react';
import { motion, 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" />
)}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.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" />}
</motion.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 && (
<motion.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>
</motion.div>
)}
</AnimatePresence>
<InteractiveMapPins
locations={finalLocations}
locale={locale}
t={t}
activeLocation={activeLocation}
handleMouseEnter={handleMouseEnter}
handleMouseLeave={handleMouseLeave}
/>
</div>
</div>
</div>

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>
);
}

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 { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface JobListingBlockProps {
@@ -37,7 +37,7 @@ const containerVariants = {
};
const itemVariants = {
hidden: { opacity: 0, y: 40 },
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
@@ -72,21 +72,21 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<div className="container mx-auto">
{showFairs && (
<div className="mb-12 md:mb-24 relative">
<motion.h3
<m.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
>
<span className="w-12 h-1.5 bg-primary rounded-full" />
{fairsTitle}
</motion.h3>
<motion.div
</m.h3>
<m.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{fairs.map((messe, idx) => {
@@ -159,44 +159,44 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
if (isLink) {
return (
<motion.a variants={itemVariants} key={`fair-${idx}`} href={messe.url} target="_blank" rel="noopener noreferrer" className={`${baseClasses} cursor-pointer`}>
<m.a variants={itemVariants} key={`fair-${idx}`} href={messe.url} target="_blank" rel="noopener noreferrer" className={`${baseClasses} cursor-pointer`}>
{innerContent}
</motion.a>
</m.a>
);
}
return (
<motion.div variants={itemVariants} key={`fair-${idx}`} className={baseClasses}>
<m.div variants={itemVariants} key={`fair-${idx}`} className={baseClasses}>
{innerContent}
</motion.div>
</m.div>
);
})}
</motion.div>
</m.div>
</div>
)}
{showJobs && (
<div className="mb-12">
<motion.h2
<m.h2
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
>
{title}
</motion.h2>
</m.h2>
{jobs.length > 0 ? (
<motion.div
<m.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
className="grid gap-6"
>
{jobs.map((job: any) => (
<motion.div variants={itemVariants} key={job.id}>
<m.div variants={itemVariants} key={job.id}>
<Link
href={`/karriere/${job.slug}`}
className="group bg-white border border-neutral-100 p-8 rounded-2xl shadow-sm hover:shadow-xl hover:border-primary/20 transition-all flex flex-col md:flex-row md:items-center justify-between gap-6 block"
@@ -214,14 +214,14 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</Link>
</motion.div>
</m.div>
))}
</motion.div>
</m.div>
) : (
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center"
>
@@ -229,7 +229,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<Button href={emptyStateLinkHref} variant="primary" className="mt-6">
{emptyStateLinkText}
</Button>
</motion.div>
</m.div>
)}
</div>
)}

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -46,7 +46,8 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const scrollLeftRef = React.useRef(0);
const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false);
const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title');
const description = props.description || data?.description || t('description');
@@ -97,73 +98,85 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
<section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div className="text-left">
<div className="container relative z-10 mb-12 flex flex-col items-center justify-center text-center gap-6">
<div>
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div>
<div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
<div className="max-w-3xl mx-auto mb-4 md:mb-8 text-neutral-300">
{description}
</div>
</div>
<div className="relative z-10">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<div
ref={containerRef}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-6 md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
>
{references.map((ref, i) => {
const imgSrc = ref.image
? (typeof ref.image === 'string' ? ref.image : ref.image.url)
: fallbacks[i % fallbacks.length];
{/* Fade edges for peek effect */}
<div className="absolute left-0 top-0 bottom-0 w-16 bg-gradient-to-r from-neutral-dark to-transparent z-20 pointer-events-none" />
<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 */}
return (
<motion.div
key={ref.id}
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-start group pointer-events-auto"
>
<Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
}
}}
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
<div
ref={containerRef}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-[calc(50vw-160px)] md:px-[calc(50vw-240px)] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
>
{references.map((ref, i) => {
const imgSrc = ref.image
? (typeof ref.image === 'string' ? ref.image : ref.image.url)
: fallbacks[i % fallbacks.length];
return (
<m.div
key={ref.id}
data-card
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
>
<Image
<Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
e.stopPropagation();
}
}}
onDragStart={(e) => e.preventDefault()}
draggable={false}
src={imgSrc}
alt={ref.title}
fill
sizes="(max-width: 768px) 320px, 480px"
className="object-cover saturate-50 opacity-70 group-hover:scale-105 group-hover:opacity-100 group-hover:saturate-100 transition-all duration-700 ease-in-out pointer-events-none"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300 pointer-events-none" />
<HoverShineOverlay />
<div className="absolute bottom-6 left-6 right-6 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none bg-black/60 backdrop-blur-sm p-4 rounded-xl border border-white/10">
<span className="inline-block py-1 px-3 rounded-full bg-primary/20 text-white text-[10px] font-bold uppercase tracking-widest mb-2 border border-primary/30">
{ref.category}
</span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</Link>
</motion.div>
);
})}
</div>
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
>
<Image
draggable={false}
src={imgSrc}
alt={ref.title}
fill
quality={50}
sizes="(max-width: 768px) 320px, 480px"
className="object-cover saturate-50 opacity-70 group-hover:scale-105 group-hover:opacity-100 group-hover:saturate-100 transition-all duration-700 ease-in-out pointer-events-none"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300 pointer-events-none" />
<HoverShineOverlay />
<div className="absolute bottom-6 left-6 right-6 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none bg-black/60 backdrop-blur-sm p-4 rounded-xl border border-white/10">
<span className="inline-block py-1 px-3 rounded-full bg-primary/20 text-white text-[10px] font-bold uppercase tracking-widest mb-2 border border-primary/30">
{ref.category}
</span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</Link>
</m.div>
);
})}
</div>
</div>
<div className="container relative z-10 mt-4 flex justify-center">

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useScroll, useTransform, m } from 'framer-motion';
import { useLocale } from 'next-intl';
export function ScaleOfImpact() {
@@ -26,14 +26,14 @@ export function ScaleOfImpact() {
className="relative w-full min-h-[40vh] md:min-h-[70vh] bg-[#050B14] py-12 md:py-0 flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
>
{/* Massive Watermark Numbers */}
<motion.div
<m.div
style={{ opacity: bgOpacity }}
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none overflow-hidden mix-blend-overlay"
>
<div className="font-heading font-black text-[20vw] leading-none text-white opacity-20 whitespace-nowrap">
<span className="font-mono tracking-tighter">1.435.223</span>
</div>
</motion.div>
</m.div>
{/* Foreground Content */}
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
@@ -70,7 +70,7 @@ export function ScaleOfImpact() {
strokeWidth="2"
/>
{/* Glowing animated line */}
<motion.path
<m.path
d="M0,50 Q250,0 500,50 T1000,50"
fill="none"
stroke="#ffc107" // warning/yellow color
@@ -84,19 +84,19 @@ export function ScaleOfImpact() {
{/* Static Data Grid (No jumping text) */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-2 md:gap-12 w-full max-w-5xl mt-4 md:mt-12">
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">200<span className="text-primary">+</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">200<span className="text-emerald-400">+</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Projects' : 'Projekte'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Cable Laying' : 'Kabelverlegung'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Open Trenching' : 'Offener Tiefbau'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { m, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
export interface ServicePanelData {
@@ -38,7 +38,7 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
@@ -133,7 +133,7 @@ export function ServiceDetailGrid({
</div>
{(problemStatement || solutionStatement) && (
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
@@ -165,10 +165,10 @@ export function ServiceDetailGrid({
<p className="text-text-secondary leading-relaxed">{solutionStatement.text}</p>
</div>
)}
</motion.div>
</m.div>
)}
<motion.div
<m.div
className="grid grid-cols-1 md:grid-cols-2 gap-8"
variants={containerVariants}
initial="hidden"
@@ -180,7 +180,7 @@ export function ServiceDetailGrid({
const number = String(idx + 1).padStart(2, '0');
return (
<motion.div
<m.div
key={panel.id}
variants={itemVariants}
className={`group relative overflow-hidden bg-white border border-neutral-200 rounded-3xl p-6 md:p-12 transition-all duration-500 hover:border-primary/40 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1 select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
@@ -224,10 +224,10 @@ export function ServiceDetailGrid({
</ul>
))}
</div>
</motion.div>
</m.div>
);
})}
</motion.div>
</m.div>
</div>
</section>
);

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, 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';
@@ -73,19 +73,19 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{(badge || title) && (
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{badge && <h2 className="text-emerald-400 font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{title && <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-white">{title}</h3>}
</div>
)}
<div className="relative -mx-4 md:mx-0">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<motion.div
<m.div
className="flex overflow-x-auto md:overflow-visible snap-x snap-mandatory pb-6 md:pb-0 md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 px-4 md:px-0 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
{companiesData.map((company: any, index: number) => {
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
@@ -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>
@@ -235,7 +235,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
}`;
return (
<motion.div
<m.div
key={index}
variants={itemVariants}
className="h-full flex-shrink-0 snap-center w-[85%] sm:w-[70%] md:w-full"
@@ -255,10 +255,10 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{CardContent}
</div>
)}
</motion.div>
</m.div>
);
})}
</motion.div>
</m.div>
</div>
</div>
</section>

View File

@@ -3,7 +3,7 @@
import React from 'react';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
export interface SupportCTAProps {
title: string;
@@ -39,18 +39,18 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
const { title, description, buttonLabel, buttonHref } = props;
return (
<motion.div
<m.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, margin: "-50px" }}
className="mt-12 md:mt-24 p-6 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 duration-700 group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<motion.h3 variants={itemVariants} className="text-2xl md:text-3xl font-bold mb-4">{title}</motion.h3>
<motion.p variants={itemVariants} className="text-lg text-white/70 mb-8">{description}</motion.p>
<motion.div variants={itemVariants}>
<m.h3 variants={itemVariants} className="text-2xl md:text-3xl font-bold mb-4">{title}</m.h3>
<m.p variants={itemVariants} className="text-lg text-white/70 mb-8">{description}</m.p>
<m.div variants={itemVariants}>
<TrackedLink
href={buttonHref}
className={getButtonClasses('accent', 'lg')}
@@ -66,8 +66,8 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
</span>
<ButtonOverlay variant="accent" />
</TrackedLink>
</motion.div>
</m.div>
</div>
</motion.div>
</m.div>
);
};

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
@@ -40,9 +40,9 @@ export function TeamGrid({ members }: TeamGridProps) {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 xl:gap-10">
{members.map((member, i) => (
<motion.div
<m.div
key={member.id}
initial={{ opacity: 0, y: 40 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
@@ -104,7 +104,7 @@ export function TeamGrid({ members }: TeamGridProps) {
)}
</div>
</div>
</motion.div>
</m.div>
))}
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
import { useScroll, useTransform, useSpring, m } from 'framer-motion';
import { LogoArcs } from '@/components/ui/LogoArcs';
function MagneticRing({
@@ -25,45 +25,56 @@ 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]);
return (
<motion.div
<m.div
ref={ref}
style={{ x, y }}
className={className}
>
{children}
</motion.div>
</m.div>
);
}
@@ -80,7 +91,7 @@ export function CorporateBackground() {
return (
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none hidden md:block" aria-hidden="true">
{/* 1. Top Right - Medium, rotating moderately */}
<motion.div
<m.div
style={{
y: y1,
animationDelay: '0s',
@@ -90,18 +101,18 @@ export function CorporateBackground() {
className="absolute top-[0%] right-0 translate-x-[40%] w-[400px] h-[400px] md:w-[700px] md:h-[700px] animate-bg-pulse-20"
>
<MagneticRing pullRadius={700} pullStrength={40} className="w-full h-full">
<motion.div
<m.div
animate={{ rotate: 360 }}
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full" />
</motion.div>
</m.div>
</MagneticRing>
</motion.div>
</m.div>
{/* 2. Quarter way down left - Smaller, rotating fast reverse */}
<motion.div
<m.div
style={{
y: y2,
animationDelay: '3s',
@@ -111,18 +122,18 @@ export function CorporateBackground() {
className="absolute top-[20%] left-0 -translate-x-[40%] w-[300px] h-[300px] md:w-[500px] md:h-[500px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={500} pullStrength={50} className="w-full h-full">
<motion.div
<m.div
animate={{ rotate: -360 }}
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-secondary"
>
<LogoArcs className="w-full h-full -rotate-45" />
</motion.div>
</m.div>
</MagneticRing>
</motion.div>
</m.div>
{/* 3. Halfway down right - Medium, rotating slow */}
<motion.div
<m.div
style={{
y: y3,
animationDelay: '6s',
@@ -132,18 +143,18 @@ export function CorporateBackground() {
className="absolute top-[45%] right-0 translate-x-[30%] w-[350px] h-[350px] md:w-[600px] md:h-[600px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={600} pullStrength={35} className="w-full h-full">
<motion.div
<m.div
animate={{ rotate: 360 }}
transition={{ duration: 60, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full rotate-90" />
</motion.div>
</m.div>
</MagneticRing>
</motion.div>
</m.div>
{/* 4. Bottom left - Larger, rotating moderately */}
<motion.div
<m.div
style={{
y: y4,
animationDelay: '9s',
@@ -153,18 +164,18 @@ export function CorporateBackground() {
className="absolute top-[70%] left-0 -translate-x-[40%] w-[500px] h-[500px] md:w-[800px] md:h-[800px] animate-bg-pulse-20"
>
<MagneticRing pullRadius={800} pullStrength={30} className="w-full h-full">
<motion.div
<m.div
animate={{ rotate: -360 }}
transition={{ duration: 50, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-secondary"
>
<LogoArcs className="w-full h-full rotate-180" />
</motion.div>
</m.div>
</MagneticRing>
</motion.div>
</m.div>
{/* 5. Deep bottom right - Smallest, very fast */}
<motion.div
<m.div
style={{
y: y5,
animationDelay: '12s',
@@ -174,15 +185,15 @@ export function CorporateBackground() {
className="absolute top-[88%] right-0 translate-x-[50%] w-[250px] h-[250px] md:w-[400px] md:h-[400px] animate-bg-pulse-15"
>
<MagneticRing pullRadius={400} pullStrength={60} className="w-full h-full">
<motion.div
<m.div
animate={{ rotate: 360 }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
className="w-full h-full text-primary"
>
<LogoArcs className="w-full h-full -rotate-12" />
</motion.div>
</m.div>
</MagneticRing>
</motion.div>
</m.div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicCorporateBackground = dynamic(
() => import('./CorporateBackground').then(mod => mod.CorporateBackground),
{ ssr: false }
);

View File

@@ -1,11 +1,11 @@
'use client';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import Image from 'next/image';
export function EUFundingBadge() {
return (
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1] }}
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
alt="European Union Background"
fill
className="object-cover object-top"
priority
loading="lazy"
/>
</div>
@@ -36,6 +36,6 @@ export function EUFundingBadge() {
Europäischen Union
</span>
</div>
</motion.div>
</m.div>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Button } from '@/components/ui/Button';
import { sendContactFormAction } from '@/app/actions/contact';
@@ -37,7 +37,7 @@ export function ContactForm() {
</div>
{status === 'success' ? (
<motion.div
<m.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-primary/10 border border-primary text-primary p-6 rounded-xl flex items-start gap-4"
@@ -49,9 +49,9 @@ export function ContactForm() {
<h4 className="font-bold text-lg mb-1">Anfrage erfolgreich gesendet</h4>
<p>Vielen Dank für Ihr Interesse an der E-TIB Gruppe. Ein Ansprechpartner wird sich zeitnah mit Ihnen in Verbindung setzen.</p>
</div>
</motion.div>
</m.div>
) : status === 'error' ? (
<motion.div
<m.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-red-500/10 border border-red-500 text-red-600 p-6 rounded-xl flex items-start gap-4 mb-6"
@@ -64,7 +64,7 @@ export function ContactForm() {
<p>Leider ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal oder kontaktieren Sie uns direkt per E-Mail oder Telefon.</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => setStatus('idle')}>Erneut versuchen</Button>
</div>
</motion.div>
</m.div>
) : null}
{status !== 'success' && status !== 'error' && (

View File

@@ -13,7 +13,7 @@ export default function Experience({ data }: { data?: any }) {
src="/uploads/2024/12/1694273920124-copy-2.webp"
alt={data?.subtitle || t('subtitle')}
fill
className="object-cover object-center scale-105 animate-slow-zoom"
className="object-cover object-center scale-105 "
sizes="100vw"
quality={100}
/>
@@ -34,7 +34,7 @@ export default function Experience({ data }: { data?: any }) {
</div>
<dl className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="animate-fade-in">
<div className="">
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
{data?.badge1 || t('certifiedQuality')}
</dt>
@@ -42,7 +42,7 @@ export default function Experience({ data }: { data?: any }) {
{data?.badge1Text || t('vdeApproved')}
</dd>
</div>
<div className="animate-fade-in" style={{ animationDelay: '100ms' }}>
<div className="" style={{ animationDelay: '100ms' }}>
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
{data?.badge2 || t('fullSpectrum')}
</dt>

View File

@@ -105,7 +105,7 @@ export default function Hero({ data }: { data?: any }) {
)
.replace(
/<\/green>/g,
'</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>',
'</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 " style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>',
),
}}
/>
@@ -117,7 +117,7 @@ export default function Hero({ data }: { data?: any }) {
{chunks}
</span>
<div
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 "
style={{ animationDelay: '500ms' }}
>
<Scribble variant="circle" />
@@ -199,12 +199,12 @@ export default function Hero({ data }: { data?: any }) {
</div>
</Container>
<div className="relative md:absolute inset-0 z-0 w-full h-[35vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none animate-in fade-in zoom-in-95 duration-1000 ease-out fill-mode-both">
<div className="relative md:absolute inset-0 z-0 w-full h-[35vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none ">
<HeroIllustration />
</div>
<div
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block animate-in fade-in slide-in-from-bottom-4 duration-1000 ease-out fill-mode-both"
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block "
style={{ animationDelay: '2000ms' }}
>
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center p-1">

View File

@@ -15,7 +15,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
src="/uploads/2024/12/DSC08036-Large.webp"
alt={data?.subtitle || t('subtitle')}
fill
className="object-cover scale-105 animate-slow-zoom"
className="object-cover scale-105 "
sizes="100vw"
quality={100}
/>
@@ -24,7 +24,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
</div>
<Container className="relative z-10">
<div className="max-w-3xl text-white animate-slide-up">
<div className="max-w-3xl text-white ">
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white mb-8">
<span className="text-white">{data?.title || t('title')}</span>
</Heading>

View File

@@ -37,7 +37,7 @@ export default function VideoSection({ data }: { data?: any }) {
</video>
)}
<div className="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-primary/60 flex items-center justify-center pointer-events-none">
<div className="max-w-5xl px-6 text-center animate-slide-up pointer-events-auto">
<div className="max-w-5xl px-6 text-center pointer-events-auto">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white leading-[1.1]">
{data?.title ? (
<span

View File

@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicMobileBottomNav = dynamic(
() => import('./MobileBottomNav').then(mod => mod.MobileBottomNav),
{ ssr: false }
);

View File

@@ -77,10 +77,11 @@ describe('Header Dropdown Navigation TDD', () => {
rerender(<Header navLinks={mockNavLinks} />);
});
// 6. Assert dropdown is closed (children are not in the document)
// 6. Assert dropdown is closed (visually hidden via CSS)
await waitFor(() => {
expect(screen.queryByText('Kabelbau')).toBeNull();
expect(screen.queryByText('Bohrtechnik')).toBeNull();
const dropdown = screen.getByText('Kabelbau').closest('div[style*="perspective"]');
expect(dropdown?.className).toContain('invisible');
expect(dropdown?.className).toContain('opacity-0');
});
});
});

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import { TransitionLink } from '@/components/ui/TransitionLink';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion, useScroll, useTransform, AnimatePresence } from 'framer-motion';
import { LanguageSwitcher } from './LanguageSwitcher';
export interface NavLink {
@@ -25,21 +24,30 @@ export function Header({ navLinks }: HeaderProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const pathname = usePathname() || '/';
// Scroll bound shine effect (continuous looping as user scrolls)
const { scrollY } = useScroll();
const shineX = useTransform(scrollY, (y) => {
const adjustedY = Math.max(0, y - 20); // Start after the solid mode kicks in
const range = 800; // 1 full sweep per 800px scrolled
const progress = (adjustedY % range) / range;
return `${-150 + progress * 300}%`;
});
const shineRef = React.useRef<HTMLDivElement>(null);
// Determine current locale from pathname
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
React.useEffect(() => {
let animationFrameId: number;
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (shineRef.current) {
const y = window.scrollY;
const adjustedY = Math.max(0, y - 20);
const range = 800;
const progress = (adjustedY % range) / range;
const x = -150 + progress * 300;
cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(() => {
if (shineRef.current) {
shineRef.current.style.transform = `translateX(${x}%)`;
}
});
}
};
window.addEventListener('scroll', handleScroll);
@@ -88,8 +96,8 @@ export function Header({ navLinks }: HeaderProps) {
>
{/* Scroll bound shine effect */}
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden rounded-full">
<motion.div
style={{ x: shineX }}
<div
ref={shineRef}
className={`absolute top-0 h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-20deg] mix-blend-overlay transition-opacity duration-300 ${isSolidMode ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
@@ -101,7 +109,9 @@ export function Header({ navLinks }: HeaderProps) {
fill
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
priority
fetchPriority="high"
sizes="192px"
unoptimized
/>
{/* Masked sweep overlay on hover */}
<div
@@ -255,12 +265,10 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
</svg>
)}
{/* Framer Motion Active Indicator */}
{/* Active Indicator */}
{isActive && !link.children && (
<motion.div
layoutId="header-active-indicator"
<div
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
@@ -268,15 +276,13 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
</TransitionLink>
<AnimatePresence>
{isHovered && link.children && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
exit={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
style={{ perspective: 1000, transformOrigin: 'top center' }}
className="absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50"
<>
{link.children && (
<div
className={`absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50 transition-all duration-300 origin-top ${
isHovered ? 'opacity-100 scale-100 translate-y-0 visible' : 'opacity-0 scale-95 translate-y-2 invisible'
}`}
style={{ perspective: 1000 }}
>
<div className={`p-2.5 rounded-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] border ${isSolidMode ? 'bg-white/95 border-neutral-200/60' : 'bg-[#050B14]/95 border-white/[0.08] shadow-[0_0_40px_rgba(0,0,0,0.4)]'} backdrop-blur-2xl flex flex-col gap-1 relative overflow-hidden`}>
{/* Top glowing accent line */}
@@ -320,9 +326,9 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
);
})}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
</div>
);
}

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 { motion, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
export interface NavLink {
label: string;
@@ -94,8 +94,8 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
return (
<>
<div className="md:hidden fixed bottom-0 left-0 right-0 z-[9999] px-2 pb-[calc(env(safe-area-inset-bottom)+12px)] pt-4 pointer-events-none flex justify-center">
<nav className="flex justify-between items-center bg-white/75 backdrop-blur-2xl border border-white/60 shadow-[0_8px_32px_rgba(0,0,0,0.12)] rounded-3xl p-1.5 pointer-events-auto w-full max-w-[480px]">
<div className="md:hidden fixed bottom-0 left-0 right-0 z-[9999] px-2 pb-[calc(env(safe-area-inset-bottom)_+_12px)] pt-4 pointer-events-none flex justify-center">
<nav className="flex justify-between items-center bg-white/95 backdrop-blur-2xl border border-white/60 shadow-[0_8px_32px_rgba(0,0,0,0.12)] rounded-3xl p-1.5 pointer-events-auto w-full max-w-[480px]">
{items.map((item) => {
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
@@ -113,7 +113,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
const innerContent = (
<>
{isActive && (
<motion.div
<m.div
layoutId="mobile-nav-active-bg"
className="absolute inset-0 bg-primary/10 rounded-2xl shadow-sm border border-primary/10"
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
@@ -121,19 +121,19 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
)}
<div className="relative flex flex-col items-center justify-center z-10 w-full h-full">
<motion.div
<m.div
whileTap={{ scale: 0.8 }}
animate={{ y: isActive ? -2 : 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{item.icon}
</motion.div>
<motion.span
animate={{ opacity: isActive ? 1 : 0.7, y: isActive ? 0 : 2 }}
className="text-[9px] font-bold mt-1 tracking-wide"
</m.div>
<m.span
animate={{ y: isActive ? 0 : 2 }}
className="text-[10px] font-bold mt-1 tracking-wide"
>
{item.label}
</motion.span>
</m.span>
</div>
</>
);
@@ -144,7 +144,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
key={item.label}
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none w-full focus:outline-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
isActive ? 'text-primary-dark' : 'text-neutral-dark hover:text-black'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
@@ -159,7 +159,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
href={mappedUrl}
onClick={() => setIsFlyoutOpen(false)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
isActive ? 'text-primary-dark' : 'text-neutral-dark hover:text-black'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
@@ -173,7 +173,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
<AnimatePresence>
{isFlyoutOpen && (
<>
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -181,7 +181,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[9997] md:hidden"
onClick={() => setIsFlyoutOpen(false)}
/>
<motion.div
<m.div
initial={{ y: '100%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '100%', opacity: 0 }}
@@ -194,7 +194,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
setIsFlyoutOpen(false);
}
}}
className="fixed bottom-0 left-0 right-0 z-[9998] bg-white rounded-t-[32px] pt-4 pb-[calc(env(safe-area-inset-bottom)+7rem)] px-6 md:hidden shadow-[0_-20px_40px_rgba(0,0,0,0.1)] border-t border-neutral-100"
className="fixed bottom-0 left-0 right-0 z-[9998] bg-white rounded-t-[32px] pt-4 pb-[calc(env(safe-area-inset-bottom)_+_7rem)] px-6 md:hidden shadow-[0_-20px_40px_rgba(0,0,0,0.1)] border-t border-neutral-100"
>
<div className="w-16 h-1.5 bg-neutral-200 rounded-full mx-auto mb-6 cursor-grab active:cursor-grabbing" />
<h3 className="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-4 px-2">{currentLocale === 'de' ? 'Über uns & Mehr' : 'About us & More'}</h3>
@@ -222,7 +222,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
);
})}
</div>
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View File

@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicInitialLoader = dynamic(
() => import('./InitialLoader').then(mod => mod.InitialLoader),
{ ssr: false }
);

View File

@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicPageTransitionShutter = dynamic(
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
{ ssr: false }
);

View File

@@ -1,61 +1,39 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
const PRELOAD_VIDEOS = [
'/assets/videos/web/hero-bohrung.mp4'
];
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
const [isLoading, setIsLoading] = useState(shouldShowLoader);
useEffect(() => {
if (!shouldShowLoader) return;
// Fallback Timeout (4 Sekunden), falls Videos hängen
const timeout = setTimeout(() => {
finishLoading();
}, 4000);
let videosLoaded = 0;
const checkAllLoaded = () => {
videosLoaded++;
if (videosLoaded === PRELOAD_VIDEOS.length) {
clearTimeout(timeout);
// Kleine Verzögerung für die smootheness
setTimeout(finishLoading, 800);
}
};
PRELOAD_VIDEOS.forEach((src) => {
const video = document.createElement('video');
video.preload = 'auto';
video.oncanplaythrough = checkAllLoaded;
video.onerror = checkAllLoaded; // bei Fehler trotzdem weitermachen
video.src = src;
video.load();
});
// Cleanup
return () => clearTimeout(timeout);
}, [shouldShowLoader]);
const finishLoading = () => {
// Set a session cookie so the server knows not to render the loader again
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
setIsLoading(false);
};
useEffect(() => {
if (!shouldShowLoader) return;
// Just show the animation for a short time to allow UI to mount,
// we don't preload 10MB videos anymore to save pagespeed!
const timeout = setTimeout(() => {
finishLoading();
}, 1200);
// Cleanup
return () => clearTimeout(timeout);
}, [shouldShowLoader]);
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
if (!shouldShowLoader) return null;
return (
<AnimatePresence>
{isLoading && (
<motion.div
<m.div
key="initial-loader"
initial={{ y: 0 }}
exit={{ y: '-100%' }}
@@ -68,7 +46,7 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
{/* Deep Glow */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/10 via-transparent to-transparent opacity-80 mix-blend-screen" />
<motion.div
<m.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
@@ -92,10 +70,10 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
<div className="absolute inset-0 bg-white/70" />
{/* Sweeping intense white light */}
<motion.div
<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>
@@ -106,22 +84,22 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
<span>System</span>
<span className="text-primary/80 animate-pulse">Online</span>
</div>
<motion.div
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
>
<motion.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%" }}
<m.div
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] }}
/>
</motion.div>
</m.div>
</div>
</motion.div>
</motion.div>
</m.div>
</m.div>
)}
</AnimatePresence>
);

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { m, AnimatePresence } from 'framer-motion';
import { useTransition } from './TransitionProvider';
import Image from 'next/image';
@@ -11,13 +11,13 @@ export function PageTransitionShutter() {
return (
<AnimatePresence>
{isTransitioning && (
<motion.div
key="shutter"
<m.div
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',
@@ -29,7 +29,7 @@ export function PageTransitionShutter() {
{/* Deep Glow inside the Shutter */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/10 via-transparent to-transparent opacity-80 mix-blend-screen" />
<motion.div
<m.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
@@ -54,10 +54,10 @@ export function PageTransitionShutter() {
<div className="absolute inset-0 bg-white/70" />
{/* Sweeping intense white light */}
<motion.div
<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>
@@ -69,7 +69,7 @@ export function PageTransitionShutter() {
(() => {
const [, fromLang, toLang] = transitionMessage.split(':');
return (
<motion.div
<m.div
key="lang-switch-graphic"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
@@ -78,7 +78,7 @@ export function PageTransitionShutter() {
>
{/* Old Language Flag - Fading out */}
<div className="w-20 h-20 sm:w-28 sm:h-28 rounded-[2rem] bg-white/5 border border-white/10 backdrop-blur-md flex items-center justify-center text-5xl sm:text-7xl relative overflow-hidden group grayscale opacity-50">
<motion.div
<m.div
className="absolute inset-0 bg-white/10"
animate={{ scale: [1, 2], opacity: [0.5, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
@@ -88,7 +88,7 @@ export function PageTransitionShutter() {
{/* Animated Arrow connecting them */}
<div className="flex flex-col items-center gap-3">
<motion.div
<m.div
animate={{ x: [0, 20, 0] }}
transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
className="text-primary drop-shadow-[0_0_15px_rgba(var(--color-primary),0.8)]"
@@ -97,7 +97,7 @@ export function PageTransitionShutter() {
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</motion.div>
</m.div>
<div className="flex gap-2 opacity-50">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" />
<span className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" style={{ animationDelay: '150ms' }} />
@@ -108,17 +108,17 @@ export function PageTransitionShutter() {
{/* New Language Flag - Intense Glow */}
<div className="w-24 h-24 sm:w-32 sm:h-32 rounded-[2.5rem] bg-primary/10 border border-primary/50 shadow-[0_0_80px_rgba(var(--color-primary),0.5)] backdrop-blur-xl flex items-center justify-center text-6xl sm:text-8xl relative overflow-hidden ring-4 ring-primary/30">
<span className="relative z-10 drop-shadow-[0_0_15px_rgba(255,255,255,0.6)] leading-none">{toLang}</span>
<motion.div
<m.div
className="absolute inset-0 w-[200%] h-full bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg]"
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/>
</div>
</motion.div>
</m.div>
);
})()
) : (
<motion.div
<m.div
key="text-message"
initial={{ opacity: 0, y: 15, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
@@ -126,13 +126,13 @@ export function PageTransitionShutter() {
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="mt-10 mb-2 px-6 py-2.5 rounded-full border border-primary/40 bg-primary/10 shadow-[0_0_40px_rgba(var(--color-primary),0.3)] backdrop-blur-xl relative overflow-hidden flex items-center justify-center"
>
<motion.div
<m.div
className="absolute inset-0 w-[200%] h-full bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-[-20deg]"
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/>
<span className="text-white font-heading tracking-[0.2em] uppercase text-xs sm:text-sm font-extrabold flex items-center gap-3 relative z-10 drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]">
<motion.svg
<m.svg
animate={{ rotate: 360 }}
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-primary-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
@@ -140,15 +140,15 @@ export function PageTransitionShutter() {
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</motion.svg>
</m.svg>
{transitionMessage}
</span>
</motion.div>
</m.div>
)
)}
</AnimatePresence>
</motion.div>
</motion.div>
</m.div>
</m.div>
)}
</AnimatePresence>
);

View File

@@ -2,6 +2,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;
@@ -14,9 +24,11 @@ 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();
const pathname = usePathname();
const searchParams = useSearchParams();
// 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);
@@ -24,18 +36,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
isTransitioningRef.current = isTransitioning;
}, [isTransitioning]);
// Watch for route changes to complete the transition
useEffect(() => {
if (isTransitioningRef.current) {
// The route has actually changed in the browser, or at least the RSC payload arrived
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams]); // Run when pathname or search parameters change
const startTransition = useCallback((href: string, message: string | null = null) => {
// Wenn wir bereits navigieren, nichts tun
if (isTransitioning) return;
@@ -55,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(() => {
@@ -75,11 +76,38 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
return (
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
{children}
<React.Suspense fallback={null}>
<TransitionRouteWatcher
isTransitioningRef={isTransitioningRef}
setIsTransitioning={setIsTransitioning}
setTransitionMessage={setTransitionMessage}
/>
</React.Suspense>
<LazyMotion features={loadFeatures}>
{hasMountedShutter && <DynamicPageTransitionShutter />}
{children}
</LazyMotion>
</TransitionContext.Provider>
);
}
function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (isTransitioningRef.current) {
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]);
return null;
}
export function useTransition() {
const context = useContext(TransitionContext);
if (!context) {

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion, useMotionValue, useTransform, animate, useInView } from 'framer-motion';
import { useMotionValue, useTransform, animate, useInView, m } from 'framer-motion';
interface AnimatedCounterProps {
value: number;
@@ -41,8 +41,8 @@ export function AnimatedCounter({
}, [inView, value, duration, delay, count]);
return (
<motion.span ref={ref} className={className}>
{prefix}<motion.span>{rounded}</motion.span>{suffix}
</motion.span>
<m.span ref={ref} className={className}>
{prefix}<m.span>{rounded}</m.span>{suffix}
</m.span>
);
}

View File

@@ -37,7 +37,7 @@ export function TransitionLink({ children, href, onClick, transitionMessage, ...
};
return (
<Link href={href} onClick={handleClick} className={cn('cursor-pointer', props.className)} {...props}>
<Link href={href} onClick={handleClick} prefetch={props.prefetch ?? false} className={cn('cursor-pointer', props.className)} {...props}>
{children}
</Link>
);

View File

@@ -1,6 +1,6 @@
'use client';
import { motion, useInView } from 'framer-motion';
import { useInView, m } from 'framer-motion';
import { useRef } from 'react';
/**
@@ -73,7 +73,7 @@ export function StratumLines({
{LINES.map((line, i) => {
const isFromRight = placement.includes('right') ? !line.fromRight : line.fromRight;
return (
<motion.div
<m.div
key={i}
style={{
width: `${line.width}%`,

View File

@@ -88,7 +88,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
{/* Right Column: Imagery & Mission */}
<div className="lg:col-span-7 relative">
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB im Einsatz" className="w-full h-full object-cover" />
<ResponsiveImage src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB im Einsatz" className="w-full h-full object-cover" loading="lazy" decoding="async" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
@@ -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",

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"

View File

@@ -88,7 +88,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
{/* Right Column: Imagery & Mission */}
<div className="lg:col-span-7 relative">
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB in action" className="w-full h-full object-cover" />
<ResponsiveImage src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB in action" className="w-full h-full object-cover" loading="lazy" decoding="async" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">

1
dummy-polyfill.js Normal file
View File

@@ -0,0 +1 @@
export default {};

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
lh-desktop.json Normal file

File diff suppressed because one or more lines are too long

12377
lh-report.json Normal file

File diff suppressed because one or more lines are too long

14117
lh-single.json Normal file

File diff suppressed because one or more lines are too long

11818
lh-standorte.json Normal file

File diff suppressed because one or more lines are too long

2
lib/empty-polyfill.js Normal file
View File

@@ -0,0 +1,2 @@
// Empty polyfill to trick Next.js into not loading legacy polyfills
export default {};

View File

@@ -5,19 +5,32 @@ 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');
try {
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
const boldFont = readFileSync(boldFontPath);
const regularFont = readFileSync(regularFontPath);
console.log(
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
const boldFontBuffer = readFileSync(boldFontPath);
const regularFontBuffer = readFileSync(regularFontPath);
// Satori strictly requires an ArrayBuffer.
const boldFont = boldFontBuffer.buffer.slice(
boldFontBuffer.byteOffset,
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
);
const regularFont = regularFontBuffer.buffer.slice(
regularFontBuffer.byteOffset,
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
);
return [
console.log(
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
);
cachedFonts = [
{
name: 'Inter',
data: boldFont,
@@ -31,12 +44,41 @@ 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');
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');
cachedLogo = `data:image/png;base64,${logoBase64}`;
return cachedLogo;
} catch (err) {
console.error('[OG] Failed to load logo', err);
return undefined;
}
}
/**
* Common configuration for OG images
*/

7
lib/placeholders.json Normal file
View File

@@ -0,0 +1,7 @@
{
"/assets/photos/DJI_0048.JPG": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADwAQCdASoKAAcABUB8JYwCdH8AGDNIbaAA92kbyv6vgOVMTIAAAA==",
"/assets/videos/web/hero-bahnkreuzung-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA=",
"/assets/videos/web/hero-bohrung-poster.jpg": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACwAQCdASoKAAYABUB8JZACdAEOO2gAAN5Rr3co8j7CDYNKNCAAAA==",
"/assets/videos/web/hero-kabelpflug-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA=",
"/assets/videos/web/hero-messe-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA="
}

View File

@@ -1,7 +1,6 @@
import { config } from './config';
const getSiteUrl = () => {
if (process.env.CI) return 'http://etib.localhost';
return (config.baseUrl as string) || 'https://e-tib.com';
};

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(

View File

@@ -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,
});

View File

@@ -66,6 +66,10 @@
"role": "Geschäftsführer",
"description": "Danny Joseph leitet die E-TIB GmbH und koordiniert die strategische Ausrichtung der gesamten Unternehmensgruppe vom Hauptsitz in Guben aus."
},
"gallery": {
"title": "Einblicke in unsere Arbeit",
"subtitle": "Eindrücke aus dem Arbeitsalltag und Projekten"
},
"groups": {
"title": "Unsere Unternehmensgruppe",
"subtitle": "Spezialisierte Einheiten für komplexe Projekte",

View File

@@ -63,8 +63,12 @@
},
"danny": {
"name": "Danny Joseph",
"role": "CEO",
"description": "Danny Joseph leads E-TIB GmbH and coordinates the strategic direction of the entire group from its headquarters in Guben."
"role": "Managing Director",
"description": "Danny Joseph leads E-TIB GmbH and coordinates the strategic direction of the entire corporate group from the headquarters in Guben."
},
"gallery": {
"title": "Insights into our work",
"subtitle": "Impressions from daily work and projects"
},
"groups": {
"title": "Our Corporate Group",

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';
@@ -23,7 +23,6 @@ const nextConfig = {
'lucide-react',
'framer-motion',
'@/components/ui',
'@sentry/nextjs',
'react-hook-form',
'zod',
'date-fns',
@@ -47,7 +46,18 @@ const nextConfig = {
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || process.env.npm_package_version || 'dev',
},
// Prevent webpack from restarting when .env files are touched via Docker volume mount
webpack: (config, { dev }) => {
webpack: (config, { dev, isServer }) => {
// Completely strip Next.js forced legacy polyfills for modern browser targets
if (!isServer) {
config.resolve.alias = {
...config.resolve.alias,
'next/dist/build/polyfills/polyfill-module': false,
'next/dist/build/polyfills/polyfill-nomodule': false,
'core-js': false,
'regenerator-runtime': false,
};
}
if (dev) {
config.watchOptions = {
...config.watchOptions,
@@ -438,7 +448,7 @@ const nextConfig = {
},
images: {
qualities: [25, 50, 75, 100],
formats: ['image/webp'],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
remotePatterns: [
{
@@ -538,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;

View File

@@ -1,5 +1,6 @@
{
"name": "e-tib-nextjs",
"version": "2.4.13",
"type": "module",
"private": true,
"packageManager": "pnpm@10.18.3",
@@ -11,6 +12,7 @@
"@mintel/next-utils": "1.9.5",
"@next/mdx": "^16.1.6",
"@react-email/components": "1.0.8",
"@react-pdf/renderer": "^4.5.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@sentry/nextjs": "^10.39.0",
@@ -105,6 +107,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"postinstall": "echo '' > node_modules/next/dist/build/polyfills/polyfill-module.js && echo '' > node_modules/next/dist/build/polyfills/polyfill-nomodule.js",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:og": "vitest run tests/og-image.test.ts",
@@ -138,7 +141,6 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.14",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
@@ -149,13 +151,15 @@
],
"overrides": {
"next": "16.1.6",
"minimatch": ">=10.2.2"
"minimatch": ">=10.2.2",
"framer-motion": "^12.38.0"
}
},
"browserslist": [
"last 3 versions",
"not dead",
"not ie 11"
"last 2 chrome versions",
"last 2 firefox versions",
"last 2 edge versions",
"safari >= 15.4"
],
"peerDependencies": {
"lucide-react": "^0.563.0"

318
pnpm-lock.yaml generated
View File

@@ -7,6 +7,7 @@ settings:
overrides:
next: 16.1.6
minimatch: '>=10.2.2'
framer-motion: ^12.38.0
importers:
@@ -33,6 +34,9 @@ importers:
'@react-email/components':
specifier: 1.0.8
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-pdf/renderer':
specifier: ^4.5.1
version: 4.5.1(react@19.2.4)
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
@@ -52,7 +56,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
@@ -2209,6 +2213,49 @@ packages:
react: ^18.0.0
react-dom: ^18.0.0
'@react-pdf/fns@3.1.3':
resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==}
'@react-pdf/font@4.0.8':
resolution: {integrity: sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==}
'@react-pdf/image@3.1.0':
resolution: {integrity: sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==}
'@react-pdf/layout@4.6.1':
resolution: {integrity: sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==}
'@react-pdf/pdfkit@5.1.1':
resolution: {integrity: sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==}
'@react-pdf/primitives@4.3.0':
resolution: {integrity: sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==}
'@react-pdf/reconciler@2.0.0':
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-pdf/render@4.5.1':
resolution: {integrity: sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==}
'@react-pdf/renderer@4.5.1':
resolution: {integrity: sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-pdf/stylesheet@6.2.1':
resolution: {integrity: sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==}
'@react-pdf/svg@1.1.0':
resolution: {integrity: sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==}
'@react-pdf/textkit@6.3.0':
resolution: {integrity: sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==}
'@react-pdf/types@2.11.1':
resolution: {integrity: sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==}
'@react-three/drei@10.7.7':
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
peerDependencies:
@@ -3383,6 +3430,9 @@ packages:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
abs-svg-path@0.1.1:
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -3718,6 +3768,9 @@ packages:
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
browserify-zlib@0.2.0:
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
browserslist@4.28.2:
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -3912,6 +3965,14 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-name@2.1.0:
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
engines: {node: '>=12.20'}
color-string@2.1.4:
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
engines: {node: '>=18'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -4363,6 +4424,9 @@ packages:
electron-to-chromium@1.5.330:
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -4843,20 +4907,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:
@@ -5100,6 +5150,12 @@ packages:
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
engines: {node: '>= 6.0.0'}
hsl-to-hex@1.0.0:
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
hsl-to-rgb-for-reals@1.1.1:
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -5181,6 +5237,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
hyphen@1.14.1:
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
i18next-fs-backend@2.6.3:
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
@@ -5472,6 +5531,9 @@ packages:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@@ -5513,6 +5575,9 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jay-peg@1.1.1:
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
@@ -5946,6 +6011,9 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
media-engine@1.0.3:
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -6142,15 +6210,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==}
@@ -6292,6 +6354,9 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
normalize-svg-path@1.1.0:
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -6438,6 +6503,9 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -6456,6 +6524,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-svg-path@0.1.2:
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
@@ -6579,6 +6650,9 @@ packages:
png-js@1.0.0:
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
png-js@2.0.0:
resolution: {integrity: sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==}
po-parser@2.1.1:
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
@@ -6723,6 +6797,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -7016,6 +7093,9 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.25.0-rc-603e6108-20241029:
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -7289,6 +7369,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@@ -7374,6 +7457,9 @@ packages:
peerDependencies:
react: '>=17.0'
svg-arc-to-cubic-bezier@3.2.0:
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
svg-to-pdfkit@0.1.8:
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
@@ -7757,6 +7843,10 @@ packages:
victory-vendor@37.3.6:
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
vite-compatible-readable-stream@3.6.1:
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
engines: {node: '>= 6'}
vite@8.0.3:
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -8095,6 +8185,9 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
yoga-layout@3.2.1:
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
@@ -9246,7 +9339,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)
@@ -10017,6 +10110,110 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@react-pdf/fns@3.1.3': {}
'@react-pdf/font@4.0.8':
dependencies:
'@react-pdf/pdfkit': 5.1.1
'@react-pdf/types': 2.11.1
fontkit: 2.0.4
is-url: 1.2.4
'@react-pdf/image@3.1.0':
dependencies:
'@react-pdf/svg': 1.1.0
jay-peg: 1.1.1
png-js: 2.0.0
'@react-pdf/layout@4.6.1':
dependencies:
'@react-pdf/fns': 3.1.3
'@react-pdf/image': 3.1.0
'@react-pdf/primitives': 4.3.0
'@react-pdf/stylesheet': 6.2.1
'@react-pdf/textkit': 6.3.0
'@react-pdf/types': 2.11.1
emoji-regex-xs: 1.0.0
queue: 6.0.2
yoga-layout: 3.2.1
'@react-pdf/pdfkit@5.1.1':
dependencies:
'@babel/runtime': 7.29.2
'@noble/ciphers': 1.3.0
'@noble/hashes': 1.8.0
browserify-zlib: 0.2.0
fontkit: 2.0.4
jay-peg: 1.1.1
js-md5: 0.8.3
linebreak: 1.1.0
png-js: 2.0.0
vite-compatible-readable-stream: 3.6.1
'@react-pdf/primitives@4.3.0': {}
'@react-pdf/reconciler@2.0.0(react@19.2.4)':
dependencies:
object-assign: 4.1.1
react: 19.2.4
scheduler: 0.25.0-rc-603e6108-20241029
'@react-pdf/render@4.5.1':
dependencies:
'@babel/runtime': 7.29.2
'@react-pdf/fns': 3.1.3
'@react-pdf/primitives': 4.3.0
'@react-pdf/textkit': 6.3.0
'@react-pdf/types': 2.11.1
abs-svg-path: 0.1.1
color-string: 2.1.4
normalize-svg-path: 1.1.0
parse-svg-path: 0.1.2
svg-arc-to-cubic-bezier: 3.2.0
'@react-pdf/renderer@4.5.1(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@react-pdf/fns': 3.1.3
'@react-pdf/font': 4.0.8
'@react-pdf/layout': 4.6.1
'@react-pdf/pdfkit': 5.1.1
'@react-pdf/primitives': 4.3.0
'@react-pdf/reconciler': 2.0.0(react@19.2.4)
'@react-pdf/render': 4.5.1
'@react-pdf/types': 2.11.1
events: 3.3.0
object-assign: 4.1.1
prop-types: 15.8.1
queue: 6.0.2
react: 19.2.4
'@react-pdf/stylesheet@6.2.1':
dependencies:
'@react-pdf/fns': 3.1.3
'@react-pdf/types': 2.11.1
color-string: 2.1.4
hsl-to-hex: 1.0.0
media-engine: 1.0.3
postcss-value-parser: 4.2.0
'@react-pdf/svg@1.1.0':
dependencies:
'@react-pdf/primitives': 4.3.0
'@react-pdf/textkit@6.3.0':
dependencies:
'@react-pdf/fns': 3.1.3
bidi-js: 1.0.3
hyphen: 1.14.1
unicode-properties: 1.4.1
'@react-pdf/types@2.11.1':
dependencies:
'@react-pdf/font': 4.0.8
'@react-pdf/primitives': 4.3.0
'@react-pdf/stylesheet': 6.2.1
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
dependencies:
'@babel/runtime': 7.29.2
@@ -11195,6 +11392,8 @@ snapshots:
abbrev@2.0.0: {}
abs-svg-path@0.1.1: {}
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -11515,6 +11714,10 @@ snapshots:
dependencies:
base64-js: 1.5.1
browserify-zlib@0.2.0:
dependencies:
pako: 1.0.11
browserslist@4.28.2:
dependencies:
baseline-browser-mapping: 2.10.13
@@ -11729,6 +11932,12 @@ snapshots:
color-name@1.1.4: {}
color-name@2.1.0: {}
color-string@2.1.4:
dependencies:
color-name: 2.1.0
colorette@2.0.20: {}
combined-stream@1.0.8:
@@ -12206,6 +12415,8 @@ snapshots:
electron-to-chromium@1.5.330: {}
emoji-regex-xs@1.0.0: {}
emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {}
@@ -12941,15 +13152,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
@@ -13267,6 +13469,12 @@ snapshots:
hoopy@0.1.4: {}
hsl-to-hex@1.0.0:
dependencies:
hsl-to-rgb-for-reals: 1.1.1
hsl-to-rgb-for-reals@1.1.1: {}
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
dependencies:
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
@@ -13365,6 +13573,8 @@ snapshots:
husky@9.1.7: {}
hyphen@1.14.1: {}
i18next-fs-backend@2.6.3: {}
i18next@25.10.10(typescript@5.9.3):
@@ -13653,6 +13863,8 @@ snapshots:
is-unicode-supported@2.1.0: {}
is-url@1.2.4: {}
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@@ -13703,6 +13915,10 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jay-peg@1.1.1:
dependencies:
restructure: 3.0.2
jest-worker@27.5.1:
dependencies:
'@types/node': 22.19.15
@@ -14251,6 +14467,8 @@ snapshots:
mdn-data@2.27.1: {}
media-engine@1.0.3: {}
media-typer@0.3.0: {}
meow@13.2.0: {}
@@ -14577,16 +14795,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: {}
@@ -14717,6 +14929,10 @@ snapshots:
normalize-path@3.0.0: {}
normalize-svg-path@1.1.0:
dependencies:
svg-arc-to-cubic-bezier: 3.2.0
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1
@@ -14927,6 +15143,8 @@ snapshots:
pako@0.2.9: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -14954,6 +15172,8 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-svg-path@0.1.2: {}
parse5-htmlparser2-tree-adapter@7.1.0:
dependencies:
domhandler: 5.0.3
@@ -15096,6 +15316,10 @@ snapshots:
png-js@1.0.0: {}
png-js@2.0.0:
dependencies:
fflate: 0.8.2
po-parser@2.1.1: {}
possible-typed-array-names@1.1.0: {}
@@ -15253,6 +15477,10 @@ snapshots:
queue-microtask@1.2.3: {}
queue@6.0.2:
dependencies:
inherits: 2.0.4
quick-format-unescaped@4.0.4: {}
range-parser@1.2.1: {}
@@ -15671,6 +15899,8 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.25.0-rc-603e6108-20241029: {}
scheduler@0.27.0: {}
schema-dts@1.1.5: {}
@@ -16059,6 +16289,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
@@ -16129,6 +16363,8 @@ snapshots:
dependencies:
react: 19.2.4
svg-arc-to-cubic-bezier@3.2.0: {}
svg-to-pdfkit@0.1.8:
dependencies:
pdfkit: 0.18.0
@@ -16597,6 +16833,12 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-compatible-readable-stream@3.6.1:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
lightningcss: 1.32.0
@@ -16928,6 +17170,8 @@ snapshots:
yoctocolors@2.1.2: {}
yoga-layout@3.2.1: {}
zod-validation-error@4.0.2(zod@3.25.76):
dependencies:
zod: 3.25.76

View File

@@ -9,8 +9,9 @@ export default createMiddleware({
export const config = {
// Match all pathnames except for
// - /api (API routes)
// - /stats (Analytics proxy)
// - /_next (Next.js internals)
// - /_static (inside /public)
// - all root files inside /public (e.g. /favicon.ico)
matcher: ['/((?!api|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
matcher: ['/((?!api|stats|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
};

39
psi-standorte.json Normal file
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"
}
]
}
]
}
}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/assets/photos/2025-09-23_12.11.43.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 330 KiB

BIN
public/assets/photos/2025-09-23_12.12.37.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 275 KiB

Some files were not shown because too many files have changed in this diff Show More