Compare commits

...

77 Commits

Author SHA1 Message Date
87942dff41 fix(perf): map img tag in MDX to Next.js Image optimizer for massive performance gains
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m22s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 12:28:47 +02:00
38bd728393 fix(perf): defer hero video loading and lazy load massive markdown images
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m26s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 12:23:58 +02:00
547e62f9d4 2.2.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 11:40:59 +02:00
27fc34e186 fix(perf): refactor HeroVideo to provide immediate Next.js optimized LCP image behind video 2026-06-21 11:40:54 +02:00
d068d68d7a 2.2.22
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 11:04:55 +02:00
908f2b0b69 fix(lint): fix react-hooks/immutability error in InitialLoader 2026-06-21 11:04:54 +02:00
272b7a397b 2.2.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Failing after 1m41s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 10:44:09 +02:00
9edfe24509 fix(analytics, perf): fix umami proxy route redirect and remove blocking 10MB video preload to fix pagespeed 2026-06-21 10:44:08 +02:00
de8f41cc6d chore(release): 2.2.20 - Optimize deployment workflow concurrency to cancel previous tag runs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-21 09:38:53 +02:00
57cc429b54 chore(release): 2.2.19 - Fix mail sender company details & add gotify secrets
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 40s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m11s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-21 09:33:32 +02:00
01cf4013df chore(ci): add default values for umami and glitchtip
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:23:33 +02:00
caf3fe8c1e chore(release): 2.2.17 - Performance and LCP optimization
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 3m32s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-21 09:17:11 +02:00
51eac9fb50 perf(LCP): add fetchPriority=high to Header logo 2026-06-21 09:17:10 +02:00
6426512192 UI: simplify reveal animations to prevent flickering and late triggers
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m38s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 3m31s
Build & Deploy / 🚀 Deploy (push) Successful in 44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-21 09:08:44 +02:00
a63ee1b9f5 chore: bump version to 2.2.16
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m9s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 22:03:05 +02:00
43667e7d68 2.2.15 2026-06-20 22:02:43 +02:00
0191fdb770 fix: resolve reveal flickering, adjust grid header UI, update Guben image
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:49:59 +02:00
afaa5e038c fix: resolve TS dynamic collision and remove anomalous map entries
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m36s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m14s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 21:35:14 +02:00
e0d6ea0d6c perf: implement KLZ blueprint optimizations (LCP, Payload, Caching)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 20:32:08 +02:00
34839ad5a5 fix: update team titles and location names
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m8s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 20:23:15 +02:00
a8d6a45aad chore: bump version to trigger SSL retry
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 19:17:04 +02:00
6de8ad8516 fix(content): correct drilling distance for Jevenstedt reference
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 17:06:20 +02:00
023fef5343 chore: trigger pipeline
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m19s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 10:24:35 +02:00
7181dafca2 fix(ui): enforce scroll lock on documentElement when flyout is open
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 20m1s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 09:32:05 +02:00
8b6d2da821 feat(header): add mail and map action buttons for mobile
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-20 09:31:48 +02:00
17736860d6 feat(header): replace mobile language switcher with phone button
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:18:59 +02:00
d67acdbcc1 fix(referenzen): link slider to overview anchors and add section ids
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m10s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-20 00:18:12 +02:00
05b517731b feat(ui): refine mobile bottom nav, add drag-to-close flyout, custom icons
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-20 00:09:11 +02:00
67696b6afa chore: release v2.2.13-rc.58
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:46:49 +02:00
24472990e5 2.2.13-rc.47 2026-06-19 23:46:36 +02:00
53a885acdb fix(ui): resolve unclosed Link tag in ReferencesSlider and MobileBottomNav type errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-19 23:42:35 +02:00
9afff09ed7 feat(ui): add buttery smooth scroll reveal animations and disable annotator
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m16s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 23:29:51 +02:00
ce5acc5558 refactor(footer): merge navigation and about us links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 23:26:54 +02:00
5027bea666 fix(ui): resolve annotator overlap, slider images, subcompany rounding and copy updates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 23:25:29 +02:00
fcfb567991 fix: remove pb-[90px] from main container which caused white gap above footer on mobile
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:57:52 +02:00
efb9bdbb3f feat: modernize mobile bottom nav with 2030 design and fix z-index
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m5s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:39:13 +02:00
11e7937e79 fix: remaining mobile paddings for CTA boxes
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m9s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:36:30 +02:00
41158697fd fix: mobile layout and visual polishing based on review
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 22:34:53 +02:00
d00a4e29ff Merge branch 'feature/annotator-integration' into main
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 22:03:13 +02:00
58be922fb3 fix(ui): comprehensive mobile optimizations across homepage and blocks
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
- fix: bottom nav and footer padding collision
- fix: reveal animation flickering (Certificates, SubCompanyTiles)
- fix: tooltip flyout overflow on mobile in Germany map
- fix: flex direction for 'Projekt anfragen' and Service Detail icons
- fix: text alignment and spacing issues in ReferencesSlider
- fix: reduce excessive whitespace above footer
2026-06-19 21:39:55 +02:00
111672a614 feat(referenzen): inject MDX project details into overview cards
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 21:27:35 +02:00
d1387d2685 fix(referenzen): assign unique, matching images to all reference projects 2026-06-19 21:21:50 +02:00
fda2944e5a content: restore correct image for Kabelleitungsnetzbau
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Restored DSC01123.JPG (Kabelpflug) for the 'Kabelleitungsnetzbau' tiles on the Home and Kompetenzen pages. Also fixed the English alt text to 'Cable Network Construction'.
2026-06-19 20:13:01 +02:00
8ae5ab73e3 ci: fix pipeline concurrency and target evaluation bugs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
1. Added native Gitea/GitHub Actions concurrency group to automatically cancel outdated/stale deploy runs for the same branch (fixes 'alte deploys durchjagen'). 2. Replaced the buggy 'github.ref_type' condition with explicit 'github.ref' matching. Gitea often failed to populate ref_type, causing staging tags to be downgraded to 'branch' target, making them equal priority to feature branches where the newer run would win (fixes 'feature branches wichtiger als staging').
2026-06-19 19:11:43 +02:00
d8d35da574 chore: remove dead code and unused PDF generation scripts
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Removed leftover brochure and PDF datasheet generation scripts from the old template that are not needed in this project.
2026-06-19 19:03:37 +02:00
792d91f9d5 content: rename Kabelleitungstiefbau to Kabelleitungsnetzbau
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Applied globally across MDX content, translations, and UI components including English analog terms (Cable Trenching -> Cable Network Construction).
2026-06-19 19:00:33 +02:00
d10d038617 perf: optimize interactive map rendering
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Memoized map nodes, map background, and hover handlers to prevent full component re-renders and SVG drop-shadow recalculations.
2026-06-19 18:58:08 +02:00
490ce4abb6 fix: load annotator assets on mount instead of requiring Shift+A
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 18:56:14 +02:00
bed9be1f81 fix: resolve react-hooks/rules-of-hooks in Annotator
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 18:40:44 +02:00
f52a3e3c46 2.2.13-rc.46
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-19 18:26:58 +02:00
71825724db fix(ui): align bento grid images to start page & optimize performance
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- fixed Sentry NDJSON parsing error in relay endpoint
- fully isolated Annotator bundle via next/dynamic (60kb chunk, completely removed from main thread)
- aligned images for Kabelleitungsbau & Bohrtechnik across Start and Kompetenzen pages
- extended transition fallback timeout for slow dev server cold starts
2026-06-19 18:14:55 +02:00
1bb26e8db4 chore: sync bento grid images across home and kompetenzen
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:49:09 +02:00
604b96afe0 2.2.13-rc.45
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 15:44:52 +02:00
59df260164 feat: close annotator modals via ESC (v1.0.9) 2026-06-19 15:44:51 +02:00
3449d2eaa4 2.2.13-rc.44
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:39:38 +02:00
1723be4008 feat: make annotator mobile friendly (v1.0.8) 2026-06-19 15:39:37 +02:00
830d425859 2.2.13-rc.43
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-06-19 15:30:27 +02:00
d6b9aca50f chore: remove tracked turbo logs 2026-06-19 15:30:26 +02:00
caefd5d525 fix: include tgz files in docker build context for local package installation 2026-06-19 15:30:03 +02:00
8d8e1dd460 2.2.13-rc.42
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 37s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:21:21 +02:00
c2a82ae9ae fix: submitAnnotations server action missing directive and invalid mail options 2026-06-19 15:21:20 +02:00
2ecbe9c63c 2.2.13-rc.41
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 32s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:16:46 +02:00
255403c2e4 2.2.13-rc.3 2026-06-19 15:16:28 +02:00
38a870bea8 feat: integrate mintel annotator with email submission and environment restrictions
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 36s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 15:13:07 +02:00
2b8dc19032 chore(git): ignore turborepo cache directories
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m24s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 10:09:22 +02:00
5101b8bd1e style: harmonize mobile and desktop vertical spacing across all components
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 01:14:33 +02:00
afe3565155 fix(map): optimize hit areas for desktop mouse clusters and add touch events for mobile responsiveness
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-19 00:02:13 +02:00
e677089ff9 fix(map): use useRef for hover timeout to prevent flyout flicker and immediate disappearance
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 23:41:09 +02:00
b7d9a4b0d0 fix(tests): update task-12-links test to verify minorLocations since defaultLocations was emptied
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 23:26:07 +02:00
6c8614f147 feat(map): completely rebuild map data from raw 2016-2026 txt files, geocode missing locations, format to spec, and downgrade all projects to minor nodes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 23:18:46 +02:00
aa4e121fea revert(map): restore stable map version from June 17 due to layout, lag, and scroll regressions
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:50:35 +02:00
6697703c91 style(map): format location names strictly to Danny's blueprint (e.g. MS-Kabeltrasse, PLZ Ort)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m8s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:45:03 +02:00
e779c6ac22 fix(map): restore minor_node type for map pins to render tooltips correctly
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:42:08 +02:00
02c4f06108 hotfix: increase footer padding, restore interactive map state, disable hero map lag animation
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m1s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:23:09 +02:00
6d77fb92c3 chore: remove unused imports causing CI failure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 21:48:59 +02:00
cb165b06e5 hotfix: fix map scroll jump, horizontal double scrollbars, and severe lag
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m38s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 21:46:22 +02:00
ca1b19cc0f hotfix: adjust EU badge distance and ScaleOfImpact unit spacing 2026-06-18 21:43:58 +02:00
1869 changed files with 311987 additions and 8873 deletions

View File

@@ -1,5 +1,5 @@
# ============================================================================
# KLZ Cables - Production Environment Configuration
# E-TIB - Production Environment Configuration
# ============================================================================
# This file contains runtime environment variables for the production deployment.
# It should be placed on the production server at: /home/deploy/sites/e-tib.com/.env
@@ -23,7 +23,7 @@ MAIL_HOST=smtp.eu.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=KLZ Cables <noreply@e-tib.com>
MAIL_FROM=E-TIB GmbH <noreply@e-tib.com>
MAIL_RECIPIENTS=info@e-tib.com
# Varnish Cache Size (optional)

View File

@@ -13,6 +13,10 @@ on:
required: false
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/v') && 'production' || github.ref }}
cancel-in-progress: true
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
@@ -89,12 +93,12 @@ jobs:
DOMAIN="e-tib.com"
PRJ="etib"
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="test.${DOMAIN}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production"
IMAGE_TAG="$REF"
@@ -231,7 +235,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
@@ -278,14 +282,18 @@ jobs:
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }}
# Monitoring
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN || 'https://dcb81958-dbf2-4a3d-b422-875f4672c14b@glitchtip.infra.mintel.me/5' }}
# Gatekeeper
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
# Analytics
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
# Notifications
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -346,6 +354,10 @@ jobs:
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo ""
echo "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
echo ""
echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME"

4
.gitignore vendored
View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -1 +0,0 @@
{"hash":"7d27d7d8e3c30797","duration":14130,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -1 +0,0 @@
{"hash":"c3b4dbd60e2d475a","duration":15270,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -1 +0,0 @@
{"hash":"ea9126c55a797120","duration":6873,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
import { mapFileSlugToTranslated } from '@/lib/slugs';
@@ -37,7 +38,7 @@ export async function generateMetadata(props: {
template: '%s | E-TIB',
default: 'E-TIB | Die Experten für Kabelnetzbau',
},
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
metadataBase: new URL(baseUrl),
manifest: '/manifest.webmanifest',
alternates: {
@@ -185,13 +186,10 @@ export default async function Layout(props: {
data-scroll-behavior="smooth"
>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="preconnect" href="https://img.infra.mintel.me" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
{/* Preload critical hero video */}
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
</head>
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
@@ -205,7 +203,7 @@ export default async function Layout(props: {
<main
id="main-content"
className="flex-grow animate-fade-in overflow-visible pb-[90px] md:pb-0"
className="flex-grow animate-fade-in overflow-visible"
tabIndex={-1}
>
{children}
@@ -214,6 +212,8 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider>
</NextIntlClientProvider>

View File

@@ -6,21 +6,22 @@ import { MDXRemote } from 'next-mdx-remote/rsc';
export const dynamic = 'force-dynamic';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
import { HeroSection } from '@/components/blocks/HeroSection';
import nextDynamic from 'next/dynamic';
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
const HeroSection = nextDynamic(() => import('@/components/blocks/HeroSection').then(mod => mod.HeroSection), { ssr: true });
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading';
import { AnimatedCounter } from '@/components/ui';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact';
const AnimatedCounter = nextDynamic(() => import('@/components/ui').then(mod => mod.AnimatedCounter));
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
const ScaleOfImpact = nextDynamic(() => import('@/components/blocks/ScaleOfImpact').then(mod => mod.ScaleOfImpact));
import { getAllReferences } from '@/lib/references';
const mdxComponents = (references: any[]) => ({
@@ -37,6 +38,10 @@ const mdxComponents = (references: any[]) => ({
AnimatedCounter,
InteractiveGermanyMap,
ScaleOfImpact,
img: (props: any) => {
// We proxy it through Next.js image optimizer
return <img {...props} src={`/_next/image?url=${encodeURIComponent(props.src)}&w=1920&q=75`} loading="lazy" decoding="async" />;
}
});
export async function generateMetadata(props: any): Promise<Metadata> {

View File

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

View File

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

View File

@@ -3,13 +3,43 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { getAllReferences } from '@/lib/references';
import TrackedLink from '@/components/analytics/TrackedLink';
import { MapPin, Calendar, Briefcase } from 'lucide-react';
import { MapPin, Calendar, Briefcase, Zap, Activity, ShieldCheck, Wrench, CheckCircle2 } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { SITE_URL } from '@/lib/schema';
import Image from 'next/image';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { HeroSection } from '@/components/blocks/HeroSection';
import { defaultLocations, minorLocations } from '@/lib/map-data';
import { MDXRemote } from 'next-mdx-remote/rsc';
const CustomLi = ({ children, ...props }: any) => {
let text = '';
if (typeof children === 'string') text = children;
else if (Array.isArray(children)) text = children.map((c: any) => typeof c === 'string' ? c : '').join(' ');
else if (children?.props?.children) text = children.props.children;
const lowerText = String(text).toLowerCase();
let Icon = CheckCircle2;
if (lowerText.includes('kabel')) Icon = Zap;
else if (lowerText.includes('bohrung')) Icon = Activity;
else if (lowerText.includes('rohr')) Icon = ShieldCheck;
else if (lowerText.includes('montage')) Icon = Wrench;
else if (lowerText.includes('vlf') || lowerText.includes('otdr')) Icon = Zap;
return (
<li className="flex items-start gap-3 p-3 bg-neutral-50/50 border border-neutral-100 rounded-xl" {...props}>
<Icon className="w-4 h-4 text-primary shrink-0 mt-[2px]" />
<span className="text-sm font-medium text-neutral-700 leading-tight">{children}</span>
</li>
);
};
const mdxComponents = {
ul: (props: any) => <ul className="grid grid-cols-1 gap-2 mb-6" {...props} />,
li: CustomLi,
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
};
interface PageProps {
params: Promise<{
@@ -72,7 +102,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
});
return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
<div className="flex flex-col min-h-screen bg-neutral-50 pb-8 md:pb-24">
{/* Map Section */}
<InteractiveGermanyMap
isHero={true}
@@ -90,11 +120,10 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
<Container className="relative z-20 mt-16 md:mt-24">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{references.map((ref) => (
<TrackedLink
<div
key={ref.slug}
href={`/${locale}/referenzen/${ref.slug}`}
eventProperties={{ location: 'reference_list_card', slug: ref.slug }}
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
id={ref.slug}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block scroll-mt-32"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
@@ -111,7 +140,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
</div>
)}
<div className="absolute inset-0 bg-black/5 transition-colors duration-500" />
<div className="absolute inset-0 bg-black/5" />
{/* Location Badge */}
<div className="absolute top-4 left-4 z-10">
@@ -124,10 +153,17 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{/* Content Section */}
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 transition-colors line-clamp-3 leading-[1.2] group-hover:text-primary">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 leading-[1.2]">
{ref.frontmatter.title}
</h3>
{/* Project Details / Content */}
{ref.content && (
<div className="flex-grow mb-6">
<MDXRemote source={ref.content} components={mdxComponents} />
</div>
)}
{/* Technical Meta Data */}
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
<div className="min-w-0">
@@ -147,12 +183,12 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</div>
</div>
</div>
</TrackedLink>
</div>
))}
</div>
{/* Support Section */}
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="mt-8 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
badge.svg Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,8 +30,8 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] } },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {
@@ -75,7 +75,7 @@ const Icons = {
export function BenefitGrid({ badge, title, description, benefits }: BenefitGridProps) {
return (
<section className="bg-white py-12 md:py-24 relative overflow-hidden border-t border-neutral-100">
<section className="bg-white py-16 md:py-24 lg:py-32 relative overflow-hidden border-t border-neutral-100">
<div className="container px-4 max-w-7xl mx-auto relative z-10">
<div className="max-w-3xl mx-auto text-center mb-16">

View File

@@ -94,12 +94,12 @@ export function CertificatesBlock({ badge, title, description, certificates = de
const itemVariants = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } },
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
if (!isMounted) {
return (
<section className="py-12 md:py-24 bg-neutral-50 relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-neutral-50 relative overflow-hidden">
<Container>
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
@@ -125,8 +125,8 @@ export function CertificatesBlock({ badge, title, description, certificates = de
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
>
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>

View File

@@ -80,7 +80,7 @@ export function CompanyTimeline({
const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<section className="py-12 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-[#FAFAFA] relative overflow-hidden">
{/* Background Decor */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
@@ -116,10 +116,10 @@ export function CompanyTimeline({
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 40 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7, ease: "easeOut" }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="relative flex flex-col md:flex-row items-center w-full group"
>
{/* Timeline Dot with Pulse */}

View File

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

View File

@@ -41,7 +41,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
const { showForm, showMap } = props;
return (
<Section className="bg-neutral-light py-12 md:py-28 relative overflow-hidden">
<Section className="bg-neutral-light relative overflow-hidden">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
<motion.div

View File

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

View File

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

View File

@@ -92,7 +92,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div>
)}
<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}
</Heading>
</motion.div>
@@ -104,7 +104,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div>
)}
{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
href={ctaHref}
variant="accent"

View File

@@ -2,7 +2,6 @@
import { Button } from '@/components/ui/Button';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
@@ -42,31 +41,42 @@ export function HeroVideo(props: HeroVideoProps) {
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
// Delay loading the video until after the page has visually painted the LCP image
const timer = setTimeout(() => {
setVideoSrcLoaded(true);
}, 500);
return () => clearTimeout(timer);
}, []);
return (
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{videoUrl ? (
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
/>
{/* Render video on top if available, but defer src to avoid blocking LCP */}
{videoUrl && (
<video
key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="auto"
/>
) : (
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
preload="none"
/>
)}
@@ -81,11 +91,9 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
<div className="container relative z-[4] text-center px-4">
<motion.div
<div
key={`hero-content-${pathname}`}
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
>
<h1
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
@@ -96,7 +104,7 @@ export function HeroVideo(props: HeroVideoProps) {
{subtitle}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-4 md:gap-6">
<Button
href={ctaHref}
variant="primary"
@@ -114,7 +122,7 @@ export function HeroVideo(props: HeroVideoProps) {
{secondaryCtaLabel}
</Button>
</div>
</motion.div>
</div>
</div>
</div>
);

View File

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

View File

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

View File

@@ -94,15 +94,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
return (
<section id="referenzen" className="py-12 md:py-24 bg-neutral-dark text-white relative overflow-hidden">
<section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6">
<div>
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div className="text-left">
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div>
<div className="text-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}
</div>
</div>
@@ -115,33 +115,31 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
onMouseLeave={onMouseLeave}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
className={`select-none flex gap-6 overflow-x-auto pb-12 pt-4 px-[max(1rem,calc((100vw-1280px)/2))] md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-6 md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
>
{references.map((ref, i) => {
const imgSrc = (ref.image && typeof ref.image === 'object' && ref.image.url)
? ref.image.url
const imgSrc = ref.image
? (typeof ref.image === 'string' ? ref.image : ref.image.url)
: fallbacks[i % fallbacks.length];
return (
<motion.div
key={ref.id}
initial={{ opacity: 0, x: 50 }}
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-start group pointer-events-auto"
>
<Link
href={`/${locale}/referenzen/${ref.slug}`}
data-testid="reference-tile"
draggable={false}
href={`/${locale}/referenzen#${ref.slug}`}
onClick={(e) => {
if (dragDistanceRef.current > 15) {
if (dragDistanceRef.current > 5) {
e.preventDefault();
}
dragDistanceRef.current = 0;
}}
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
>
<Image
draggable={false}

View File

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

View File

@@ -38,8 +38,8 @@ const containerVariants: Variants = {
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] } },
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
};
const Icons = {
@@ -105,7 +105,7 @@ export function ServiceDetailGrid({
panels
}: ServiceDetailGridProps) {
return (
<section className="bg-neutral-50 py-12 md:py-24 border-b border-neutral-100 relative overflow-hidden">
<section className="bg-neutral-50 py-16 md:py-24 lg:py-32 border-b border-neutral-100 relative overflow-hidden">
<div className="container relative z-10 px-4 max-w-7xl mx-auto">
<div className="max-w-3xl mb-12 md:mb-20 text-left">
@@ -137,7 +137,7 @@ export function ServiceDetailGrid({
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
>
{problemStatement && (
@@ -193,7 +193,7 @@ export function ServiceDetailGrid({
{/* On a white card, a very subtle primary/grey shine works best */}
<HoverShineOverlay shineColor="via-primary/5" />
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
<div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
<div className="pt-2 flex-1 min-w-0 pr-4">
<h4 className="text-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>

View File

@@ -68,7 +68,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})) || defaultCompanies;
return (
<section id="unternehmen" className="py-12 md:py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<section id="unternehmen" className="pt-12 pb-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4">
{(badge || title) && (
@@ -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
? '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}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
viewport={{ once: true, margin: "-50px" }}
className="mt-12 md:mt-24 p-6 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
>
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform duration-700 group-hover:translate-x-1/3" />

View File

@@ -28,7 +28,7 @@ export function TeamGrid({ members }: TeamGridProps) {
if (!members || members.length === 0) return null;
return (
<section className="py-12 md:py-24 bg-white relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-white relative overflow-hidden">
<div className="container relative z-10">
<div className="mb-12">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{t('badge')}</h2>
@@ -42,10 +42,10 @@ export function TeamGrid({ members }: TeamGridProps) {
{members.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
{/* Card Banner */}

View File

@@ -28,7 +28,7 @@ export function EUFundingBadge() {
</div>
{/* Text overlaid on the flag */}
<div className="absolute top-0 right-0 p-5 md:p-8 flex flex-col items-end text-right z-10 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]">
<div className="absolute top-0 right-0 pt-8 pr-8 md:pt-12 md:pr-12 flex flex-col items-end text-right z-10 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]">
<span className="block text-[9px] md:text-[11px] font-bold text-white/90 tracking-wide leading-snug drop-shadow-md">
Kofinanziert von der
</span>

View File

@@ -1,145 +0,0 @@
import {
Body,
Container,
Head,
Heading,
Hr,
Html,
Preview,
Section,
Text,
Button,
} from '@react-email/components';
import * as React from 'react';
interface BrochureDeliveryEmailProps {
_email: string;
brochureUrl: string;
locale: 'en' | 'de';
}
export const BrochureDeliveryEmail = ({
_email,
brochureUrl,
locale = 'en',
}: BrochureDeliveryEmailProps) => {
const t =
locale === 'de'
? {
subject: 'Ihr E-TIB Kabelkatalog',
greeting: 'Vielen Dank für Ihr Interesse an E-TIB.',
body: 'Anbei erhalten Sie den Link zu unserem aktuellen Produktkatalog. Dieser enthält alle wichtigen technischen Spezifikationen und detaillierten Produktdaten.',
button: 'Katalog herunterladen',
footer: 'Diese E-Mail wurde von e-tib.com gesendet.',
}
: {
subject: 'Your E-TIB Cable Catalog',
greeting: 'Thank you for your interest in E-TIB.',
body: 'Below you will find the link to our current product catalog. It contains all key technical specifications and detailed product data.',
button: 'Download Catalog',
footer: 'This email was sent from e-tib.com.',
};
return (
<Html>
<Head />
<Preview>{t.subject}</Preview>
<Body style={main}>
<Container style={container}>
<Section style={headerSection}>
<Heading style={h1}>{t.subject}</Heading>
</Section>
<Section style={section}>
<Text style={text}>
<strong>{t.greeting}</strong>
</Text>
<Text style={text}>{t.body}</Text>
<Section style={buttonContainer}>
<Button style={button} href={brochureUrl}>
{t.button}
</Button>
</Section>
<Hr style={hr} />
</Section>
<Text style={footer}>{t.footer}</Text>
</Container>
</Body>
</Html>
);
};
export default BrochureDeliveryEmail;
const main = {
backgroundColor: '#f6f9fc',
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '0 0 48px',
marginBottom: '64px',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #e6ebf1',
};
const headerSection = {
backgroundColor: '#111111',
padding: '32px 48px',
borderBottom: '4px solid #117c61',
};
const h1 = {
color: '#ffffff',
fontSize: '24px',
fontWeight: 'bold',
margin: '0',
};
const section = {
padding: '32px 48px 0',
};
const text = {
color: '#333',
fontSize: '16px',
lineHeight: '24px',
textAlign: 'left' as const,
};
const buttonContainer = {
textAlign: 'center' as const,
marginTop: '32px',
marginBottom: '32px',
};
const button = {
backgroundColor: '#117c61',
borderRadius: '4px',
color: '#ffffff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'inline-block',
padding: '16px 32px',
};
const hr = {
borderColor: '#e6ebf1',
margin: '20px 0',
};
const footer = {
color: '#8898aa',
fontSize: '12px',
lineHeight: '16px',
textAlign: 'center' as const,
marginTop: '20px',
};

View File

@@ -7,7 +7,7 @@ export default function CTA({ data }: { data?: any }) {
const locale = useLocale();
return (
<Section className="bg-primary text-white py-16 md:py-32 relative overflow-hidden">
<Section className="bg-primary text-white relative overflow-hidden">
<div className="absolute top-0 right-0 w-1/3 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-primary/10 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />

View File

@@ -7,7 +7,7 @@ export default function Experience({ data }: { data?: any }) {
const t = useTranslations('Home.experience');
return (
<Section className="relative py-16 md:py-48 overflow-hidden text-white">
<Section className="relative overflow-hidden text-white">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy-2.webp"

View File

@@ -24,7 +24,7 @@ export default function GallerySection({ data }: { data?: any }) {
const lightboxIndex = photoParam ? parseInt(photoParam, 10) : 0;
return (
<Section className="bg-white text-white py-16 md:py-32">
<Section className="bg-white text-white">
<Container>
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} align="center">
{data?.title || t('title')}

View File

@@ -9,7 +9,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
const locale = useLocale();
return (
<Section className="relative py-16 md:py-48 overflow-hidden">
<Section className="relative overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC08036-Large.webp"

View File

@@ -19,7 +19,7 @@ export function Footer({ companyInfo }: FooterProps) {
const locale = useLocale();
return (
<footer className="bg-[#050B14] text-neutral-light pt-40 pb-10 md:pt-16 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 */}
<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" />
@@ -46,7 +46,7 @@ export function Footer({ companyInfo }: FooterProps) {
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
{locale === 'de'
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
: 'Experts in cable network construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
</p>
</div>
@@ -108,8 +108,13 @@ export function Footer({ companyInfo }: FooterProps) {
{locale === 'de' ? 'Navigation' : 'Navigation'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 mb-8">
<ul className="grid grid-cols-2 gap-x-8 gap-y-3 mb-8">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -123,28 +128,6 @@ export function Footer({ companyInfo }: FooterProps) {
</li>
))}
</ul>
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-4 text-white">
<TransitionLink href={`/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}`} className="hover:text-primary transition-colors cursor-pointer inline-block">
{locale === 'de' ? 'Über uns' : 'About us'}
</TransitionLink>
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 border-l border-white/10 pl-4 ml-1">
{[
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
{/* Mobile View (Accordions) */}
@@ -157,6 +140,11 @@ export function Footer({ companyInfo }: FooterProps) {
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
@@ -171,32 +159,6 @@ export function Footer({ companyInfo }: FooterProps) {
</ul>
</div>
</details>
<div className="h-px bg-white/10 w-full"></div>
<details className="group/acc">
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
{locale === 'de' ? 'Über uns' : 'About us'}
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
</summary>
<div className="pt-2 pb-4">
<ul className="space-y-3">
{[
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
</details>
</div>
</div>

View File

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

View File

@@ -3,12 +3,13 @@
import * as React from 'react';
import { usePathname } from 'next/navigation';
import { TransitionLink } from '@/components/ui/TransitionLink';
import { Home, Briefcase, Users, Mail } from 'lucide-react';
import { motion } from 'framer-motion';
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
export interface NavLink {
label: string;
url: string;
children?: NavLink[];
}
interface MobileBottomNavProps {
@@ -18,84 +19,213 @@ interface MobileBottomNavProps {
export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProps) {
const pathname = usePathname() || '/';
const [isFlyoutOpen, setIsFlyoutOpen] = React.useState(false);
// Scroll lock when flyout is open
React.useEffect(() => {
if (isFlyoutOpen) {
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
};
}, [isFlyoutOpen]);
// Construct our bottom nav items based on the passed links
const items = React.useMemo(() => {
const homeUrl = `/${currentLocale}`;
// Find the specific links from the provided navLinks (which might be translated)
// We do a simple match based on index or keywords if needed, but since we know the order:
// 0: Kompetenzen, 1: Über uns, 2: Karriere, 3: Messen
const competenciesUrl = navLinks[0]?.url || `/${currentLocale}/kompetenzen`;
const aboutUrl = navLinks[1]?.url || `/${currentLocale}/ueber-uns`;
const referencesUrl = navLinks[2]?.url || `/${currentLocale}/referenzen`;
const contactUrl = `/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`;
return [
{
id: 'home',
label: currentLocale === 'de' ? 'Start' : 'Home',
url: homeUrl,
icon: <Home className="w-6 h-6 mb-1" strokeWidth={2} />,
icon: <Home className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />,
exactMatch: true
},
{
id: 'kompetenzen',
label: navLinks[0]?.label || (currentLocale === 'de' ? 'Kompetenzen' : 'Competencies'),
url: competenciesUrl,
icon: <Briefcase className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Layers className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
label: navLinks[1]?.label || (currentLocale === 'de' ? 'Über uns' : 'About Us'),
url: aboutUrl,
icon: <Users className="w-6 h-6 mb-1" strokeWidth={2} />
id: 'referenzen',
label: navLinks[2]?.label || (currentLocale === 'de' ? 'Referenzen' : 'References'),
url: referencesUrl,
icon: <Star className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'kontakt',
label: currentLocale === 'de' ? 'Kontakt' : 'Contact',
url: contactUrl,
icon: <Mail className="w-6 h-6 mb-1" strokeWidth={2} />
icon: <Mail className="w-[22px] h-[22px] md:w-6 md:h-6" strokeWidth={2.2} />
},
{
id: 'menu',
label: currentLocale === 'de' ? 'Mehr' : 'Menu',
url: '#',
icon: <Menu className="w-[24px] h-[24px] md:w-6 md:h-6" strokeWidth={2.5} />,
isFlyoutTrigger: true
}
];
}, [currentLocale, navLinks]);
return (
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-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">
<nav className="flex justify-around items-center max-w-md mx-auto">
{items.map((item) => {
// Determine if active
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
: item.url;
const isActive = item.exactMatch
? pathname === mappedUrl
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
const flyoutLinks = React.useMemo(() => {
const aboutNode = navLinks[1];
const karriereNode = navLinks[3];
const messenNode = navLinks[4];
let links: {label: string, url: string}[] = [];
if (aboutNode?.children) {
links.push(...aboutNode.children.map(c => ({ label: c.label, url: c.url })));
}
if (karriereNode) links.push({ label: karriereNode.label, url: karriereNode.url });
if (messenNode) links.push({ label: messenNode.label, url: messenNode.url });
return links;
}, [navLinks]);
return (
<TransitionLink
key={item.label}
href={mappedUrl}
className={`relative flex flex-col items-center justify-center w-full min-h-[44px] py-1 transition-colors duration-300 select-none ${
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
}`}
>
<div className="relative flex flex-col items-center justify-center">
{/* Active Icon Background */}
return (
<>
<div className="md:hidden fixed bottom-0 left-0 right-0 z-[9999] px-2 pb-[calc(env(safe-area-inset-bottom)+12px)] pt-4 pointer-events-none flex justify-center">
<nav className="flex justify-between items-center bg-white/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]">
{items.map((item) => {
const mappedUrl = item.url.startsWith('/') && !item.url.match(/^\/(en|de)/)
? `/${currentLocale}${item.url}`
: item.url;
let isActive = false;
if (item.isFlyoutTrigger) {
isActive = isFlyoutOpen;
} else {
isActive = item.exactMatch
? pathname === mappedUrl
: (pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`)));
}
const innerContent = (
<>
{isActive && (
<motion.div
layoutId="mobile-nav-active-bg"
className="absolute inset-0 bg-primary/10 rounded-full scale-150"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className="absolute inset-0 bg-primary/10 rounded-2xl shadow-sm border border-primary/10"
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
/>
)}
{/* Icon wrapper to ensure it sits above the background */}
<div className="relative z-10 transition-transform duration-300 active:scale-90">
{item.icon}
<div className="relative flex flex-col items-center justify-center z-10 w-full h-full">
<motion.div
whileTap={{ scale: 0.8 }}
animate={{ y: isActive ? -2 : 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{item.icon}
</motion.div>
<motion.span
animate={{ opacity: isActive ? 1 : 0.7, y: isActive ? 0 : 2 }}
className="text-[9px] font-bold mt-1 tracking-wide"
>
{item.label}
</motion.span>
</div>
</>
);
if (item.isFlyoutTrigger) {
return (
<button
key={item.label}
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
className={`relative flex flex-col items-center justify-center flex-1 h-[62px] transition-colors duration-300 select-none w-full focus:outline-none ${
isActive ? 'text-primary' : 'text-neutral-500 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-500 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>
<span className="text-[10px] font-semibold mt-0.5 tracking-wide z-10">{item.label}</span>
</TransitionLink>
);
})}
</nav>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}

View File

@@ -4,51 +4,29 @@ import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
const PRELOAD_VIDEOS = [
'/assets/videos/web/hero-bohrung.mp4'
];
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
const [isLoading, setIsLoading] = useState(shouldShowLoader);
useEffect(() => {
if (!shouldShowLoader) return;
// Fallback Timeout (4 Sekunden), falls Videos hängen
const timeout = setTimeout(() => {
finishLoading();
}, 4000);
let videosLoaded = 0;
const checkAllLoaded = () => {
videosLoaded++;
if (videosLoaded === PRELOAD_VIDEOS.length) {
clearTimeout(timeout);
// Kleine Verzögerung für die smootheness
setTimeout(finishLoading, 800);
}
};
PRELOAD_VIDEOS.forEach((src) => {
const video = document.createElement('video');
video.preload = 'auto';
video.oncanplaythrough = checkAllLoaded;
video.onerror = checkAllLoaded; // bei Fehler trotzdem weitermachen
video.src = src;
video.load();
});
// Cleanup
return () => clearTimeout(timeout);
}, [shouldShowLoader]);
const finishLoading = () => {
// Set a session cookie so the server knows not to render the loader again
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
setIsLoading(false);
};
useEffect(() => {
if (!shouldShowLoader) return;
// Just show the animation for a short time to allow UI to mount,
// we don't preload 10MB videos anymore to save pagespeed!
const timeout = setTimeout(() => {
finishLoading();
}, 1200);
// Cleanup
return () => clearTimeout(timeout);
}, [shouldShowLoader]);
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
if (!shouldShowLoader) return null;

View File

@@ -60,7 +60,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTimeout(() => {
router.push(href);
// Fallback: If for some reason the route doesn't change after 5 seconds, open the shutter anyway
// Fallback: If for some reason the route doesn't change after 60 seconds (useful for slow dev servers), open the shutter anyway
setTimeout(() => {
setIsTransitioning((current) => {
if (current) {
@@ -69,7 +69,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
}
return current;
});
}, 5000);
}, 60000);
}, 700);
}, [isTransitioning, router]);

View File

@@ -22,7 +22,7 @@ export default function Gallery() {
];
return (
<Section className="bg-primary-dark py-16 md:py-32">
<Section className="bg-primary-dark">
<Container>
<Heading level={2} subtitle={t('gallery.subtitle')} align="center" className="text-white mb-12 md:mb-20">
<span className="text-white">{t('gallery.title')}</span>

View File

@@ -6,12 +6,14 @@ interface AnimatedGlossyBorderProps {
borderWidth?: 1 | 2;
opacity?: number;
color?: 'white' | 'primary';
disableAnimation?: boolean;
}
export function AnimatedGlossyBorder({
className,
borderWidth = 1,
color = 'white'
color = 'white',
disableAnimation = false
}: AnimatedGlossyBorderProps) {
const isWhite = color === 'white';
const glowColor = isWhite ? 'rgba(255,255,255,0.6)' : 'rgba(17,124,97,0.6)';
@@ -35,7 +37,10 @@ export function AnimatedGlossyBorder({
}}
>
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[250%] aspect-square animate-[spin_8s_linear_infinite]"
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[250%] aspect-square",
!disableAnimation && "animate-[spin_8s_linear_infinite]"
)}
style={{
background: `conic-gradient(from 0deg, ${gradientColors})`
}}

View File

@@ -3,7 +3,7 @@ import { cn } from './utils';
export function Section({ className, children, ...props }: React.HTMLAttributes<HTMLElement>) {
return (
<section className={cn('py-12 md:py-28 lg:py-36 overflow-hidden content-visibility-auto', className)} {...props}>
<section className={cn('py-16 md:py-24 lg:py-32 overflow-hidden content-visibility-auto', className)} {...props}>
{children}
</section>
);

View File

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

BIN
contact_sheet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,7 +1,7 @@
---
title: "Kabelleitungstiefbau im Bereich Telekommunikation"
title: "Kabelleitungsnetzbau im Bereich Telekommunikation"
date: "2024-03-20"
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungstiefbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungsnetzbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
layout: "fullBleed"
---

View File

@@ -79,8 +79,8 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeitende</div>
</div>
<div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Kunden</div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">200+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Projekte</div>
</div>
</div>
</div>
@@ -88,7 +88,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
{/* Right Column: Imagery & Mission */}
<div className="lg:col-span-7 relative">
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB im Einsatz" className="w-full h-full object-cover" />
<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 bottom-0 left-0 w-full p-8 md:p-12">
@@ -117,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
ctaHref="/de/kompetenzen"
items={[
{
title: "Kabelleitungstiefbau",
description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
title: "Kabelleitungsnetzbau",
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
tag: "Energie",
size: "large",
href: "/de/kabelnetzbau",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
href: "/de/kabeltiefbau",
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
},
{
title: "Bohrtechnik",

View File

@@ -1,13 +1,13 @@
---
title: "Kabelleitungstiefbau"
title: "Kabelleitungsnetzbau"
date: "2024-03-20"
excerpt: "Professioneller Kabelleitungstiefbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
excerpt: "Professioneller Kabelleitungsnetzbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
layout: "fullBleed"
---
<HeroSection
badge="Kernkompetenz"
title="Kabelleitungstiefbau"
title="Kabelleitungsnetzbau"
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
alignment="center"

View File

@@ -8,7 +8,7 @@ layout: "fullBleed"
<HomeHero
badge="Unsere Leistungen"
title="Alles aus einer Hand"
description="Als Full-Service-Partner im Bereich Kabelleitungstiefbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
description="Als Full-Service-Partner im Bereich Kabelleitungsnetzbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
videoUrl="/assets/videos/web/hero-bohrung.mp4"
linkText="Kontakt aufnehmen"
linkHref="/de/kontakt"
@@ -23,12 +23,12 @@ layout: "fullBleed"
ctaHref="/de/kontakt"
items={[
{
title: "Kabelleitungstiefbau",
title: "Kabelleitungsnetzbau",
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
tag: "Energie",
size: "large",
href: "/de/kabelnetzbau",
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungstiefbau" }
href: "/de/kabeltiefbau",
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
},
{
title: "Bohrtechnik",
@@ -36,7 +36,7 @@ layout: "fullBleed"
tag: "Innovation",
size: "medium",
href: "/de/bohrtechnik",
image: { url: "/assets/photos/DSC08653.JPG", alt: "Bohrtechnik" }
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg", alt: "Bohrtechnik" }
},
{
title: "Glasfaser",
@@ -70,7 +70,7 @@ layout: "fullBleed"
panels={[
{
id: "energy",
title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)",
title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)",
icon: "energy",
lists: [
[
@@ -86,7 +86,7 @@ layout: "fullBleed"
},
{
id: "fiber",
title: "Kabelleitungstiefbau (Telekommunikation)",
title: "Kabelleitungsnetzbau (Telekommunikation)",
icon: "fiber",
lists: [
[
@@ -143,7 +143,7 @@ layout: "fullBleed"
<JsonLd data={[
{
"@type": "Service",
"name": "Kabelleitungstiefbau",
"name": "Kabelleitungsnetzbau",
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
},

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