Compare commits

..

119 Commits

Author SHA1 Message Date
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
212 changed files with 77798 additions and 891 deletions

View File

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

View File

@@ -14,7 +14,7 @@ on:
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/v') && 'production' || github.ref }}
cancel-in-progress: true
env:
@@ -235,7 +235,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
@@ -282,14 +282,18 @@ 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
@@ -350,6 +354,10 @@ jobs:
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo ""
echo "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
echo ""
echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME"
@@ -392,10 +400,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'"
# ──────────────────────────────────────────────────────────────────────────────

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

View File

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

View File

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

@@ -10,7 +10,7 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs';
@@ -32,7 +32,8 @@ export async function generateMetadata(props: {
const params = await props.params;
const { locale } = params;
const baseUrl = process.env.CI ? 'http://etib.localhost' : SITE_URL;
const baseUrl = SITE_URL;
return {
title: {
template: '%s | E-TIB',
@@ -42,10 +43,11 @@ export async function generateMetadata(props: {
metadataBase: new URL(baseUrl),
manifest: '/manifest.webmanifest',
alternates: {
canonical: `${baseUrl}/${locale}`,
canonical: `/${locale}`,
languages: {
de: `${baseUrl}/de`,
en: `${baseUrl}/en`,
'de': `/de`,
'en': `/en`,
'x-default': `/de`,
},
},
};
@@ -60,6 +62,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 +153,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 +170,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 +196,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,7 +205,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{process.env.TARGET !== 'production' && <AnnotatorClientWrapper />}
{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

@@ -3,19 +3,41 @@ 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="list-disc pl-5 mb-6 text-neutral-600 text-sm space-y-2" {...props} />,
li: (props: any) => <li className="pl-1" {...props} />,
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} />,
};
@@ -80,7 +102,7 @@ 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">
<div className="flex flex-col min-h-screen bg-neutral-50 pb-8 md:pb-24">
{/* Map Section */}
<InteractiveGermanyMap
isHero={true}
@@ -100,7 +122,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{references.map((ref) => (
<div
key={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm 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 */}
@@ -165,7 +188,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</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

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

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

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

@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
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";
@@ -239,30 +239,31 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
return (
<div className="annotator-ui-ignore">
{/* 1. Global Toolbar */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] w-max max-w-[95vw]">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-1 sm:gap-2 overflow-x-auto hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<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(
"flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-medium",
"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={18} /> : <MessageSquare size={18} />}
{isActive ? "Modus beenden" : "Korrekturen erfassen"}
{isActive ? <X size={20} /> : <MessageSquare size={20} />}
</button>
<div className="w-px h-6 bg-white/10 mx-1" />
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={() => setShowList(!showList)}
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
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-5 h-5 bg-blue-500 text-[10px] flex items-center justify-center rounded-full text-white font-bold border-2 border-[#1a1a1a]">
<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>
)}
@@ -270,13 +271,13 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
{annotations.length > 0 && (
<>
<div className="w-px h-6 bg-white/10 mx-1" />
<div className="w-6 h-px bg-white/10 my-1" />
<button
onClick={exportJSON}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all font-medium"
className="p-2.5 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all"
title="JSON Export"
>
<Download size={18} />
JSON Export
<Download size={20} />
</button>
</>
)}
@@ -288,7 +289,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
{isActive && (
<div className="fixed inset-0 pointer-events-none z-[9998]">
{hoveredRect && (
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -303,7 +304,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
)}
{selectedRect && (
<motion.div
<m.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="absolute border-2 border-yellow-400 bg-yellow-400/20 rounded-sm"
@@ -323,7 +324,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<AnimatePresence>
{selectedElement && (
<div className="fixed inset-0 flex items-center justify-center z-[10000] bg-black/40 backdrop-blur-sm">
<motion.div
<m.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
@@ -474,7 +475,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<Check size={20} />
Speichern
</button>
</motion.div>
</m.div>
</div>
)}
</AnimatePresence>
@@ -483,14 +484,14 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
<AnimatePresence>
{showList && (
<>
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowList(false)}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[10001]"
/>
<motion.div
<m.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
@@ -617,7 +618,7 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
</button>
</div>
)}
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View File

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

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -94,7 +94,7 @@ 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) {
@@ -122,29 +122,29 @@ 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-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</motion.div>
</m.div>
</div>
)}
<motion.div
<m.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
{certificates.map((cert, index) => {
const isIso = cert.type === 'iso';
const Wrapper = cert.pdfUrl ? motion.a : motion.div;
const Wrapper = cert.pdfUrl ? m.a : m.div;
const wrapperProps = cert.pdfUrl ? {
href: encodeURI(cert.pdfUrl),
target: "_blank",
@@ -219,7 +219,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
</Wrapper>
);
})}
</motion.div>
</m.div>
</Container>
</section>
);

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
@@ -102,7 +102,7 @@ export function CompanyTimeline({
{/* Animated Central Line */}
<div className="absolute left-[38px] md:left-1/2 top-0 bottom-0 w-1.5 -translate-x-1/2 rounded-full overflow-hidden">
<motion.div
<m.div
className="w-full bg-primary drop-shadow-[0_0_8px_rgba(130,237,32,0.8)]"
style={{ height: lineHeight }}
/>
@@ -114,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';
@@ -70,7 +70,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[minmax(220px,auto)]">
{/* Simplified static grid */}
</div>
</Container>
@@ -86,13 +86,9 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<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="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
{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] px-4 md:px-0">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[minmax(220px,auto)] px-4 md:px-0">
{items.map((item, idx) => {
const isLarge = item.size === 'large';
const isMedium = item.size === 'medium';
@@ -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-2xl 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,
@@ -44,21 +44,21 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<Section className="bg-neutral-light relative overflow-hidden">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
<motion.div
<m.div
className="lg:col-span-5 flex flex-col justify-center"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
<motion.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</motion.h2>
<motion.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
<motion.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
<m.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</m.h2>
<m.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</m.h3>
<m.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
Haben Sie Fragen zu unseren Leistungen oder möchten Sie ein konkretes Projekt besprechen? Rufen Sie uns an oder schreiben Sie uns eine E-Mail.
</motion.p>
</m.p>
<div className="space-y-6">
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
@@ -68,9 +68,9 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<a href="tel:+4935616857733">+49 (0) 3561 / 68577 33</a>
</p>
</div>
</motion.div>
</m.div>
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</div>
@@ -80,9 +80,9 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
<a href="mailto:info@e-tib.com">info@e-tib.com</a>
</p>
</div>
</motion.div>
</m.div>
<motion.div variants={itemVariants} className="group flex items-start gap-4">
<m.div variants={itemVariants} className="group flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary shrink-0 transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</div>
@@ -94,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,7 +1,7 @@
'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() {
@@ -19,12 +19,12 @@ export function DataGridPulse() {
/>
{/* Pulsing horizontal lines (Data Flow) */}
<motion.div
<m.div
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/30 to-transparent blur-[1px]"
/>
<motion.div
<m.div
animate={{ x: ["100%", "-100%"] }}
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/25 to-transparent blur-[1px]"

View File

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

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
@@ -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

@@ -3,7 +3,7 @@
import React from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
@@ -47,10 +47,10 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
>
{bgSrc && (
<motion.div
<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" }}
>
@@ -74,10 +74,10 @@ 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}
@@ -85,26 +85,26 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
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"
@@ -115,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,7 +2,6 @@
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';
@@ -23,6 +22,8 @@ interface HeroVideoProps {
badge?: string;
}
import placeholders from '@/lib/placeholders.json';
export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
@@ -31,7 +32,12 @@ export function HeroVideo(props: HeroVideoProps) {
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const pathname = usePathname();
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster.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';
@@ -42,31 +48,61 @@ export function HeroVideo(props: HeroVideoProps) {
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
let interactionTriggered = false;
const handleInteraction = () => {
if (interactionTriggered) return;
interactionTriggered = true;
setVideoSrcLoaded(true);
// Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
// Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true })
);
return () => {
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
}, []);
return (
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{videoUrl ? (
{/* 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={50}
priority
/>
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video
key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
className={`absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90 transition-opacity duration-1000 ${videoSrcLoaded ? 'opacity-100' : 'opacity-0'}`}
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="auto"
/>
) : (
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
preload="none"
/>
)}
@@ -81,11 +117,9 @@ 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
<div
key={`hero-content-${pathname}`}
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
className="relative"
>
<h1
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
@@ -96,7 +130,7 @@ export function HeroVideo(props: HeroVideoProps) {
{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"
@@ -114,7 +148,7 @@ export function HeroVideo(props: HeroVideoProps) {
{secondaryCtaLabel}
</Button>
</div>
</motion.div>
</div>
</div>
</div>
);

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useState, useRef, useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
@@ -62,7 +62,7 @@ const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: L
{(isHQ || isBranch) && (
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
<motion.div
<m.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', delay: idx * 0.1 }}
@@ -75,7 +75,7 @@ const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: L
}`}
>
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
</motion.div>
</m.div>
</div>
);
});
@@ -233,13 +233,13 @@ export function InteractiveGermanyMap({
<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-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)]"
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
style={{
'--md-left': `${activeLocation.x}%`,
'--md-top': `${activeLocation.y}%`,
@@ -317,7 +317,7 @@ export function InteractiveGermanyMap({
)}
</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 },
},
};
@@ -72,21 +72,21 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<div className="container mx-auto">
{showFairs && (
<div className="mb-12 md:mb-24 relative">
<motion.h3
<m.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, 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-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-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

@@ -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,70 +108,87 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
return (
<section id="referenzen" className="py-16 md:py-24 lg:py-32 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-start md:items-end gap-6">
<div className="text-left">
<div className="container relative z-10 mb-12 flex flex-col items-center justify-center text-center gap-6">
<div>
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div>
<div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
<div className="max-w-3xl mx-auto mb-4 md:mb-8 text-neutral-300">
{description}
</div>
</div>
<div className="relative z-10">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<div
ref={containerRef}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-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"
>
<div
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
return (
<m.div
key={ref.id}
data-card
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
>
<Image
<Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
e.stopPropagation();
}
}}
onDragStart={(e) => e.preventDefault()}
draggable={false}
src={imgSrc}
alt={ref.title}
fill
sizes="(max-width: 768px) 320px, 480px"
className="object-cover saturate-50 opacity-70 group-hover:scale-105 group-hover:opacity-100 group-hover:saturate-100 transition-all duration-700 ease-in-out pointer-events-none"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300 pointer-events-none" />
<HoverShineOverlay />
<div className="absolute bottom-6 left-6 right-6 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none bg-black/60 backdrop-blur-sm p-4 rounded-xl border border-white/10">
<span className="inline-block py-1 px-3 rounded-full bg-primary/20 text-white text-[10px] font-bold uppercase tracking-widest mb-2 border border-primary/30">
{ref.category}
</span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</div>
</motion.div>
);
})}
</div>
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
>
<Image
draggable={false}
src={imgSrc}
alt={ref.title}
fill
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() {
@@ -26,19 +26,19 @@ export function ScaleOfImpact() {
className="relative w-full min-h-[40vh] md:min-h-[70vh] bg-[#050B14] py-12 md:py-0 flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
>
{/* Massive Watermark Numbers */}
<motion.div
<m.div
style={{ opacity: bgOpacity }}
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none overflow-hidden mix-blend-overlay"
>
<div className="font-heading font-black text-[20vw] leading-none text-white opacity-20 whitespace-nowrap">
<span className="font-mono tracking-tighter">1.435.223</span>
</div>
</motion.div>
</m.div>
{/* Foreground Content */}
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
<h2 className="text-2xl 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.</>
) : (
@@ -70,7 +70,7 @@ export function ScaleOfImpact() {
strokeWidth="2"
/>
{/* Glowing animated line */}
<motion.path
<m.path
d="M0,50 Q250,0 500,50 T1000,50"
fill="none"
stroke="#ffc107" // warning/yellow color
@@ -84,19 +84,19 @@ export function ScaleOfImpact() {
{/* Static Data Grid (No jumping text) */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-2 md:gap-12 w-full max-w-5xl mt-4 md:mt-12">
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">200<span className="text-primary">+</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">200<span className="text-emerald-400">+</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Projects' : 'Projekte'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Cable Laying' : 'Kabelverlegung'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Open Trenching' : 'Offener Tiefbau'}</div>
</div>
<div className="flex flex-col items-center">
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-primary text-xl md:text-2xl ml-1">km</span></div>
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-emerald-400 text-xl md:text-2xl ml-1">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
</div>
</div>

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import React from 'react';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { motion } from 'framer-motion';
import { m, 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" }}
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';
@@ -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>
);
}
@@ -80,7 +80,7 @@ export function CorporateBackground() {
return (
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none hidden md:block" aria-hidden="true">
{/* 1. Top Right - Medium, rotating moderately */}
<motion.div
<m.div
style={{
y: y1,
animationDelay: '0s',
@@ -90,18 +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

@@ -1,11 +1,11 @@
'use client';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
import Image from 'next/image';
export function EUFundingBadge() {
return (
<motion.div
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1] }}
@@ -36,6 +36,6 @@ export function EUFundingBadge() {
Europäischen Union
</span>
</div>
</motion.div>
</m.div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,8 +108,13 @@ export function Footer({ companyInfo }: FooterProps) {
{locale === 'de' ? 'Navigation' : 'Navigation'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 mb-8">
<ul className="grid grid-cols-2 gap-x-8 gap-y-3 mb-8">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -123,28 +128,6 @@ export function Footer({ companyInfo }: FooterProps) {
</li>
))}
</ul>
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-4 text-white">
<TransitionLink href={`/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}`} className="hover:text-primary transition-colors cursor-pointer inline-block">
{locale === 'de' ? 'Über uns' : 'About us'}
</TransitionLink>
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 border-l border-white/10 pl-4 ml-1">
{[
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
{/* Mobile View (Accordions) */}
@@ -157,6 +140,11 @@ export function Footer({ companyInfo }: FooterProps) {
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -171,32 +159,6 @@ export function Footer({ companyInfo }: FooterProps) {
</ul>
</div>
</details>
<div className="h-px bg-white/10 w-full"></div>
<details className="group/acc">
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
{locale === 'de' ? 'Über uns' : 'About us'}
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
</summary>
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
</details>
</div>
</div>

View File

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

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import { TransitionLink } from '@/components/ui/TransitionLink';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion, useScroll, useTransform, AnimatePresence } from 'framer-motion';
import { LanguageSwitcher } from './LanguageSwitcher';
export interface NavLink {
@@ -25,21 +24,30 @@ export function Header({ navLinks }: HeaderProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const pathname = usePathname() || '/';
// Scroll bound shine effect (continuous looping as user scrolls)
const { scrollY } = useScroll();
const shineX = useTransform(scrollY, (y) => {
const adjustedY = Math.max(0, y - 20); // Start after the solid mode kicks in
const range = 800; // 1 full sweep per 800px scrolled
const progress = (adjustedY % range) / range;
return `${-150 + progress * 300}%`;
});
const shineRef = React.useRef<HTMLDivElement>(null);
// Determine current locale from pathname
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
React.useEffect(() => {
let animationFrameId: number;
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (shineRef.current) {
const y = window.scrollY;
const adjustedY = Math.max(0, y - 20);
const range = 800;
const progress = (adjustedY % range) / range;
const x = -150 + progress * 300;
cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(() => {
if (shineRef.current) {
shineRef.current.style.transform = `translateX(${x}%)`;
}
});
}
};
window.addEventListener('scroll', handleScroll);
@@ -88,8 +96,8 @@ export function Header({ navLinks }: HeaderProps) {
>
{/* Scroll bound shine effect */}
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden rounded-full">
<motion.div
style={{ x: shineX }}
<div
ref={shineRef}
className={`absolute top-0 h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-20deg] mix-blend-overlay transition-opacity duration-300 ${isSolidMode ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
@@ -101,6 +109,7 @@ export function Header({ navLinks }: HeaderProps) {
fill
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
priority
fetchPriority="high"
sizes="192px"
/>
{/* Masked sweep overlay on hover */}
@@ -138,8 +147,54 @@ export function Header({ navLinks }: HeaderProps) {
<div className="flex items-center gap-2 md:gap-4 lg:gap-6">
<div className={`hidden md:block h-5 w-px transition-colors duration-300 ${isSolidMode ? 'bg-neutral-200' : 'bg-white/20'}`} />
{/* Language Switcher visible on all devices */}
<div className="z-50 relative flex items-center">
{/* Quick Actions (Mobile Only) */}
<div className="md:hidden flex items-center gap-2">
<a
href="tel:+492572946760"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-primary text-white shadow-md shadow-primary/20 hover:-translate-y-0.5'
: 'bg-white text-primary shadow-md hover:-translate-y-0.5'
}`}
aria-label="Anrufen"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</a>
<a
href="mailto:info@e-tib.com"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
}`}
aria-label="E-Mail"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</a>
<a
href="https://maps.google.com/?q=E-TIB+GmbH,+Greven"
target="_blank"
rel="noopener noreferrer"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
}`}
aria-label="Route"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</a>
</div>
{/* Language Switcher visible on desktop only */}
<div className="z-50 relative hidden md:flex items-center">
<LanguageSwitcher isSolidMode={isSolidMode} />
</div>
@@ -157,81 +212,10 @@ export function Header({ navLinks }: HeaderProps) {
<span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" />
</TransitionLink>
{/* Hamburger Menu (Mobile Only) */}
<button
className={`md:hidden ml-2 p-2 transition-colors focus:outline-none ${isSolidMode && !isMobileMenuOpen ? 'text-text-primary hover:text-primary' : 'text-white hover:text-primary z-[60] relative'}`}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle Menu"
>
<div className="w-6 flex flex-col gap-[6px] relative items-center justify-center">
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? 'rotate-45 translate-y-[8px]' : ''}`} />
<span className={`w-full h-[2px] rounded-full bg-current transition-all duration-300 ease-in-out ${isMobileMenuOpen ? 'opacity-0 translate-x-3' : 'opacity-100'}`} />
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? '-rotate-45 -translate-y-[8px]' : ''}`} />
</div>
</button>
</div>
</div>
</div>
</header>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 z-[40] bg-[#050B14]/95 backdrop-blur-xl md:hidden pt-24 pb-24 px-6 overflow-y-auto"
>
<nav className="flex flex-col gap-6 mt-8">
{navLinks && navLinks.length > 0 && navLinks.map((link) => {
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
? `/${currentLocale}${link.url}`
: link.url;
const isActive = pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`));
return (
<div key={link.url} className="flex flex-col">
<TransitionLink
href={mappedUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-2xl font-bold uppercase tracking-widest transition-colors ${isActive ? 'text-primary' : 'text-white'}`}
>
{link.label}
</TransitionLink>
{link.children && (
<div className="flex flex-col gap-3 mt-3 pl-4 border-l-2 border-white/10">
{link.children.map((child, idx) => {
if (child.isGroupLabel) {
return <div key={idx} className="text-sm font-bold text-neutral-500 uppercase tracking-widest mt-2">{child.label}</div>;
}
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
? `/${currentLocale}${child.url}`
: child.url;
const isChildActive = pathname === childUrl;
return (
<TransitionLink
key={child.url}
href={childUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-lg font-medium transition-colors ${isChildActive ? 'text-primary' : 'text-white/70'}`}
>
{child.label}
</TransitionLink>
);
})}
</div>
)}
</div>
);
})}
</nav>
</motion.div>
)}
</AnimatePresence>
</>
);
}
@@ -280,12 +264,10 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
</svg>
)}
{/* Framer Motion Active Indicator */}
{/* Active Indicator */}
{isActive && !link.children && (
<motion.div
layoutId="header-active-indicator"
<div
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
@@ -293,15 +275,13 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
</TransitionLink>
<AnimatePresence>
{isHovered && link.children && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
exit={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
style={{ perspective: 1000, transformOrigin: 'top center' }}
className="absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50"
<>
{link.children && (
<div
className={`absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50 transition-all duration-300 origin-top ${
isHovered ? 'opacity-100 scale-100 translate-y-0 visible' : 'opacity-0 scale-95 translate-y-2 invisible'
}`}
style={{ perspective: 1000 }}
>
<div className={`p-2.5 rounded-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] border ${isSolidMode ? 'bg-white/95 border-neutral-200/60' : 'bg-[#050B14]/95 border-white/[0.08] shadow-[0_0_40px_rgba(0,0,0,0.4)]'} backdrop-blur-2xl flex flex-col gap-1 relative overflow-hidden`}>
{/* Top glowing accent line */}
@@ -345,9 +325,9 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
);
})}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
</div>
);
}

View File

@@ -3,12 +3,13 @@
import * as React from 'react';
import { usePathname } from 'next/navigation';
import { TransitionLink } from '@/components/ui/TransitionLink';
import { Home, Briefcase, Users, Mail } from 'lucide-react';
import { motion } from 'framer-motion';
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
export interface NavLink {
label: string;
url: string;
children?: NavLink[];
}
interface MobileBottomNavProps {
@@ -18,84 +19,213 @@ interface MobileBottomNavProps {
export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProps) {
const pathname = usePathname() || '/';
const [isFlyoutOpen, setIsFlyoutOpen] = React.useState(false);
// Scroll lock when flyout is open
React.useEffect(() => {
if (isFlyoutOpen) {
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
};
}, [isFlyoutOpen]);
// Construct our bottom nav items based on the passed links
const items = React.useMemo(() => {
const homeUrl = `/${currentLocale}`;
// Find the specific links from the provided navLinks (which might be translated)
// We do a simple match based on index or keywords if needed, but since we know the order:
// 0: Kompetenzen, 1: Über uns, 2: Karriere, 3: Messen
const competenciesUrl = navLinks[0]?.url || `/${currentLocale}/kompetenzen`;
const aboutUrl = navLinks[1]?.url || `/${currentLocale}/ueber-uns`;
const referencesUrl = navLinks[2]?.url || `/${currentLocale}/referenzen`;
const contactUrl = `/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`;
return [
{
id: 'home',
label: currentLocale === 'de' ? 'Start' : 'Home',
url: homeUrl,
icon: <Home className="w-6 h-6 mb-1" strokeWidth={2} />,
icon: <Home className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />,
exactMatch: true
},
{
id: 'kompetenzen',
label: navLinks[0]?.label || (currentLocale === 'de' ? 'Kompetenzen' : 'Competencies'),
url: competenciesUrl,
icon: <Briefcase className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Layers className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
label: navLinks[1]?.label || (currentLocale === 'de' ? 'Über uns' : 'About Us'),
url: aboutUrl,
icon: <Users className="w-6 h-6 mb-1" strokeWidth={2} />
id: 'referenzen',
label: navLinks[2]?.label || (currentLocale === 'de' ? 'Referenzen' : 'References'),
url: referencesUrl,
icon: <Star className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'kontakt',
label: currentLocale === 'de' ? 'Kontakt' : 'Contact',
url: contactUrl,
icon: <Mail className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Mail className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'menu',
label: currentLocale === 'de' ? 'Mehr' : 'Menu',
url: '#',
icon: <Menu className="w-[24px] h-[24px] md:w-6 md:h-6" strokeWidth={2.5} />,
isFlyoutTrigger: true
}
];
}, [currentLocale, navLinks]);
return (
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-2 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
<nav className="flex justify-around items-center max-w-md mx-auto">
{items.map((item) => {
// Determine if active
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
: item.url;
const isActive = item.exactMatch
? pathname === mappedUrl
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
const flyoutLinks = React.useMemo(() => {
const aboutNode = navLinks[1];
const karriereNode = navLinks[3];
const messenNode = navLinks[4];
let links: {label: string, url: string}[] = [];
if (aboutNode?.children) {
links.push(...aboutNode.children.map(c => ({ label: c.label, url: c.url })));
}
if (karriereNode) links.push({ label: karriereNode.label, url: karriereNode.url });
if (messenNode) links.push({ label: messenNode.label, url: messenNode.url });
return links;
}, [navLinks]);
return (
<TransitionLink
key={item.label}
href={mappedUrl}
className={`relative flex flex-col items-center justify-center w-full min-h-[44px] py-1 transition-colors duration-300 select-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
}`}
>
<div className="relative flex flex-col items-center justify-center">
{/* Active Icon Background */}
return (
<>
<div className="md:hidden fixed bottom-0 left-0 right-0 z-[9999] px-2 pb-[calc(env(safe-area-inset-bottom)_+_12px)] pt-4 pointer-events-none flex justify-center">
<nav className="flex justify-between items-center bg-white/95 backdrop-blur-2xl border border-white/60 shadow-[0_8px_32px_rgba(0,0,0,0.12)] rounded-3xl p-1.5 pointer-events-auto w-full max-w-[480px]">
{items.map((item) => {
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
: item.url;
let isActive = false;
if (item.isFlyoutTrigger) {
isActive = isFlyoutOpen;
} else {
isActive = item.exactMatch
? pathname === mappedUrl
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
}
const innerContent = (
<>
{isActive && (
<motion.div
<m.div
layoutId="mobile-nav-active-bg"
className="absolute inset-0 bg-primary/10 rounded-full scale-150"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className="absolute inset-0 bg-primary/10 rounded-2xl shadow-sm border border-primary/10"
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
/>
)}
{/* Icon wrapper to ensure it sits above the background */}
<div className="relative z-10 transition-transform duration-300 active:scale-90">
{item.icon}
<div className="relative flex flex-col items-center justify-center z-10 w-full h-full">
<m.div
whileTap={{ scale: 0.8 }}
animate={{ y: isActive ? -2 : 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{item.icon}
</m.div>
<m.span
animate={{ y: isActive ? 0 : 2 }}
className="text-[10px] font-bold mt-1 tracking-wide"
>
{item.label}
</m.span>
</div>
</>
);
if (item.isFlyoutTrigger) {
return (
<button
key={item.label}
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none w-full focus:outline-none ${
isActive ? 'text-primary-dark' : 'text-neutral-dark hover:text-black'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
{innerContent}
</button>
);
}
return (
<TransitionLink
key={item.label}
href={mappedUrl}
onClick={() => setIsFlyoutOpen(false)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none ${
isActive ? 'text-primary-dark' : 'text-neutral-dark hover:text-black'
}`}
style={{ WebkitTapHighlightColor: 'transparent', touchAction: 'manipulation' }}
>
{innerContent}
</TransitionLink>
);
})}
</nav>
</div>
<AnimatePresence>
{isFlyoutOpen && (
<>
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[9997] md:hidden"
onClick={() => setIsFlyoutOpen(false)}
/>
<m.div
initial={{ y: '100%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '100%', opacity: 0 }}
transition={{ type: 'spring', stiffness: 350, damping: 30 }}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.2}
onDragEnd={(e, info) => {
if (info.offset.y > 50 || info.velocity.y > 100) {
setIsFlyoutOpen(false);
}
}}
className="fixed bottom-0 left-0 right-0 z-[9998] bg-white rounded-t-[32px] pt-4 pb-[calc(env(safe-area-inset-bottom)_+_7rem)] px-6 md:hidden shadow-[0_-20px_40px_rgba(0,0,0,0.1)] border-t border-neutral-100"
>
<div className="w-16 h-1.5 bg-neutral-200 rounded-full mx-auto mb-6 cursor-grab active:cursor-grabbing" />
<h3 className="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-4 px-2">{currentLocale === 'de' ? 'Über uns & Mehr' : 'About us & More'}</h3>
<div className="flex flex-col gap-1">
{flyoutLinks.map((link, i) => {
const childUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
? `/${currentLocale}${link.url}`
: link.url;
const isChildActive = pathname === childUrl;
return (
<TransitionLink
key={i}
href={childUrl}
onClick={() => setIsFlyoutOpen(false)}
className={`px-4 py-4 rounded-2xl text-[15px] font-semibold transition-all flex items-center justify-between ${
isChildActive ? 'bg-primary/10 text-primary' : 'text-neutral-700 hover:bg-neutral-50 active:bg-neutral-100'
}`}
>
{link.label}
<svg className={`w-4 h-4 ${isChildActive ? 'text-primary' : 'text-neutral-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</TransitionLink>
);
})}
</div>
<span className="text-[10px] font-semibold mt-0.5 tracking-wide z-10">{item.label}</span>
</TransitionLink>
);
})}
</nav>
</div>
</m.div>
</>
)}
</AnimatePresence>
</>
);
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { LazyMotion, domAnimation } from 'framer-motion';
interface TransitionContextType {
isTransitioning: boolean;
@@ -15,8 +16,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning);
@@ -24,18 +23,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
isTransitioningRef.current = isTransitioning;
}, [isTransitioning]);
// Watch for route changes to complete the transition
useEffect(() => {
if (isTransitioningRef.current) {
// The route has actually changed in the browser, or at least the RSC payload arrived
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams]); // Run when pathname or search parameters change
const startTransition = useCallback((href: string, message: string | null = null) => {
// Wenn wir bereits navigieren, nichts tun
if (isTransitioning) return;
@@ -75,11 +62,37 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
return (
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
{children}
<React.Suspense fallback={null}>
<TransitionRouteWatcher
isTransitioningRef={isTransitioningRef}
setIsTransitioning={setIsTransitioning}
setTransitionMessage={setTransitionMessage}
/>
</React.Suspense>
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
</TransitionContext.Provider>
);
}
function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (isTransitioningRef.current) {
const timer = setTimeout(() => {
setIsTransitioning(false);
setTimeout(() => setTransitionMessage(null), 300);
}, 100);
return () => clearTimeout(timer);
}
}, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]);
return null;
}
export function useTransition() {
const context = useContext(TransitionContext);
if (!context) {

View File

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

View File

@@ -1,4 +1,6 @@
import React, { ReactNode } from 'react';
'use client';
import React, { ReactNode, useState, useEffect, useRef } from 'react';
interface TooltipProps {
children: ReactNode;
@@ -6,10 +8,33 @@ interface TooltipProps {
}
export function Tooltip({ children, content }: TooltipProps) {
const [isActive, setIsActive] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsActive(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, []);
return (
<div className="group relative flex items-center justify-center">
<div
ref={containerRef}
className="group relative flex items-center justify-center cursor-help"
onClick={() => setIsActive(!isActive)}
onMouseEnter={() => setIsActive(true)}
onMouseLeave={() => setIsActive(false)}
>
{children}
<div className="absolute bottom-full mb-3 invisible opacity-0 translate-y-2 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 w-max max-w-[250px] px-4 py-2.5 text-[11px] md:text-xs text-white bg-neutral-900/95 backdrop-blur-md rounded-xl shadow-2xl border border-white/10 transition-all duration-300 pointer-events-none z-50 text-center leading-relaxed font-normal normal-case tracking-normal">
<div className={`absolute bottom-full mb-3 w-max max-w-[250px] px-4 py-2.5 text-[11px] md:text-xs text-white bg-neutral-900/95 backdrop-blur-md rounded-xl shadow-2xl border border-white/10 transition-all duration-300 z-50 text-center leading-relaxed font-normal normal-case tracking-normal ${isActive ? 'visible opacity-100 translate-y-0 pointer-events-auto' : 'invisible opacity-0 translate-y-2 pointer-events-none'}`}>
{content}
{/* Triangle Arrow */}
<div className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-neutral-900/95" />

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
- 500 m Kabelgraben ausheben und verfüllen
- 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160
- 3.200 m Horizontalspülbohrung d 160
- 12.000 m Horizontalspülbohrung d 160
- 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV
- 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV
- 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV

View File

@@ -42,7 +42,7 @@ layout: "fullBleed"
{
id: "martin-polan",
name: "Martin Polan",
position: "Bauleiter",
position: "Bauleitung",
email: "m.polan@e-tib.com",
phone: "+49 171 3868167",
image: "/assets/photos/team/martin.jpg"

View File

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

View File

@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
- Excavation and backfilling of 500 m cable trench
- 4,500 m plowing of 3 x PE protective pipe d 160
- 3,200 m HDD directional drilling d 160
- 12,000 m HDD directional drilling d 160
- 8,500 m laying of HV cable 1,000 mm² 64/110 kV
- 11 sets of connection joints 1,000 mm² 64/110 kV
- 2 sets of outdoor terminations 1,000 mm² 64/110 kV

View File

@@ -42,7 +42,7 @@ layout: "fullBleed"
{
id: "martin-polan",
name: "Martin Polan",
position: "Site Manager",
position: "Site Management",
email: "m.polan@e-tib.com",
phone: "+49 171 3868167",
image: "/assets/photos/team/martin.jpg"

14117
lh-single.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -189,62 +189,6 @@ export const minorLocations: Location[] = [
y: 83.33,
description: 'fiber',
},
{
id: 'hdd-29436-meter-29436',
name: 'HDD 29436 Meter',
type: 'minor_node',
x: 46.89,
y: 41.34,
description: 'power',
},
{
id: 'hdd-28680-meter-28680',
name: 'HDD 28680 Meter',
type: 'minor_node',
x: 44.84,
y: 43.61,
description: 'power',
},
{
id: 'hdd-44655-meter-44655',
name: 'HDD 44655 Meter',
type: 'minor_node',
x: 49.36,
y: 48.57,
description: 'power',
},
{
id: 'kabelverlegung-76727-meter-76727',
name: 'Kabelverlegung 76727 Meter',
type: 'minor_node',
x: 45.57,
y: 49.95,
description: 'power',
},
{
id: 'offener-tiefbau-21288-meter-21288',
name: 'Offener Tiefbau 21288 Meter',
type: 'minor_node',
x: 40.15,
y: 49.41,
description: 'power',
},
{
id: 'offener-tiefbau-30773-meter-30773',
name: 'Offener Tiefbau 30773 Meter',
type: 'minor_node',
x: 41.74,
y: 43.78,
description: 'power',
},
{
id: 'offener-tiefbau-34815-meter-34815',
name: 'Offener Tiefbau 34815 Meter',
type: 'minor_node',
x: 43.91,
y: 42.14,
description: 'power',
},
{
id: 'pv-anlage-01909-grosshartau-01909',
name: 'PV-Anlage, 01909 Großhartau',

View File

@@ -11,10 +11,21 @@ export async function getOgFonts() {
try {
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
const boldFont = readFileSync(boldFontPath);
const regularFont = readFileSync(regularFontPath);
const boldFontBuffer = readFileSync(boldFontPath);
const regularFontBuffer = readFileSync(regularFontPath);
// Satori strictly requires an ArrayBuffer.
const boldFont = boldFontBuffer.buffer.slice(
boldFontBuffer.byteOffset,
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
);
const regularFont = regularFontBuffer.buffer.slice(
regularFontBuffer.byteOffset,
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
);
console.log(
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
);
return [
@@ -37,6 +48,28 @@ export async function getOgFonts() {
}
}
export function getOgBackground() {
try {
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
const bgBase64 = readFileSync(bgPath, 'base64');
return `data:image/jpeg;base64,${bgBase64}`;
} catch (err) {
console.error('[OG] Failed to load background', err);
return undefined;
}
}
export function getOgLogo() {
try {
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
const logoBase64 = readFileSync(logoPath, 'base64');
return `data:image/png;base64,${logoBase64}`;
} catch (err) {
console.error('[OG] Failed to load logo', err);
return undefined;
}
}
/**
* Common configuration for OG images
*/

7
lib/placeholders.json Normal file
View File

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

View File

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

View File

@@ -35,7 +35,7 @@ export const standorteData: StandortData[] = [
{
id: 'guben',
name: 'E-TIB GmbH',
shortName: 'Guben (Hauptsitz)',
shortName: 'Standort Brandenburg (Hauptsitz)',
type: 'hq',
description: {
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
@@ -67,7 +67,7 @@ export const standorteData: StandortData[] = [
'Central project management'
]
},
image: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg',
image: '/assets/photos/DSC02114.jpg',
gallery: [
'/assets/photos/DSC02114.jpg',
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-45.jpg'
@@ -117,7 +117,7 @@ export const standorteData: StandortData[] = [
{
id: 'buelstedt',
name: 'E-TIB Ingenieurgesellschaft mbH',
shortName: 'Bülstedt (Ingenieurgesellschaft)',
shortName: 'Standort Niedersachsen',
type: 'branch',
description: {
de: 'Unsere Ingenieurgesellschaft in Bülstedt ist spezialisiert auf die hochpräzise Planung, Vermessung und Projektierung von Energie- und Telekommunikationsnetzen. Hier verschmelzen technische Expertise und modernes Datenmanagement.',

View File

@@ -15,6 +15,10 @@ const nextConfig = {
pagesBufferLength: 2,
},
experimental: {
staleTimes: {
dynamic: 0,
static: 30,
},
optimizePackageImports: [
'lucide-react',
'framer-motion',
@@ -43,7 +47,16 @@ const nextConfig = {
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || process.env.npm_package_version || 'dev',
},
// Prevent webpack from restarting when .env files are touched via Docker volume mount
webpack: (config, { dev }) => {
webpack: (config, { dev, isServer }) => {
// Completely strip Next.js forced legacy polyfills for modern browser targets
if (!isServer) {
config.resolve.alias = {
...config.resolve.alias,
'next/dist/build/polyfills/polyfill-module': false,
'next/dist/build/polyfills/polyfill-nomodule': false,
};
}
if (dev) {
config.watchOptions = {
...config.watchOptions,
@@ -434,7 +447,7 @@ const nextConfig = {
},
images: {
qualities: [25, 50, 75, 100],
formats: ['image/webp'],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
remotePatterns: [
{

View File

@@ -105,6 +105,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"postinstall": "echo '' > node_modules/next/dist/build/polyfills/polyfill-module.js && echo '' > node_modules/next/dist/build/polyfills/polyfill-nomodule.js",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:og": "vitest run tests/og-image.test.ts",
@@ -138,7 +139,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.13-rc.46",
"version": "2.2.52",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
@@ -153,9 +154,11 @@
}
},
"browserslist": [
"last 3 versions",
"not dead",
"not ie 11"
"last 2 chrome versions",
"last 2 firefox versions",
"last 2 edge versions",
"safari >= 15.4",
"not dead"
],
"peerDependencies": {
"lucide-react": "^0.563.0"

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 360 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 297 KiB

BIN
public/assets/photos/DJI_0037.JPG Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

After

Width:  |  Height:  |  Size: 160 KiB

BIN
public/assets/photos/DJI_0048.JPG Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 MiB

After

Width:  |  Height:  |  Size: 126 KiB

BIN
public/assets/photos/DJI_0243.JPG Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 MiB

After

Width:  |  Height:  |  Size: 161 KiB

BIN
public/assets/photos/DJI_0301.JPG Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 MiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 172 KiB

BIN
public/assets/photos/DSC00373.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/assets/photos/DSC00375.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 112 KiB

BIN
public/assets/photos/DSC00388.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 121 KiB

BIN
public/assets/photos/DSC00400.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 134 KiB

BIN
public/assets/photos/DSC00430.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 198 KiB

BIN
public/assets/photos/DSC00440.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 145 KiB

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