Compare commits

...

248 Commits

Author SHA1 Message Date
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
0191fdb770 fix: resolve reveal flickering, adjust grid header UI, update Guben image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:49:59 +02:00
afaa5e038c fix: resolve TS dynamic collision and remove anomalous map entries
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:35:14 +02:00
e0d6ea0d6c perf: implement KLZ blueprint optimizations (LCP, Payload, Caching)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
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-20 20:32:08 +02:00
34839ad5a5 fix: update team titles and location names
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 20:23:15 +02:00
a8d6a45aad chore: bump version to trigger SSL retry
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 19:17:04 +02:00
6de8ad8516 fix(content): correct drilling distance for Jevenstedt reference
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 17:06:20 +02:00
023fef5343 chore: trigger pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m19s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 10:24:35 +02:00
7181dafca2 fix(ui): enforce scroll lock on documentElement when flyout is open
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 20m1s
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 2s
2026-06-20 09:32:05 +02:00
8b6d2da821 feat(header): add mail and map action buttons for mobile
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-20 09:31:48 +02:00
17736860d6 feat(header): replace mobile language switcher with phone button
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:18:59 +02:00
d67acdbcc1 fix(referenzen): link slider to overview anchors and add section ids
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 00:18:12 +02:00
05b517731b feat(ui): refine mobile bottom nav, add drag-to-close flyout, custom icons
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:09:11 +02:00
67696b6afa chore: release v2.2.13-rc.58
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:46:49 +02:00
24472990e5 2.2.13-rc.47 2026-06-19 23:46:36 +02:00
53a885acdb fix(ui): resolve unclosed Link tag in ReferencesSlider and MobileBottomNav type errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
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-19 23:42:35 +02:00
9afff09ed7 feat(ui): add buttery smooth scroll reveal animations and disable annotator
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m16s
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-19 23:29:51 +02:00
ce5acc5558 refactor(footer): merge navigation and about us links
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 2s
2026-06-19 23:26:54 +02:00
5027bea666 fix(ui): resolve annotator overlap, slider images, subcompany rounding and copy updates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (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-19 23:25:29 +02:00
fcfb567991 fix: remove pb-[90px] from main container which caused white gap above footer on mobile
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:57:52 +02:00
efb9bdbb3f feat: modernize mobile bottom nav with 2030 design and fix z-index
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:39:13 +02:00
11e7937e79 fix: remaining mobile paddings for CTA boxes
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:36:30 +02:00
41158697fd fix: mobile layout and visual polishing based on review
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:34:53 +02:00
d00a4e29ff Merge branch 'feature/annotator-integration' into main
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:03:13 +02:00
58be922fb3 fix(ui): comprehensive mobile optimizations across homepage and blocks
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
- fix: bottom nav and footer padding collision
- fix: reveal animation flickering (Certificates, SubCompanyTiles)
- fix: tooltip flyout overflow on mobile in Germany map
- fix: flex direction for 'Projekt anfragen' and Service Detail icons
- fix: text alignment and spacing issues in ReferencesSlider
- fix: reduce excessive whitespace above footer
2026-06-19 21:39:55 +02:00
111672a614 feat(referenzen): inject MDX project details into overview cards
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 21:27:35 +02:00
d1387d2685 fix(referenzen): assign unique, matching images to all reference projects 2026-06-19 21:21:50 +02:00
fda2944e5a content: restore correct image for Kabelleitungsnetzbau
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Restored DSC01123.JPG (Kabelpflug) for the 'Kabelleitungsnetzbau' tiles on the Home and Kompetenzen pages. Also fixed the English alt text to 'Cable Network Construction'.
2026-06-19 20:13:01 +02:00
8ae5ab73e3 ci: fix pipeline concurrency and target evaluation bugs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
1. Added native Gitea/GitHub Actions concurrency group to automatically cancel outdated/stale deploy runs for the same branch (fixes 'alte deploys durchjagen'). 2. Replaced the buggy 'github.ref_type' condition with explicit 'github.ref' matching. Gitea often failed to populate ref_type, causing staging tags to be downgraded to 'branch' target, making them equal priority to feature branches where the newer run would win (fixes 'feature branches wichtiger als staging').
2026-06-19 19:11:43 +02:00
d8d35da574 chore: remove dead code and unused PDF generation scripts
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Removed leftover brochure and PDF datasheet generation scripts from the old template that are not needed in this project.
2026-06-19 19:03:37 +02:00
792d91f9d5 content: rename Kabelleitungstiefbau to Kabelleitungsnetzbau
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
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
Applied globally across MDX content, translations, and UI components including English analog terms (Cable Trenching -> Cable Network Construction).
2026-06-19 19:00:33 +02:00
d10d038617 perf: optimize interactive map rendering
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
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
Memoized map nodes, map background, and hover handlers to prevent full component re-renders and SVG drop-shadow recalculations.
2026-06-19 18:58:08 +02:00
490ce4abb6 fix: load annotator assets on mount instead of requiring Shift+A
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 18:56:14 +02:00
bed9be1f81 fix: resolve react-hooks/rules-of-hooks in Annotator
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 18:40:44 +02:00
f52a3e3c46 2.2.13-rc.46
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
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 1s
2026-06-19 18:26:58 +02:00
71825724db fix(ui): align bento grid images to start page & optimize performance
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
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
- fixed Sentry NDJSON parsing error in relay endpoint
- fully isolated Annotator bundle via next/dynamic (60kb chunk, completely removed from main thread)
- aligned images for Kabelleitungsbau & Bohrtechnik across Start and Kompetenzen pages
- extended transition fallback timeout for slow dev server cold starts
2026-06-19 18:14:55 +02:00
1bb26e8db4 chore: sync bento grid images across home and kompetenzen
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:49:09 +02:00
604b96afe0 2.2.13-rc.45
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 15:44:52 +02:00
59df260164 feat: close annotator modals via ESC (v1.0.9) 2026-06-19 15:44:51 +02:00
3449d2eaa4 2.2.13-rc.44
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:39:38 +02:00
1723be4008 feat: make annotator mobile friendly (v1.0.8) 2026-06-19 15:39:37 +02:00
830d425859 2.2.13-rc.43
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-06-19 15:30:27 +02:00
d6b9aca50f chore: remove tracked turbo logs 2026-06-19 15:30:26 +02:00
caefd5d525 fix: include tgz files in docker build context for local package installation 2026-06-19 15:30:03 +02:00
8d8e1dd460 2.2.13-rc.42
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 37s
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-19 15:21:21 +02:00
c2a82ae9ae fix: submitAnnotations server action missing directive and invalid mail options 2026-06-19 15:21:20 +02:00
2ecbe9c63c 2.2.13-rc.41
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 32s
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-19 15:16:46 +02:00
255403c2e4 2.2.13-rc.3 2026-06-19 15:16:28 +02:00
38a870bea8 feat: integrate mintel annotator with email submission and environment restrictions
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 36s
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-19 15:13:07 +02:00
2b8dc19032 chore(git): ignore turborepo cache directories
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m24s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 10:09:22 +02:00
5101b8bd1e style: harmonize mobile and desktop vertical spacing across all components
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 01:14:33 +02:00
afe3565155 fix(map): optimize hit areas for desktop mouse clusters and add touch events for mobile responsiveness
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-19 00:02:13 +02:00
e677089ff9 fix(map): use useRef for hover timeout to prevent flyout flicker and immediate disappearance
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 23:41:09 +02:00
b7d9a4b0d0 fix(tests): update task-12-links test to verify minorLocations since defaultLocations was emptied
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 23:26:07 +02:00
6c8614f147 feat(map): completely rebuild map data from raw 2016-2026 txt files, geocode missing locations, format to spec, and downgrade all projects to minor nodes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
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-18 23:18:46 +02:00
aa4e121fea revert(map): restore stable map version from June 17 due to layout, lag, and scroll regressions
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:50:35 +02:00
6697703c91 style(map): format location names strictly to Danny's blueprint (e.g. MS-Kabeltrasse, PLZ Ort)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m8s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:45:03 +02:00
e779c6ac22 fix(map): restore minor_node type for map pins to render tooltips correctly
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:42:08 +02:00
02c4f06108 hotfix: increase footer padding, restore interactive map state, disable hero map lag animation
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m1s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:23:09 +02:00
6d77fb92c3 chore: remove unused imports causing CI failure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 21:48:59 +02:00
cb165b06e5 hotfix: fix map scroll jump, horizontal double scrollbars, and severe lag
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m38s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 21:46:22 +02:00
ca1b19cc0f hotfix: adjust EU badge distance and ScaleOfImpact unit spacing 2026-06-18 21:43:58 +02:00
cc22aa6c8c hotfix: resolve double header on referenzen page and fix hash link scroll bug
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m24s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-18 15:53:24 +02:00
3ae1e6257d chore: trigger deployment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 15:07:28 +02:00
1419bab7cf Revert "fix(ci): explicitly mount docker.sock via container options"
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 1s
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 1s
This reverts commit ac6b121685.
2026-06-18 15:05:10 +02:00
ac6b121685 fix(ci): explicitly mount docker.sock via container options
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
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 2s
2026-06-18 15:03:45 +02:00
4a9a7adf93 fix(ci): restore deploy.yml to last working version and remove test workflow
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
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 2s
2026-06-18 15:02:02 +02:00
3b297aae3f chore: test dind service
Some checks failed
Build & Deploy / 🧪 QA (push) Blocked by required conditions
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Failing after 10s
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-18 14:59:19 +02:00
c6b81a3d77 chore: test gateway dind reachability
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Test Docker Host / test (push) Successful in 2s
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-18 14:58:39 +02:00
c8f3420a5c chore: test act-runner-dind reachability
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Successful in 2s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 27s
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-18 14:55:41 +02:00
a6559eb7bb chore: test docker env
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Failing after 27s
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-18 14:52:27 +02:00
8d75c2ec12 fix(ci): restore full deployment pipeline with buildx driver docker
Some checks failed
Build & Deploy / 🏗️ Build (push) Failing after 29s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-18 14:46:38 +02:00
6a18eb4ee1 chore: test barebones build with driver: docker
Some checks failed
Build and Deploy / 🏗️ Build (push) Has been cancelled
Build and Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-18 14:45:44 +02:00
d91eb220be fix(ci): restore build-push-action to fix docker socket access in runner
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
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-18 14:41:53 +02:00
5c40d55b21 chore: test docker
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 25s
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-18 14:38:00 +02:00
a512bbe226 fix(ci): restore container blocks and explicitly mount docker.sock volume
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 24s
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-18 14:19:01 +02:00
8b3851f45c fix: run CI jobs on runner host to fix docker daemon socket access
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 24s
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-18 14:11:38 +02:00
e2667ed750 fix(ui): unify reference project naming scheme in map-data
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Failing after 26s
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-18 14:03:36 +02:00
0253956bd9 fix(ci): remove broken maintenance step and clean up prepare
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
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-18 14:02:04 +02:00
42cd4394c7 fix(ci): checkout repository in prepare job to fix priority-gate execution crash
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 25s
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-18 13:59:53 +02:00
276ceed010 debug(ci): bypass priority gate to prevent prepare failure
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
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 2s
2026-06-18 13:58:02 +02:00
cdf10bd670 debug(ci): hardcode GITHUB_OUTPUT entirely
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 2s
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 4s
2026-06-18 13:55:08 +02:00
a1ea41b42f ci(deploy): map main branch to staging environment automatically
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
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 2s
2026-06-18 13:14:31 +02:00
561d75d2b8 Merge fix/ui-and-map-formatting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 2s
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 2s
2026-06-18 13:08:56 +02:00
685c0ffff6 Merge fix/referenzen-map-glow
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 7s
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 2s
2026-06-18 13:08:46 +02:00
17b001a97d Merge fix/header-dropdown-nav 2026-06-18 13:08:00 +02:00
275a857554 fix: ui component updates and project formatting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 4s
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 2s
2026-06-17 15:38:56 +02:00
f766c9c9db refactor: remove redundant CallToAction component from all MDX pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 33s
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 2s
2026-06-17 10:51:04 +02:00
c851a4f95e fix(ui): use standard Badge and Button components instead of raw HTML in MDX
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 10:42:24 +02:00
1dca7f79fe feat(map): add 15 missing minor locations from 2023-2025
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 2m3s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 10:16:41 +02:00
d3eee45112 fix(content): shorten guben hq name to E-TIB GmbH
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 2m0s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 21:07:48 +02:00
c308035e77 feat(ui): display standorte on references map
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-16 20:54:57 +02:00
44d8c22c97 fix(nav): add mobile hamburger menu to header to ensure parity with desktop
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 47s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 20:48:51 +02:00
9d3f918aef fix(ui): scale subcompany logos dynamically to prevent overflow
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 58s
Build & Deploy / 🏗️ Build (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 15:45:50 +02:00
a34bee6272 feat: append missing locations to map data
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 15:32:06 +02:00
373017059e fix: reduce mobile gaps in ScaleOfImpact component
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 15:14:41 +02:00
309a53624e feat: manually assign appropriate reference images as requested by client
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m3s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 14:48:25 +02:00
e994d79895 chore: release 2.2.13-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
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-16 13:57:24 +02:00
88e88c8b99 fix: restore large project pins on the reference map 2026-06-16 13:57:24 +02:00
e2a29b41e9 chore: release 2.2.13-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 58s
Build & Deploy / 🏗️ Build (push) Successful in 1m36s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 46s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 12:53:07 +02:00
62727a7231 test: update homepage test to match new copy (Mitarbeitende)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m2s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 12:20:50 +02:00
3a6189e0d8 chore: optimize new client images (HEIC to JPG, resize)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Failing after 55s
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-16 12:11:45 +02:00
01e2fdbb7d fix: move EU funding badge from header to footer to prevent overlap
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
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-16 12:08:55 +02:00
fa7182d351 fix: address remaining UI and content feedback from Marisa
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Failing after 56s
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-16 11:43:26 +02:00
e0d43f7f5f fix: implement client feedback (map points, hero text, stats spacing, lang toggle)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 48s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-16 10:40:01 +02:00
d09b576137 style: hide scrollbars on mobile and fix desktop overlay scrollbars for company tiles
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 09:28:25 +02:00
04599827f9 feat(ui): add actual EU flag image and update path to webp
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 16:47:54 +02:00
5368f1a64d feat(ui): smooth radial fade on EU badge and setup custom image path
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
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-15 16:45:40 +02:00
241c97e346 feat(ui): refine EU badge into subtle triangular corner fade with text
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 57s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 16:34:50 +02:00
3334e327bf fix(ui): correct z-index and remove mix-blend-screen on EU badge
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 16:21:51 +02:00
145bff90b6 ci: implement dynamic environment priority gate (prod > rc > testing > branch)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
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-15 16:14:16 +02:00
d2bf48fc60 chore: add project instructions and workflow rules to INSTRUCTIONS.md 2026-06-15 16:08:23 +02:00
5e7ee84dc9 ci: fix deployment concurrency groups by target environment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 16:05:42 +02:00
b42e53495d 2.2.13-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 15:54:45 +02:00
8813ed3910 fix: enforce hero remounts on route change 2026-06-15 15:54:38 +02:00
1d3c6a0f70 fix(ui): remove dummy certificates from english page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
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-15 15:52:36 +02:00
8fb3b96bd3 feat(ui): redesign EU flag with perspective image and gradient fade mask
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
2026-06-15 15:46:41 +02:00
67dc56d3ee fix(ui): remove hardcoded dummy certificates without downloads
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
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-15 15:40:19 +02:00
e5933bad69 feat(ui): optimize career page header layout 2026-06-15 15:40:14 +02:00
11b9e4630b feat(ui): add EU Funding Badge to global layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 2m32s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has started running
2026-06-15 15:29:23 +02:00
a47d378521 fix(ui): prevent text selection and ghost images during references slider drag
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-15 15:28:21 +02:00
30047cb1c4 fix(nav): optimize dropdown behavior on navigation and scroll
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 44s
Build & Deploy / 🧪 QA (push) Has started running
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-15 15:24:46 +02:00
394ac5a19c style(mobile): optimize mobile layout and component spacing
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
- Hide CorporateBackground on mobile to prevent content overlap
- Increase Header padding on mobile for better logo spacing
- Reduce top spacing in HeroSection on mobile
- Change SubCompanyTiles to horizontally scrollable row on mobile
- Add bottom padding to Footer to clear MobileBottomNav
- Reduce Footer Bento card padding for compact mobile view
2026-06-15 15:23:23 +02:00
1dc2fe8a25 fix(header): close navigation dropdown immediately on pathname change
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m9s
Build & Deploy / 🏗️ Build (push) Successful in 2m39s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 12:15:33 +02:00
ed802d03e7 fix: clean location names in reference badges and detail pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 17s
Build & Deploy / 🧪 QA (push) Failing after 58s
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-05-15 21:52:58 +02:00
68b0ed7ba0 fix: render map as card on references page to enable glowing border 2026-05-15 21:15:57 +02:00
2059 changed files with 356866 additions and 9445 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

@@ -13,13 +13,15 @@ on:
required: false
default: 'false'
concurrency:
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
concurrency:
group: deploy-pipeline
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────────────────────────────────────
@@ -48,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 ==="
@@ -59,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
@@ -92,12 +76,12 @@ jobs:
DOMAIN="e-tib.com"
PRJ="etib"
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="test.${DOMAIN}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production"
IMAGE_TAG="$REF"
@@ -111,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}"
@@ -146,6 +139,13 @@ jobs:
echo "short_sha=$SHORT_SHA"
} >> "$GITHUB_OUTPUT"
- name: 🚦 Priority Gate
env:
GITEA_PAT: ${{ secrets.GITEA_PAT }}
CURRENT_RUN_ID: ${{ github.run_id }}
CURRENT_TARGET: ${{ steps.determine.outputs.target }}
run: bash scripts/priority-gate.sh
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: QA (Lint, Typecheck, Test)
# ──────────────────────────────────────────────────────────────────────────────
@@ -210,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:
@@ -227,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 }}
@@ -274,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:
@@ -342,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"
@@ -384,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'"
# ──────────────────────────────────────────────────────────────────────────────

4
.gitignore vendored
View File

@@ -36,3 +36,7 @@ pnpm-debug.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# turborepo
.turbo
.turbo-test

View File

@@ -0,0 +1 @@
{"chromeFlags":"--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --headless=new","port":59457}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
.lighthouseci/links.json Normal file
View File

@@ -0,0 +1,3 @@
{
"http://localhost:3000/de": "https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1782417688223-28941.report.html"
}

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-lint.log":{"size":11972,"mtime_nanos":1779970374061729177,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}

View File

@@ -1 +0,0 @@
{"hash":"0864e4a10f76dcaf","duration":6769,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-test.log":{"size":2776,"mtime_nanos":1781469605328811961,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}

View File

@@ -1 +0,0 @@
{"hash":"41698960411f6bf6","duration":4168,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1779970373499305257,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}

View File

@@ -1 +0,0 @@
{"hash":"7668a73054ba889d","duration":6206,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-test.log":{"size":2150,"mtime_nanos":1779970370282819916,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}

View File

@@ -1 +0,0 @@
{"hash":"9c3fa4d9e1993aff","duration":2989,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-lint.log":{"size":12987,"mtime_nanos":1781469609469080321,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}

View File

@@ -1 +0,0 @@
{"hash":"bb46018d4a7bc516","duration":8307,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-typecheck.log":{"size":92,"mtime_nanos":1779097674383861303,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}

View File

@@ -1 +0,0 @@
{"hash":"32905076da5f4894","duration":4492,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-test.log":{"size":818,"mtime_nanos":1779097671747078359,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}

View File

@@ -1 +0,0 @@
{"hash":"7e2629942c881f80","duration":1855,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-lint.log":{"size":10746,"mtime_nanos":1779097675143794164,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}

View File

@@ -1 +0,0 @@
{"hash":"85340b254e0f10f1","duration":5252,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}

Binary file not shown.

View File

@@ -1 +0,0 @@
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1781469608726900998,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}

View File

@@ -1 +0,0 @@
{"hash":"eb4eeeb717c1af0c","duration":7565,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}

Binary file not shown.

View File

@@ -1,5 +0,0 @@
> e-tib-nextjs@2.2.12 lint /Volumes/Alpha SSD/Coding/e-tib.com
> eslint .
ELIFECYCLE Command failed.

View File

@@ -1,55 +0,0 @@
> e-tib-nextjs@2.2.12 test /Volumes/Alpha SSD/Coding/e-tib.com
> vitest run --passWithNoTests
 RUN  v4.1.2 /Volumes/Alpha SSD/Coding/e-tib.com
(node:64961) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✓ tests/mdx-links.test.ts (1 test) 29ms
✓ tests/homepage-and-competence-content.test.ts (4 tests) 14ms
 tests/legal-pages.test.ts (2 tests | 1 failed) 9ms
✓ should contain highly professional B2B sections in German AGB 5ms
 × should contain professional corresponding B2B sections in English Terms 2ms
✓ tests/task-12-links.test.ts (2 tests) 8ms
✓ tests/messen-content.test.ts (2 tests) 8ms
✓ lib/mail/mailer.test.ts (1 test) 7ms
stderr | components/layout/LanguageSwitcher.test.tsx > LanguageSwitcher TDD > translates /en/trade-fairs to /de/messen when switching to German
React does not recognize the `transitionMessage` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `transitionmessage` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
✓ components/layout/LanguageSwitcher.test.tsx (5 tests) 25ms
✓ components/search/AISearchResults.test.tsx (1 test) 52ms
stderr | components/blocks/ReferencesSlider.test.tsx > ReferencesSlider TDD > renders reference tiles correctly
Received `true` for a non-boolean attribute `fill`.
If you want to write it to the DOM, pass a string instead: fill="true" or fill={value.toString()}.
✓ components/blocks/ReferencesSlider.test.tsx (2 tests) 43ms
✓ lib/env.test.ts (6 tests) 4ms
stdout | app/[locale]/referenzen/[slug]/page.test.tsx
[Config] Initializing Toggles: { feedbackEnabled: false }
✓ app/[locale]/referenzen/[slug]/page.test.tsx (1 test) 26ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  tests/legal-pages.test.ts > Legal Pages Content Validation > should contain professional corresponding B2B sections in English Terms
Error: ENOENT: no such file or directory, open '/Volumes/Alpha SSD/Coding/e-tib.com/content/en/terms.mdx'
  tests/legal-pages.test.ts:25:24
 23|
 24| it('should contain professional corresponding B2B sections in Englis…
 25| const content = fs.readFileSync(enTermsPath, 'utf8');
 | ^
 26|
 27| expect(content).toContain('title: "Terms and Conditions"');
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
 Test Files  1 failed | 10 passed (11)
 Tests  1 failed | 26 passed (27)
 Start at  14:54:33
 Duration  5.94s (transform 5.27s, setup 0ms, import 9.43s, tests 222ms, environment 13.80s)
ELIFECYCLE Test failed. See above for more details.

View File

@@ -1,5 +0,0 @@
> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
> tsc --noEmit
ELIFECYCLE Command failed.

View File

@@ -17,7 +17,7 @@ ENV SKIP_RUNTIME_ENV_VALIDATION=true
ENV CI=true
# Copy lockfile and manifest for dependency installation caching
COPY pnpm-lock.yaml package.json .npmrc* ./
COPY pnpm-lock.yaml package.json .npmrc* *.tgz ./
COPY patches* ./patches/
ARG NPM_DOMAIN=git.infra.mintel.me

25
INSTRUCTIONS.md Normal file
View File

@@ -0,0 +1,25 @@
# Antigravity Instructions & Project Rules
This document contains critical project-specific rules for the e-tib.com codebase. Antigravity must read and follow these rules at all times.
## 1. Bilingual Site (German & English) - STRICT
- **Always update BOTH languages!** The website is bilingual.
- When changing content, adding pages, or updating text, you MUST always check and update both `content/de/` and `content/en/` simultaneously.
- When adding new components with text, ensure they are built to support both languages or update the respective translation files/mechanisms. Do NOT just hardcode German text and forget the English version.
## 2. No Dummy Data / No Hallucinations
- Do not invent or hallucinate placeholder text (e.g., "Nachweis liegt vor" for certificates that don't exist).
- Only implement what is explicitly requested or what matches provided assets/data.
- If data is missing, ask the user instead of inventing placeholders.
## 3. Git Workflow & RC Tagging
- When instructed to "commit and tag" (or similar), always follow this process:
1. Add and commit the changes with conventional commits (`feat:`, `fix:`, `chore:`, etc.).
2. Check the latest tag using `git describe --tags --abbrev=0`.
3. Increment the RC (Release Candidate) number of the current version (e.g., if latest is `v2.2.13-rc19`, the new tag is `v2.2.13-rc20`).
4. Create the tag: `git tag vX.Y.Z-rcN`.
5. Push the commit and the tag: `git push origin main --tags`.
## 4. Work Exclusively on the Correct Branch
- Follow the `USER_GLOBAL` directive: If on `main`, create a branch unless explicitly told to work on `main`.
- For e-tib.com, often small fixes are done on `main` if the user commands it ("auf main arbeiten"), but always verify context.

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

@@ -17,7 +17,6 @@ import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blo
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { CompanyTimeline } from '@/components/blocks/CompanyTimeline';
import { JobListingBlock } from '@/components/blocks/JobListingBlock';
import { CallToAction } from '@/components/blocks/CallToAction';
import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
@@ -40,7 +39,6 @@ const mdxComponents = {
HomeReferencesSlider,
CompanyTimeline,
JobListingBlock,
CallToAction,
ServiceDetailGrid,
BenefitGrid,
InteractiveGermanyMap,
@@ -50,6 +48,7 @@ const mdxComponents = {
TeamGrid,
JsonLd,
Button,
Badge,
AnimatedCounter,
GrowthChart,
DeepDrillAnimation,
@@ -186,7 +185,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
<div className={`flex flex-col min-h-screen ${isFullBleed ? '' : 'bg-white'}`}>
{/* Generic Hero Section (only for standard pages) */}
{!isFullBleed && (
<section className="bg-primary-dark text-white pt-40 pb-12 md:pt-56 md:pb-16 min-h-[40vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<section key={`hero-section-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<div className="absolute inset-0 opacity-20 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
</div>
@@ -230,7 +229,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
{/* Support Section */}
<Container>
<div className="mt-24 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<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('needHelp')}</h3>

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

@@ -99,7 +99,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
<div className="relative w-full h-[50vh] md:h-[70vh] min-h-[350px] md:min-h-[500px] overflow-hidden group">
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
<Image
src={post.frontmatter.featuredImage.split('?')[0]}

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

@@ -59,7 +59,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<div className="bg-neutral-light min-h-screen">
{/* Hero Section - Immersive Magazine Feel */}
<Reveal>
<article className="relative h-[50vh] md:h-[70vh] min-h-[400px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
<article className="relative h-[40vh] md:h-[70vh] min-h-[300px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<Image

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' });
@@ -150,7 +150,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
{/* Contact Info */}
<div className="lg:col-span-5 space-y-6 md:space-y-12">
<div className="lg:col-span-5 order-2 lg:order-1 space-y-6 md:space-y-12">
<div className="animate-fade-in">
<Heading level={3} subtitle={t('info.subtitle')} className="mb-6 md:mb-8">
{t('info.howToReachUs')}
@@ -249,7 +249,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
</div>
{/* Contact Form */}
<div className="lg:col-span-7">
<div className="lg:col-span-7 order-1 lg:order-2">
<Suspense
fallback={
<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>

View File

@@ -5,7 +5,7 @@ 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,
@@ -65,7 +65,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 +81,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 +90,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

@@ -0,0 +1,13 @@
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
try {
const rawText = await req.text();
// Sentry sends NDJSON (Newline Delimited JSON). Split by newline to parse safely.
const items = rawText.split('\n').filter(Boolean).map(line => JSON.parse(line));
console.log("CLIENT ERROR INTERCEPTED:", JSON.stringify(items[0], null, 2));
} catch (e) {
console.log("Failed to parse relay body (NDJSON)", e);
}
return NextResponse.json({ success: true });
}

View File

@@ -10,15 +10,16 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
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 { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
const inter = Inter({
subsets: ['latin'],
@@ -32,20 +33,22 @@ 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',
default: 'E-TIB | Die Experten für Kabeltiefbau',
default: 'E-TIB | Die Experten für Kabelnetzbau',
},
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
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 +63,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 +154,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,26 +171,21 @@ 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://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<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 />
@@ -206,7 +197,7 @@ export default async function Layout(props: {
<main
id="main-content"
className="flex-grow animate-fade-in overflow-visible pb-[90px] md:pb-0"
className="flex-grow animate-fade-in overflow-visible"
tabIndex={-1}
>
{children}
@@ -215,6 +206,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider>
</NextIntlClientProvider>

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,23 +4,23 @@ 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';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
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));
import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading';
import { AnimatedCounter } from '@/components/ui';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact';
const AnimatedCounter = nextDynamic(() => import('@/components/ui').then(mod => mod.AnimatedCounter));
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
const ScaleOfImpact = nextDynamic(() => import('@/components/blocks/ScaleOfImpact').then(mod => mod.ScaleOfImpact));
import { getAllReferences } from '@/lib/references';
const mdxComponents = (references: any[]) => ({
@@ -37,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> {

View File

@@ -1,38 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import ReferenceDetail from './page';
// Mock next/navigation
vi.mock('next/navigation', () => ({
notFound: vi.fn(),
}));
// Mock next-intl/server
vi.mock('next-intl/server', () => ({
setRequestLocale: vi.fn(),
getTranslations: vi.fn().mockResolvedValue(() => 'translated text'),
}));
// Mock @/lib/references
vi.mock('@/lib/references', () => ({
getReferenceBySlug: vi.fn().mockResolvedValue({
slug: 'test-slug',
frontmatter: {
title: 'Test Title',
location: 'Test Location',
category: 'Test Category',
},
content: 'Test content',
}),
}));
describe('ReferenceDetail Page TDD', () => {
it('renders reference detail correctly', async () => {
const props = {
params: Promise.resolve({ locale: 'de', slug: 'test-slug' })
};
// We expect the execution to fail if getTranslations is not defined/imported inside page.tsx
const component = await ReferenceDetail(props);
expect(component).toBeTruthy();
});
});

View File

@@ -1,212 +0,0 @@
import { notFound } from 'next/navigation';
import { Container, Badge, Heading } from '@/components/ui';
import { setRequestLocale, getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getReferenceBySlug, getAllReferences } from '@/lib/references';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { SITE_URL } from '@/lib/schema';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { MapPin, Calendar, Briefcase, ChevronLeft } from 'lucide-react';
import Image from 'next/image';
interface PageProps {
params: Promise<{
locale: string;
slug: string;
}>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params;
if (locale !== 'de' && locale !== 'en') return {};
const reference = await getReferenceBySlug(slug, locale);
if (!reference) return {};
return {
title: `${reference.frontmatter.title} | Referenzen | E-TIB Gruppe`,
description: `Details zum Projekt ${reference.frontmatter.title} in ${reference.frontmatter.location}.`,
alternates: {
canonical: `${SITE_URL}/${locale}/referenzen/${slug}`,
},
openGraph: {
title: `${reference.frontmatter.title} | E-TIB`,
description: `Details zum Projekt ${reference.frontmatter.title} in ${reference.frontmatter.location}.`,
url: `${SITE_URL}/${locale}/referenzen/${slug}`,
},
};
}
export async function generateStaticParams(props: any) {
const params = await props.params;
const locale = params?.locale || 'de'; // fallback to 'de' if undefined
const references = await getAllReferences(locale);
return references.map((ref) => ({
slug: ref.slug,
locale
}));
}
export default async function ReferenceDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
const { locale, slug } = await props.params;
if (locale !== 'de' && locale !== 'en') {
notFound();
}
setRequestLocale(locale);
const reference = await getReferenceBySlug(slug, locale);
const t = await getTranslations('ReferenceDetail');
if (!reference) {
notFound();
}
// MDX components mapping for industrial typography
const mdxComponents = {
Heading,
h1: (props: any) => <Heading level={1} size="2" className="hidden" {...props} />, // Hidden because Hero handles H1
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4 text-neutral-dark" {...props} />,
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
ul: (props: any) => <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 my-12" {...props} />,
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
li: (props: any) => {
// Smart Metric Card Logic
let text = '';
if (typeof props.children === 'string') {
text = props.children;
} else if (Array.isArray(props.children)) {
text = props.children.map((c: any) => typeof c === 'string' ? c : '').join('');
}
const match = text.match(/^([\d.,]+)\s*(m|Satz|Stück|km|kV|Fasern)\b(.*)/);
if (match && typeof props.children === 'string') {
const [, value, unit, rest] = match;
return (
<li className="bg-white p-6 md:p-8 rounded-3xl border border-neutral-100 flex flex-col justify-center hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:border-primary/30 transition-all duration-500 group hover:-translate-y-1 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-neutral-50 to-transparent rounded-bl-full -z-10 group-hover:from-primary/5 transition-colors duration-500" />
<div className="flex items-baseline gap-2 mb-3">
<span className="text-4xl md:text-5xl font-bold font-heading text-primary tracking-tight">{value}</span>
<span className="text-lg md:text-xl font-bold text-neutral-400">{unit}</span>
</div>
<span className="text-neutral-600 font-semibold text-sm md:text-base leading-snug">{rest.replace(/^[\s-]*\s/, '')}</span>
</li>
);
}
return (
<li className="bg-neutral-50 p-6 rounded-3xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-all duration-300 group hover:-translate-y-1">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-primary/20 transition-colors">
<svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-neutral-700 font-semibold leading-relaxed">{props.children}</span>
</li>
);
},
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
blockquote: (props: any) => (
<blockquote className="border-l-4 border-primary pl-6 py-3 my-10 italic bg-neutral-50 rounded-r-2xl text-neutral-700 font-medium max-w-3xl shadow-sm" {...props} />
),
hr: (props: any) => <hr className="my-16 border-t-2 border-neutral-100 max-w-3xl" {...props} />,
img: (props: any) => <img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" {...props} />,
};
return (
<div className="flex flex-col min-h-screen bg-white">
{/* Hero Section */}
<section className="bg-[#050B14] text-white pt-40 pb-16 md:pt-52 md:pb-24 min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
{reference.frontmatter.featuredImage && (
<div className="absolute inset-0 z-0">
<Image
src={reference.frontmatter.featuredImage}
alt={reference.frontmatter.title}
fill
className="object-cover opacity-30 mix-blend-luminosity scale-105"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/70 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-[#050B14]/40 to-transparent" />
</div>
)}
<div className="absolute inset-0 opacity-20 z-0 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/30 via-transparent to-transparent" />
</div>
<Container className="relative z-10">
<div className="mb-12">
<TrackedLink
href={`/${locale}/referenzen`}
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-xs font-bold"
eventProperties={{ location: 'reference_back_btn' }}
>
<ChevronLeft className="w-4 h-4" />
{t('backToOverview')}
</TrackedLink>
</div>
<div className="max-w-5xl">
<Badge variant="accent" className="mb-6">
{t('projectReference')}
</Badge>
<Heading level={1} variant="white" className="mb-10 leading-[1.1] md:text-6xl lg:text-7xl drop-shadow-lg">
{reference.frontmatter.title}
</Heading>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mt-12 md:mt-16">
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<MapPin className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('location')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.location}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<Briefcase className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('client')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90 line-clamp-2">{reference.frontmatter.client}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<Calendar className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('period')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
</div>
</div>
</div>
</div>
</Container>
</section>
{/* Main Content Area */}
<Container className="py-16 md:py-24">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-neutral-dark mb-8">{t('scopeTitle')}</h2>
<div className="w-full">
<MDXRemote source={reference.content} components={mdxComponents} />
</div>
<div className="mt-20 flex justify-center">
<TrackedLink
href={`/${locale}/referenzen`}
className={getButtonClasses('primary', 'lg')}
eventProperties={{ location: 'reference_bottom_back_btn' }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
{t('viewAll')}
</span>
<ButtonOverlay variant="primary" />
</TrackedLink>
</div>
</div>
</Container>
</div>
);
}

View File

@@ -3,12 +3,43 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { getAllReferences } from '@/lib/references';
import TrackedLink from '@/components/analytics/TrackedLink';
import { MapPin, Calendar, Briefcase } from 'lucide-react';
import { MapPin, Calendar, Briefcase, Zap, Activity, ShieldCheck, Wrench, CheckCircle2 } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { SITE_URL } from '@/lib/schema';
import Image from 'next/image';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { HeroSection } from '@/components/blocks/HeroSection';
import { defaultLocations, minorLocations } from '@/lib/map-data';
import { MDXRemote } from 'next-mdx-remote/rsc';
const CustomLi = ({ children, ...props }: any) => {
let text = '';
if (typeof children === 'string') text = children;
else if (Array.isArray(children)) text = children.map((c: any) => typeof c === 'string' ? c : '').join(' ');
else if (children?.props?.children) text = children.props.children;
const lowerText = String(text).toLowerCase();
let Icon = CheckCircle2;
if (lowerText.includes('kabel')) Icon = Zap;
else if (lowerText.includes('bohrung')) Icon = Activity;
else if (lowerText.includes('rohr')) Icon = ShieldCheck;
else if (lowerText.includes('montage')) Icon = Wrench;
else if (lowerText.includes('vlf') || lowerText.includes('otdr')) Icon = Zap;
return (
<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>
);
};
const mdxComponents = {
ul: (props: any) => <ul className="grid grid-cols-1 gap-2 mb-6" {...props} />,
li: CustomLi,
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
};
interface PageProps {
params: Promise<{
@@ -22,7 +53,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
return {
title: 'Referenzen | E-TIB Gruppe',
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabeltiefbau, Spülbohrtechnik und Netzinfrastruktur.',
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabelnetzbau, Spülbohrtechnik und Netzinfrastruktur.',
alternates: {
canonical: `${SITE_URL}/${locale}/referenzen`,
languages: {
@@ -71,25 +102,28 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
});
return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
{/* Map Hero Section */}
<div className="flex flex-col min-h-screen bg-neutral-50 pb-8 md:pb-24">
{/* Map Section */}
<InteractiveGermanyMap
isHero={true}
badge={locale === 'en' ? 'Our References' : 'Unsere Referenzen'}
title={locale === 'en' ? 'Successfully realized projects.' : 'Erfolgreich umgesetzte Projekte.'}
description={locale === 'en' ? 'From broadband expansion to complex 110kV lines: A selection of our nationwide projects where we have created infrastructure for the future.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben.'}
locations={enrichedLocations}
stats={[
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
{ value: '200', suffix: '+', label: locale === 'en' ? 'Completed Projects' : 'Abgeschlossene Projekte' }
]}
/>
{/* Main Content Area */}
<Container className="relative z-20 mt-16 md:mt-24">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{references.map((ref) => (
<TrackedLink
<div
key={ref.slug}
href={`/${locale}/referenzen/${ref.slug}`}
eventProperties={{ location: 'reference_list_card', slug: ref.slug }}
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
id={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block scroll-mt-32"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
@@ -106,7 +140,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
</div>
)}
<div className="absolute inset-0 bg-black/5 transition-colors duration-500" />
<div className="absolute inset-0 bg-black/5" />
{/* Location Badge */}
<div className="absolute top-4 left-4 z-10">
@@ -119,10 +153,17 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{/* Content Section */}
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 transition-colors line-clamp-3 leading-[1.2] group-hover:text-primary">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 leading-[1.2]">
{ref.frontmatter.title}
</h3>
{/* Project Details / Content */}
{ref.content && (
<div className="flex-grow mb-6">
<MDXRemote source={ref.content} components={mdxComponents} />
</div>
)}
{/* Technical 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">
@@ -142,12 +183,12 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</div>
</div>
</div>
</TrackedLink>
</div>
))}
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="mt-8 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>

View File

@@ -65,7 +65,7 @@ export default async function StandortDetail(props: { params: Promise<{ locale:
return (
<div className="flex flex-col min-h-screen bg-white">
{/* Generic Hero Section to match standard pages */}
<section className="bg-primary-dark text-white pt-40 pb-12 md:pt-56 md:pb-16 min-h-[40vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<section key={`standort-hero-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<div className="absolute inset-0 opacity-20 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
</div>
@@ -158,7 +158,7 @@ export default async function StandortDetail(props: { params: Promise<{ locale:
{/* Support Section */}
<Container>
<div className="mt-12 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<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('needHelp')}</h3>

View File

@@ -114,7 +114,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<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>

View File

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

View File

@@ -1,102 +0,0 @@
'use server';
import { getServerAppServices } from '@/lib/services/create-services.server';
export async function requestBrochureAction(formData: FormData) {
const services = getServerAppServices();
const logger = services.logger.child({ action: 'requestBrochureAction' });
const { headers } = await import('next/headers');
const requestHeaders = await headers();
if ('setServerContext' in services.analytics) {
(services.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,
});
}
services.analytics.track('brochure-request-attempt');
const email = formData.get('email') as string;
const locale = (formData.get('locale') as string) || 'en';
// Anti-spam Honeypot Check
const honeypot = formData.get('company_website') as string;
if (honeypot) {
logger.warn('Spam detected via honeypot in brochure request', { email });
// Silently succeed to fool the bot without doing actual work
return { success: true };
}
if (!email) {
logger.warn('Missing email in brochure request');
return { success: false, error: 'Missing email address' };
}
// Basic email validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return { success: false, error: 'Invalid email address' };
}
// 1. Save to CMS - Removed along with Payload CMS
// 2. Notify via Gotify
try {
await services.notifications.notify({
title: '📑 Brochure Download Request',
message: `New brochure download request from ${email} (${locale})`,
priority: 3,
});
} catch (error) {
logger.error('Failed to send notification', { error });
}
// 3. Send Brochure via Email
const brochureUrl = `https://e-tib.com/brochure/etib-product-catalog-${locale}.pdf`;
try {
const { sendEmail } = await import('@/lib/mail/mailer');
const { render } = await import('@mintel/mail');
const React = await import('react');
const { BrochureDeliveryEmail } = await import('@/components/emails/BrochureDeliveryEmail');
const html = await render(
React.createElement(BrochureDeliveryEmail, {
_email: email,
brochureUrl,
locale: locale as 'en' | 'de',
}),
);
const emailResult = await sendEmail({
to: email,
subject: locale === 'de' ? 'Ihr E-TIB Kabelkatalog' : 'Your E-TIB Cable Catalog',
html,
});
if (emailResult.success) {
logger.info('Brochure email sent successfully', { email });
} else {
logger.error('Failed to send brochure email', { error: emailResult.error, email });
services.errors.captureException(new Error(`Brochure email failed: ${emailResult.error}`), {
action: 'requestBrochureAction_email',
email,
});
return { success: false, error: 'Failed to send email. Please try again later.' };
}
} catch (error) {
logger.error('Exception while sending brochure email', { error });
return { success: false, error: 'Failed to send email. Please try again later.' };
}
// 4. Track success
services.analytics.track('brochure-request-success', {
locale,
delivery_method: 'email',
});
return { success: true };
}

View File

@@ -0,0 +1,36 @@
'use server';
import fs from 'fs';
import path from 'path';
export async function getAnnotatorAssets() {
// Try /app/public first (docker), then process.cwd()/public
let publicDir = '/app/public';
if (!fs.existsSync(publicDir)) {
publicDir = path.join(process.cwd(), 'public');
}
const walk = async (dir: string): Promise<string[]> => {
let results: string[] = [];
if (!fs.existsSync(dir)) return results;
const list = await fs.promises.readdir(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = await fs.promises.stat(filePath);
if (stat && stat.isDirectory()) {
const nested = await walk(filePath);
results = results.concat(nested);
} else {
if (filePath.match(/\.(png|jpg|jpeg|webp|mp4|svg)$/i)) {
results.push(filePath.replace(publicDir, ''));
}
}
}
return results;
};
const results = await walk(publicDir);
// Console logs removed for absolute silence & speed
return results;
}

View File

@@ -0,0 +1,52 @@
'use server';
import { sendEmail } from '@/lib/mail/mailer';
import { getServerAppServices } from '@/lib/services/create-services.server';
export async function submitAnnotations(annotations: any[]) {
const services = getServerAppServices();
const logger = services.logger.child({ action: 'submitAnnotations' });
if (!annotations || annotations.length === 0) {
return { success: false, error: 'Keine Korrekturen vorhanden.' };
}
try {
const jsonContent = JSON.stringify(annotations, null, 2);
const htmlContent = `
<h2>Neue Website-Korrekturen (${annotations.length})</h2>
<p>Es wurden neue Korrekturen über den Annotator eingereicht.</p>
<ul>
${annotations.map(ann => `
<li>
<b>${new URL(ann.url).pathname}</b> [${ann.type}]<br />
Element: <code>${ann.selector}</code><br />
<i>${ann.type === 'image' ? 'Neues Bild: ' : 'Text: '}${ann.newContent}</i>
</li>
`).join('')}
</ul>
<p><i>Die genauen Daten (inkl. Scroll-Positionen, Viewport und Zeitstempel) befinden sich hier:</i></p>
<pre style="background: #f4f4f4; padding: 10px; border-radius: 5px; font-size: 12px; overflow-x: auto;">
${jsonContent}
</pre>
`;
const notificationResult = await sendEmail({
replyTo: 'info@e-tib.com', // Falls Rückfragen nötig
subject: `[Korrekturen] ${annotations.length} neue Anmerkungen für E-TIB`,
html: htmlContent
});
if (!notificationResult.success) {
logger.error('Annotation email FAILED', { error: notificationResult.error });
return { success: false, error: notificationResult.error };
}
logger.info('Annotation email sent successfully', { count: annotations.length });
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error('Failed to send annotations', { error: errorMsg });
return { success: false, error: errorMsg };
}
}

View File

@@ -10,11 +10,13 @@
}
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
display: none !important; /* Chrome, Safari and Opera */
width: 0 !important;
height: 0 !important;
}
@keyframes bg-pulse-15 {

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', {

View File

@@ -1,7 +0,0 @@
export default function TestPage() {
return (
<div>
<h1>TEST PAGE WORKS</h1>
</div>
);
}

1
badge.svg Normal file
View File

@@ -0,0 +1 @@
Not found.

View File

@@ -0,0 +1,31 @@
'use client';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
import { getAnnotatorAssets } from '@/app/actions/getAnnotatorAssets';
import { submitAnnotations } from '@/app/actions/submitAnnotations';
const Annotator = dynamic(() => import('@/components/annotator-local/Annotator').then(mod => mod.Annotator), { ssr: false });
export default function AnnotatorClientWrapper() {
const [assets, setAssets] = useState<string[]>([]);
useEffect(() => {
getAnnotatorAssets()
.then(fetchedAssets => {
if (Array.isArray(fetchedAssets)) {
setAssets(fetchedAssets);
}
})
.catch(err => console.error('[AnnotatorClientWrapper] Fetch failed:', err));
}, []);
const handleSubmit = async (annotations: any[]) => {
const result = await submitAnnotations(annotations);
if (!result.success) {
throw new Error(result.error);
}
};
return <Annotator assets={assets} onSubmit={handleSubmit} />;
}

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

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

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import React from 'react';
import { cn } from '@/components/ui';
interface RevealProps {
@@ -10,40 +10,12 @@ interface RevealProps {
delay?: number;
}
export default function Reveal({ children, className, threshold = 0.1, delay = 0 }: RevealProps) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(entry.target);
}
},
{ threshold }
);
const currentRef = ref.current;
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [threshold]);
export default function Reveal({ children, className }: RevealProps) {
// Reveal animation disabled due to flickering issues on mobile/hydration
return (
<div
ref={ref}
className={cn('reveal-on-scroll', isVisible && 'is-visible', className)}
style={{ transitionDelay: `${delay}ms` }}
>
<div className={cn(className)}>
{children}
</div>
);
}

View File

@@ -0,0 +1,627 @@
import { useState, useEffect, useMemo } from "react";
import { AnimatePresence, m, LazyMotion, domAnimation } 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";
import { finder } from "@medv/finder";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export interface Annotation {
id: string;
x: number;
y: number;
selector: string;
type: "text" | "image" | "note";
originalContent: string;
newContent: string;
url: string;
elementRect: Record<string, number> | null;
scrollX: number;
scrollY: number;
viewportWidth: number;
viewportHeight: number;
userAgent: string;
timestamp: string;
}
export interface AnnotatorProps {
assets?: string[]; // Array of image URLs for the media browser
onSubmit?: (annotations: Annotation[]) => Promise<void>;
}
export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
const [isActive, setIsActive] = useState(false);
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(null);
const [annotations, setAnnotations] = useState<Annotation[]>(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("mintel-annotations");
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
console.error("Failed to parse saved annotations", e);
}
}
}
return [];
});
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("mintel-annotations", JSON.stringify(annotations));
}
}, [annotations]);
// Modal state
const [currentType, setCurrentType] = useState<"text" | "image" | "note">("text");
const [currentText, setCurrentText] = useState("");
const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
const [showList, setShowList] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
// Disable overlays if needed (e.g. inside iframes)
const isExcluded = useMemo(() => {
if (typeof window === "undefined") return false;
return window.self !== window.top;
}, []);
const getSelector = (el: HTMLElement): string => {
try {
return finder(el, {
root: document.body,
className: (name) =>
!name.startsWith("annotator-") &&
!name.includes("[") &&
!name.includes("/") &&
!name.match(/^[a-z]-[0-9]/),
idName: (name) => !name.startsWith("__next") && !name.includes(":"),
});
} catch {
return "unknown";
}
};
useEffect(() => {
if (!isActive) {
setHoveredElement(null);
return;
}
const handleMouseMove = (e: MouseEvent) => {
if (selectedElement) return;
const target = e.target as HTMLElement;
if (target.closest(".annotator-ui-ignore")) {
setHoveredElement(null);
return;
}
setHoveredElement(target);
};
const handleClick = (e: MouseEvent) => {
if (selectedElement) return;
const target = e.target as HTMLElement;
if (target.closest(".annotator-ui-ignore")) return;
e.preventDefault();
e.stopPropagation();
setSelectedElement(target);
setHoveredElement(null);
// Auto-detect type
if (target.tagName.toLowerCase() === "img") {
setCurrentType("image");
} else {
setCurrentType("text");
setCurrentText(target.innerText || "");
}
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("click", handleClick, true);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("click", handleClick, true);
};
}, [isActive, selectedElement]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (selectedElement) {
setSelectedElement(null);
} else if (isActive) {
setIsActive(false);
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isActive, selectedElement]);
const saveAnnotation = () => {
if (!selectedElement) return;
const rect = selectedElement.getBoundingClientRect();
const newContent = currentType === "image" ? selectedAsset || "" : currentText;
if (!newContent) return;
const annotation: Annotation = {
id: Math.random().toString(36).substring(2, 9),
x: rect.left + rect.width / 2 + window.scrollX,
y: rect.top + rect.height / 2 + window.scrollY,
selector: getSelector(selectedElement),
type: currentType,
originalContent: currentType === "image" ? (selectedElement as HTMLImageElement).src || "" : selectedElement.innerText,
newContent,
url: window.location.href,
elementRect: rect ? {
x: rect.x, y: rect.y, width: rect.width, height: rect.height,
top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left
} : null,
scrollX: window.scrollX,
scrollY: window.scrollY,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
setAnnotations([...annotations, annotation]);
setSelectedElement(null);
setCurrentText("");
setSelectedAsset(null);
};
const exportJSON = () => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(annotations, null, 2));
const downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "corrections.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
};
const handleSubmit = async () => {
if (!onSubmit || annotations.length === 0) return;
setIsSubmitting(true);
try {
await onSubmit(annotations);
setAnnotations([]);
setShowList(false);
} catch (error) {
console.error("Submission failed", error);
alert("Fehler beim Senden der Korrekturen. Bitte JSON Export nutzen.");
} finally {
setIsSubmitting(false);
}
};
const deleteAnnotation = (id: string) => {
setAnnotations(annotations.filter((a) => a.id !== id));
};
const [updateTick, setUpdateTick] = useState(0);
useEffect(() => {
if (!isActive && !selectedElement) return;
let rafId: number;
const handleScroll = () => {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => setUpdateTick(t => t + 1));
};
// Use capture phase to catch scrolls on any nested container
window.addEventListener("scroll", handleScroll, true);
window.addEventListener("resize", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll, true);
window.removeEventListener("resize", handleScroll);
cancelAnimationFrame(rafId);
};
}, [isActive, selectedElement]);
const hoveredRect = useMemo(() => hoveredElement?.getBoundingClientRect(), [hoveredElement, updateTick]);
const selectedRect = useMemo(() => selectedElement?.getBoundingClientRect(), [selectedElement, updateTick]);
if (isExcluded) return null;
return (
<div className="annotator-ui-ignore">
{/* 1. Global Toolbar */}
<div className="fixed bottom-36 md:bottom-24 right-4 z-[10000] flex flex-col items-end gap-2">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-1.5 rounded-2xl shadow-2xl flex flex-col items-center gap-1">
<button
onClick={() => setIsActive(!isActive)}
className={cn(
"p-2.5 rounded-xl transition-all",
isActive
? "bg-blue-500 text-white shadow-lg shadow-blue-500/20"
: "text-white/70 hover:text-white hover:bg-white/10"
)}
title={isActive ? "Modus beenden" : "Korrekturen erfassen"}
>
{isActive ? <X size={20} /> : <MessageSquare size={20} />}
</button>
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={() => setShowList(!showList)}
className="p-2.5 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
title="Liste anzeigen"
>
<List size={20} />
{annotations.length > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 text-[9px] flex items-center justify-center rounded-full text-white font-bold border border-[#1a1a1a]">
{annotations.length}
</span>
)}
</button>
{annotations.length > 0 && (
<>
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={exportJSON}
className="p-2.5 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all"
title="JSON Export"
>
<Download size={20} />
</button>
</>
)}
</div>
</div>
{/* 2. Highlights */}
<AnimatePresence>
{isActive && (
<div className="fixed inset-0 pointer-events-none z-[9998]">
{hoveredRect && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute border-2 border-blue-400 bg-blue-400/10 rounded-sm transition-all duration-200"
style={{
top: hoveredRect.top,
left: hoveredRect.left,
width: hoveredRect.width,
height: hoveredRect.height,
}}
/>
)}
{selectedRect && (
<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"
style={{
top: selectedRect.top,
left: selectedRect.left,
width: selectedRect.width,
height: selectedRect.height,
}}
/>
)}
</div>
)}
</AnimatePresence>
{/* 3. Action Modal */}
<AnimatePresence>
{selectedElement && (
<div className="fixed inset-0 flex items-center justify-center z-[10000] bg-black/40 backdrop-blur-sm">
<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 }}
className="bg-[#1c1c1e] border border-white/10 rounded-3xl p-4 sm:p-6 w-[95vw] md:w-[800px] shadow-2xl flex flex-col max-h-[85vh]"
>
<div className="flex items-center justify-between mb-6">
<h3 className="text-white font-bold text-lg">Änderung erfassen</h3>
<button
onClick={() => setSelectedElement(null)}
className="text-white/40 hover:text-white"
>
<X size={20} />
</button>
</div>
<div className="flex gap-2 mb-6 shrink-0">
<button
onClick={() => setCurrentType("text")}
className={cn(
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
currentType === "text"
? "bg-white text-black shadow-lg"
: "bg-white/5 text-white/40 hover:bg-white/10"
)}
>
<Type size={16} /> Text
</button>
<button
onClick={() => setCurrentType("image")}
className={cn(
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
currentType === "image"
? "bg-white text-black shadow-lg"
: "bg-white/5 text-white/40 hover:bg-white/10"
)}
>
<ImageIcon size={16} /> Bild
</button>
<button
onClick={() => setCurrentType("note")}
className={cn(
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
currentType === "note"
? "bg-white text-black shadow-lg"
: "bg-white/5 text-white/40 hover:bg-white/10"
)}
>
<StickyNote size={16} /> Notiz
</button>
</div>
<div className="flex-1 overflow-y-auto min-h-[200px] mb-6 hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{currentType === "text" && (
<div className="space-y-2">
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Neuer Text</label>
<textarea
autoFocus
value={currentText}
onChange={(e) => setCurrentText(e.target.value)}
className="w-full h-32 bg-black/40 border border-white/10 rounded-2xl p-4 text-white placeholder:text-white/20 focus:outline-none focus:border-blue-500/50 transition-colors resize-none"
/>
</div>
)}
{currentType === "note" && (
<div className="space-y-2">
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Anmerkung</label>
<textarea
autoFocus
value={currentText}
onChange={(e) => setCurrentText(e.target.value)}
placeholder="Was soll hier gemacht werden?"
className="w-full h-32 bg-black/40 border border-white/10 rounded-2xl p-4 text-white placeholder:text-white/20 focus:outline-none focus:border-blue-500/50 transition-colors resize-none"
/>
</div>
)}
{currentType === "image" && (
<div className="space-y-2 h-full">
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Neues Bild wählen</label>
{assets.length === 0 ? (
<div className="text-sm text-white/40 p-4 bg-white/5 rounded-xl border border-white/5 text-center">
Keine Assets übergeben. Bitte das prop <code>assets</code> befüllen.
</div>
) : (
<>
<style>{`.hide-scrollbar::-webkit-scrollbar { display: none; }`}</style>
<div className="mb-3">
<input
type="text"
placeholder="Suchen nach Dateinamen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-blue-500/50 transition-colors"
/>
</div>
<div
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 h-[400px] overflow-y-auto hide-scrollbar"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{assets.filter(asset => asset.toLowerCase().includes(searchTerm.toLowerCase())).map((asset) => {
const filename = asset.split('/').pop() || asset;
return (
<button
key={asset}
title={asset}
onClick={() => setSelectedAsset(asset)}
className={cn(
"relative flex flex-col items-center bg-black/20 rounded-xl overflow-hidden border-2 transition-all p-2 gap-2 w-full",
selectedAsset === asset
? "border-blue-500 shadow-[0_0_0_4px_rgba(59,130,246,0.3)] bg-blue-500/10"
: "border-transparent hover:border-white/20 hover:bg-white/5"
)}
style={{ height: '130px' }}
>
<div className="w-full flex-1 rounded-lg overflow-hidden bg-black/40 flex items-center justify-center relative">
{asset.match(/\.(mp4|webm)$/i) ? (
<video src={asset} className="w-full h-full object-cover opacity-80" />
) : (
<img src={asset} loading="lazy" alt={filename} className="w-full h-full object-contain" />
)}
{selectedAsset === asset && (
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/20 backdrop-blur-[2px]">
<div className="bg-blue-500 text-white rounded-full p-1.5 shadow-lg">
<Check size={18} strokeWidth={3} />
</div>
</div>
)}
</div>
<span className="text-[11px] font-medium text-white/70 truncate w-full text-center shrink-0" title={filename}>
{filename}
</span>
</button>
);
})}
</div>
</>
)}
</div>
)}
</div>
<button
disabled={(!currentText && currentType !== "image") || (currentType === "image" && !selectedAsset)}
onClick={saveAnnotation}
className="w-full bg-blue-500 hover:bg-blue-400 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-4 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-500/20 shrink-0"
>
<Check size={20} />
Speichern
</button>
</m.div>
</div>
)}
</AnimatePresence>
{/* 4. Annotations List Sidebar */}
<AnimatePresence>
{showList && (
<>
<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]"
/>
<m.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed top-0 right-0 h-full w-full sm:w-[400px] bg-[#1c1c1e] border-l border-white/10 z-[10002] shadow-2xl flex flex-col"
>
<div className="p-8 border-b border-white/10 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white mb-1">Korrekturen</h2>
<p className="text-white/40 text-sm">{annotations.length} erfasst</p>
</div>
<button
onClick={() => setShowList(false)}
className="p-2 text-white/40 hover:text-white bg-white/5 rounded-xl transition-colors"
>
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{annotations.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center px-8 opacity-40">
<List size={48} className="mb-4" />
<p>Noch keine Korrekturen vorhanden.</p>
</div>
) : (
annotations.map((ann) => (
<div
key={ann.id}
className="bg-white/5 border border-white/5 rounded-2xl p-4 flex flex-col gap-3 group relative"
>
<button
onClick={() => deleteAnnotation(ann.id)}
className="absolute top-4 right-4 text-white/20 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash2 size={16} />
</button>
<div className="flex items-center gap-2">
<span className={cn(
"px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider flex items-center gap-1",
ann.type === "text" ? "bg-blue-500/20 text-blue-400" :
ann.type === "image" ? "bg-purple-500/20 text-purple-400" :
"bg-orange-500/20 text-orange-400"
)}>
{ann.type === "text" && <Type size={12} />}
{ann.type === "image" && <ImageIcon size={12} />}
{ann.type === "note" && <StickyNote size={12} />}
{ann.type}
</span>
</div>
{ann.type === "image" ? (
<div className="flex items-center gap-2">
<img src={ann.newContent} alt="New" className="w-16 h-16 object-cover rounded-lg bg-white/10" />
<div className="text-xs text-white/60 flex-1 truncate">{ann.newContent}</div>
</div>
) : (
<p className="text-white text-sm whitespace-pre-wrap">{ann.newContent}</p>
)}
<div className="text-[10px] text-white/30 truncate mt-2 font-mono flex items-center gap-2">
<span>{ann.selector}</span>
{ann.url && <span className="truncate max-w-[100px] text-blue-400" title={ann.url}>{new URL(ann.url).pathname}</span>}
</div>
<div className="flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-white/5">
{ann.timestamp && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
{new Date(ann.timestamp).toLocaleString()}
</span>
)}
{ann.viewportWidth && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
{ann.viewportWidth}x{ann.viewportHeight}
</span>
)}
{ann.scrollX !== undefined && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
Scroll: {Math.round(ann.scrollX)},{Math.round(ann.scrollY)}
</span>
)}
</div>
</div>
))
)}
</div>
{annotations.length > 0 && (
<div className="p-6 border-t border-white/10 flex flex-col gap-3">
{onSubmit && (
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="w-full bg-blue-500 hover:bg-blue-400 text-white font-bold py-4 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<Check size={20} />
)}
{isSubmitting ? "Wird gesendet..." : "Korrekturen an Entwickler senden"}
</button>
)}
<button
onClick={exportJSON}
className={cn(
"w-full font-bold py-3 rounded-2xl flex items-center justify-center gap-2 transition-all",
onSubmit ? "bg-white/5 text-white/70 hover:bg-white/10 hover:text-white" : "bg-blue-500 hover:bg-blue-400 text-white shadow-lg shadow-blue-500/20 py-4"
)}
>
<Download size={onSubmit ? 18 : 20} />
JSON {onSubmit && "lokal "}Exportieren
</button>
<button
onClick={() => {
if (confirm("Möchtest du wirklich alle erfassten Korrekturen löschen?")) {
setAnnotations([]);
}
}}
className="w-full bg-red-500/10 text-red-500 hover:bg-red-500/20 font-bold py-3 rounded-xl flex items-center justify-center gap-2 transition-all mt-2"
>
<Trash2 size={18} />
Alle löschen
</button>
</div>
)}
</m.div>
</>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1 @@
export { Annotator } from "./Annotator";

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { LogoArcs } from '@/components/ui/LogoArcs';
@@ -30,8 +30,8 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {
@@ -75,13 +75,13 @@ const Icons = {
export function BenefitGrid({ badge, title, description, benefits }: BenefitGridProps) {
return (
<section className="bg-white py-24 relative overflow-hidden border-t border-neutral-100">
<section className="bg-white py-16 md:py-24 lg:py-32 relative overflow-hidden border-t border-neutral-100">
<div className="container px-4 max-w-7xl mx-auto relative z-10">
<div className="max-w-3xl mx-auto text-center mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
{title && (
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
{title}
</h3>
)}
@@ -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,76 +0,0 @@
'use client';
import React from 'react';
import { Button } from '@/components/ui/Button';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface CallToActionProps {
title?: string;
description?: string;
text?: string; // Alias for description
ctaLabel?: string;
buttonText?: string; // Alias for ctaLabel
ctaHref?: string;
buttonLink?: string; // Alias for ctaHref
theme?: 'light' | 'dark';
}
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
},
};
export const CallToAction: React.FC<CallToActionProps> = (props) => {
const { theme = 'light' } = props;
const isDark = theme === 'dark';
const t = useTranslations('CallToAction');
const title = props.title || t('title');
const description = props.description || props.text || t('description');
const ctaLabel = props.ctaLabel || props.buttonText || t('ctaLabel');
const ctaHref = props.ctaHref || props.buttonLink || t('ctaHref');
return (
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>
<motion.div
className="container max-w-3xl mx-auto px-4"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
<motion.h2 variants={itemVariants} className={`font-heading text-4xl font-extrabold mb-6 ${isDark ? 'text-white' : 'text-neutral-dark'}`}>
{title}
</motion.h2>
<motion.p variants={itemVariants} className={`text-xl mb-10 leading-relaxed ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>
{description}
</motion.p>
<motion.div variants={itemVariants}>
<Button
href={ctaHref}
size="xl"
variant={isDark ? 'white' : 'primary'}
>
{ctaLabel}
</Button>
</motion.div>
</motion.div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -68,31 +68,7 @@ const defaultCertificates: Certificate[] = [
type: 'tax',
date: '13.02.2025',
},
{
title: 'Unbedenklichkeit IHK',
description: 'Industrie- und Handelskammer',
type: 'general',
},
{
title: 'Unbedenklichkeit HWK',
description: 'Handwerkskammer',
type: 'general',
},
{
title: 'Handelsregisterauszug',
description: 'Amtsgericht Cottbus',
type: 'general',
},
{
title: 'Gewerbe-Anmeldung',
description: 'Stadt Guben',
type: 'general',
},
{
title: 'Gewerbezentralregister',
description: 'Auskunft aus dem Register',
type: 'general',
},
];
export function CertificatesBlock({ badge, title, description, certificates = defaultCertificates, hideHeader = false }: CertificatesBlockProps) {
@@ -118,17 +94,17 @@ export function CertificatesBlock({ badge, title, description, certificates = de
const itemVariants = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } },
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
if (!isMounted) {
return (
<section className="py-24 bg-neutral-50 relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-neutral-50 relative overflow-hidden">
<Container>
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<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>
</div>
)}
@@ -138,7 +114,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
}
return (
<section className="py-24 bg-neutral-50 relative overflow-hidden">
<section className="py-12 md:py-24 bg-neutral-50 relative overflow-hidden">
{/* Background Accents */}
<div className="absolute top-0 right-0 w-1/3 h-full bg-primary/5 -skew-x-12 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-accent/5 skew-y-12 -translate-x-1/2 blur-3xl rounded-full" />
@@ -146,20 +122,20 @@ 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 }}
transition={{ duration: 0.5 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
>
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<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"
@@ -168,7 +144,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
>
{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",
@@ -180,7 +156,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
key={index}
{...wrapperProps as any}
variants={itemVariants}
className={`group relative rounded-2xl p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
className={`group relative rounded-2xl p-6 md:p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
isIso
? 'bg-neutral-dark text-white border-neutral-800 shadow-lg'
: 'bg-white text-neutral-dark border-neutral-200 shadow-md'
@@ -243,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, LazyMotion, domAnimation } from 'framer-motion';
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
@@ -80,18 +80,18 @@ export function CompanyTimeline({
const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<section className="py-24 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-[#FAFAFA] relative overflow-hidden">
{/* Background Decor */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
<div className="container max-w-5xl relative z-10">
<div className="text-center mb-24">
<div className="text-center mb-12 md:mb-24">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-neutral-200 text-neutral-dark text-xs font-bold uppercase tracking-wider mb-6 shadow-sm">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
{finalBadge}
</div>
<h3 className="font-heading text-4xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
<h3 className="font-heading text-3xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
{finalTitle}
</h3>
</div>
@@ -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,12 +114,12 @@ 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: "-100px" }}
transition={{ duration: 0.7, ease: "easeOut" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="relative flex flex-col md:flex-row items-center w-full group"
>
{/* Timeline Dot with Pulse */}
@@ -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, LazyMotion, domAnimation } from 'framer-motion';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -62,15 +62,15 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
// Static fallback for SSR
if (!isMounted) {
return (
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
<section className="py-16 md:py-24 lg:py-32 bg-neutral text-neutral-dark overflow-hidden relative">
<Container>
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
<div className="max-w-2xl">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
<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>
@@ -79,20 +79,16 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
}
return (
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
<div className="flex flex-col md:flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl">
<div className="flex flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl text-left">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
{ctaLabel}
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" 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>
</Button>
</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)] px-4 md:px-0">
{items.map((item, idx) => {
const isLarge = item.size === 'large';
const isMedium = item.size === 'medium';
@@ -107,12 +103,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
if (isAccent) {
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
@@ -127,19 +123,19 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<HoverShineOverlay shineColor="via-white/30" />
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
<h4 className="font-bold text-xl md:text-2xl relative z-10 leading-snug">{item.title}</h4>
{item.description && <p className="text-white/60 text-sm mt-3 relative z-10">{item.description}</p>}
</motion.div>
<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>}
</m.div>
);
}
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
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] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
@@ -159,7 +155,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
sizes={isLarge ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 100vw, 25vw"}
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90 transition-opacity duration-500 z-0" />
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/60 to-black/30 opacity-100 transition-opacity duration-500 z-0 group-hover:from-black/80 group-hover:via-black/50 group-hover:to-black/20" />
</>
)}
@@ -177,16 +173,16 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
{item.tag}
</span>
)}
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-3xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
<h4 className="font-heading font-extrabold mb-2 text-2xl md:text-3xl">
{item.title}
</h4>
{item.description && (
<p className={`text-white/80 ${isLarge ? 'text-lg md:text-xl max-w-sm' : 'text-sm'}`}>
<p className="text-white/90 text-sm md:text-base max-w-md">
{item.description}
</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, LazyMotion, domAnimation } from 'framer-motion';
const ContactMap = dynamic(() => import('@/components/ContactMap'), {
ssr: false,
@@ -41,24 +41,24 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
const { showForm, showMap } = props;
return (
<Section className="bg-neutral-light py-12 md:py-28 relative overflow-hidden">
<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, 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-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,12 +94,12 @@ 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"
@@ -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,13 +1,13 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DataGridPulse() {
const locale = useLocale();
return (
<div className="relative w-full py-16 md:py-24 bg-neutral-50 border border-neutral-100/80 overflow-hidden flex flex-col items-center rounded-3xl mb-16 shadow-sm">
<div className="relative w-full py-12 md:py-24 bg-neutral-50 border border-neutral-100/80 overflow-hidden flex flex-col items-center rounded-3xl mb-16 shadow-sm">
{/* Blueprint grid background */}
<div
@@ -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]"
@@ -36,7 +36,7 @@ export function DataGridPulse() {
</span>
<div className="bg-white/80 backdrop-blur-md p-8 md:p-12 rounded-2xl border border-neutral-200/50 shadow-lg w-full">
<div className="font-mono text-5xl md:text-7xl font-black text-neutral-900 tracking-tighter mb-4 leading-none animate-pulse-slow" style={{ fontVariantNumeric: 'tabular-nums' }}>
<div className="font-mono text-4xl md:text-7xl font-black text-neutral-900 tracking-tighter mb-4 leading-none animate-pulse-slow" style={{ fontVariantNumeric: 'tabular-nums' }}>
372.161<span className="text-primary text-2xl md:text-4xl ml-1 font-sans">m</span>
</div>
<p className="text-text-secondary text-base md:text-lg font-medium max-w-md mx-auto leading-relaxed">

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DeepDrillAnimation() {
@@ -16,13 +16,13 @@ export function DeepDrillAnimation() {
const drillDepth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<div ref={containerRef} className="relative w-full py-32 flex flex-col items-center">
<div ref={containerRef} className="relative w-full py-12 md:py-32 flex flex-col items-center">
<div className="absolute inset-0 bg-[#050B14] -mx-[50vw] px-[50vw]" />
<div className="relative z-10 w-full max-w-3xl mx-auto flex flex-col items-center text-center text-white">
<h3 className="text-3xl font-heading font-extrabold mb-8 tracking-tight">
<h3 className="text-2xl md:text-3xl font-heading font-extrabold mb-8 tracking-tight">
{locale === 'en' ? 'Precision in Depth' : 'Präzision in der Tiefe'}
</h3>
<p className="text-white/60 text-lg mb-16 max-w-xl">
@@ -33,30 +33,30 @@ 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"
>
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
102.771<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
<div className="font-mono text-5xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
102.771<span className="text-primary text-2xl md:text-5xl ml-2">m</span>
</div>
<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

@@ -21,7 +21,7 @@ export const FaqBlock: React.FC<FaqBlockProps> = ({
if (!questions || questions.length === 0) return null;
return (
<section className="py-12 md:py-16">
<section className="py-16 md:py-24 lg:py-32">
<div className="max-w-3xl mx-auto">
<div className="mb-10 text-center">
{subtitle && (
@@ -29,7 +29,7 @@ export const FaqBlock: React.FC<FaqBlockProps> = ({
{subtitle}
</span>
)}
<h2 className="text-3xl md:text-4xl font-bold text-text-primary">
<h2 className="text-2xl md:text-4xl font-bold text-text-primary">
{title}
</h2>
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
@@ -23,9 +23,9 @@ export function GrowthChart() {
const maxVal = 163470; // Highest value for scaling
return (
<div className="w-full bg-white rounded-3xl p-8 md:p-12 shadow-sm border border-neutral-100 my-16">
<div className="w-full bg-white rounded-3xl p-6 md:p-12 shadow-sm border border-neutral-100 my-8 md:my-16">
<div className="mb-10">
<h3 className="text-3xl font-heading font-extrabold text-neutral-dark mb-2">{t('title')}</h3>
<h3 className="text-2xl md:text-3xl font-heading font-extrabold text-neutral-dark mb-2">{t('title')}</h3>
<p className="text-neutral-500 font-medium">{t('subtitle')}</p>
</div>
@@ -42,7 +42,7 @@ 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, margin: "-50px" }}
@@ -58,7 +58,7 @@ 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, margin: "-50px" }}
@@ -74,7 +74,7 @@ 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, margin: "-50px" }}

View File

@@ -2,7 +2,8 @@
import React from 'react';
import Image from 'next/image';
import { motion } from 'framer-motion';
import { usePathname } from 'next/navigation';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
@@ -39,15 +40,17 @@ const itemVariants = {
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
const bgSrc = backgroundImage?.sizes?.card?.url || backgroundImage?.url;
const pathname = usePathname();
return (
<section
className={`relative min-h-[45vh] md:min-h-[55vh] flex items-center pt-32 pb-20 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
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
<m.div
key={`hero-bg-${pathname}`}
className="absolute inset-0 z-0"
initial={{ scale: 1.05, opacity: 0 }}
initial={{ scale: 1.05, opacity: 1 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
>
@@ -71,36 +74,37 @@ 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>
</m.div>
)}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
<motion.div
<m.div
key={`hero-content-${pathname}`}
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
variants={containerVariants}
initial="hidden"
animate="visible"
>
{badge && (
<motion.div variants={itemVariants}>
<m.div variants={itemVariants}>
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{badge}
</Badge>
</motion.div>
</m.div>
)}
<motion.div variants={itemVariants}>
<Heading level={1} variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
<m.div variants={itemVariants}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</motion.div>
</m.div>
{subtitle && (
<motion.div variants={itemVariants}>
<m.div variants={itemVariants}>
<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>
</m.div>
)}
{ctaLabel && ctaHref && (
<motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex justify-center' : ''}`}>
<m.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
<Button
href={ctaHref}
variant="accent"
@@ -111,9 +115,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
&rarr;
</span>
</Button>
</motion.div>
</m.div>
)}
</motion.div>
</m.div>
</Container>
</section>
);

View File

@@ -2,8 +2,8 @@
import { Button } from '@/components/ui/Button';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface HeroVideoProps {
data?: any;
@@ -22,57 +22,88 @@ 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 KABELTIEFBAU';
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const explicitNoVideo = props.videoUrl === 'none' || props.videoUrl === '';
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.jpg');
}
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';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
let currentLocale = 'de';
try {
// We can't unconditionally call hooks if they might fail outside context, but HeroVideo is usually in context.
// However, to be safe and avoid adding new imports, we can use window.location or default to 'de' if not available.
if (typeof window !== 'undefined') {
currentLocale = window.location.pathname.startsWith('/en') ? 'en' : 'de';
}
} catch (e) {
// Fallback to default locale if window/location access fails
}
const currentLocale = pathname?.startsWith('/en') ? 'en' : 'de';
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/${currentLocale === 'de' ? 'contact' : 'contact'}`;
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-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
<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 ? (
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
<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"
quality={25}
priority
fetchPriority="high"
/>
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video
key={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}
key={`${pathname}-${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
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"
/>
)}
@@ -87,21 +118,20 @@ export function HeroVideo(props: HeroVideoProps) {
<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" />
<div className="container relative z-[4] text-center px-4">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
<div
key={`hero-content-${pathname}`}
className="relative"
>
<h1
className="font-heading font-bold text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
dangerouslySetInnerHTML={{ __html: title.replace('KABELTIEFBAU', '<span class="text-primary-light">KABELTIEFBAU</span>').replace(/\\n|\n/g, '<br />') }}
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"
dangerouslySetInnerHTML={{ __html: title.replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/\\n|\n/g, '<br />') }}
/>
<p className="text-lg md:text-xl lg:text-2xl text-white/90 max-w-3xl mx-auto mb-12 drop-shadow">
{subtitle}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-4 md:gap-6">
<Button
href={ctaHref}
variant="primary"
@@ -119,7 +149,7 @@ export function HeroVideo(props: HeroVideoProps) {
{secondaryCtaLabel}
</Button>
</div>
</motion.div>
</div>
</div>
</div>
);

View File

@@ -1,13 +1,13 @@
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
import React, { useState, useRef, useCallback, useMemo } from 'react';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { Location, defaultLocations, minorLocations, projectLocations } from '@/lib/map-data';
import { Location, projectLocations } from '@/lib/map-data';
import { standorteLocations } from '@/lib/standorte-data';
import { useLocale, useTranslations } from 'next-intl';
@@ -25,18 +25,90 @@ interface InteractiveGermanyMapProps {
stats?: Stat[];
locations?: Location[];
isHero?: boolean;
hideStandorte?: boolean;
}
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
<div
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
style={{
left: `${loc.x}%`,
top: `${loc.y}%`,
transform: 'translate(-50%, -50%)',
}}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
</div>
));
MinorNode.displayName = 'MinorNode';
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
const isHQ = loc.type === 'hq';
const isBranch = loc.type === 'branch';
return (
<div
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
onMouseEnter={() => onEnter(loc)}
onMouseLeave={onLeave}
onTouchStart={() => onEnter(loc)}
onClick={() => onEnter(loc)}
>
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
<m.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
} ${
isHQ || isBranch
? 'w-7 h-7 bg-primary text-[#050B14]'
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
}`}
>
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
</m.div>
</div>
);
});
MajorNode.displayName = 'MajorNode';
export function InteractiveGermanyMap({
badge,
title,
description,
stats,
locations = projectLocations,
isHero = false
isHero = false,
hideStandorte = false
}: InteractiveGermanyMapProps) {
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
const router = useRouter();
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleMouseEnter = useCallback((loc: Location) => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
setActiveLocation(loc);
}, []);
const handleMouseLeave = useCallback(() => {
const timeout = setTimeout(() => {
setActiveLocation(null);
}, 200);
hoverTimeoutRef.current = timeout;
}, []);
const pathname = usePathname();
const locale = useLocale();
const t = useTranslations('InteractiveGermanyMap');
const tStandard = useTranslations('StandardPage');
@@ -47,28 +119,42 @@ export function InteractiveGermanyMap({
];
const finalBadge = badge || tStandard('badge');
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
const hq = locations.find((l) => l.type === 'hq');
const branch = locations.find((l) => l.type === 'branch');
const projects = locations.filter((l) => l.type === 'project');
const minorNodes = locations.filter((l) => l.type === 'minor_node');
const finalLocations = useMemo(() => {
if (hideStandorte) return locations;
const base = [...locations];
standorteLocations.forEach(sl => {
if (!base.some(l => l.id === sl.id)) {
base.push(sl as Location);
}
});
return base;
}, [locations, hideStandorte]);
const mapBackground = useMemo(() => (
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
<Image
src="/germany-map.svg"
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
fill
className="object-cover"
priority
/>
</div>
), [locale]);
return (
<div className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-16 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
{/* Animated Border Glow */}
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
{/* Background Effects */}
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3 overflow-hidden" />
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
{/* Content Left */}
<div className="xl:col-span-5 text-white z-20">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
<MapPin className="w-4 h-4" />
@@ -76,11 +162,11 @@ export function InteractiveGermanyMap({
</div>
{isHero ? (
<h1 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
<h1 className="font-heading text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
{finalTitle}
</h1>
) : (
<h3 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
<h3 className="font-heading text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
{finalTitle}
</h3>
)}
@@ -89,14 +175,13 @@ export function InteractiveGermanyMap({
{finalDescription}
</p>
{/* Industrial Stats Grid */}
<div className="grid grid-cols-2 gap-4">
{finalStats.map((stat, i) => {
const StatCard = () => (
<div className={`bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group h-full ${stat.href ? 'cursor-pointer' : ''}`}>
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10 flex flex-col h-full justify-between">
<div className="text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
<div className="text-3xl md:text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
</div>
<div className={`text-sm text-white/50 font-medium flex items-center ${stat.href ? 'group-hover:text-primary transition-colors' : ''}`}>
@@ -120,102 +205,50 @@ export function InteractiveGermanyMap({
</div>
</div>
{/* Map Right */}
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
{/* Map Container - Enforce strict aspect ratio matching the SVG (1024x1024) */}
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
{/* Map SVG */}
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none">
<Image
src="/germany-map.svg"
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
fill
className="object-cover"
priority
/>
</div>
{mapBackground}
{/* Minor Location Markers (The 100+ generic projects) */}
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
<div
key={`minor-${idx}`}
className="absolute z-10 group/minor cursor-pointer w-10 h-10 flex items-center justify-center"
style={{
left: `${loc.x}%`,
top: `${loc.y}%`,
transform: 'translate(-50%, -50%)',
}}
onMouseEnter={() => setActiveLocation(loc)}
onMouseLeave={() => setActiveLocation(null)}
>
<div className="w-1.5 h-1.5 bg-primary/80 rounded-full group-hover/minor:bg-white group-hover/minor:scale-150 transition-all duration-300" />
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
</div>
{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}
/>
))}
{/* Major & Reference Location Markers (HQ, Branch, Project) */}
{locations.filter(l => l.type !== 'minor_node').map((loc, idx) => {
const isHQ = loc.type === 'hq';
const isBranch = loc.type === 'branch';
const isActive = activeLocation?.id === loc.id;
{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}
/>
))}
return (
<div
key={loc.id}
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'}`}
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
onMouseEnter={() => setActiveLocation(loc)}
onMouseLeave={() => setActiveLocation(null)}
onClick={() => {
if (loc.href) {
const href = loc.href.startsWith('/') ? `/${locale}${loc.href}` : loc.href;
router.push(href);
}
}}
>
{/* Ping Animation for HQ / Branch */}
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
{/* Elegant Marker Dot */}
<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>
);
})}
{/* Global Tooltip */}
<AnimatePresence>
{activeLocation && (
<motion.div
<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-none"
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={{
left: `${activeLocation.x}%`,
top: `${activeLocation.y}%`,
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
}}
'--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 min-w-[200px] w-max max-w-[300px] text-white overflow-hidden">
<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
@@ -272,15 +305,19 @@ export function InteractiveGermanyMap({
</ul>
)}
{activeLocation.href && (
<div className="inline-flex items-center text-xs font-bold text-primary">
{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" />
</div>
</Link>
)}
</div>
</div>
</motion.div>
</m.div>
)}
</AnimatePresence>
</div>

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, LazyMotion, domAnimation } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface JobListingBlockProps {
@@ -37,11 +37,11 @@ const containerVariants = {
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] as const },
},
};
@@ -71,22 +71,22 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<div className="py-12 md:py-24">
<div className="container mx-auto">
{showFairs && (
<div className="mb-24 relative">
<motion.h3
<div className="mb-12 md:mb-24 relative">
<m.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-4xl text-neutral-dark mb-12 flex items-center gap-4"
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, margin: "-100px" }}
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, margin: "-100px" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-4xl text-neutral-dark mb-8"
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, margin: "-100px" }}
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, margin: "-100px" }}
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

@@ -34,7 +34,7 @@ describe('ReferencesSlider TDD', () => {
id: '1',
title: 'Referenz Projekt Eins',
slug: 'referenz-projekt-eins',
category: 'Kabeltiefbau',
category: 'Kabelnetzbau',
},
{
id: '2',

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -46,7 +46,21 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const scrollLeftRef = React.useRef(0);
const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false);
// Center the first card on mount so it appears in the middle with peek on both sides
React.useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Wait for layout to settle
requestAnimationFrame(() => {
const firstCard = el.querySelector('[data-card]') as HTMLElement | null;
if (!firstCard) return;
const cardCenter = firstCard.offsetLeft + firstCard.offsetWidth / 2;
const containerCenter = el.offsetWidth / 2;
el.scrollLeft = cardCenter - containerCenter;
});
}, [references.length]);
const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title');
const description = props.description || data?.description || t('description');
@@ -94,74 +108,88 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
return (
<section id="referenzen" className="py-24 bg-neutral-dark text-white relative overflow-hidden">
<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-end gap-6">
<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-4xl md:text-5xl font-extrabold">{title}</h3>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div>
<p className="text-white/60 max-w-md">
<div className="max-w-3xl mx-auto mb-4 md:mb-8 text-neutral-300">
{description}
</p>
</div>
</div>
<div className="relative z-10">
<div
ref={containerRef}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-12 pt-4 px-[max(1rem,calc((100vw-1280px)/2))] 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 === 'object' && ref.image.url)
? 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 */}
<LazyMotion features={domAnimation}>
<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 (
<motion.div
key={ref.id}
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
>
<Link
href={`/${locale}/referenzen/${ref.slug}`}
data-testid="reference-tile"
onClick={(e) => {
if (dragDistanceRef.current > 15) {
e.preventDefault();
}
}}
className="block relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
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
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>
<Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
e.stopPropagation();
}
}}
onDragStart={(e) => e.preventDefault()}
draggable={false}
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>
</LazyMotion>
</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, LazyMotion, domAnimation } from 'framer-motion';
import { useLocale } from 'next-intl';
export function ScaleOfImpact() {
@@ -23,22 +23,22 @@ export function ScaleOfImpact() {
return (
<div
ref={containerRef}
className="relative w-full min-h-[70vh] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
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">
<h2 className="text-3xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight leading-tight">
{locale === 'en' ? (
<>An infrastructure <br className="md:hidden"/> that connects the country.</>
) : (
@@ -46,7 +46,7 @@ export function ScaleOfImpact() {
)}
</h2>
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-16">
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-4 md:mb-16">
{locale === 'en'
? "We don't just talk about the energy transition. We build it. In the last 36 months alone, we have completed a route that stretches from our headquarters in Guben all the way to Munich."
: "Wir sprechen nicht nur über die Energiewende. Wir bauen sie. Allein in den letzten 36 Monaten haben wir eine Strecke realisiert, die von unserem Hauptsitz in Guben bis nach München reicht."
@@ -54,7 +54,7 @@ export function ScaleOfImpact() {
</p>
{/* Abstract SVG Line Animation */}
<div className="w-full max-w-4xl h-32 relative">
<div className="w-full max-w-4xl h-12 md:h-32 relative">
<svg
width="100%"
height="100%"
@@ -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
@@ -82,21 +82,21 @@ export function ScaleOfImpact() {
</div>
{/* Static Data Grid (No jumping text) */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12 w-full max-w-5xl mt-12">
<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-4xl 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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-primary text-2xl">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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-primary text-2xl">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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-primary text-2xl">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, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
export interface ServicePanelData {
@@ -38,8 +38,8 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] } },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {
@@ -105,17 +105,17 @@ export function ServiceDetailGrid({
panels
}: ServiceDetailGridProps) {
return (
<section className="bg-neutral-50 py-24 border-b border-neutral-100 relative overflow-hidden">
<section className="bg-neutral-50 py-16 md:py-24 lg:py-32 border-b border-neutral-100 relative overflow-hidden">
<div className="container relative z-10 px-4 max-w-7xl mx-auto">
<div className="max-w-3xl mx-auto text-center mb-20">
<div className="max-w-3xl mb-12 md:mb-20 text-left">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
{title && (
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
{title}
</h3>
)}
<div className="w-20 h-2 bg-primary mx-auto mb-8"></div>
<div className="w-20 h-2 bg-primary mb-8"></div>
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
{descriptionParagraphs?.map((p, i) => {
@@ -133,15 +133,15 @@ 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" }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
>
{problemStatement && (
<div className="p-8 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
<div className="p-6 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
<div className="flex items-center mb-6 text-red-600">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
<circle cx="12" cy="12" r="10"></circle>
@@ -154,7 +154,7 @@ export function ServiceDetailGrid({
</div>
)}
{solutionStatement && (
<div className="p-8 md:p-12 bg-primary/5">
<div className="p-6 md:p-12 bg-primary/5">
<div className="flex items-center mb-6 text-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
@@ -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,10 +180,10 @@ 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-8 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' : ''}`}
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' : ''}`}
>
{/* Premium Watermark Number */}
<div className="absolute -right-8 -bottom-12 text-[12rem] font-heading font-black text-neutral-50 z-0 pointer-events-none select-none transition-all duration-700 group-hover:scale-105 group-hover:-translate-y-4 group-hover:text-primary/[0.03]">
@@ -193,9 +193,9 @@ export function ServiceDetailGrid({
{/* On a white card, a very subtle primary/grey shine works best */}
<HoverShineOverlay shineColor="via-primary/5" />
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
<div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
<div className="pt-2 flex-1 min-w-0 pr-4">
<h4 className="text-xl md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
</div>
@@ -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, LazyMotion, domAnimation, Variants } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -68,23 +68,25 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})) || defaultCompanies;
return (
<section id="unternehmen" className="py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<section id="unternehmen" className="pt-12 pb-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4">
{(badge || title) && (
<div className="text-center max-w-3xl mx-auto mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{title && <h3 className="font-heading text-4xl md:text-5xl font-extrabold text-white">{title}</h3>}
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
{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>
)}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
<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" />
<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, margin: "-50px" }}
>
{companiesData.map((company: any, index: number) => {
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
const hasUrl = !!company.url;
@@ -172,7 +174,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{/* LOGO RENDERING */}
{isEtib ? (
<div className="flex flex-col items-center">
<div className="relative w-48 h-14 mb-2">
<div className="relative w-32 md:w-40 lg:w-44 h-10 md:h-12 lg:h-14 mb-2 max-w-[85%]">
<Image
src="/assets/logo-white.png"
alt="E-TIB Logo"
@@ -181,17 +183,17 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
/>
</div>
{isIng && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Ingenieurgesellschaft
</div>
)}
{isVerwaltung && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Verwaltung GmbH
</div>
)}
{isBohrtechnik && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Bohrtechnik GmbH
</div>
)}
@@ -226,17 +228,17 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
</>
);
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full rounded-2xl select-none ${
isCurrent
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
}`;
return (
<motion.div
<m.div
key={index}
variants={itemVariants}
className="h-full"
className="h-full flex-shrink-0 snap-center w-[85%] sm:w-[70%] md:w-full"
>
{hasUrl ? (
isExternal ? (
@@ -253,10 +255,11 @@ 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, LazyMotion, domAnimation } 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" }}
className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
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, LazyMotion, domAnimation } from 'framer-motion';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
@@ -28,11 +28,11 @@ export function TeamGrid({ members }: TeamGridProps) {
if (!members || members.length === 0) return null;
return (
<section className="py-24 bg-white relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-white relative overflow-hidden">
<div className="container relative z-10">
<div className="mb-12">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{t('badge')}</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
<h3 className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
<p className="text-text-secondary max-w-2xl text-lg">
{t('subtitle')}
</p>
@@ -40,12 +40,12 @@ 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: 30 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
{/* Card Banner */}
@@ -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, LazyMotion, domAnimation } from 'framer-motion';
import { LogoArcs } from '@/components/ui/LogoArcs';
function MagneticRing({
@@ -57,13 +57,13 @@ function MagneticRing({
}, [x, y, pullRadius, pullStrength]);
return (
<motion.div
<m.div
ref={ref}
style={{ x, y }}
className={className}
>
{children}
</motion.div>
</m.div>
);
}
@@ -78,9 +78,9 @@ export function CorporateBackground() {
const y5 = useTransform(scrollYProgress, [0, 1], [0, -500]);
return (
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none" aria-hidden="true">
<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 +90,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 +111,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 +132,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 +153,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 +174,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 }
);

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