Compare commits

...

89 Commits

Author SHA1 Message Date
da9d1954d6 chore: bump version to 2.2.37
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m12s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-23 00:06:46 +02:00
4fc00098f2 Fix: Perfectly align carousel left padding with text container using JS calculated offset 2026-06-23 00:06:44 +02:00
2df7aece5c chore: bump version to 2.2.36
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 23:59:14 +02:00
d65be95e95 Fix: Add base64 placeholder for hero video and squash all massive photo assets down to max 1920px 2026-06-22 23:59:09 +02:00
e83d3c0933 2.2.35
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 23:39:30 +02:00
f69d1977ad fix(ui): resolve lighthouse a11y contrast and css blocking issues 2026-06-22 23:39:30 +02:00
4f73ac109e 2.2.34
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 23:05:24 +02:00
535d6d0588 2.2.34-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 22:40:51 +02:00
c2ce09c106 fix(ui): use video posters for hero and adjust reference list padding 2026-06-22 22:40:49 +02:00
265f93716e 2.2.34-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-22 22:35:48 +02:00
66fb4f3939 fix(ui): use minmax for bento grid to prevent overflow 2026-06-22 22:35:43 +02:00
10f5dd9774 chore: release candidate 1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 21:55:14 +02:00
30a177663c perf: use responsive next/image for mdx images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 21:20:16 +02:00
8d4f085c58 chore: release v2.2.32
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m4s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 18:11:51 +02:00
c0a577cff3 2.2.32-rc.0
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m17s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-22 14:16:39 +02:00
bec2a53d3d 2.2.32
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-22 14:12:57 +02:00
b4fc3f6a28 fix(og): Remove redundant description on root OG image 2026-06-22 14:12:49 +02:00
3e5d7dbce9 2.2.31
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-22 14:11:31 +02:00
c4e5b44d5e 2.2.30-rc.8
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 46s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 46s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:47:00 +02:00
0378dde328 feat(og): Standardize OG images across all subpages with logo and background 2026-06-22 13:46:50 +02:00
3202139b1e 2.2.30-rc.7
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m12s
Build & Deploy / 🚀 Deploy (push) Successful in 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 13:05:39 +02:00
dc5489706d fix(og): Add missing title and description variables 2026-06-22 13:05:33 +02:00
e8b8493740 2.2.30-rc.6
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:49:57 +02:00
ef44ebcf4f 2.2.30-rc.5
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 2m55s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 12:42:55 +02:00
816aef924e 2.2.30-rc.4
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20m43s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 12:05:04 +02:00
1bd9f26356 style(og): improve spacing, truncate text, and switch to flex layout 2026-06-22 12:04:53 +02:00
156ae66dec 2.2.30-rc.3
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-22 11:39:23 +02:00
b2d1865f40 fix(og): add missing logo type to OGImageTemplateProps 2026-06-22 11:39:20 +02:00
dd363d0a55 2.2.30-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 11:31:20 +02:00
84f10694bb feat(og): improve design with background image, logo and dynamic geometry 2026-06-22 11:31:19 +02:00
3e2b20ce0e 2.2.30-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
2026-06-22 10:04:17 +02:00
4cd919545e fix(og): add missing Inter fonts for Satori rendering 2026-06-22 10:04:17 +02:00
e5891e8214 2.2.30-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-22 09:12:50 +02:00
4e4e44a5fd feat(og): adapt OG images to E-TIB branding and fix ArrayBuffer font parsing 2026-06-22 09:12:50 +02:00
3e37c37863 fix: useSuspense bailout for TransitionProvider
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m43s
Build & Deploy / 🏗️ Build (push) Successful in 3m18s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-22 00:23:53 +02:00
40ec588d52 perf: lighthouse 100 optimization and UI cleanup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-22 00:20:35 +02:00
b2c38fca7d refactor: improve deployment cleanup logic using explicit project-based teardown and label-based ghost container removal 2026-06-22 00:12:09 +02:00
1b6f1d9405 2.2.29
All checks were successful
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔍 Prepare (push) Successful in 1m30s
Build & Deploy / 🧪 QA (push) Successful in 1m45s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:44:44 +02:00
6cc9ccdea8 fix: entirely remove auto-play timeout for video to ensure 100 LCP 2026-06-21 19:44:44 +02:00
aafc705f0e 2.2.28
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 19:31:25 +02:00
691f02a5b8 2.2.24 2026-06-21 19:31:19 +02:00
4b04178f01 chore: retry pipeline 2
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m20s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:55:56 +02:00
f76940dc34 chore: retry pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 11m39s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 17:42:21 +02:00
2653bd2569 fix: restore UMAMI tracking and aggressively defer Hero Video to reach 100 PageSpeed
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Failing after 3h3m59s
2026-06-21 13:26:51 +02:00
adbf2855d8 fix(perf): hardcode Next.js optimized image URLs into MDX for massive LCP boost
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m16s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:44:01 +02:00
87942dff41 fix(perf): map img tag in MDX to Next.js Image optimizer for massive performance gains
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 12:28:47 +02:00
38bd728393 fix(perf): defer hero video loading and lazy load massive markdown images
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:23:58 +02:00
547e62f9d4 2.2.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 11:40:59 +02:00
27fc34e186 fix(perf): refactor HeroVideo to provide immediate Next.js optimized LCP image behind video 2026-06-21 11:40:54 +02:00
d068d68d7a 2.2.22
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 11:04:55 +02:00
908f2b0b69 fix(lint): fix react-hooks/immutability error in InitialLoader 2026-06-21 11:04:54 +02:00
272b7a397b 2.2.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 10:44:09 +02:00
9edfe24509 fix(analytics, perf): fix umami proxy route redirect and remove blocking 10MB video preload to fix pagespeed 2026-06-21 10:44:08 +02:00
de8f41cc6d chore(release): 2.2.20 - Optimize deployment workflow concurrency to cancel previous tag runs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 09:38:53 +02:00
57cc429b54 chore(release): 2.2.19 - Fix mail sender company details & add gotify secrets
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-21 09:33:32 +02:00
01cf4013df chore(ci): add default values for umami and glitchtip
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:23:33 +02:00
caf3fe8c1e chore(release): 2.2.17 - Performance and LCP optimization
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m32s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:17:11 +02:00
51eac9fb50 perf(LCP): add fetchPriority=high to Header logo 2026-06-21 09:17:10 +02:00
6426512192 UI: simplify reveal animations to prevent flickering and late triggers
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m38s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 3m31s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-21 09:08:44 +02:00
a63ee1b9f5 chore: bump version to 2.2.16
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 22:03:05 +02:00
43667e7d68 2.2.15 2026-06-20 22:02:43 +02:00
0191fdb770 fix: resolve reveal flickering, adjust grid header UI, update Guben image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:49:59 +02:00
afaa5e038c fix: resolve TS dynamic collision and remove anomalous map entries
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:35:14 +02:00
e0d6ea0d6c perf: implement KLZ blueprint optimizations (LCP, Payload, Caching)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 20:32:08 +02:00
34839ad5a5 fix: update team titles and location names
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 20:23:15 +02:00
a8d6a45aad chore: bump version to trigger SSL retry
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 19:17:04 +02:00
6de8ad8516 fix(content): correct drilling distance for Jevenstedt reference
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 17:06:20 +02:00
023fef5343 chore: trigger pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m19s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 10:24:35 +02:00
7181dafca2 fix(ui): enforce scroll lock on documentElement when flyout is open
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 20m1s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 09:32:05 +02:00
8b6d2da821 feat(header): add mail and map action buttons for mobile
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-20 09:31:48 +02:00
17736860d6 feat(header): replace mobile language switcher with phone button
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:18:59 +02:00
d67acdbcc1 fix(referenzen): link slider to overview anchors and add section ids
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 00:18:12 +02:00
05b517731b feat(ui): refine mobile bottom nav, add drag-to-close flyout, custom icons
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:09:11 +02:00
67696b6afa chore: release v2.2.13-rc.58
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:46:49 +02:00
24472990e5 2.2.13-rc.47 2026-06-19 23:46:36 +02:00
53a885acdb fix(ui): resolve unclosed Link tag in ReferencesSlider and MobileBottomNav type errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-19 23:42:35 +02:00
9afff09ed7 feat(ui): add buttery smooth scroll reveal animations and disable annotator
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m16s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 23:29:51 +02:00
ce5acc5558 refactor(footer): merge navigation and about us links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:26:54 +02:00
5027bea666 fix(ui): resolve annotator overlap, slider images, subcompany rounding and copy updates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 23:25:29 +02:00
fcfb567991 fix: remove pb-[90px] from main container which caused white gap above footer on mobile
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:57:52 +02:00
efb9bdbb3f feat: modernize mobile bottom nav with 2030 design and fix z-index
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:39:13 +02:00
11e7937e79 fix: remaining mobile paddings for CTA boxes
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:36:30 +02:00
41158697fd fix: mobile layout and visual polishing based on review
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:34:53 +02:00
d00a4e29ff Merge branch 'feature/annotator-integration' into main
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:03:13 +02:00
58be922fb3 fix(ui): comprehensive mobile optimizations across homepage and blocks
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
- fix: bottom nav and footer padding collision
- fix: reveal animation flickering (Certificates, SubCompanyTiles)
- fix: tooltip flyout overflow on mobile in Germany map
- fix: flex direction for 'Projekt anfragen' and Service Detail icons
- fix: text alignment and spacing issues in ReferencesSlider
- fix: reduce excessive whitespace above footer
2026-06-19 21:39:55 +02:00
111672a614 feat(referenzen): inject MDX project details into overview cards
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 21:27:35 +02:00
d1387d2685 fix(referenzen): assign unique, matching images to all reference projects 2026-06-19 21:21:50 +02:00
fda2944e5a content: restore correct image for Kabelleitungsnetzbau
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Restored DSC01123.JPG (Kabelpflug) for the 'Kabelleitungsnetzbau' tiles on the Home and Kompetenzen pages. Also fixed the English alt text to 'Cable Network Construction'.
2026-06-19 20:13:01 +02:00
8ae5ab73e3 ci: fix pipeline concurrency and target evaluation bugs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
1. Added native Gitea/GitHub Actions concurrency group to automatically cancel outdated/stale deploy runs for the same branch (fixes 'alte deploys durchjagen'). 2. Replaced the buggy 'github.ref_type' condition with explicit 'github.ref' matching. Gitea often failed to populate ref_type, causing staging tags to be downgraded to 'branch' target, making them equal priority to feature branches where the newer run would win (fixes 'feature branches wichtiger als staging').
2026-06-19 19:11:43 +02:00
1891 changed files with 378572 additions and 603 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. # 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 # 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 NEXT_PUBLIC_BASE_URL=https://e-tib.com
# Analytics (Umami) # Analytics (Umami)
UMAMI_WEBSITE_ID= UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking (GlitchTip/Sentry) # Error Tracking (GlitchTip/Sentry)
@@ -23,7 +23,7 @@ MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587 MAIL_PORT=587
MAIL_USERNAME= MAIL_USERNAME=
MAIL_PASSWORD= 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 MAIL_RECIPIENTS=info@e-tib.com
# Varnish Cache Size (optional) # Varnish Cache Size (optional)

View File

@@ -13,6 +13,10 @@ on:
required: false required: false
default: 'false' default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/v') && 'production' || github.ref }}
cancel-in-progress: true
env: env:
PUPPETEER_SKIP_DOWNLOAD: "true" PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com" COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
@@ -89,12 +93,12 @@ jobs:
DOMAIN="e-tib.com" DOMAIN="e-tib.com"
PRJ="etib" PRJ="etib"
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
TARGET="testing" TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}" IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing" ENV_FILE=".env.testing"
TRAEFIK_HOST="test.${DOMAIN}" TRAEFIK_HOST="test.${DOMAIN}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production" TARGET="production"
IMAGE_TAG="$REF" IMAGE_TAG="$REF"
@@ -231,7 +235,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }} NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }} 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' }} UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN: ${{ steps.auth.outputs.working_token }} NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }} NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
@@ -278,14 +282,18 @@ jobs:
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }} MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }}
# Monitoring # 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
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
# Analytics # 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' }} 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -346,6 +354,10 @@ jobs:
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID" echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT" echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo "" echo ""
echo "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
echo ""
echo "TARGET=$TARGET" echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET" echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME" echo "PROJECT_NAME=$PROJECT_NAME"
@@ -388,10 +400,19 @@ jobs:
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml 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 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 && 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 '${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' up -d --remove-orphans"
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'" 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 { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages'; import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate'; 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 size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -20,12 +20,16 @@ export default async function Image({
} }
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
title={pageData.frontmatter.title} title="Die Experten für Kabelnetzbau"
description={pageData.frontmatter.excerpt} description={`${pageData.frontmatter.title}`}
label="Information" label="Information"
image={bg}
logo={logo}
/>, />,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,

View File

@@ -229,7 +229,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
{/* Support Section */} {/* Support Section */}
<Container> <Container>
<div className="mt-24 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom"> <div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" /> <div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl"> <div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3> <h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>

View File

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

View File

@@ -1,7 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; 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 size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,9 +11,17 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' }); const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const bg = getOgBackground();
const logo = getOgLogo();
return new ImageResponse( 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, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

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

View File

@@ -57,7 +57,7 @@ export async function generateStaticParams() {
} }
export default async function ContactPage({ params }: ContactPageProps) { export default async function ContactPage({ params }: ContactPageProps) {
console.log('--- RENDERING CONTACT PAGE FOR LOCALE ---');
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Contact' }); const t = await getTranslations({ locale, namespace: 'Contact' });
@@ -150,7 +150,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
<Container> <Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
{/* Contact Info */} {/* Contact Info */}
<div className="lg:col-span-5 space-y-6 md:space-y-12"> <div className="lg:col-span-5 order-2 lg:order-1 space-y-6 md:space-y-12">
<div className="animate-fade-in"> <div className="animate-fade-in">
<Heading level={3} subtitle={t('info.subtitle')} className="mb-6 md:mb-8"> <Heading level={3} subtitle={t('info.subtitle')} className="mb-6 md:mb-8">
{t('info.howToReachUs')} {t('info.howToReachUs')}
@@ -249,7 +249,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
</div> </div>
{/* Contact Form */} {/* Contact Form */}
<div className="lg:col-span-7"> <div className="lg:col-span-7 order-1 lg:order-2">
<Suspense <Suspense
fallback={ fallback={
<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div> <div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>

View File

@@ -10,7 +10,7 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css'; import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper'; import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs'; import { mapFileSlugToTranslated } from '@/lib/slugs';
@@ -175,9 +175,7 @@ export default async function Layout(props: {
// Read directly from process.env — bypasses all abstraction to guarantee correctness // Read directly from process.env — bypasses all abstraction to guarantee correctness
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true'; const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
const cookieStore = await cookies();
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
return ( return (
<html <html
@@ -186,13 +184,9 @@ export default async function Layout(props: {
data-scroll-behavior="smooth" data-scroll-behavior="smooth"
> >
<head> <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="preconnect" href="https://img.infra.mintel.me" />
<link rel="icon" href="/favicon.ico" sizes="any" /> <link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" /> <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> </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">
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}> <NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
@@ -206,7 +200,7 @@ export default async function Layout(props: {
<main <main
id="main-content" id="main-content"
className="flex-grow animate-fade-in overflow-visible pb-[90px] md:pb-0" className="flex-grow animate-fade-in overflow-visible"
tabIndex={-1} tabIndex={-1}
> >
{children} {children}
@@ -215,7 +209,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} /> <Footer companyInfo={companyInfo} />
<JsonLd /> <JsonLd />
<AnalyticsShell /> <AnalyticsShell />
{process.env.TARGET !== 'production' && <AnnotatorClientWrapper />}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />} {feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider> </TransitionProvider>
</NextIntlClientProvider> </NextIntlClientProvider>

View File

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

View File

@@ -4,23 +4,23 @@ import { notFound } from 'next/navigation';
import { getMdxContent } from '@/lib/mdx'; import { getMdxContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc'; import { MDXRemote } from 'next-mdx-remote/rsc';
export const dynamic = 'force-dynamic'; import nextDynamic from 'next/dynamic';
import { getImageProps } from 'next/image';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo'; import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles'; const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid'; const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider'; const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
import { FaqBlock } from '@/components/blocks/FaqBlock'; const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock'; const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
import { HeroSection } from '@/components/blocks/HeroSection'; import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd'; import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading'; import { Heading } from '@/components/ui/Heading';
import { AnimatedCounter } from '@/components/ui'; const AnimatedCounter = nextDynamic(() => import('@/components/ui').then(mod => mod.AnimatedCounter));
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap'; const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact'; const ScaleOfImpact = nextDynamic(() => import('@/components/blocks/ScaleOfImpact').then(mod => mod.ScaleOfImpact));
import { getAllReferences } from '@/lib/references'; import { getAllReferences } from '@/lib/references';
const mdxComponents = (references: any[]) => ({ const mdxComponents = (references: any[]) => ({
@@ -37,6 +37,42 @@ const mdxComponents = (references: any[]) => ({
AnimatedCounter, AnimatedCounter,
InteractiveGermanyMap, InteractiveGermanyMap,
ScaleOfImpact, ScaleOfImpact,
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> { export async function generateMetadata(props: any): Promise<Metadata> {

View File

@@ -3,13 +3,43 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { getAllReferences } from '@/lib/references'; import { getAllReferences } from '@/lib/references';
import TrackedLink from '@/components/analytics/TrackedLink'; import TrackedLink from '@/components/analytics/TrackedLink';
import { MapPin, Calendar, Briefcase } from 'lucide-react'; import { MapPin, Calendar, Briefcase, Zap, Activity, ShieldCheck, Wrench, CheckCircle2 } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button'; import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
import Image from 'next/image'; import Image from 'next/image';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap'; import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { HeroSection } from '@/components/blocks/HeroSection'; import { HeroSection } from '@/components/blocks/HeroSection';
import { defaultLocations, minorLocations } from '@/lib/map-data'; import { defaultLocations, minorLocations } from '@/lib/map-data';
import { MDXRemote } from 'next-mdx-remote/rsc';
const CustomLi = ({ children, ...props }: any) => {
let text = '';
if (typeof children === 'string') text = children;
else if (Array.isArray(children)) text = children.map((c: any) => typeof c === 'string' ? c : '').join(' ');
else if (children?.props?.children) text = children.props.children;
const lowerText = String(text).toLowerCase();
let Icon = CheckCircle2;
if (lowerText.includes('kabel')) Icon = Zap;
else if (lowerText.includes('bohrung')) Icon = Activity;
else if (lowerText.includes('rohr')) Icon = ShieldCheck;
else if (lowerText.includes('montage')) Icon = Wrench;
else if (lowerText.includes('vlf') || lowerText.includes('otdr')) Icon = Zap;
return (
<li className="flex items-start gap-3 pl-4 pr-6 py-3.5 bg-neutral-50/50 border border-neutral-100 rounded-xl" {...props}>
<Icon className="w-5 h-5 text-primary shrink-0 mt-0.5" />
<span className="text-sm font-medium text-neutral-700 leading-snug [&>p]:mb-0">{children}</span>
</li>
);
};
const mdxComponents = {
ul: (props: any) => <ul className="grid grid-cols-1 gap-2 mb-6" {...props} />,
li: CustomLi,
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
};
interface PageProps { interface PageProps {
params: Promise<{ params: Promise<{
@@ -72,7 +102,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
}); });
return ( return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24"> <div className="flex flex-col min-h-screen bg-neutral-50 pb-8 md:pb-24">
{/* Map Section */} {/* Map Section */}
<InteractiveGermanyMap <InteractiveGermanyMap
isHero={true} isHero={true}
@@ -92,7 +122,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{references.map((ref) => ( {references.map((ref) => (
<div <div
key={ref.slug} key={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block" id={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block scroll-mt-32"
> >
<div className="flex flex-col h-full relative"> <div className="flex flex-col h-full relative">
{/* Image Section */} {/* Image Section */}
@@ -126,6 +157,13 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{ref.frontmatter.title} {ref.frontmatter.title}
</h3> </h3>
{/* Project Details / Content */}
{ref.content && (
<div className="flex-grow mb-6">
<MDXRemote source={ref.content} components={mdxComponents} />
</div>
)}
{/* Technical Meta Data */} {/* Technical Meta Data */}
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10"> <div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
<div className="min-w-0"> <div className="min-w-0">
@@ -150,7 +188,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</div> </div>
{/* Support Section */} {/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"> <div className="mt-8 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" /> <div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl"> <div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3> <h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>

View File

@@ -158,7 +158,7 @@ export default async function StandortDetail(props: { params: Promise<{ locale:
{/* Support Section */} {/* Support Section */}
<Container> <Container>
<div className="mt-12 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom"> <div className="mt-8 mb-8 md:mt-24 md:mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" /> <div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl"> <div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3> <h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>

View File

@@ -114,7 +114,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
</div> </div>
{/* Support Section */} {/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"> <div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" /> <div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl"> <div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3> <h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>

View File

@@ -40,7 +40,10 @@ export async function POST(request: NextRequest) {
website: websiteId, 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) // Log the event (internal only)
logger.debug('Forwarding analytics event', { logger.debug('Forwarding analytics event', {

View File

@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
description?: string; description?: string;
label?: string; label?: string;
image?: string; image?: string;
logo?: string;
mode?: 'dark' | 'light' | 'image'; mode?: 'dark' | 'light' | 'image';
} }
@@ -13,11 +14,11 @@ export function OGImageTemplate({
description, description,
label, label,
image, image,
logo,
mode = 'dark', mode = 'dark',
}: OGImageTemplateProps) { }: OGImageTemplateProps) {
const primaryBlue = '#001a4d'; const backgroundDark = '#1a1a1a';
const accentGreen = '#10a379'; const primaryGreen = '#0e7a5c';
const saturatedBlue = '#011dff';
const containerStyle: React.CSSProperties = { const containerStyle: React.CSSProperties = {
height: '100%', height: '100%',
@@ -26,7 +27,7 @@ export function OGImageTemplate({
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue, backgroundColor: mode === 'light' ? '#ffffff' : backgroundDark,
padding: '80px', padding: '80px',
position: 'relative', position: 'relative',
fontFamily: 'Inter', fontFamily: 'Inter',
@@ -65,7 +66,7 @@ export function OGImageTemplate({
left: 0, left: 0,
right: 0, right: 0,
bottom: 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> </div>
@@ -75,101 +76,127 @@ export function OGImageTemplate({
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '-150px', top: '-300px',
right: '-150px', right: '-200px',
width: '600px', width: '800px',
height: '600px', height: '1200px',
borderRadius: '300px', backgroundColor: `${primaryGreen}1A`,
backgroundColor: `${accentGreen}15`, transform: 'rotate(25deg)',
display: 'flex', display: 'flex',
}} }}
/> />
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}> {/* Main Content Wrapper */}
{/* 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 */}
<div <div
style={{ style={{
position: 'absolute',
bottom: '80px',
left: '80px',
display: 'flex', 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 <div
style={{ 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', 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>
</div> </div>
{/* Saturated Blue Brand Strip */} {/* Primary Green Brand Strip */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -177,7 +204,7 @@ export function OGImageTemplate({
right: 0, right: 0,
width: '12px', width: '12px',
height: '100%', height: '100%',
backgroundColor: saturatedBlue, backgroundColor: primaryGreen,
}} }}
/> />
</div> </div>

View File

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

View File

@@ -239,30 +239,31 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
return ( return (
<div className="annotator-ui-ignore"> <div className="annotator-ui-ignore">
{/* 1. Global Toolbar */} {/* 1. Global Toolbar */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] w-max max-w-[95vw]"> <div className="fixed bottom-36 md:bottom-24 right-4 z-[10000] flex flex-col items-end gap-2">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-1 sm:gap-2 overflow-x-auto hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}> <div className="bg-black/80 backdrop-blur-xl border border-white/10 p-1.5 rounded-2xl shadow-2xl flex flex-col items-center gap-1">
<button <button
onClick={() => setIsActive(!isActive)} onClick={() => setIsActive(!isActive)}
className={cn( className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-medium", "p-2.5 rounded-xl transition-all",
isActive isActive
? "bg-blue-500 text-white shadow-lg shadow-blue-500/20" ? "bg-blue-500 text-white shadow-lg shadow-blue-500/20"
: "text-white/70 hover:text-white hover:bg-white/10" : "text-white/70 hover:text-white hover:bg-white/10"
)} )}
title={isActive ? "Modus beenden" : "Korrekturen erfassen"}
> >
{isActive ? <X size={18} /> : <MessageSquare size={18} />} {isActive ? <X size={20} /> : <MessageSquare size={20} />}
{isActive ? "Modus beenden" : "Korrekturen erfassen"}
</button> </button>
<div className="w-px h-6 bg-white/10 mx-1" /> <div className="w-6 h-px bg-white/10 my-1" />
<button <button
onClick={() => setShowList(!showList)} onClick={() => setShowList(!showList)}
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative" className="p-2.5 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
title="Liste anzeigen"
> >
<List size={20} /> <List size={20} />
{annotations.length > 0 && ( {annotations.length > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 text-[10px] flex items-center justify-center rounded-full text-white font-bold border-2 border-[#1a1a1a]"> <span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 text-[9px] flex items-center justify-center rounded-full text-white font-bold border border-[#1a1a1a]">
{annotations.length} {annotations.length}
</span> </span>
)} )}
@@ -270,13 +271,13 @@ export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
{annotations.length > 0 && ( {annotations.length > 0 && (
<> <>
<div className="w-px h-6 bg-white/10 mx-1" /> <div className="w-6 h-px bg-white/10 my-1" />
<button <button
onClick={exportJSON} onClick={exportJSON}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all font-medium" className="p-2.5 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all"
title="JSON Export"
> >
<Download size={18} /> <Download size={20} />
JSON Export
</button> </button>
</> </>
)} )}

View File

@@ -30,8 +30,8 @@ const containerVariants: Variants = {
}; };
const itemVariants: Variants = { const itemVariants: Variants = {
hidden: { opacity: 0, y: 30 }, hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } }, visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
}; };
const Icons = { const Icons = {

View File

@@ -94,7 +94,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } }, show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
}; };
if (!isMounted) { if (!isMounted) {
@@ -125,8 +125,8 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5 }} transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
> >
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2> <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> <h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>

View File

@@ -116,10 +116,10 @@ export function CompanyTimeline({
return ( return (
<motion.div <motion.div
key={i} key={i}
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.7, ease: "easeOut" }} transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="relative flex flex-col md:flex-row items-center w-full group" className="relative flex flex-col md:flex-row items-center w-full group"
> >
{/* Timeline Dot with Pulse */} {/* Timeline Dot with Pulse */}

View File

@@ -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> <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div> </div>
</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 */} {/* Simplified static grid */}
</div> </div>
</Container> </Container>
@@ -81,18 +81,14 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
return ( return (
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden"> <section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<Container className="relative z-10 px-0 md:px-12 lg:px-16"> <Container className="relative z-10 px-0 md:px-12 lg:px-16">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-12 px-4 md:px-0"> <div className="flex flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl text-left"> <div className="max-w-2xl text-left">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2> <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3> <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div> </div>
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
{ctaLabel}
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</Button>
</div> </div>
<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) => { {items.map((item, idx) => {
const isLarge = item.size === 'large'; const isLarge = item.size === 'large';
const isMedium = item.size === 'medium'; const isMedium = item.size === 'medium';
@@ -111,8 +107,8 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
key={idx} key={idx}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1 }} transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`} className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
> >
{item.href && ( {item.href && (
@@ -127,8 +123,8 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
<HoverShineOverlay shineColor="via-white/30" /> <HoverShineOverlay shineColor="via-white/30" />
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} /> <AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
<h4 className="font-bold text-xl md:text-2xl relative z-10 leading-snug">{item.title}</h4> <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/60 text-sm mt-3 relative z-10">{item.description}</p>} {item.description && <p className="text-white/80 text-base md:text-lg mt-3 relative z-10 font-medium">{item.description}</p>}
</motion.div> </motion.div>
); );
} }
@@ -136,10 +132,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
return ( return (
<motion.div <motion.div
key={idx} key={idx}
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, y: 20, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true, margin: "-50px" }}
transition={{ delay: idx * 0.1 }} transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`} className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
> >
{item.href && ( {item.href && (
@@ -159,7 +155,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
sizes={isLarge ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 100vw, 25vw"} sizes={isLarge ? "(max-width: 768px) 100vw, 50vw" : "(max-width: 768px) 100vw, 25vw"}
/> />
</div> </div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-90 transition-opacity duration-500 z-0" /> <div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/60 to-black/30 opacity-100 transition-opacity duration-500 z-0 group-hover:from-black/80 group-hover:via-black/50 group-hover:to-black/20" />
</> </>
)} )}
@@ -177,11 +173,11 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
{item.tag} {item.tag}
</span> </span>
)} )}
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-2xl md:text-5xl' : 'text-xl md:text-2xl'}`}> <h4 className="font-heading font-extrabold mb-2 text-2xl md:text-3xl">
{item.title} {item.title}
</h4> </h4>
{item.description && ( {item.description && (
<p className={`text-white/80 ${isLarge ? 'text-lg md:text-xl max-w-sm' : 'text-sm'}`}> <p className="text-white/90 text-sm md:text-base max-w-md">
{item.description} {item.description}
</p> </p>
)} )}

View File

@@ -46,7 +46,7 @@ export function DeepDrillAnimation() {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="flex flex-col items-center" className="flex flex-col items-center"
> >

View File

@@ -92,7 +92,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div> </motion.div>
)} )}
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<Heading level={1} variant="white" align={alignment || 'left'} className="mb-4 md:mb-8"> <Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title} {title}
</Heading> </Heading>
</motion.div> </motion.div>
@@ -104,7 +104,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div> </motion.div>
)} )}
{ctaLabel && ctaHref && ( {ctaLabel && ctaHref && (
<motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex justify-center' : ''}`}> <motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
<Button <Button
href={ctaHref} href={ctaHref}
variant="accent" variant="accent"

View File

@@ -2,7 +2,6 @@
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import Image from 'next/image'; import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
@@ -23,6 +22,8 @@ interface HeroVideoProps {
badge?: string; badge?: string;
} }
import placeholders from '@/lib/placeholders.json';
export function HeroVideo(props: HeroVideoProps) { export function HeroVideo(props: HeroVideoProps) {
const { data } = props; const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU'; const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
@@ -31,8 +32,14 @@ export function HeroVideo(props: HeroVideoProps) {
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4'); const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const pathname = usePathname(); 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 posterAlt = props.backgroundImage?.alt || data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
const blurDataURL = (placeholders as Record<string, string>)[posterSrc] || undefined;
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken'; const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen'; const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
@@ -42,31 +49,62 @@ export function HeroVideo(props: HeroVideoProps) {
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project'); const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`; const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
let interactionTriggered = false;
const handleInteraction = () => {
if (interactionTriggered) return;
interactionTriggered = true;
setVideoSrcLoaded(true);
// Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
// Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true })
);
return () => {
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
}, []);
return ( return (
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark"> <div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */} {/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" /> <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"
priority
placeholder={blurDataURL ? "blur" : "empty"}
blurDataURL={blurDataURL}
/>
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video <video
key={`${pathname}-${videoUrl}`} 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" className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl} src={videoSrcLoaded ? videoUrl : undefined}
autoPlay autoPlay
muted muted
loop loop
playsInline playsInline
preload="auto" preload="none"
/>
) : (
<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
/> />
)} )}
@@ -81,11 +119,9 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" /> <div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
<div className="container relative z-[4] text-center px-4"> <div className="container relative z-[4] text-center px-4">
<motion.div <div
key={`hero-content-${pathname}`} key={`hero-content-${pathname}`}
initial={{ opacity: 0, y: 40 }} className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
> >
<h1 <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" className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
@@ -96,7 +132,7 @@ export function HeroVideo(props: HeroVideoProps) {
{subtitle} {subtitle}
</p> </p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-6"> <div className="flex flex-wrap items-center justify-center gap-4 md:gap-6">
<Button <Button
href={ctaHref} href={ctaHref}
variant="primary" variant="primary"
@@ -114,7 +150,7 @@ export function HeroVideo(props: HeroVideoProps) {
{secondaryCtaLabel} {secondaryCtaLabel}
</Button> </Button>
</div> </div>
</motion.div> </div>
</div> </div>
</div> </div>
); );

View File

@@ -239,16 +239,16 @@ export function InteractiveGermanyMap({
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }} exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="absolute z-[9999] pointer-events-auto" 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={{ style={{
left: `${activeLocation.x}%`, '--md-left': `${activeLocation.x}%`,
top: `${activeLocation.y}%`, '--md-top': `${activeLocation.y}%`,
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)', '--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
}} } as React.CSSProperties}
onMouseEnter={() => handleMouseEnter(activeLocation)} onMouseEnter={() => handleMouseEnter(activeLocation)}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl min-w-[200px] w-max max-w-[300px] text-white overflow-hidden"> <div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
{activeLocation.featuredImage && ( {activeLocation.featuredImage && (
<div className="relative w-full h-32 bg-neutral-900"> <div className="relative w-full h-32 bg-neutral-900">
<Image <Image

View File

@@ -37,11 +37,11 @@ const containerVariants = {
}; };
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, y: 30 }, hidden: { opacity: 0, y: 20 },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] as const },
}, },
}; };
@@ -75,7 +75,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.h3 <motion.h3
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }} transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4" className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
> >
@@ -86,7 +86,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
> >
{fairs.map((messe, idx) => { {fairs.map((messe, idx) => {
@@ -180,7 +180,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.h2 <motion.h2
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }} transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8" className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
> >
@@ -192,7 +192,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
className="grid gap-6" className="grid gap-6"
> >
{jobs.map((job: any) => ( {jobs.map((job: any) => (
@@ -221,7 +221,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }} transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center" className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center"
> >

View File

@@ -46,6 +46,22 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const scrollLeftRef = React.useRef(0); const scrollLeftRef = React.useRef(0);
const isDraggingRef = React.useRef(false); const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
const [paddingX, setPaddingX] = React.useState(24);
React.useEffect(() => {
const updatePadding = () => {
const containerEl = document.querySelector('#referenzen .container');
if (containerEl) {
const rect = containerEl.getBoundingClientRect();
const computedStyle = window.getComputedStyle(containerEl);
const pLeft = parseFloat(computedStyle.paddingLeft || '0');
setPaddingX(rect.left + pLeft);
}
};
updatePadding();
window.addEventListener('resize', updatePadding);
return () => window.removeEventListener('resize', updatePadding);
}, []);
const badge = props.badge || data?.badge || t('badge'); const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title'); const title = props.title || data?.title || t('title');
@@ -94,15 +110,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
return ( return (
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden"> <section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" /> <div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6"> <div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div> <div className="text-left">
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2> <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> <h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div> </div>
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16"> <div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
{description} {description}
</div> </div>
</div> </div>
@@ -115,25 +131,32 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-12 pt-4 px-[max(1rem,calc((100vw-1280px)/2))] md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`} style={{ paddingLeft: paddingX, paddingRight: paddingX }}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 [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) => { {references.map((ref, i) => {
const imgSrc = (ref.image && typeof ref.image === 'object' && ref.image.url) const imgSrc = ref.image
? ref.image.url ? (typeof ref.image === 'string' ? ref.image : ref.image.url)
: fallbacks[i % fallbacks.length]; : fallbacks[i % fallbacks.length];
return ( return (
<motion.div <motion.div
key={ref.id} key={ref.id}
initial={{ opacity: 0, x: 50 }} initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }} viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }} 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" className="flex-shrink-0 w-[320px] md:w-[480px] snap-start group pointer-events-auto"
> >
<div <Link
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 5) {
e.preventDefault();
}
}}
data-testid="reference-tile" 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" 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 <Image
draggable={false} draggable={false}
@@ -153,7 +176,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
</span> </span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4> <h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div> </div>
</div> </Link>
</motion.div> </motion.div>
); );
})} })}

View File

@@ -38,7 +38,7 @@ export function ScaleOfImpact() {
{/* Foreground Content */} {/* Foreground Content */}
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center"> <div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
<h2 className="text-2xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight"> <h2 className="text-3xl sm:text-4xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight leading-tight">
{locale === 'en' ? ( {locale === 'en' ? (
<>An infrastructure <br className="md:hidden"/> that connects the country.</> <>An infrastructure <br className="md:hidden"/> that connects the country.</>
) : ( ) : (
@@ -84,19 +84,19 @@ export function ScaleOfImpact() {
{/* Static Data Grid (No jumping text) */} {/* 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="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="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 className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Projects' : 'Projekte'}</div>
</div> </div>
<div className="flex flex-col items-center"> <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 className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Cable Laying' : 'Kabelverlegung'}</div>
</div> </div>
<div className="flex flex-col items-center"> <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 className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Open Trenching' : 'Offener Tiefbau'}</div>
</div> </div>
<div className="flex flex-col items-center"> <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 className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
</div> </div>
</div> </div>

View File

@@ -38,8 +38,8 @@ const containerVariants: Variants = {
}; };
const itemVariants: Variants = { const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 }, hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] } }, visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
}; };
const Icons = { const Icons = {
@@ -137,7 +137,7 @@ export function ServiceDetailGrid({
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left" className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
> >
{problemStatement && ( {problemStatement && (
@@ -193,7 +193,7 @@ export function ServiceDetailGrid({
{/* On a white card, a very subtle primary/grey shine works best */} {/* On a white card, a very subtle primary/grey shine works best */}
<HoverShineOverlay shineColor="via-primary/5" /> <HoverShineOverlay shineColor="via-primary/5" />
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6"> <div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
<div className="pt-2 flex-1 min-w-0 pr-4"> <div className="pt-2 flex-1 min-w-0 pr-4">
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4> <h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div> <div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>

View File

@@ -68,12 +68,12 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})) || defaultCompanies; })) || defaultCompanies;
return ( return (
<section id="unternehmen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden"> <section id="unternehmen" className="pt-12 pb-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4"> <div className="container relative z-10 px-4">
{(badge || title) && ( {(badge || title) && (
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16"> <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>} {title && <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-white">{title}</h3>}
</div> </div>
)} )}
@@ -228,7 +228,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
</> </>
); );
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${ const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full rounded-2xl select-none ${
isCurrent isCurrent
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer' ? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer' : 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'

View File

@@ -43,7 +43,7 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
viewport={{ once: true, margin: "-100px" }} viewport={{ once: true, margin: "-50px" }}
className="mt-12 md:mt-24 p-6 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group" className="mt-12 md:mt-24 p-6 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
> >
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform duration-700 group-hover:translate-x-1/3" /> <div className="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" />

View File

@@ -42,10 +42,10 @@ export function TeamGrid({ members }: TeamGridProps) {
{members.map((member, i) => ( {members.map((member, i) => (
<motion.div <motion.div
key={member.id} key={member.id}
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }} transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden" className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
> >
{/* Card Banner */} {/* Card Banner */}

View File

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

View File

@@ -101,6 +101,7 @@ export function Header({ navLinks }: HeaderProps) {
fill fill
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left" className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
priority priority
fetchPriority="high"
sizes="192px" sizes="192px"
/> />
{/* Masked sweep overlay on hover */} {/* Masked sweep overlay on hover */}
@@ -138,8 +139,54 @@ export function Header({ navLinks }: HeaderProps) {
<div className="flex items-center gap-2 md:gap-4 lg:gap-6"> <div className="flex items-center gap-2 md:gap-4 lg:gap-6">
<div className={`hidden md:block h-5 w-px transition-colors duration-300 ${isSolidMode ? 'bg-neutral-200' : 'bg-white/20'}`} /> <div className={`hidden md:block h-5 w-px transition-colors duration-300 ${isSolidMode ? 'bg-neutral-200' : 'bg-white/20'}`} />
{/* Language Switcher visible on all devices */} {/* Quick Actions (Mobile Only) */}
<div className="z-50 relative flex items-center"> <div className="md:hidden flex items-center gap-2">
<a
href="tel:+492572946760"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-primary text-white shadow-md shadow-primary/20 hover:-translate-y-0.5'
: 'bg-white text-primary shadow-md hover:-translate-y-0.5'
}`}
aria-label="Anrufen"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</a>
<a
href="mailto:info@e-tib.com"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
}`}
aria-label="E-Mail"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</a>
<a
href="https://maps.google.com/?q=E-TIB+GmbH,+Greven"
target="_blank"
rel="noopener noreferrer"
className={`relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-300 ${
isSolidMode
? 'bg-neutral-100 text-neutral-600 hover:text-primary hover:bg-neutral-200'
: 'bg-white/10 text-white backdrop-blur-md border border-white/20 hover:bg-white/20'
}`}
aria-label="Route"
>
<svg className="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</a>
</div>
{/* Language Switcher visible on desktop only */}
<div className="z-50 relative hidden md:flex items-center">
<LanguageSwitcher isSolidMode={isSolidMode} /> <LanguageSwitcher isSolidMode={isSolidMode} />
</div> </div>
@@ -157,81 +204,10 @@ export function Header({ navLinks }: HeaderProps) {
<span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" /> <span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" />
</TransitionLink> </TransitionLink>
{/* Hamburger Menu (Mobile Only) */}
<button
className={`md:hidden ml-2 p-2 transition-colors focus:outline-none ${isSolidMode && !isMobileMenuOpen ? 'text-text-primary hover:text-primary' : 'text-white hover:text-primary z-[60] relative'}`}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle Menu"
>
<div className="w-6 flex flex-col gap-[6px] relative items-center justify-center">
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? 'rotate-45 translate-y-[8px]' : ''}`} />
<span className={`w-full h-[2px] rounded-full bg-current transition-all duration-300 ease-in-out ${isMobileMenuOpen ? 'opacity-0 translate-x-3' : 'opacity-100'}`} />
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? '-rotate-45 -translate-y-[8px]' : ''}`} />
</div>
</button>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 z-[40] bg-[#050B14]/95 backdrop-blur-xl md:hidden pt-24 pb-24 px-6 overflow-y-auto"
>
<nav className="flex flex-col gap-6 mt-8">
{navLinks && navLinks.length > 0 && navLinks.map((link) => {
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
? `/${currentLocale}${link.url}`
: link.url;
const isActive = pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`));
return (
<div key={link.url} className="flex flex-col">
<TransitionLink
href={mappedUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-2xl font-bold uppercase tracking-widest transition-colors ${isActive ? 'text-primary' : 'text-white'}`}
>
{link.label}
</TransitionLink>
{link.children && (
<div className="flex flex-col gap-3 mt-3 pl-4 border-l-2 border-white/10">
{link.children.map((child, idx) => {
if (child.isGroupLabel) {
return <div key={idx} className="text-sm font-bold text-neutral-500 uppercase tracking-widest mt-2">{child.label}</div>;
}
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
? `/${currentLocale}${child.url}`
: child.url;
const isChildActive = pathname === childUrl;
return (
<TransitionLink
key={child.url}
href={childUrl}
onClick={() => setIsMobileMenuOpen(false)}
className={`text-lg font-medium transition-colors ${isChildActive ? 'text-primary' : 'text-white/70'}`}
>
{child.label}
</TransitionLink>
);
})}
</div>
)}
</div>
);
})}
</nav>
</motion.div>
)}
</AnimatePresence>
</> </>
); );
} }

View File

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

View File

@@ -4,51 +4,29 @@ import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image'; import Image from 'next/image';
const PRELOAD_VIDEOS = [
'/assets/videos/web/hero-bohrung.mp4'
];
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) { export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
const [isLoading, setIsLoading] = useState(shouldShowLoader); 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 = () => { const finishLoading = () => {
// Set a session cookie so the server knows not to render the loader again // Set a session cookie so the server knows not to render the loader again
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax"; document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
setIsLoading(false); 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 // Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
if (!shouldShowLoader) return null; if (!shouldShowLoader) return null;

View File

@@ -15,8 +15,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null); const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const router = useRouter(); 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 // We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning); const isTransitioningRef = React.useRef(isTransitioning);
@@ -24,18 +22,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
isTransitioningRef.current = isTransitioning; isTransitioningRef.current = isTransitioning;
}, [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) => { const startTransition = useCallback((href: string, message: string | null = null) => {
// Wenn wir bereits navigieren, nichts tun // Wenn wir bereits navigieren, nichts tun
if (isTransitioning) return; if (isTransitioning) return;
@@ -75,11 +61,35 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
return ( return (
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}> <TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
<React.Suspense fallback={null}>
<TransitionRouteWatcher
isTransitioningRef={isTransitioningRef}
setIsTransitioning={setIsTransitioning}
setTransitionMessage={setTransitionMessage}
/>
</React.Suspense>
{children} {children}
</TransitionContext.Provider> </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() { export function useTransition() {
const context = useContext(TransitionContext); const context = useContext(TransitionContext);
if (!context) { if (!context) {

View File

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

BIN
contact_sheet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -79,8 +79,8 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeitende</div> <div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeitende</div>
</div> </div>
<div> <div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div> <div className="text-4xl font-black text-primary mb-1 tracking-tighter">200+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Kunden</div> <div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Projekte</div>
</div> </div>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
{/* Right Column: Imagery & Mission */} {/* Right Column: Imagery & Mission */}
<div className="lg:col-span-7 relative"> <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"> <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" /> <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" 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 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"> <div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
@@ -122,7 +122,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
tag: "Energie", tag: "Energie",
size: "large", size: "large",
href: "/de/kabeltiefbau", href: "/de/kabeltiefbau",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungsnetzbau" } image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
}, },
{ {
title: "Bohrtechnik", title: "Bohrtechnik",

View File

@@ -28,7 +28,7 @@ layout: "fullBleed"
tag: "Energie", tag: "Energie",
size: "large", size: "large",
href: "/de/kabeltiefbau", href: "/de/kabeltiefbau",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungsnetzbau" } image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
}, },
{ {
title: "Bohrtechnik", title: "Bohrtechnik",

View File

@@ -4,7 +4,7 @@ client: "Energiekontor AG"
date: "2026-06-01" date: "2026-06-01"
dateString: "01/2025 bis 06/2026" dateString: "01/2025 bis 06/2026"
location: "18334 Dettmannsdorf" location: "18334 Dettmannsdorf"
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg" featuredImage: "/assets/photos/DJI_0301.JPG"
--- ---
- 12.300 m Kabelgraben ausheben und verfüllen - 12.300 m Kabelgraben ausheben und verfüllen

View File

@@ -4,12 +4,12 @@ client: "wpd GmbH"
date: "2026-03-01" date: "2026-03-01"
dateString: "seit 03/2026" dateString: "seit 03/2026"
location: "24808 Jevenstedt" location: "24808 Jevenstedt"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg" featuredImage: "/assets/photos/DJI_0037.JPG"
--- ---
- 500 m Kabelgraben ausheben und verfüllen - 500 m Kabelgraben ausheben und verfüllen
- 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160 - 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160
- 3.200 m Horizontalspülbohrung d 160 - 12.000 m Horizontalspülbohrung d 160
- 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV - 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV
- 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV - 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV
- 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV - 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV

View File

@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
date: "2025-06-01" date: "2025-06-01"
dateString: "01/2025 bis 06/2025" dateString: "01/2025 bis 06/2025"
location: "14669 Ketzin" location: "14669 Ketzin"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg" featuredImage: "/assets/photos/DSC02643.JPG"
--- ---
- 1.600 m Kabelgraben ausheben und verfüllen - 1.600 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
date: "2026-03-01" date: "2026-03-01"
dateString: "02/2025 bis 03/2026" dateString: "02/2025 bis 03/2026"
location: "48346 Ostbevern" location: "48346 Ostbevern"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" featuredImage: "/assets/photos/DJI_0048.JPG"
--- ---
- 4.100 m Kabelgraben ausheben und verfüllen - 4.100 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
date: "2025-02-01" date: "2025-02-01"
dateString: "04/2024 bis 02/2025" dateString: "04/2024 bis 02/2025"
location: "15326 Petershagen" location: "15326 Petershagen"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg" featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
--- ---
- 12.000 m Kabelgraben ausheben und verfüllen - 12.000 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,7 @@ client: "Trianel GmbH"
date: "2025-08-01" date: "2025-08-01"
dateString: "02/2025 bis 08/2025" dateString: "02/2025 bis 08/2025"
location: "15345 Prötzel" location: "15345 Prötzel"
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg" featuredImage: "/assets/photos/DJI_0243.JPG"
--- ---
- 2.600 m Kabelgraben ausheben und verfüllen - 2.600 m Kabelgraben ausheben und verfüllen

View File

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

View File

@@ -79,8 +79,8 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Employees</div> <div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Employees</div>
</div> </div>
<div> <div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div> <div className="text-4xl font-black text-primary mb-1 tracking-tighter">200+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Clients</div> <div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Projects</div>
</div> </div>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
{/* Right Column: Imagery & Mission */} {/* Right Column: Imagery & Mission */}
<div className="lg:col-span-7 relative"> <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"> <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" /> <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" 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 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"> <div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
@@ -122,7 +122,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
tag: "Energy", tag: "Energy",
size: "large", size: "large",
href: "/en/kabeltiefbau", href: "/en/kabeltiefbau",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" } image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
}, },
{ {
title: "Drilling Technology", title: "Drilling Technology",

View File

@@ -28,7 +28,7 @@ layout: "fullBleed"
tag: "Energy", tag: "Energy",
size: "large", size: "large",
href: "/en/kabeltiefbau", href: "/en/kabeltiefbau",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" } image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
}, },
{ {
title: "Drilling Technology", title: "Drilling Technology",

View File

@@ -4,7 +4,7 @@ client: "Energiekontor AG"
date: "2026-06-01" date: "2026-06-01"
dateString: "01/2025 to 06/2026" dateString: "01/2025 to 06/2026"
location: "18334 Dettmannsdorf" location: "18334 Dettmannsdorf"
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg" featuredImage: "/assets/photos/DJI_0301.JPG"
--- ---
- Excavation and backfilling of 12,300 m cable trench - Excavation and backfilling of 12,300 m cable trench

View File

@@ -4,12 +4,12 @@ client: "wpd GmbH"
date: "2026-03-01" date: "2026-03-01"
dateString: "since 03/2026" dateString: "since 03/2026"
location: "24808 Jevenstedt" location: "24808 Jevenstedt"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg" featuredImage: "/assets/photos/DJI_0037.JPG"
--- ---
- Excavation and backfilling of 500 m cable trench - Excavation and backfilling of 500 m cable trench
- 4,500 m plowing of 3 x PE protective pipe d 160 - 4,500 m plowing of 3 x PE protective pipe d 160
- 3,200 m HDD directional drilling d 160 - 12,000 m HDD directional drilling d 160
- 8,500 m laying of HV cable 1,000 mm² 64/110 kV - 8,500 m laying of HV cable 1,000 mm² 64/110 kV
- 11 sets of connection joints 1,000 mm² 64/110 kV - 11 sets of connection joints 1,000 mm² 64/110 kV
- 2 sets of outdoor terminations 1,000 mm² 64/110 kV - 2 sets of outdoor terminations 1,000 mm² 64/110 kV

View File

@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
date: "2025-06-01" date: "2025-06-01"
dateString: "01/2025 to 06/2025" dateString: "01/2025 to 06/2025"
location: "14669 Ketzin" location: "14669 Ketzin"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg" featuredImage: "/assets/photos/DSC02643.JPG"
--- ---
- Excavation and backfilling of 1,600 m cable trench - Excavation and backfilling of 1,600 m cable trench

View File

@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
date: "2026-03-01" date: "2026-03-01"
dateString: "02/2025 to 03/2026" dateString: "02/2025 to 03/2026"
location: "48346 Ostbevern" location: "48346 Ostbevern"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" featuredImage: "/assets/photos/DJI_0048.JPG"
--- ---
- Excavation and backfilling of 4,100 m cable trench - Excavation and backfilling of 4,100 m cable trench

View File

@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
date: "2025-02-01" date: "2025-02-01"
dateString: "04/2024 to 02/2025" dateString: "04/2024 to 02/2025"
location: "15326 Petershagen" location: "15326 Petershagen"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg" featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
--- ---
- Excavation and backfilling of 12,000 m cable trench - Excavation and backfilling of 12,000 m cable trench

View File

@@ -4,7 +4,7 @@ client: "Trianel GmbH"
date: "2025-08-01" date: "2025-08-01"
dateString: "02/2025 to 08/2025" dateString: "02/2025 to 08/2025"
location: "15345 Prötzel" location: "15345 Prötzel"
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg" featuredImage: "/assets/photos/DJI_0243.JPG"
--- ---
- Excavation and backfilling of 2,600 m cable trench - Excavation and backfilling of 2,600 m cable trench

View File

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

5303
lh-report.json Normal file

File diff suppressed because one or more lines are too long

14117
lh-single.json Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -11,10 +11,21 @@ export async function getOgFonts() {
try { try {
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`); console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
const boldFont = readFileSync(boldFontPath); const boldFontBuffer = readFileSync(boldFontPath);
const regularFont = readFileSync(regularFontPath); 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( console.log(
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`, `[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
); );
return [ 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 * 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

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

31
make_contact_sheet.py Normal file
View File

@@ -0,0 +1,31 @@
import os
import math
from PIL import Image, ImageDraw
folder = 'public/assets/photos'
files = [f for f in os.listdir(folder) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
files.sort()
thumb_w, thumb_h = 300, 300
cols = 8
rows = math.ceil(len(files) / cols)
contact_sheet = Image.new('RGB', (cols * thumb_w, rows * thumb_h), (255, 255, 255))
draw = ImageDraw.Draw(contact_sheet)
for i, f in enumerate(files):
path = os.path.join(folder, f)
try:
img = Image.open(path)
img.thumbnail((thumb_w, thumb_h - 40))
x = (i % cols) * thumb_w
y = (i // cols) * thumb_h
img_x = x + (thumb_w - img.width) // 2
img_y = y + (thumb_h - 40 - img.height) // 2
contact_sheet.paste(img, (img_x, img_y))
draw.text((x + 10, y + thumb_h - 30), f, fill=(0,0,0))
except Exception as e:
pass
contact_sheet.save('contact_sheet.jpg')
print("Saved contact_sheet.jpg")

View File

@@ -15,6 +15,11 @@ const nextConfig = {
pagesBufferLength: 2, pagesBufferLength: 2,
}, },
experimental: { experimental: {
optimizeCss: true,
staleTimes: {
dynamic: 0,
static: 30,
},
optimizePackageImports: [ optimizePackageImports: [
'lucide-react', 'lucide-react',
'framer-motion', 'framer-motion',

View File

@@ -138,7 +138,7 @@
"prepare": "husky", "prepare": "husky",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"version": "2.2.13-rc.46", "version": "2.2.37",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",

View File

@@ -9,8 +9,9 @@ export default createMiddleware({
export const config = { export const config = {
// Match all pathnames except for // Match all pathnames except for
// - /api (API routes) // - /api (API routes)
// - /stats (Analytics proxy)
// - /_next (Next.js internals) // - /_next (Next.js internals)
// - /_static (inside /public) // - /_static (inside /public)
// - all root files inside /public (e.g. /favicon.ico) // - 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/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: 1007 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: 777 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: 522 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: 412 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: 488 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: 574 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: 304 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: 304 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: 342 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: 322 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: 540 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: 358 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: 618 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 314 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 345 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 MiB

After

Width:  |  Height:  |  Size: 597 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 MiB

After

Width:  |  Height:  |  Size: 493 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

After

Width:  |  Height:  |  Size: 371 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 240 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 325 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 629 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

After

Width:  |  Height:  |  Size: 659 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

After

Width:  |  Height:  |  Size: 664 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 MiB

After

Width:  |  Height:  |  Size: 648 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 MiB

After

Width:  |  Height:  |  Size: 442 KiB

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