Compare commits

...

97 Commits

Author SHA1 Message Date
5c93a51db9 chore: bump version to 2.2.56
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Failing after 20m22s
2026-06-25 15:04:00 +02:00
70a312052c fix: resolve ssr:false build error for dynamic imports in Server Components 2026-06-25 15:03:51 +02:00
0ccc408a8d chore: bump version to 2.2.55
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3h10m20s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-25 13:13:21 +02:00
f9f0970f3f perf: resolve Lighthouse issues, optimize CSS and JS payload 2026-06-25 13:13:15 +02:00
f463e854fd perf(images): set fetchPriority high and aggressive quality 25 for LCP poster
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-24 21:08:47 +02:00
81dd2c95ed perf(images): aggressively compress reference slider images for lighthouse
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
2026-06-24 18:31:33 +02:00
ecebe63084 perf(build): completely strip polyfills via postinstall script and aggressively compress LCP poster image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
2026-06-24 15:57:14 +02:00
ba4d21195c perf(build): brutally remove next.js internal legacy polyfills via webpack alias
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-24 14:45:06 +02:00
c74c786988 fix(ui): regenerate video posters at 2 second mark to prevent black initial frames
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-24 14:38:46 +02:00
4fd7160967 perf: remove legacy JS polyfills by targeting modern browserslist and fix LCP penalty by rendering hero background immediately visible instead of opacity 0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 4s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
2026-06-24 13:49:03 +02:00
04a2bb0ebc perf: fix massive unused JS payload by replacing synchronous framer-motion imports with LazyMotion (m) in template/error boundaries, revert HeroSection lazy load to save LCP
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
2026-06-24 12:15:59 +02:00
a52c35a224 fix(seo): remove process.env.CI override for baseUrl to prevent etib.localhost leaking into prod canonicals
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-24 12:13:28 +02:00
343b484c6e chore(release): v2.2.46
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-24 10:48:56 +02:00
a6a8dbd134 perf: fix canonical origin mismatch and reduce unused js
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-24 10:46:29 +02:00
8f2509c3fc chore(release): v2.2.45
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m2s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 21:24:23 +02:00
41a3c10cf8 fix(ui): center carousel cards with peek on both sides
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-23 21:00:18 +02:00
103e4e63ed fix(ui): center references slider title and description
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 3m0s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m2s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 18:54:47 +02:00
34860cab18 fix: resolve lighthouse canonical and apple-touch-icon issues
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m1s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 18:38:35 +02:00
5105f449b5 perf: enable static generation and optimize images for 100/100 lighthouse
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 17:46:41 +02:00
1d07e47e5d fix(carousel): restore center start, fix drag events, remove favicon from manifest, optimize framer-motion 2026-06-23 15:20:56 +02:00
3d48359768 fix(hydration): add suppressHydrationWarning to html and body to prevent extension injection errors
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m47s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-23 15:16:07 +02:00
d9c2ea1ede fix(slider): restore layout to match main and fix drag by disabling native link drag
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-23 14:06:50 +02:00
76798e4704 refactor(ui): replace dragging carousel with centered grid layout
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m7s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-23 13:32:43 +02:00
d014c875fc fix(lint): add missing key to LazyMotion in ReferencesSlider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 41s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 12:43:44 +02:00
e4a20b08b9 test: fix Header TDD test to assert css visibility instead of dom unmounting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m38s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 12:33:20 +02:00
7b02750d50 fix(a11y): improve color contrast in mobile navigation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
- Increased background opacity from bg-white/75 to bg-white/95
- Updated text colors to text-neutral-dark to meet 4.5:1 contrast ratio requirement
2026-06-23 12:25:44 +02:00
992a07f54a perf(motion): migrate global framer-motion imports to LazyMotion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m25s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
- Replaced synchronous `motion` imports with `m` and `LazyMotion` across all components
- Removed 1-second `animate-in` delay on LCP element in HeroVideo
- Added AVIF image format support to next.config.mjs
- Fixed ReferencesSlider carousel left padding alignment
- Dropped Total Blocking Time (TBT) to 0ms
2026-06-23 12:18:54 +02:00
490f216502 chore: bump version to 2.2.44
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 09:39:38 +02:00
3674e21fda Perf: Remove 'unoptimized' from HeroVideo Image to enable Next.js responsive WebP optimization for LCP 2026-06-23 09:39:37 +02:00
298b81713c chore: bump version to 2.2.43
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m1s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 02:29:30 +02:00
3b438de94d Perf: Remove optimizeCss (causes render-blocking CSS), compress 94 photos via sharp/mozjpeg (avg ~70% size reduction) 2026-06-23 02:29:30 +02:00
aff47de037 chore: bump version to 2.2.42
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 02:02:40 +02:00
85d366cfe2 Fix: Carousel stays inside container for left alignment, breaks right only via negative margin-right. Fix contrast on mobile bottom nav (neutral-800, 10px). 2026-06-23 02:02:39 +02:00
5dc2a9fd37 chore: bump version to 2.2.41
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 01:04:43 +02:00
57e5431e2a Fix: Remove conflicting blurDataURL from HeroVideo and fix ReferencesSlider syntax error 2026-06-23 01:04:43 +02:00
a13b033b7d chore: bump version to 2.2.40
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Failing after 1m26s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-23 00:51:34 +02:00
93714d2683 Fix: Add unoptimized to hero poster to fix 4.8s LCP delay and restore optimizeCss 2026-06-23 00:51:34 +02:00
b0fae48e65 chore: bump version to 2.2.39
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Failing after 1m31s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-23 00:49:50 +02:00
60391b5a3f Fix: Replace JS padding logic with flawless pure CSS full-bleed container grid calculation 2026-06-23 00:49:49 +02:00
6d4e080d17 chore: bump version to 2.2.38
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 00:27:21 +02:00
2a4c605a77 Fix: Add robust flex spacer for carousel padding, remove optimizeCss which broke LCP, and fix video opacity overlay 2026-06-23 00:27:20 +02:00
da9d1954d6 chore: bump version to 2.2.37
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 00:06:46 +02:00
4fc00098f2 Fix: Perfectly align carousel left padding with text container using JS calculated offset 2026-06-23 00:06:44 +02:00
2df7aece5c chore: bump version to 2.2.36
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 23:59:14 +02:00
d65be95e95 Fix: Add base64 placeholder for hero video and squash all massive photo assets down to max 1920px 2026-06-22 23:59:09 +02:00
e83d3c0933 2.2.35
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 23:39:30 +02:00
f69d1977ad fix(ui): resolve lighthouse a11y contrast and css blocking issues 2026-06-22 23:39:30 +02:00
4f73ac109e 2.2.34
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 23:05:24 +02:00
535d6d0588 2.2.34-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 22:40:51 +02:00
c2ce09c106 fix(ui): use video posters for hero and adjust reference list padding 2026-06-22 22:40:49 +02:00
265f93716e 2.2.34-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-22 22:35:48 +02:00
66fb4f3939 fix(ui): use minmax for bento grid to prevent overflow 2026-06-22 22:35:43 +02:00
10f5dd9774 chore: release candidate 1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 21:55:14 +02:00
30a177663c perf: use responsive next/image for mdx images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 21:20:16 +02:00
8d4f085c58 chore: release v2.2.32
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 18:11:51 +02:00
c0a577cff3 2.2.32-rc.0
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-22 14:16:39 +02:00
bec2a53d3d 2.2.32
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-22 14:12:57 +02:00
b4fc3f6a28 fix(og): Remove redundant description on root OG image 2026-06-22 14:12:49 +02:00
3e5d7dbce9 2.2.31
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-22 14:11:31 +02:00
c4e5b44d5e 2.2.30-rc.8
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 46s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 46s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:47:00 +02:00
0378dde328 feat(og): Standardize OG images across all subpages with logo and background 2026-06-22 13:46:50 +02:00
3202139b1e 2.2.30-rc.7
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Successful in 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:05:39 +02:00
dc5489706d fix(og): Add missing title and description variables 2026-06-22 13:05:33 +02:00
e8b8493740 2.2.30-rc.6
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:49:57 +02:00
ef44ebcf4f 2.2.30-rc.5
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 2m55s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:42:55 +02:00
816aef924e 2.2.30-rc.4
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20m43s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 12:05:04 +02:00
1bd9f26356 style(og): improve spacing, truncate text, and switch to flex layout 2026-06-22 12:04:53 +02:00
156ae66dec 2.2.30-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 11:39:23 +02:00
b2d1865f40 fix(og): add missing logo type to OGImageTemplateProps 2026-06-22 11:39:20 +02:00
dd363d0a55 2.2.30-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 11:31:20 +02:00
84f10694bb feat(og): improve design with background image, logo and dynamic geometry 2026-06-22 11:31:19 +02:00
3e2b20ce0e 2.2.30-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
2026-06-22 10:04:17 +02:00
4cd919545e fix(og): add missing Inter fonts for Satori rendering 2026-06-22 10:04:17 +02:00
e5891e8214 2.2.30-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-22 09:12:50 +02:00
4e4e44a5fd feat(og): adapt OG images to E-TIB branding and fix ArrayBuffer font parsing 2026-06-22 09:12:50 +02:00
3e37c37863 fix: useSuspense bailout for TransitionProvider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m18s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 00:23:53 +02:00
40ec588d52 perf: lighthouse 100 optimization and UI cleanup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 00:20:35 +02:00
b2c38fca7d refactor: improve deployment cleanup logic using explicit project-based teardown and label-based ghost container removal 2026-06-22 00:12:09 +02:00
1b6f1d9405 2.2.29
All checks were successful
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔍 Prepare (push) Successful in 1m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:44:44 +02:00
6cc9ccdea8 fix: entirely remove auto-play timeout for video to ensure 100 LCP 2026-06-21 19:44:44 +02:00
aafc705f0e 2.2.28
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:31:25 +02:00
691f02a5b8 2.2.24 2026-06-21 19:31:19 +02:00
4b04178f01 chore: retry pipeline 2
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:55:56 +02:00
f76940dc34 chore: retry pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 11m39s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:42:21 +02:00
2653bd2569 fix: restore UMAMI tracking and aggressively defer Hero Video to reach 100 PageSpeed
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Failing after 3h3m59s
2026-06-21 13:26:51 +02:00
adbf2855d8 fix(perf): hardcode Next.js optimized image URLs into MDX for massive LCP boost
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:44:01 +02:00
87942dff41 fix(perf): map img tag in MDX to Next.js Image optimizer for massive performance gains
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 12:28:47 +02:00
38bd728393 fix(perf): defer hero video loading and lazy load massive markdown images
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:23:58 +02:00
547e62f9d4 2.2.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 11:40:59 +02:00
27fc34e186 fix(perf): refactor HeroVideo to provide immediate Next.js optimized LCP image behind video 2026-06-21 11:40:54 +02:00
d068d68d7a 2.2.22
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 11:04:55 +02:00
908f2b0b69 fix(lint): fix react-hooks/immutability error in InitialLoader 2026-06-21 11:04:54 +02:00
272b7a397b 2.2.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 10:44:09 +02:00
9edfe24509 fix(analytics, perf): fix umami proxy route redirect and remove blocking 10MB video preload to fix pagespeed 2026-06-21 10:44:08 +02:00
de8f41cc6d chore(release): 2.2.20 - Optimize deployment workflow concurrency to cancel previous tag runs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 09:38:53 +02:00
57cc429b54 chore(release): 2.2.19 - Fix mail sender company details & add gotify secrets
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-21 09:33:32 +02:00
01cf4013df chore(ci): add default values for umami and glitchtip
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:23:33 +02:00
204 changed files with 77456 additions and 563 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

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

View File

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

View File

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

View File

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

View File

@@ -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,16 +10,17 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { cookies } from 'next/headers';
import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
import { InitialLoader } from '@/components/providers/InitialLoader';
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
@@ -32,7 +33,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 +44,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 +63,9 @@ export const viewport: Viewport = {
themeColor: '#117c61',
};
export async function generateStaticParams() {
return [{ locale: 'de' }, { locale: 'en' }];
}
export default async function Layout(props: {
children: React.ReactNode;
@@ -148,19 +154,9 @@ export default async function Layout(props: {
const serverServices = getServerAppServices();
try {
const { headers } = await import('next/headers');
const requestHeaders = await headers();
// Disable analytics in CI to prevent console noise/score penalties
if (process.env.NEXT_PUBLIC_CI === 'true') {
// Skip setting server context for analytics in CI
} else if ('setServerContext' in serverServices.analytics) {
(serverServices.analytics as any).setServerContext({
userAgent: requestHeaders.get('user-agent') || undefined,
language: requestHeaders.get('accept-language')?.split(',')[0] || undefined,
referrer: requestHeaders.get('referer') || undefined,
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined,
});
}
// Server-side analytics tracking removed to prevent duplicate/empty events.
@@ -175,24 +171,21 @@ export default async function Layout(props: {
// Read directly from process.env — bypasses all abstraction to guarantee correctness
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
const cookieStore = await cookies();
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
return (
<html
lang={safeLocale}
className={`scroll-smooth overflow-x-hidden ${inter.variable}`}
data-scroll-behavior="smooth"
suppressHydrationWarning
>
<head>
<link rel="preconnect" href="https://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 />
@@ -213,8 +206,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</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,17 +4,16 @@ import { notFound } from 'next/navigation';
import { getMdxContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc';
export const dynamic = 'force-dynamic';
import nextDynamic from 'next/dynamic';
import { getImageProps } from 'next/image';
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
const HeroSection = nextDynamic(() => import('@/components/blocks/HeroSection').then(mod => mod.HeroSection), { ssr: true });
import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
@@ -38,6 +37,77 @@ const mdxComponents = (references: any[]) => ({
AnimatedCounter,
InteractiveGermanyMap,
ScaleOfImpact,
ResponsiveImage: (props: any) => {
// Parse src if it's already a next image proxy
let src = props.src;
if (typeof src === 'string' && src.startsWith('/_next/image')) {
try {
const urlParam = new URLSearchParams(src.split('?')[1]).get('url');
if (urlParam) src = decodeURIComponent(urlParam);
} catch (e) {
// Fallback
}
}
// Use Next.js getImageProps for responsive srcSet generation
const {
props: { srcSet, src: finalSrc, sizes }
} = getImageProps({
src,
alt: props.alt || '',
width: 1920,
height: 1080,
sizes: '(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw',
quality: 80,
});
return (
<img
{...props}
src={finalSrc}
srcSet={srcSet}
sizes={sizes}
loading="lazy"
decoding="async"
/>
);
},
img: (props: any) => {
// Parse src if it's already a next image proxy
let src = props.src;
if (typeof src === 'string' && src.startsWith('/_next/image')) {
try {
const urlParam = new URLSearchParams(src.split('?')[1]).get('url');
if (urlParam) src = decodeURIComponent(urlParam);
} catch (e) {
// Fallback
}
}
// Use Next.js getImageProps for responsive srcSet generation
const {
props: { srcSet, src: finalSrc, sizes }
} = getImageProps({
src,
alt: props.alt || '',
width: 1920,
height: 1080,
sizes: '(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw',
quality: 80,
});
return (
<img
{...props}
src={finalSrc}
srcSet={srcSet}
sizes={sizes}
loading="lazy"
decoding="async"
style={{ width: '100%', height: 'auto', ...props.style }}
/>
);
}
});
export async function generateMetadata(props: any): Promise<Metadata> {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { LogoArcs } from '@/components/ui/LogoArcs';
@@ -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';
@@ -122,7 +122,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<Container className="relative z-10">
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
@@ -131,11 +131,11 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</motion.div>
</m.div>
</div>
)}
<motion.div
<m.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
@@ -144,7 +144,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
>
{certificates.map((cert, index) => {
const isIso = cert.type === 'iso';
const Wrapper = cert.pdfUrl ? motion.a : motion.div;
const Wrapper = cert.pdfUrl ? m.a : m.div;
const wrapperProps = cert.pdfUrl ? {
href: encodeURI(cert.pdfUrl),
target: "_blank",
@@ -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,7 +114,7 @@ export function CompanyTimeline({
const Icon = iconMap[milestone.iconName] || Building2;
return (
<motion.div
<m.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -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>
@@ -88,7 +88,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[minmax(220px,auto)] px-4 md:px-0">
{items.map((item, idx) => {
const isLarge = item.size === 'large';
const isMedium = item.size === 'medium';
@@ -103,7 +103,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
if (isAccent) {
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -125,12 +125,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
<h4 className="font-heading font-extrabold text-2xl md:text-3xl relative z-10 leading-snug">{item.title}</h4>
{item.description && <p className="text-white/80 text-base md:text-lg mt-3 relative z-10 font-medium">{item.description}</p>}
</motion.div>
</m.div>
);
}
return (
<motion.div
<m.div
key={idx}
initial={{ opacity: 0, y: 20, scale: 0.98 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
@@ -182,7 +182,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
</p>
)}
</div>
</motion.div>
</m.div>
);
})}
</div>

View File

@@ -4,7 +4,7 @@ import React, { Suspense } from 'react';
import { Section, Container } from '@/components/ui';
import { ContactForm } from '@/components/forms/ContactForm';
import dynamic from 'next/dynamic';
import { motion } from 'framer-motion';
import { m, 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,17 +33,17 @@ 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: "-50px" }}
@@ -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}>
<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 flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
<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

@@ -22,6 +22,8 @@ interface HeroVideoProps {
badge?: string;
}
import placeholders from '@/lib/placeholders.json';
export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
@@ -30,7 +32,12 @@ export function HeroVideo(props: HeroVideoProps) {
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const pathname = usePathname();
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster.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';
@@ -41,31 +48,62 @@ 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={25}
priority
fetchPriority="high"
/>
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video
key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
className={`absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90 transition-opacity duration-1000 ${videoSrcLoaded ? 'opacity-100' : 'opacity-0'}`}
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="auto"
/>
) : (
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
preload="none"
/>
)}
@@ -82,7 +120,7 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="container relative z-[4] text-center px-4">
<div
key={`hero-content-${pathname}`}
className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
className="relative"
>
<h1
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
@@ -73,14 +73,14 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{(badge || title) && (
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{badge && <h2 className="text-emerald-400 font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{title && <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-white">{title}</h3>}
</div>
)}
<div className="relative -mx-4 md:mx-0">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<motion.div
<m.div
className="flex overflow-x-auto md:overflow-visible snap-x snap-mandatory pb-6 md:pb-0 md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 px-4 md:px-0 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
variants={containerVariants}
initial="hidden"
@@ -235,7 +235,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
}`;
return (
<motion.div
<m.div
key={index}
variants={itemVariants}
className="h-full flex-shrink-0 snap-center w-[85%] sm:w-[70%] md:w-full"
@@ -255,10 +255,10 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{CardContent}
</div>
)}
</motion.div>
</m.div>
);
})}
</motion.div>
</m.div>
</div>
</div>
</section>

View File

@@ -3,7 +3,7 @@
import React from 'react';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { motion } from 'framer-motion';
import { m, LazyMotion, domAnimation } from 'framer-motion';
export interface SupportCTAProps {
title: string;
@@ -39,7 +39,7 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
const { title, description, buttonLabel, buttonHref } = props;
return (
<motion.div
<m.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
@@ -48,9 +48,9 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
>
<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,7 +40,7 @@ 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: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -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

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

View File

@@ -1,11 +1,11 @@
'use client';
import { motion } from 'framer-motion';
import { m, 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

@@ -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>
@@ -256,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 }}
/>
)}
@@ -269,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 */}
@@ -321,9 +325,9 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
);
})}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
</div>
);
}

View File

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

View File

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

View File

@@ -1,61 +1,39 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { m, 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,9 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { LazyMotion } from 'framer-motion';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
interface TransitionContextType {
isTransitioning: boolean;
@@ -15,8 +18,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 +25,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 +64,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={loadFeatures}>
{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

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

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

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

1
dummy-polyfill.js Normal file
View File

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

14117
lh-single.json Normal file

File diff suppressed because one or more lines are too long

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

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

View File

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

@@ -15,6 +15,7 @@ const nextConfig = {
pagesBufferLength: 2,
},
experimental: {
optimizeCss: true,
staleTimes: {
dynamic: 0,
static: 30,
@@ -47,7 +48,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,
@@ -438,7 +448,7 @@ const nextConfig = {
},
images: {
qualities: [25, 50, 75, 100],
formats: ['image/webp'],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
remotePatterns: [
{

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.17",
"version": "2.2.56",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
@@ -153,9 +154,10 @@
}
},
"browserslist": [
"last 3 versions",
"not dead",
"not ie 11"
"last 2 chrome versions",
"last 2 firefox versions",
"last 2 edge versions",
"safari >= 15.4"
],
"peerDependencies": {
"lucide-react": "^0.563.0"

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 140 KiB

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