Compare commits

..

160 Commits

Author SHA1 Message Date
58be922fb3 fix(ui): comprehensive mobile optimizations across homepage and blocks
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
- fix: bottom nav and footer padding collision
- fix: reveal animation flickering (Certificates, SubCompanyTiles)
- fix: tooltip flyout overflow on mobile in Germany map
- fix: flex direction for 'Projekt anfragen' and Service Detail icons
- fix: text alignment and spacing issues in ReferencesSlider
- fix: reduce excessive whitespace above footer
2026-06-19 21:39:55 +02:00
111672a614 feat(referenzen): inject MDX project details into overview cards
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 37s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 21:27:35 +02:00
d1387d2685 fix(referenzen): assign unique, matching images to all reference projects 2026-06-19 21:21:50 +02:00
fda2944e5a content: restore correct image for Kabelleitungsnetzbau
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Restored DSC01123.JPG (Kabelpflug) for the 'Kabelleitungsnetzbau' tiles on the Home and Kompetenzen pages. Also fixed the English alt text to 'Cable Network Construction'.
2026-06-19 20:13:01 +02:00
8ae5ab73e3 ci: fix pipeline concurrency and target evaluation bugs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
1. Added native Gitea/GitHub Actions concurrency group to automatically cancel outdated/stale deploy runs for the same branch (fixes 'alte deploys durchjagen'). 2. Replaced the buggy 'github.ref_type' condition with explicit 'github.ref' matching. Gitea often failed to populate ref_type, causing staging tags to be downgraded to 'branch' target, making them equal priority to feature branches where the newer run would win (fixes 'feature branches wichtiger als staging').
2026-06-19 19:11:43 +02:00
d8d35da574 chore: remove dead code and unused PDF generation scripts
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 3m11s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Removed leftover brochure and PDF datasheet generation scripts from the old template that are not needed in this project.
2026-06-19 19:03:37 +02:00
792d91f9d5 content: rename Kabelleitungstiefbau to Kabelleitungsnetzbau
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Applied globally across MDX content, translations, and UI components including English analog terms (Cable Trenching -> Cable Network Construction).
2026-06-19 19:00:33 +02:00
d10d038617 perf: optimize interactive map rendering
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Memoized map nodes, map background, and hover handlers to prevent full component re-renders and SVG drop-shadow recalculations.
2026-06-19 18:58:08 +02:00
490ce4abb6 fix: load annotator assets on mount instead of requiring Shift+A
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-19 18:56:14 +02:00
bed9be1f81 fix: resolve react-hooks/rules-of-hooks in Annotator
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 3m6s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 18:40:44 +02:00
f52a3e3c46 2.2.13-rc.46
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-19 18:26:58 +02:00
71825724db fix(ui): align bento grid images to start page & optimize performance
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m16s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- fixed Sentry NDJSON parsing error in relay endpoint
- fully isolated Annotator bundle via next/dynamic (60kb chunk, completely removed from main thread)
- aligned images for Kabelleitungsbau & Bohrtechnik across Start and Kompetenzen pages
- extended transition fallback timeout for slow dev server cold starts
2026-06-19 18:14:55 +02:00
1bb26e8db4 chore: sync bento grid images across home and kompetenzen
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:49:09 +02:00
604b96afe0 2.2.13-rc.45
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 15:44:52 +02:00
59df260164 feat: close annotator modals via ESC (v1.0.9) 2026-06-19 15:44:51 +02:00
3449d2eaa4 2.2.13-rc.44
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m0s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:39:38 +02:00
1723be4008 feat: make annotator mobile friendly (v1.0.8) 2026-06-19 15:39:37 +02:00
830d425859 2.2.13-rc.43
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-06-19 15:30:27 +02:00
d6b9aca50f chore: remove tracked turbo logs 2026-06-19 15:30:26 +02:00
caefd5d525 fix: include tgz files in docker build context for local package installation 2026-06-19 15:30:03 +02:00
8d8e1dd460 2.2.13-rc.42
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 37s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:21:21 +02:00
c2a82ae9ae fix: submitAnnotations server action missing directive and invalid mail options 2026-06-19 15:21:20 +02:00
2ecbe9c63c 2.2.13-rc.41
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 32s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 15:16:46 +02:00
255403c2e4 2.2.13-rc.3 2026-06-19 15:16:28 +02:00
38a870bea8 feat: integrate mintel annotator with email submission and environment restrictions
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Failing after 36s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 15:13:07 +02:00
2b8dc19032 chore(git): ignore turborepo cache directories
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m24s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-19 10:09:22 +02:00
5101b8bd1e style: harmonize mobile and desktop vertical spacing across all components
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-19 01:14:33 +02:00
afe3565155 fix(map): optimize hit areas for desktop mouse clusters and add touch events for mobile responsiveness
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m31s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-19 00:02:13 +02:00
e677089ff9 fix(map): use useRef for hover timeout to prevent flyout flicker and immediate disappearance
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 23:41:09 +02:00
b7d9a4b0d0 fix(tests): update task-12-links test to verify minorLocations since defaultLocations was emptied
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 23:26:07 +02:00
6c8614f147 feat(map): completely rebuild map data from raw 2016-2026 txt files, geocode missing locations, format to spec, and downgrade all projects to minor nodes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 23:18:46 +02:00
aa4e121fea revert(map): restore stable map version from June 17 due to layout, lag, and scroll regressions
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m0s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:50:35 +02:00
6697703c91 style(map): format location names strictly to Danny's blueprint (e.g. MS-Kabeltrasse, PLZ Ort)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m8s
Build & Deploy / 🚀 Deploy (push) Successful in 35s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m1s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:45:03 +02:00
e779c6ac22 fix(map): restore minor_node type for map pins to render tooltips correctly
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m34s
Build & Deploy / 🏗️ Build (push) Successful in 3m10s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 22:42:08 +02:00
02c4f06108 hotfix: increase footer padding, restore interactive map state, disable hero map lag animation
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m1s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m4s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 22:23:09 +02:00
6d77fb92c3 chore: remove unused imports causing CI failure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 21:48:59 +02:00
cb165b06e5 hotfix: fix map scroll jump, horizontal double scrollbars, and severe lag
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m38s
Build & Deploy / 🚀 Deploy (push) Successful in 40s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 21:46:22 +02:00
ca1b19cc0f hotfix: adjust EU badge distance and ScaleOfImpact unit spacing 2026-06-18 21:43:58 +02:00
cc22aa6c8c hotfix: resolve double header on referenzen page and fix hash link scroll bug
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m24s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-18 15:53:24 +02:00
3ae1e6257d chore: trigger deployment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 15:07:28 +02:00
1419bab7cf Revert "fix(ci): explicitly mount docker.sock via container options"
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 1s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Failing after 1s
This reverts commit ac6b121685.
2026-06-18 15:05:10 +02:00
ac6b121685 fix(ci): explicitly mount docker.sock via container options
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 15:03:45 +02:00
4a9a7adf93 fix(ci): restore deploy.yml to last working version and remove test workflow
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 15:02:02 +02:00
3b297aae3f chore: test dind service
Some checks failed
Build & Deploy / 🧪 QA (push) Blocked by required conditions
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Failing after 10s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-18 14:59:19 +02:00
c6b81a3d77 chore: test gateway dind reachability
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Test Docker Host / test (push) Successful in 2s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-18 14:58:39 +02:00
c8f3420a5c chore: test act-runner-dind reachability
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Successful in 2s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 27s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 14:55:41 +02:00
a6559eb7bb chore: test docker env
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Test Docker Host / test (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Failing after 27s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 14:52:27 +02:00
8d75c2ec12 fix(ci): restore full deployment pipeline with buildx driver docker
Some checks failed
Build & Deploy / 🏗️ Build (push) Failing after 29s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-18 14:46:38 +02:00
6a18eb4ee1 chore: test barebones build with driver: docker
Some checks failed
Build and Deploy / 🏗️ Build (push) Has been cancelled
Build and Deploy / 🔍 Prepare (push) Has been cancelled
2026-06-18 14:45:44 +02:00
d91eb220be fix(ci): restore build-push-action to fix docker socket access in runner
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-18 14:41:53 +02:00
5c40d55b21 chore: test docker
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 14:38:00 +02:00
a512bbe226 fix(ci): restore container blocks and explicitly mount docker.sock volume
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 24s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 14:19:01 +02:00
8b3851f45c fix: run CI jobs on runner host to fix docker daemon socket access
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 24s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 14:11:38 +02:00
e2667ed750 fix(ui): unify reference project naming scheme in map-data
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Failing after 26s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 14:03:36 +02:00
0253956bd9 fix(ci): remove broken maintenance step and clean up prepare
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-18 14:02:04 +02:00
42cd4394c7 fix(ci): checkout repository in prepare job to fix priority-gate execution crash
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 25s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-18 13:59:53 +02:00
276ceed010 debug(ci): bypass priority gate to prevent prepare failure
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 13:58:02 +02:00
cdf10bd670 debug(ci): hardcode GITHUB_OUTPUT entirely
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 2s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-18 13:55:08 +02:00
a1ea41b42f ci(deploy): map main branch to staging environment automatically
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 3s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 13:14:31 +02:00
561d75d2b8 Merge fix/ui-and-map-formatting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 2s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 13:08:56 +02:00
685c0ffff6 Merge fix/referenzen-map-glow
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 7s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-18 13:08:46 +02:00
17b001a97d Merge fix/header-dropdown-nav 2026-06-18 13:08:00 +02:00
275a857554 fix: ui component updates and project formatting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 4s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 15:38:56 +02:00
f766c9c9db refactor: remove redundant CallToAction component from all MDX pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 33s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 10:51:04 +02:00
c851a4f95e fix(ui): use standard Badge and Button components instead of raw HTML in MDX
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 10:42:24 +02:00
1dca7f79fe feat(map): add 15 missing minor locations from 2023-2025
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 2m3s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-17 10:16:41 +02:00
d3eee45112 fix(content): shorten guben hq name to E-TIB GmbH
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 2m0s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 21:07:48 +02:00
c308035e77 feat(ui): display standorte on references map
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-16 20:54:57 +02:00
44d8c22c97 fix(nav): add mobile hamburger menu to header to ensure parity with desktop
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 47s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 20:48:51 +02:00
9d3f918aef fix(ui): scale subcompany logos dynamically to prevent overflow
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 58s
Build & Deploy / 🏗️ Build (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 15:45:50 +02:00
a34bee6272 feat: append missing locations to map data
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 15:32:06 +02:00
373017059e fix: reduce mobile gaps in ScaleOfImpact component
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 15:14:41 +02:00
309a53624e feat: manually assign appropriate reference images as requested by client
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m3s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 14:48:25 +02:00
e994d79895 chore: release 2.2.13-rc.2
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-16 13:57:24 +02:00
88e88c8b99 fix: restore large project pins on the reference map 2026-06-16 13:57:24 +02:00
e2a29b41e9 chore: release 2.2.13-rc.1
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 58s
Build & Deploy / 🏗️ Build (push) Successful in 1m36s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 46s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 12:53:07 +02:00
62727a7231 test: update homepage test to match new copy (Mitarbeitende)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m2s
Build & Deploy / 🏗️ Build (push) Successful in 1m51s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-16 12:20:50 +02:00
3a6189e0d8 chore: optimize new client images (HEIC to JPG, resize)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Failing after 55s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 12:11:45 +02:00
01e2fdbb7d fix: move EU funding badge from header to footer to prevent overlap
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 31s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-16 12:08:55 +02:00
fa7182d351 fix: address remaining UI and content feedback from Marisa
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Failing after 56s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 11:43:26 +02:00
e0d43f7f5f fix: implement client feedback (map points, hero text, stats spacing, lang toggle)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 48s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-16 10:40:01 +02:00
d09b576137 style: hide scrollbars on mobile and fix desktop overlay scrollbars for company tiles
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-16 09:28:25 +02:00
04599827f9 feat(ui): add actual EU flag image and update path to webp
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m57s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 16:47:54 +02:00
5368f1a64d feat(ui): smooth radial fade on EU badge and setup custom image path
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-15 16:45:40 +02:00
241c97e346 feat(ui): refine EU badge into subtle triangular corner fade with text
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 57s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 16:34:50 +02:00
3334e327bf fix(ui): correct z-index and remove mix-blend-screen on EU badge
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 58s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 16:21:51 +02:00
145bff90b6 ci: implement dynamic environment priority gate (prod > rc > testing > branch)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-15 16:14:16 +02:00
d2bf48fc60 chore: add project instructions and workflow rules to INSTRUCTIONS.md 2026-06-15 16:08:23 +02:00
5e7ee84dc9 ci: fix deployment concurrency groups by target environment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Successful in 2m56s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 16:05:42 +02:00
b42e53495d 2.2.13-rc.0
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 15:54:45 +02:00
8813ed3910 fix: enforce hero remounts on route change 2026-06-15 15:54:38 +02:00
1d3c6a0f70 fix(ui): remove dummy certificates from english page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-15 15:52:36 +02:00
8fb3b96bd3 feat(ui): redesign EU flag with perspective image and gradient fade mask
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
2026-06-15 15:46:41 +02:00
67dc56d3ee fix(ui): remove hardcoded dummy certificates without downloads
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-15 15:40:19 +02:00
e5933bad69 feat(ui): optimize career page header layout 2026-06-15 15:40:14 +02:00
11b9e4630b feat(ui): add EU Funding Badge to global layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 2m32s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has started running
2026-06-15 15:29:23 +02:00
a47d378521 fix(ui): prevent text selection and ghost images during references slider drag
Some checks failed
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-15 15:28:21 +02:00
30047cb1c4 fix(nav): optimize dropdown behavior on navigation and scroll
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 44s
Build & Deploy / 🧪 QA (push) Has started running
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-15 15:24:46 +02:00
394ac5a19c style(mobile): optimize mobile layout and component spacing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
- Hide CorporateBackground on mobile to prevent content overlap
- Increase Header padding on mobile for better logo spacing
- Reduce top spacing in HeroSection on mobile
- Change SubCompanyTiles to horizontally scrollable row on mobile
- Add bottom padding to Footer to clear MobileBottomNav
- Reduce Footer Bento card padding for compact mobile view
2026-06-15 15:23:23 +02:00
d63f196693 chore(content): remove team statistics section from about page
All checks were successful
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 2m32s
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Successful in 2m34s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
2026-06-15 15:13:20 +02:00
6092f1d827 fix(tests): update english terms path from terms.mdx to agb.mdx
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 1m37s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 14:55:48 +02:00
766c385704 fix(content): replace video hero with image hero on messen pages
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 58s
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Failing after 59s
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 1s
2026-06-15 14:42:56 +02:00
6c9d2b6832 fix(content): update images and review feedback
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 21s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Failing after 59s
Build & Deploy / 🧪 QA (push) Failing after 49s
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 1s
2026-06-15 13:54:57 +02:00
7a9d4d7f18 fix(content): Update URL for Werkstatt Kabel 2026
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 49s
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-15 13:41:03 +02:00
0b9929c2ec fix(content): Address all user review feedback
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Failing after 50s
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
- Replace Mitarbeiter with Mitarbeitenden
- Add specialization sentence to Kabeltiefbau
- Update Werkstatt Kabel link
- Add Karriere to main navigation
- Add Referenzen to Footer
- Fix 404 for EN legal pages by using consistent internal slugs
2026-06-15 13:38:52 +02:00
aa0dfdf331 fix: refactor standorte detail page layout to match standard mdx pages and fix bohrtechnik links
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m10s
Build & Deploy / 🏗️ Build (push) Successful in 2m13s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 45s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 11:26:09 +02:00
4c9028f58b feat(map): separate standorte and project map variants
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 1m36s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 00:19:56 +02:00
9de5f2938b fix(map): include standorteLocations in md pages
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-15 00:02:20 +02:00
de41a0e29d fix(nav): simplify standorte in dropdown
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m7s
Build & Deploy / 🏗️ Build (push) Successful in 2m6s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 23:47:25 +02:00
6689dd3013 fix(standorte): fix layout and navigation structure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 57s
Build & Deploy / 🏗️ Build (push) Successful in 2m6s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 23:40:19 +02:00
31fbcb32dd feat(standorte): re-add kirchheilingen, add to dropdown, unify maps
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 23:23:31 +02:00
68eca73df2 fix: rename Bohrtechnik competence title, remove Kirchheilingen from standorte description, fix google maps lychee link
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 57s
Build & Deploy / 🏗️ Build (push) Successful in 1m44s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 23:05:51 +02:00
3bb6eb3447 fix: revert bohrtechnik to competence link, remove kirchheilingen location
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 42s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 22:46:16 +02:00
a3c732dd13 fix: add individual standorte to header dropdown
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 44s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Failing after 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 22:33:33 +02:00
aaa1a8793c fix(types): resolve typecheck errors for SubCompanyTiles and standorteLocations
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 41s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-14 22:02:36 +02:00
233509db67 fix(links): update all maps and tiles to use standorte pages as single source of truth
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 43s
Build & Deploy / 🧪 QA (push) Failing after 1m5s
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-14 21:40:48 +02:00
17a807c48b feat: integrate standalone standorte pages with real photos and map routing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 41s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-14 10:03:46 +02:00
d8634efa9a fix(ui): make reference slider tiles clickable links
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 1m38s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-14 09:52:11 +02:00
0f2aa500e9 test(smoke): fix ERR_ABORTED and form hydration timing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 47s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 41s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-14 09:26:14 +02:00
2c465bc16b test(smoke): apply Gatekeeper Test Resilience standard
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m8s
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-14 09:23:48 +02:00
68717bf0ce chore: setup analytics, error tracking, and configure mailgun sender
All checks were successful
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m39s
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 22:55:34 +02:00
6444281c6f fix(mail): use E-TIB GmbH instead of KLZ Cables for confirmation emails
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m39s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 2m7s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 20:10:01 +02:00
4b98fa9b4c fix(map): update locations count to 3 & sync EN md structure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m40s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 19:47:21 +02:00
fa4385af4c feat: Add historical data from 2016 to GrowthChart with i18n support
All checks were successful
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m31s
Build & Deploy / 🔍 Prepare (push) Successful in 33s
Build & Deploy / 🧪 QA (push) Successful in 1m7s
Build & Deploy / 🏗️ Build (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Successful in 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 19:07:49 +02:00
d98eb20430 fix(content): sync English copy with recent German corrections
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m38s
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 2m6s
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-13 19:02:16 +02:00
3cfe8ff379 fix(content): update copy as requested by user screenshots
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m30s
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
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-13 18:57:25 +02:00
2abcd386f0 feat(referenzen): implement smart metric cards and typography improvements
Some checks failed
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m30s
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Has started running
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-13 18:52:57 +02:00
d7ff6a3415 fix(map): update Kirchheilingen marker naming to exactly match requested text from email
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m39s
2026-06-13 18:52:11 +02:00
d8f5d9bfc6 fix(data): update project quantities and totals from data sources
All checks were successful
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m34s
Build & Deploy / 🔍 Prepare (push) Successful in 35s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 2m6s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 14:36:14 +02:00
f81a85fdd7 feat: Implement client feedback & visual verifications
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 30s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m41s
Build & Deploy / 🧪 QA (push) Successful in 57s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
- Update contact map container alignment
- Remove outdated marketing copy on service pages
- Update HDD specs to 400m / 600mm
- Unify reference slider titles globally
- Add employee breakdown stats to Über Uns page
2026-06-13 14:30:23 +02:00
4968315ebf fix(content): update stats, links, and design accents
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m42s
Build & Deploy / 🧪 QA (push) Successful in 57s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-13 12:08:59 +02:00
eaaa3b7251 fix(ci): clean up stray docker containers on ssh deploy to prevent network delete failure
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m36s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 2m4s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-13 10:22:22 +02:00
bbc8716da6 chore: retry deploy after network release
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m36s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Failing after 29s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-13 10:09:06 +02:00
f4d65452a6 chore: restrict build worker CPUs to 1 on CI to prevent OOM
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 1m39s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Failing after 56s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-06-13 09:16:20 +02:00
1b2e0793d3 chore: trigger rebuild
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m40s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m6s
Build & Deploy / 🏗️ Build (push) Failing after 15m43s
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-13 02:35:20 +02:00
b378bb168f feat: implement visual feedback updates (team grid, contact maps, job titles, and interactive map link)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 2m11s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m43s
Build & Deploy / 🏗️ Build (push) Failing after 13m14s
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-12 19:17:13 +02:00
9134d6b070 feat(task-12): link Bohrtechnik to service pages and remove reference detailed links
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 2m7s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m45s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-10 11:39:38 +02:00
0105c8c9a3 feat(content): update website content for tasks 6-10 including competencies, team, references, and statistics
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 54s
Build & Deploy / 🧪 QA (push) Successful in 2m7s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m31s
Build & Deploy / 🏗️ Build (push) Successful in 3m47s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-10 10:59:14 +02:00
0138064bcc fix(ci): switch network subnet mapping to 10.199.x.x space to guarantee no overlaps on host
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 37s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m15s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 23:23:23 +02:00
f74df4e8a2 fix(ci): switch network subnet mapping to 192.168.x.x space to avoid overlap with host Docker pools
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 46s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m24s
Build & Deploy / 🚀 Deploy (push) Failing after 33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-05-28 23:14:47 +02:00
cf29d0c45a fix(ci): introduce dynamic static subnet mapping to bypass Docker automatic address pool allocation limits
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 50s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m21s
Build & Deploy / 🚀 Deploy (push) Failing after 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-05-28 23:07:09 +02:00
4aebd226f1 fix(ci): replace Buildx with Native Docker Buildkit to run on host with 92.3 GB free space and prevent overlayfs ResourceExhausted limits
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 3m38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-28 22:59:12 +02:00
528bf83a67 fix(ci): restore Buildx caching by removing no-cache now that registry cache conflict is resolved
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Failing after 4m2s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-28 22:51:55 +02:00
05ca944828 fix(ci): use docker system prune on target host to clean stopped containers and unlock networks
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Failing after 4m9s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-28 22:44:04 +02:00
f2209c4815 fix(ci): bypass Gitea registry layer cache conflict by adding no-cache to build-push-action
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m49s
Build & Deploy / 🏗️ Build (push) Successful in 4m25s
Build & Deploy / 🚀 Deploy (push) Failing after 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-28 22:35:49 +02:00
ded2aee989 fix(ci): add docker network prune before deployment on target host
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Failing after 4m38s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 22:10:34 +02:00
37ecb81444 feat(legal): write comprehensive B2B AGB and Terms and Conditions pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Failing after 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 20:26:27 +02:00
343e5a976c fix(ci): remove slow du commands to prevent runner hang
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 4m27s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 17:19:51 +02:00
8817b5d586 fix(ci): replace slow du with fast checks and log truncation to prevent runner hang
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-05-28 17:17:12 +02:00
fb1e7efd30 fix(ci): refine disk breakdown diagnostics with sh -c to expand wildcards correctly
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-05-28 17:13:12 +02:00
3a7100d8b0 fix(ci): aggressive buildkit container removal and system prune with volumes to solve ENOSPC
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m11s
Build & Deploy / 🏗️ Build (push) Failing after 2m45s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 17:08:05 +02:00
50d3d827e8 fix(messen): update trade fair dates and details for 2026
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m9s
Build & Deploy / 🏗️ Build (push) Failing after 2m57s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 16:42:49 +02:00
79813f5d8a chore(qa): add compare-mdx verification script and finalize parity check
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m8s
Build & Deploy / 🏗️ Build (push) Failing after 2m50s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 15:04:08 +02:00
4c0ee13d97 fix(en): add missing clearance and trade register certificates to English page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-05-28 15:03:05 +02:00
a95fddca56 fix(cta): add backwards-compatible aliases in CallToAction for MDX parameters
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m8s
Build & Deploy / 🏗️ Build (push) Failing after 2m43s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 14:56:19 +02:00
f110ce9d46 fix(en): add missing href links to Everything from a single source bento grid
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 51s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-05-28 14:55:03 +02:00
68c4a056b7 fix: update trade fair dates to 2026
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m9s
Build & Deploy / 🏗️ Build (push) Successful in 2m45s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 46s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 13:43:49 +02:00
57a67ffdf3 feat: refine subpages layout, slugs and B2B copywriting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m8s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🚀 Deploy (push) Failing after 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 13:22:37 +02:00
ed802d03e7 fix: clean location names in reference badges and detail pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 17s
Build & Deploy / 🧪 QA (push) Failing after 58s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-15 21:52:58 +02:00
68b0ed7ba0 fix: render map as card on references page to enable glowing border 2026-05-15 21:15:57 +02:00
1966 changed files with 314250 additions and 9356 deletions

8
.env
View File

@@ -1,5 +1,5 @@
# ============================================================================
# KLZ Cables - Environment Configuration
# E-TIB GmbH - Environment Configuration
# ============================================================================
# Copy this file to .env for local development
# For production, use .env.production as a template
@@ -20,14 +20,14 @@ NEXT_PUBLIC_RECORD_MODE_ENABLED=true
# Analytics (Umami)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable analytics
UMAMI_WEBSITE_ID=
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# ────────────────────────────────────────────────────────────────────────────
# Error Tracking (GlitchTip/Sentry)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable error tracking
SENTRY_DSN=
SENTRY_DSN=https://dcb81958-dbf2-4a3d-b422-875f4672c14b@glitchtip.infra.mintel.me/5
# ────────────────────────────────────────────────────────────────────────────
# Email Configuration (SMTP)
@@ -37,7 +37,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@mg.mintel.me>
MAIL_RECIPIENTS=info@e-tib.com
# ────────────────────────────────────────────────────────────────────────────

View File

@@ -13,13 +13,14 @@ on:
required: false
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
concurrency:
group: deploy-pipeline
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────────────────────────────────────
@@ -64,10 +65,17 @@ jobs:
echo "Pruning Buildkit inside container: $container"
docker exec -i "$container" buildctl prune --all || true
done
echo "Removing buildkit builder containers to clear overlay storage..."
containers=$(docker ps -a --filter "name=buildx_buildkit_builder" -q)
if [ -n "$containers" ]; then
docker rm -f $containers || true
fi
echo "=== Truncating host Docker container log files ==="
docker run --rm -v /:/host-root alpine sh -c "truncate -s 0 /host-root/mnt/HC_Volume_104796416/docker/containers/*/*.log" || true
echo "Purging old build layers, unused volumes and images..."
docker builder prune -a -f || true
docker volume prune -f || true
docker system prune -a -f || true
docker system prune -a -f --volumes || true
echo "=== System Disk Usage After Prune ==="
df -h || true
@@ -85,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"
@@ -139,6 +147,13 @@ jobs:
echo "short_sha=$SHORT_SHA"
} >> "$GITHUB_OUTPUT"
- name: 🚦 Priority Gate
env:
GITEA_PAT: ${{ secrets.GITEA_PAT }}
CURRENT_RUN_ID: ${{ github.run_id }}
CURRENT_TARGET: ${{ steps.determine.outputs.target }}
run: bash scripts/priority-gate.sh
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: QA (Lint, Typecheck, Test)
# ──────────────────────────────────────────────────────────────────────────────
@@ -213,28 +228,33 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/registry-auth.sh
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: 🏗️ Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
provenance: false
platforms: linux/amd64
build-args: |
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_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 }}
tags: registry.infra.mintel.me/mintel/e-tib.com:${{ needs.prepare.outputs.image_tag }}
secrets: |
"NPM_TOKEN=${{ steps.auth.outputs.working_token }}"
- name: 🏗️ Build and Push (Native Docker Buildkit)
env:
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_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 }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
run: |
echo "$NPM_TOKEN" > npm_token.txt
DOCKER_BUILDKIT=1 docker build \
--secret id=NPM_TOKEN,src=npm_token.txt \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_TARGET="$NEXT_PUBLIC_TARGET" \
--build-arg NEXT_PUBLIC_APP_VERSION="$IMAGE_TAG" \
--build-arg UMAMI_WEBSITE_ID="$UMAMI_WEBSITE_ID" \
--build-arg UMAMI_API_ENDPOINT="$UMAMI_API_ENDPOINT" \
--build-arg NPM_TOKEN="$NPM_TOKEN" \
--build-arg NPM_DOMAIN="$NPM_DOMAIN" \
-t registry.infra.mintel.me/mintel/e-tib.com:"$IMAGE_TAG" \
.
docker push registry.infra.mintel.me/mintel/e-tib.com:"$IMAGE_TAG"
rm -f npm_token.txt
# ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy
@@ -294,6 +314,19 @@ jobs:
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
if [[ "$TARGET" == "production" ]]; then
INTERNAL_SUBNET="10.199.10.0/24"
elif [[ "$TARGET" == "testing" ]]; then
INTERNAL_SUBNET="10.199.11.0/24"
elif [[ "$TARGET" == "staging" ]]; then
INTERNAL_SUBNET="10.199.12.0/24"
else
HEX_SHA="${{ needs.prepare.outputs.short_sha }}"
DEC_SHA=$((16#$HEX_SHA))
SUBNET_NUM=$(( 13 + (DEC_SHA % 230) ))
INTERNAL_SUBNET="10.199.${SUBNET_NUM}.0/24"
fi
{
echo "# Generated by CI - $TARGET"
echo "IMAGE_TAG=$IMAGE_TAG"
@@ -330,6 +363,7 @@ jobs:
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
echo "AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE"
echo "AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED"
echo "INTERNAL_SUBNET=$INTERNAL_SUBNET"
} > .env.deploy
- name: 🚀 SSH Deploy
@@ -337,6 +371,7 @@ jobs:
env:
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
SLUG=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
@@ -349,7 +384,6 @@ jobs:
elif [[ "$TARGET" == "staging" ]]; then
SITE_DIR="/home/deploy/sites/staging.e-tib.com"
else
SLUG=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
SITE_DIR="/home/deploy/sites/branch.e-tib.com/$SLUG"
fi
@@ -357,6 +391,8 @@ jobs:
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
ssh root@alpha.mintel.me "docker system prune -f"
ssh root@alpha.mintel.me "docker rm -f \$(docker ps -a -q --filter name=${SLUG}-etib-) 2>/dev/null || true"
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"

4
.gitignore vendored
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
2019
Offener Tiefbau 32445 Meter
HDD 8505Meter
Kabelverlegung 87468 Meter
2020
Offener Tiefbau 31305 Meter
HDD 9050 Meter
Kabelverlegung 93556 Meter
2021
Offener Tiefbau 38756 Meter
HDD 10957 Meter
Kabelverlegung 110127 Meter

Binary file not shown.

View File

@@ -0,0 +1,8 @@
2022
Offener Tiefbau 41445 Meter
HDD 9505Meter
Kabelverlegung 102468 Meter

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,119 +0,0 @@

> e-tib-nextjs@2.2.12 lint /Volumes/Alpha SSD/Coding/e-tib.com
> eslint .
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/[slug]/page.tsx
1:20 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars
67:24 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/blog/[slug]/page.tsx
1:20 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars
39:24 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/error.tsx
7:30 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/not-found.tsx
6:10 warning 'Terminal' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/[slug]/page.tsx
91:26 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/page.tsx
1:21 warning 'Heading' is defined but never used @typescript-eslint/no-unused-vars
1:30 warning 'Badge' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/JsonLd.tsx
1:30 warning 'Graph' is defined but never used @typescript-eslint/no-unused-vars
43:31 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars
48:29 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CertificatesBlock.tsx
6:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
9:10 warning 'FileText' is defined but never used @typescript-eslint/no-unused-vars
77:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CertificatesBlock.tsx:77:5
75 |
76 | useEffect(() => {
> 77 | setIsMounted(true);
| ^^^^^^^^^^^^ Avoid calling setState() directly within an effect
78 | }, []);
79 |
80 | const badgeText = badge || 'Zertifizierungen & Nachweise'; react-hooks/set-state-in-effect
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CompetenceBentoGrid.tsx
53:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CompetenceBentoGrid.tsx:53:5
51 |
52 | useEffect(() => {
> 53 | setIsMounted(true);
| ^^^^^^^^^^^^ Avoid calling setState() directly within an effect
54 | }, []);
55 |
56 | const badge = props.badge || data?.badge || 'Leistungsspektrum'; react-hooks/set-state-in-effect
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/HeroVideo.tsx
5:18 warning 'AnimatePresence' is defined but never used @typescript-eslint/no-unused-vars
6:10 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars
6:20 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars
45:12 warning 'e' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/InteractiveGermanyMap.tsx
5:46 warning 'ArrowUpRight' is defined but never used @typescript-eslint/no-unused-vars
147:23 warning 'isNearRight' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/TeamGridBlock.tsx
20:39 warning 'title' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
20:46 warning 'department' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
20:58 warning 'showContact' is assigned a value but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/decorations/CorporateBackground.tsx
36:13 warning 'distance' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/home/Hero.tsx
9:39 warning 'useCallback' is defined but never used @typescript-eslint/no-unused-vars
77:6 warning React Hook useEffect has a missing dependency: 'HERO_PLACEHOLDERS'. Either include it or remove the dependency array react-hooks/exhaustive-deps
/Volumes/Alpha SSD/Coding/e-tib.com/components/home/MeetTheTeam.tsx
8:9 warning 'teamT' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/layout/Footer.tsx
4:35 warning 'Accessibility' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/providers/InitialLoader.tsx
5:8 warning 'Image' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/providers/PageTransitionShutter.tsx
6:8 warning 'Image' is defined but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/search/AIOrb.tsx
153:13 warning 'breathe' is assigned a value but never used @typescript-eslint/no-unused-vars
212:15 warning 'targetExcite' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/Alpha SSD/Coding/e-tib.com/components/search/AISearchResults.tsx
99:6 warning React Hook useEffect has missing dependencies: 'handleSearch' and 'initialQuery'. Either include them or remove the dependency array react-hooks/exhaustive-deps
/Volumes/Alpha SSD/Coding/e-tib.com/lib/blog.ts
6:7 warning 'BLOG_DIR' is assigned a value but never used @typescript-eslint/no-unused-vars
94:7 warning 'next' is never reassigned. Use 'const' instead prefer-const
95:7 warning 'prev' is never reassigned. Use 'const' instead prefer-const
✖ 38 problems (0 errors, 38 warnings)
0 errors and 2 warnings potentially fixable with the `--fix` option.

View File

@@ -1,31 +0,0 @@

> e-tib-nextjs@2.2.12 test /Volumes/Alpha SSD/Coding/e-tib.com
> vitest run --passWithNoTests
[?25l
RUN v4.1.2 /Volumes/Alpha SSD/Coding/e-tib.com
[?2026h
lib/mail/mailer.test.ts [queued]
Test Files 0 passed (2)
Tests 0 passed (0)
Start at 11:47:51
Duration 504ms
[?2026l[?2026h ✓ lib/mail/mailer.test.ts (1 test) 2ms
lib/env.test.ts 0/6
Test Files 1 passed (2)
Tests 1 passed (7)
Start at 11:47:51
Duration 605ms
[?2026l ✓ lib/env.test.ts (6 tests) 3ms
Test Files 2 passed (2)
Tests 7 passed (7)
Start at 11:47:51
Duration 672ms (transform 113ms, setup 0ms, import 222ms, tests 5ms, environment 813ms)
[?25h

View File

@@ -1,5 +0,0 @@

> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
> tsc --noEmit

View File

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

25
INSTRUCTIONS.md Normal file
View File

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

BIN
after-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -10,7 +10,6 @@ import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { getAllReferences } from '@/lib/references';
import { defaultLocations, minorLocations } from '@/lib/map-data';
// Import components used in MDX
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
@@ -18,7 +17,6 @@ import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blo
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { CompanyTimeline } from '@/components/blocks/CompanyTimeline';
import { JobListingBlock } from '@/components/blocks/JobListingBlock';
import { CallToAction } from '@/components/blocks/CallToAction';
import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
@@ -41,7 +39,6 @@ const mdxComponents = {
HomeReferencesSlider,
CompanyTimeline,
JobListingBlock,
CallToAction,
ServiceDetailGrid,
BenefitGrid,
InteractiveGermanyMap,
@@ -51,6 +48,7 @@ const mdxComponents = {
TeamGrid,
JsonLd,
Button,
Badge,
AnimatedCounter,
GrowthChart,
DeepDrillAnimation,
@@ -187,7 +185,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
<div className={`flex flex-col min-h-screen ${isFullBleed ? '' : 'bg-white'}`}>
{/* Generic Hero Section (only for standard pages) */}
{!isFullBleed && (
<section className="bg-primary-dark text-white pt-40 pb-12 md:pt-56 md:pb-16 min-h-[40vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<section key={`hero-section-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<div className="absolute inset-0 opacity-20 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
</div>

View File

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

View File

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

View File

@@ -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';
@@ -19,7 +20,6 @@ import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
import { InitialLoader } from '@/components/providers/InitialLoader';
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
@@ -36,9 +36,9 @@ export async function generateMetadata(props: {
return {
title: {
template: '%s | E-TIB',
default: 'E-TIB | Die Experten für Kabeltiefbau',
default: 'E-TIB | Die Experten für Kabelnetzbau',
},
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
metadataBase: new URL(baseUrl),
manifest: '/manifest.webmanifest',
alternates: {
@@ -93,13 +93,18 @@ export default async function Layout(props: {
children: [
{ label: safeLocale === 'de' ? 'Firma' : 'Company', url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}` },
{ label: safeLocale === 'de' ? 'Unser Team' : 'Our Team', url: `/${safeLocale}/team` },
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` }
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` },
{ label: safeLocale === 'de' ? 'Standorte' : 'Locations', url: `/${safeLocale}/standorte` }
]
},
{
label: safeLocale === 'de' ? 'Referenzen' : 'References',
url: `/${safeLocale}/referenzen`
},
{
label: safeLocale === 'de' ? 'Karriere' : 'Career',
url: `/${safeLocale}/${safeLocale === 'de' ? 'karriere' : 'career'}`
},
{
label: safeLocale === 'de' ? 'Messen' : 'Events',
url: `/${safeLocale}/${await mapFileSlugToTranslated('messen', safeLocale)}`
@@ -130,6 +135,7 @@ export default async function Layout(props: {
'CompanyTimeline',
'TeamGrid',
'AISearch',
'GrowthChart',
];
const clientMessages: Record<string, any> = {};
for (const key of clientKeys) {
@@ -209,6 +215,7 @@ export default async function Layout(props: {
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
{process.env.TARGET !== 'production' && <AnnotatorClientWrapper />}
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
</TransitionProvider>
</NextIntlClientProvider>

View File

@@ -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,186 +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" {...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 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) => (
<li className="bg-neutral-50 p-6 rounded-2xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-shadow group" {...props}>
<div className="w-6 h-6 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-3.5 h-3.5 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-snug">{props.children}</span>
</li>
),
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
blockquote: (props: any) => (
<blockquote className="border-l-4 border-primary pl-6 py-3 my-10 italic bg-neutral-50 rounded-r-2xl text-neutral-700 font-medium max-w-3xl shadow-sm" {...props} />
),
hr: (props: any) => <hr className="my-16 border-t-2 border-neutral-100 max-w-3xl" {...props} />,
img: (props: any) => <img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" {...props} />,
};
return (
<div className="flex flex-col min-h-screen bg-white">
{/* Hero Section */}
<section className="bg-[#050B14] text-white pt-40 pb-16 md:pt-48 md:pb-20 min-h-[35vh] 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-25 mix-blend-luminosity scale-105"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/60 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-transparent to-transparent" />
</div>
)}
<div className="absolute inset-0 opacity-20 z-0">
<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"
eventProperties={{ location: 'reference_back_btn' }}
>
<ChevronLeft className="w-5 h-5" />
{t('backToOverview')}
</TrackedLink>
</div>
<div className="max-w-4xl">
<Badge variant="accent" className="mb-4 md:mb-6">
{t('projectReference')}
</Badge>
<Heading level={1} variant="white" className="mb-8 leading-tight">
{reference.frontmatter.title}
</Heading>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12">
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<MapPin className="w-8 h-8 text-primary shrink-0" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('location')}</p>
<p className="font-semibold text-lg">{reference.frontmatter.location}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Briefcase className="w-8 h-8 text-primary shrink-0" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('client')}</p>
<p className="font-semibold text-lg">{reference.frontmatter.client}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Calendar className="w-8 h-8 text-primary shrink-0" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('period')}</p>
<p className="font-semibold text-lg">{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,22 @@ 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, ArrowUpRight } from 'lucide-react';
import { MapPin, Calendar, Briefcase } 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 mdxComponents = {
ul: (props: any) => <ul className="list-disc pl-5 mb-6 text-neutral-600 text-sm space-y-2" {...props} />,
li: (props: any) => <li className="pl-1" {...props} />,
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
};
interface PageProps {
params: Promise<{
locale: string;
@@ -22,7 +31,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
return {
title: 'Referenzen | E-TIB Gruppe',
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabeltiefbau, Spülbohrtechnik und Netzinfrastruktur.',
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabelnetzbau, Spülbohrtechnik und Netzinfrastruktur.',
alternates: {
canonical: `${SITE_URL}/${locale}/referenzen`,
languages: {
@@ -72,27 +81,26 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
{/* Map Hero Section */}
{/* Map Section */}
<InteractiveGermanyMap
isHero={true}
badge={locale === 'en' ? 'Our References' : 'Unsere Referenzen'}
title={locale === 'en' ? 'Successfully realized projects.' : 'Erfolgreich umgesetzte Projekte.'}
description={locale === 'en' ? 'From broadband expansion to complex 110kV lines: A selection of our nationwide projects where we have created infrastructure for the future. Discover our locations.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben. Entdecken Sie unsere Standorte.'}
description={locale === 'en' ? 'From broadband expansion to complex 110kV lines: A selection of our nationwide projects where we have created infrastructure for the future.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben.'}
locations={enrichedLocations}
stats={[
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
{ value: '200', suffix: '+', label: locale === 'en' ? 'Completed Projects' : 'Abgeschlossene Projekte' }
]}
/>
{/* Main Content Area */}
<Container className="relative z-20 mt-16 md:mt-24">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{references.map((ref) => (
<TrackedLink
<div
key={ref.slug}
href={`/${locale}/referenzen/${ref.slug}`}
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-2"
eventProperties={{
location: 'referenzen_grid',
target_slug: ref.slug,
}}
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
@@ -102,14 +110,14 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
src={ref.frontmatter.featuredImage}
alt={ref.frontmatter.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:grayscale-0"
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%]"
/>
) : (
<div className="absolute inset-0 bg-[#f4f4f5] flex items-center justify-center">
<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 group-hover:bg-transparent transition-colors duration-500" />
<div className="absolute inset-0 bg-black/5" />
{/* Location Badge */}
<div className="absolute top-4 left-4 z-10">
@@ -122,12 +130,19 @@ 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 group-hover:text-primary transition-colors line-clamp-3 leading-[1.2]">
<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 pr-16 md:pr-20 relative z-10">
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
<div className="min-w-0">
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">{t('client')}</span>
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
@@ -144,13 +159,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
</div>
</div>
</div>
{/* Industrial Hover Indicator */}
<div className="absolute right-6 bottom-6 md:right-8 md:bottom-8 w-10 h-10 bg-neutral-50 border border-neutral-200 rounded flex items-center justify-center transform group-hover:bg-primary group-hover:border-primary transition-colors duration-300">
<ArrowUpRight className="w-5 h-5 text-neutral-dark group-hover:text-white transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</TrackedLink>
</div>
))}
</div>

View File

@@ -0,0 +1,187 @@
import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { standorteData, getStandortById } from '@/lib/standorte-data';
import { SITE_URL } from '@/lib/schema';
import Image from 'next/image';
import { MapPin, Phone, Mail, Navigation, CheckCircle2, ArrowUpRight } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import TrackedLink from '@/components/analytics/TrackedLink';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
interface PageProps {
params: Promise<{
locale: string;
slug: string;
}>;
}
export async function generateStaticParams() {
const locales = ['de', 'en'];
const params: { locale: string; slug: string }[] = [];
for (const locale of locales) {
for (const standort of standorteData) {
params.push({ locale, slug: standort.id });
}
}
return params;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params;
if (locale !== 'de' && locale !== 'en') return {};
const standort = getStandortById(slug);
if (!standort) return {};
return {
title: `${standort.name} | E-TIB Gruppe`,
description: standort.description[locale],
alternates: {
canonical: `${SITE_URL}/${locale}/standorte/${slug}`,
languages: {
de: `${SITE_URL}/de/standorte/${slug}`,
en: `${SITE_URL}/en/standorte/${slug}`,
'x-default': `${SITE_URL}/en/standorte/${slug}`,
},
},
};
}
export default async function StandortDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
const { locale, slug } = await props.params;
const safeLocale = locale === 'de' ? 'de' : 'en';
setRequestLocale(safeLocale);
const t = await getTranslations('StandardPage');
const standort = getStandortById(slug);
if (!standort) {
notFound();
}
return (
<div className="flex flex-col min-h-screen bg-white">
{/* Generic Hero Section to match standard pages */}
<section key={`standort-hero-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
<div className="absolute inset-0 opacity-20 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
</div>
<Container className="relative z-10">
<div className="max-w-4xl">
<Badge variant="accent" className="mb-4 md:mb-6 shadow-sm flex inline-flex items-center gap-2">
<MapPin className="w-4 h-4" />
{standort.type === 'hq' ? (safeLocale === 'de' ? 'Hauptsitz' : 'Headquarters') : (safeLocale === 'de' ? 'Niederlassung' : 'Branch')}
</Badge>
<Heading level={1} variant="white" className="mb-0 drop-shadow-sm">
{standort.name}
</Heading>
</div>
</Container>
</section>
{/* Main Content Area */}
<Container className="py-16 md:py-24">
{/* Excerpt/Lead paragraph */}
<div className="mb-16 max-w-4xl mx-auto">
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
{standort.description[safeLocale]}
</p>
</div>
{/* Main content in prose style */}
<div className="max-w-4xl mx-auto">
<div className="prose prose-lg md:prose-xl prose-neutral max-w-none prose-headings:font-heading prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:border-b prose-h2:border-neutral-200 prose-h2:pb-4 prose-h3:text-2xl prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:font-medium prose-a:underline prose-a:underline-offset-4 prose-a:decoration-primary/30 hover:prose-a:decoration-primary prose-strong:text-text-primary prose-strong:font-semibold prose-li:text-text-secondary prose-ul:list-disc prose-ol:list-decimal">
<img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" src={standort.image} alt={standort.name} />
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">{safeLocale === 'de' ? 'Unsere Leistungen vor Ort' : 'Our Local Services'}</h2>
<ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl">
{standort.keyFeatures[safeLocale].map((feature, idx) => (
<li key={idx} className="relative pl-6 before:content-[''] before:absolute before:left-0 before:top-[0.6em] before:w-2 before:h-2 before:bg-primary/50 before:rounded-sm">
{feature}
</li>
))}
</ul>
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">{safeLocale === 'de' ? 'Kontakt & Anfahrt' : 'Contact & Directions'}</h2>
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
<strong>{safeLocale === 'de' ? 'Adresse:' : 'Address:'}</strong><br/>
{standort.address.street}<br/>
{standort.address.city}
</p>
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
<TrackedLink
href={`https://maps.google.com/?q=${encodeURIComponent(`${standort.address.street}, ${standort.address.city}`)}`}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold flex items-center gap-1 w-fit"
eventProperties={{ location: 'standort_maps_link', slug: standort.id }}
>
{safeLocale === 'de' ? 'In Google Maps öffnen' : 'Open in Google Maps'}
<ArrowUpRight className="w-4 h-4 inline-block" />
</TrackedLink>
</p>
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
<strong>{safeLocale === 'de' ? 'Telefon:' : 'Phone:'}</strong><br/>
<a href={`tel:${standort.contact.phone.replace(/[^0-9+]/g, '')}`} className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold">
{standort.contact.phone}
</a>
</p>
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
<strong>Email:</strong><br/>
<a href={`mailto:${standort.contact.email}`} className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold">
{standort.contact.email}
</a>
</p>
{standort.gallery && standort.gallery.length > 0 && (
<>
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">Galerie</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose my-12">
{standort.gallery.map((img, idx) => (
<div key={idx} className="relative aspect-[4/3] rounded-2xl overflow-hidden bg-neutral-100 border border-neutral-200 shadow-md hover:shadow-xl transition-shadow duration-300">
<Image src={img} alt={`Gallery ${idx + 1}`} fill className="object-cover transition-transform duration-700 hover:scale-105" />
</div>
))}
</div>
</>
)}
</div>
</div>
</Container>
{/* 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="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>
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
<TrackedLink
href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
className={getButtonClasses('accent', 'lg')}
eventProperties={{
location: 'standort_page_support_cta',
page_slug: slug,
}}
>
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
&rarr;
</span>
</span>
<ButtonOverlay variant="accent" />
</TrackedLink>
</div>
</div>
</Container>
</div>
);
}

View File

@@ -0,0 +1,142 @@
import { Container } from '@/components/ui';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import TrackedLink from '@/components/analytics/TrackedLink';
import { MapPin, Navigation, ArrowUpRight } from 'lucide-react';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { SITE_URL } from '@/lib/schema';
import Image from 'next/image';
import { standorteData, standorteLocations } from '@/lib/standorte-data';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
interface PageProps {
params: Promise<{
locale: string;
}>;
}
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
const safeLocale = locale === 'de' ? 'de' : 'en';
return {
title: safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte',
description: safeLocale === 'en' ? 'Our operational locations across Germany.' : 'Unsere operativen Standorte deutschlandweit.',
alternates: {
canonical: `${SITE_URL}/${safeLocale}/standorte`,
languages: {
de: `${SITE_URL}/de/standorte`,
en: `${SITE_URL}/en/standorte`,
'x-default': `${SITE_URL}/en/standorte`,
},
},
};
}
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
const { locale } = await props.params;
const safeLocale = locale === 'de' ? 'de' : 'en';
setRequestLocale(safeLocale);
const t = await getTranslations('StandardPage');
return (
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
{/* Map Hero Section */}
<InteractiveGermanyMap
isHero={true}
locations={standorteLocations}
badge={safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte'}
title={safeLocale === 'en' ? 'Nationwide operational for you.' : 'Deutschlandweit für Sie im Einsatz.'}
description={safeLocale === '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.'}
/>
{/* Main Content Area */}
<Container className="relative z-20 mt-16 md:mt-24">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{standorteData.map((standort) => (
<TrackedLink
key={standort.id}
href={`/${safeLocale}/standorte/${standort.id}`}
eventProperties={{ location: 'standorte_list_card', slug: standort.id }}
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"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
<div className="relative h-64 w-full bg-[#050B14] overflow-hidden shrink-0 border-b border-neutral-100">
<Image
src={standort.image}
alt={standort.name}
fill
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0"
/>
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
{/* Location Badge */}
<div className="absolute top-4 left-4 z-10">
<span className="bg-white/95 backdrop-blur-md text-neutral-dark px-3 py-1.5 rounded-sm text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 shadow-sm border border-neutral-200">
<MapPin className="w-3 h-3 text-primary" />
{standort.address.city}
</span>
</div>
</div>
{/* Content Section */}
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
<div className="flex items-start justify-between gap-4 mb-4">
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
{standort.name}
</h3>
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
</div>
<p className="text-neutral-500 text-sm leading-relaxed mb-8 line-clamp-3">
{standort.description[safeLocale]}
</p>
{/* 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">
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">
{safeLocale === 'de' ? 'Adresse' : 'Address'}
</span>
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
<Navigation className="w-3.5 h-3.5 text-primary shrink-0 mt-[2px]" />
<span className="line-clamp-2">{standort.address.street}, {standort.address.city}</span>
</span>
</div>
</div>
</div>
</div>
</TrackedLink>
))}
</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="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>
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
<TrackedLink
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
className={getButtonClasses('accent', 'lg')}
eventProperties={{
location: 'standorte_support_cta',
}}
>
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
&rarr;
</span>
</span>
<ButtonOverlay variant="accent" />
</TrackedLink>
</div>
</div>
</Container>
</div>
);
}

View File

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

View File

@@ -99,7 +99,7 @@ export async function sendContactFormAction(formData: FormData) {
const confirmationHtml = await render(
React.createElement(ConfirmationMessage, {
name,
clientName: 'KLZ Cables',
clientName: 'E-TIB GmbH',
// brandColor: '#82ed20', // Optional: could be KLZ specific
}),
);

View File

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

View File

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

View File

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

View File

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

@@ -13,11 +13,13 @@ const LeafletMap = dynamic(() => import('@/components/LeafletMap'), {
});
interface ContactMapProps {
address: string;
lat: number;
lng: number;
address?: string;
lat?: number;
lng?: number;
locations?: Array<{name: string, address: string, lat: number, lng: number}>;
}
export default function ContactMap({ address, lat, lng }: ContactMapProps) {
return <LeafletMap locations={[{ name: 'E-TIB Gruppe', address, lat, lng }]} />;
export default function ContactMap({ address, lat, lng, locations }: ContactMapProps) {
const mapLocations = locations || (address && lat && lng ? [{ name: 'E-TIB Gruppe', address, lat, lng }] : []);
return <LeafletMap locations={mapLocations} />;
}

View File

@@ -12,6 +12,8 @@ interface TrackedLinkProps {
className?: string;
children: React.ReactNode;
onClick?: () => void;
target?: string;
rel?: string;
}
/**
@@ -25,6 +27,8 @@ export default function TrackedLink({
className,
children,
onClick,
target,
rel,
}: TrackedLinkProps) {
const { trackEvent } = useAnalytics();
@@ -41,7 +45,7 @@ export default function TrackedLink({
};
return (
<Link href={href} className={className} onClick={handleClick}>
<Link href={href} className={className} onClick={handleClick} target={target} rel={rel}>
{children}
</Link>
);

View File

@@ -0,0 +1,626 @@
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-6 left-1/2 -translate-x-1/2 z-[9999] w-max max-w-[95vw]">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-1 sm:gap-2 overflow-x-auto hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<button
onClick={() => setIsActive(!isActive)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-medium",
isActive
? "bg-blue-500 text-white shadow-lg shadow-blue-500/20"
: "text-white/70 hover:text-white hover:bg-white/10"
)}
>
{isActive ? <X size={18} /> : <MessageSquare size={18} />}
{isActive ? "Modus beenden" : "Korrekturen erfassen"}
</button>
<div className="w-px h-6 bg-white/10 mx-1" />
<button
onClick={() => setShowList(!showList)}
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
>
<List size={20} />
{annotations.length > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 text-[10px] flex items-center justify-center rounded-full text-white font-bold border-2 border-[#1a1a1a]">
{annotations.length}
</span>
)}
</button>
{annotations.length > 0 && (
<>
<div className="w-px h-6 bg-white/10 mx-1" />
<button
onClick={exportJSON}
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all font-medium"
>
<Download size={18} />
JSON Export
</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

@@ -75,13 +75,13 @@ const Icons = {
export function BenefitGrid({ badge, title, description, benefits }: BenefitGridProps) {
return (
<section className="bg-white py-24 relative overflow-hidden border-t border-neutral-100">
<section className="bg-white py-16 md:py-24 lg:py-32 relative overflow-hidden border-t border-neutral-100">
<div className="container px-4 max-w-7xl mx-auto relative z-10">
<div className="max-w-3xl mx-auto text-center mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
{title && (
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
{title}
</h3>
)}

View File

@@ -1,73 +0,0 @@
'use client';
import React from 'react';
import { Button } from '@/components/ui/Button';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface CallToActionProps {
title?: string;
description?: string;
ctaLabel?: string;
ctaHref?: string;
theme?: 'light' | 'dark';
}
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
},
};
export const CallToAction: React.FC<CallToActionProps> = (props) => {
const { theme = 'light' } = props;
const isDark = theme === 'dark';
const t = useTranslations('CallToAction');
const title = props.title || t('title');
const description = props.description || t('description');
const ctaLabel = props.ctaLabel || t('ctaLabel');
const ctaHref = props.ctaHref || t('ctaHref');
return (
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>
<motion.div
className="container max-w-3xl mx-auto px-4"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
<motion.h2 variants={itemVariants} className={`font-heading text-4xl font-extrabold mb-6 ${isDark ? 'text-white' : 'text-neutral-dark'}`}>
{title}
</motion.h2>
<motion.p variants={itemVariants} className={`text-xl mb-10 leading-relaxed ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>
{description}
</motion.p>
<motion.div variants={itemVariants}>
<Button
href={ctaHref}
size="xl"
variant={isDark ? 'white' : 'primary'}
>
{ctaLabel}
</Button>
</motion.div>
</motion.div>
</div>
);
};

View File

@@ -68,31 +68,7 @@ const defaultCertificates: Certificate[] = [
type: 'tax',
date: '13.02.2025',
},
{
title: 'Unbedenklichkeit IHK',
description: 'Industrie- und Handelskammer',
type: 'general',
},
{
title: 'Unbedenklichkeit HWK',
description: 'Handwerkskammer',
type: 'general',
},
{
title: 'Handelsregisterauszug',
description: 'Amtsgericht Cottbus',
type: 'general',
},
{
title: 'Gewerbe-Anmeldung',
description: 'Stadt Guben',
type: 'general',
},
{
title: 'Gewerbezentralregister',
description: 'Auskunft aus dem Register',
type: 'general',
},
];
export function CertificatesBlock({ badge, title, description, certificates = defaultCertificates, hideHeader = false }: CertificatesBlockProps) {
@@ -123,12 +99,12 @@ export function CertificatesBlock({ badge, title, description, certificates = de
if (!isMounted) {
return (
<section className="py-24 bg-neutral-50 relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-neutral-50 relative overflow-hidden">
<Container>
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</div>
)}
@@ -138,7 +114,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
}
return (
<section className="py-24 bg-neutral-50 relative overflow-hidden">
<section className="py-12 md:py-24 bg-neutral-50 relative overflow-hidden">
{/* Background Accents */}
<div className="absolute top-0 right-0 w-1/3 h-full bg-primary/5 -skew-x-12 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-accent/5 skew-y-12 -translate-x-1/2 blur-3xl rounded-full" />
@@ -153,7 +129,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
transition={{ duration: 0.5 }}
>
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</motion.div>
</div>
@@ -164,7 +140,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-50px" }}
viewport={{ once: true, amount: 0.1 }}
>
{certificates.map((cert, index) => {
const isIso = cert.type === 'iso';
@@ -180,7 +156,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
key={index}
{...wrapperProps as any}
variants={itemVariants}
className={`group relative rounded-2xl p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
className={`group relative rounded-2xl p-6 md:p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
isIso
? 'bg-neutral-dark text-white border-neutral-800 shadow-lg'
: 'bg-white text-neutral-dark border-neutral-200 shadow-md'

View File

@@ -80,18 +80,18 @@ export function CompanyTimeline({
const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<section className="py-24 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-[#FAFAFA] relative overflow-hidden">
{/* Background Decor */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
<div className="container max-w-5xl relative z-10">
<div className="text-center mb-24">
<div className="text-center mb-12 md:mb-24">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-neutral-200 text-neutral-dark text-xs font-bold uppercase tracking-wider mb-6 shadow-sm">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
{finalBadge}
</div>
<h3 className="font-heading text-4xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
<h3 className="font-heading text-3xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
{finalTitle}
</h3>
</div>

View File

@@ -62,12 +62,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
// Static fallback for SSR
if (!isMounted) {
return (
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
<section className="py-16 md:py-24 lg:py-32 bg-neutral text-neutral-dark overflow-hidden relative">
<Container>
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
<div className="max-w-2xl">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
@@ -79,20 +79,20 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
}
return (
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
<div className="flex flex-col md:flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl">
<div className="flex flex-row justify-between items-end mb-12 px-4 md:px-0">
<div className="max-w-2xl text-left">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
</div>
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
<Button href={ctaHref} variant="ghost" className="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
{ctaLabel}
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
{items.map((item, idx) => {
const isLarge = item.size === 'large';
const isMedium = item.size === 'medium';
@@ -177,7 +177,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
{item.tag}
</span>
)}
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-3xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-2xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
{item.title}
</h4>
{item.description && (

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
@@ -52,7 +52,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
viewport={{ once: true, margin: "-50px" }}
>
<motion.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</motion.h2>
<motion.h3 variants={itemVariants} className="font-heading text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
<motion.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
<motion.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
Haben Sie Fragen zu unseren Leistungen oder möchten Sie ein konkretes Projekt besprechen? Rufen Sie uns an oder schreiben Sie uns eine E-Mail.
</motion.p>
@@ -129,9 +129,10 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
fallback={<div className="h-full w-full bg-neutral-medium animate-pulse" />}
>
<ContactMap
address="Gewerbestraße 22\n03172 Guben"
lat={51.9547}
lng={14.7214}
locations={[
{ name: 'E-TIB GmbH (Hauptsitz)', address: 'Gewerbestraße 22\n03172 Guben', lat: 51.9547, lng: 14.7214 },
{ name: 'E-TIB Bohrtechnik GmbH', address: 'Bahnhofstraße 180a\n99947 Kirchheilingen', lat: 51.1852, lng: 10.6865 }
]}
/>
</Suspense>
</motion.section>

View File

@@ -7,13 +7,13 @@ import { useLocale } from 'next-intl';
export function DataGridPulse() {
const locale = useLocale();
return (
<div className="relative w-full py-32 bg-[#050B14] overflow-hidden flex flex-col items-center border-y border-primary/20 -mx-[50vw] px-[50vw] mb-16">
<div className="relative w-full py-12 md:py-24 bg-neutral-50 border border-neutral-100/80 overflow-hidden flex flex-col items-center rounded-3xl mb-16 shadow-sm">
{/* Blueprint grid background */}
<div
className="absolute inset-0 opacity-20 pointer-events-none"
className="absolute inset-0 opacity-[0.03] pointer-events-none"
style={{
backgroundImage: 'linear-gradient(#82ED20 1px, transparent 1px), linear-gradient(90deg, #82ED20 1px, transparent 1px)',
backgroundImage: 'linear-gradient(#117C61 1px, transparent 1px), linear-gradient(90deg, #117C61 1px, transparent 1px)',
backgroundSize: '40px 40px'
}}
/>
@@ -21,25 +21,25 @@ export function DataGridPulse() {
{/* Pulsing horizontal lines (Data Flow) */}
<motion.div
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-50 blur-[2px]"
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/30 to-transparent blur-[1px]"
/>
<motion.div
animate={{ x: ["100%", "-100%"] }}
transition={{ duration: 4, repeat: Infinity, ease: "linear" }}
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-white to-transparent opacity-30 blur-[2px]"
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/25 to-transparent blur-[1px]"
/>
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center text-center">
<h3 className="text-sm font-bold uppercase tracking-widest text-primary mb-6">
<div className="relative z-10 w-full max-w-2xl px-6 mx-auto flex flex-col items-center text-center">
<span className="text-xs md:text-sm font-bold uppercase tracking-widest text-primary mb-4 bg-primary/10 px-3 py-1 rounded-full">
{locale === 'en' ? 'Network Expansion in Numbers' : 'Netzausbau in Zahlen'}
</h3>
</span>
<div className="bg-black/40 backdrop-blur-md p-12 md:p-16 rounded-3xl border border-white/10 shadow-2xl">
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4 leading-none" style={{ fontVariantNumeric: 'tabular-nums' }}>
372.161<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
<div className="bg-white/80 backdrop-blur-md p-8 md:p-12 rounded-2xl border border-neutral-200/50 shadow-lg w-full">
<div className="font-mono text-4xl md:text-7xl font-black text-neutral-900 tracking-tighter mb-4 leading-none animate-pulse-slow" style={{ fontVariantNumeric: 'tabular-nums' }}>
372.161<span className="text-primary text-2xl md:text-4xl ml-1 font-sans">m</span>
</div>
<p className="text-white/70 text-lg md:text-xl font-medium max-w-lg mx-auto">
<p className="text-text-secondary text-base md:text-lg font-medium max-w-md mx-auto leading-relaxed">
{locale === 'en'
? 'Cable routes laid since 2023. A massive infrastructure achievement, driven by our own machinery.'
: 'Verlegte Kabeltrassen seit 2023. Eine massive Infrastrukturleistung, getragen von unserem eigenen Maschinenpark.'}

View File

@@ -16,13 +16,13 @@ export function DeepDrillAnimation() {
const drillDepth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<div ref={containerRef} className="relative w-full py-32 flex flex-col items-center">
<div ref={containerRef} className="relative w-full py-12 md:py-32 flex flex-col items-center">
<div className="absolute inset-0 bg-[#050B14] -mx-[50vw] px-[50vw]" />
<div className="relative z-10 w-full max-w-3xl mx-auto flex flex-col items-center text-center text-white">
<h3 className="text-3xl font-heading font-extrabold mb-8 tracking-tight">
<h3 className="text-2xl md:text-3xl font-heading font-extrabold mb-8 tracking-tight">
{locale === 'en' ? 'Precision in Depth' : 'Präzision in der Tiefe'}
</h3>
<p className="text-white/60 text-lg mb-16 max-w-xl">
@@ -50,8 +50,8 @@ export function DeepDrillAnimation() {
transition={{ duration: 0.8 }}
className="flex flex-col items-center"
>
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
102.771<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
<div className="font-mono text-5xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
102.771<span className="text-primary text-2xl md:text-5xl ml-2">m</span>
</div>
<div className="text-sm font-bold uppercase tracking-widest text-primary">
{locale === 'en' ? 'Drilled distance since 2023' : 'Gebohrte Strecke seit 2023'}

View File

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

View File

@@ -2,27 +2,38 @@
import React from 'react';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
const t = useTranslations('GrowthChart');
const data = [
{ year: '2023', hdd: 29436, kabel: 76727 },
{ year: '2024', hdd: 28680, kabel: 131964 },
{ year: '2025', hdd: 44655, kabel: 163470 },
{ year: '2016', offener: 5000, hdd: 1200, kabel: 16064 },
{ year: '2017', offener: 33401, hdd: 8363, kabel: 84675 },
{ year: '2018', offener: 30773, hdd: 10225, kabel: 108127 },
{ year: '2019', offener: 32445, hdd: 8505, kabel: 87468 },
{ year: '2020', offener: 31305, hdd: 9050, kabel: 93556 },
{ year: '2021', offener: 38756, hdd: 10957, kabel: 110127 },
{ year: '2022', offener: 41445, hdd: 9505, kabel: 102468 },
{ year: '2023', offener: 17908, hdd: 29436, kabel: 76727 },
{ year: '2024', offener: 30801, hdd: 28680, kabel: 131964 },
{ year: '2025', offener: 38166, hdd: 44655, kabel: 163470 },
];
const maxVal = 163470; // Highest value for scaling
return (
<div className="w-full bg-white rounded-3xl p-8 md:p-12 shadow-sm border border-neutral-100 my-16">
<div className="w-full bg-white rounded-3xl p-6 md:p-12 shadow-sm border border-neutral-100 my-8 md:my-16">
<div className="mb-10">
<h3 className="text-3xl font-heading font-extrabold text-neutral-dark mb-2">Bewiesene Leistungsfähigkeit</h3>
<p className="text-neutral-500 font-medium">Unsere verlässliche Baukapazität der letzten drei Jahre (in Metern)</p>
<h3 className="text-2xl md:text-3xl font-heading font-extrabold text-neutral-dark mb-2">{t('title')}</h3>
<p className="text-neutral-500 font-medium">{t('subtitle')}</p>
</div>
<div className="space-y-8">
{data.map((item, index) => {
const hddWidth = (item.hdd / maxVal) * 100;
const kabelWidth = (item.kabel / maxVal) * 100;
const offenerWidth = (item.offener / maxVal) * 100;
const hddWidth = (item.hdd / maxVal) * 100;
return (
<div key={item.year} className="relative">
@@ -40,7 +51,23 @@ export function GrowthChart() {
/>
</div>
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
Kabel: {new Intl.NumberFormat('de-DE').format(item.kabel)}m
{t('kabel')}: {new Intl.NumberFormat('de-DE').format(item.kabel)}m
</div>
</div>
{/* Offener Tiefbau Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-5 overflow-hidden relative">
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${offenerWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.15 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-300 rounded-full"
/>
</div>
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
{t('offener')}: {new Intl.NumberFormat('de-DE').format(item.offener)}m
</div>
</div>
@@ -56,7 +83,7 @@ export function GrowthChart() {
/>
</div>
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
HDD: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
{t('hdd')}: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
</div>
</div>
</div>
@@ -65,14 +92,18 @@ export function GrowthChart() {
})}
</div>
<div className="mt-8 flex gap-6 border-t border-neutral-100 pt-6">
<div className="mt-8 flex flex-wrap gap-6 border-t border-neutral-100 pt-6">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-primary" />
<span className="text-xs font-bold text-neutral-500 uppercase">Kabelverlegung</span>
<span className="text-xs font-bold text-neutral-500 uppercase">{t('kabel')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-neutral-300" />
<span className="text-xs font-bold text-neutral-500 uppercase">{t('offener')}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-neutral-dark" />
<span className="text-xs font-bold text-neutral-500 uppercase">HDD Spülbohrung</span>
<span className="text-xs font-bold text-neutral-500 uppercase">{t('hdd')}</span>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@
import React from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion } from 'framer-motion';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
@@ -39,13 +40,15 @@ const itemVariants = {
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
const bgSrc = backgroundImage?.sizes?.card?.url || backgroundImage?.url;
const pathname = usePathname();
return (
<section
className={`relative min-h-[45vh] md:min-h-[55vh] flex items-center pt-32 pb-20 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
>
{bgSrc && (
<motion.div
key={`hero-bg-${pathname}`}
className="absolute inset-0 z-0"
initial={{ scale: 1.05, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@@ -75,6 +78,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
)}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
<motion.div
key={`hero-content-${pathname}`}
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
variants={containerVariants}
initial="hidden"

View File

@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/Button';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface HeroVideoProps {
data?: any;
@@ -24,10 +25,11 @@ interface HeroVideoProps {
export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELTIEFBAU';
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const explicitNoVideo = props.videoUrl === 'none' || props.videoUrl === '';
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const pathname = usePathname();
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
const posterAlt = props.backgroundImage?.alt || data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
@@ -35,28 +37,19 @@ export function HeroVideo(props: HeroVideoProps) {
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
let currentLocale = 'de';
try {
// We can't unconditionally call hooks if they might fail outside context, but HeroVideo is usually in context.
// However, to be safe and avoid adding new imports, we can use window.location or default to 'de' if not available.
if (typeof window !== 'undefined') {
currentLocale = window.location.pathname.startsWith('/en') ? 'en' : 'de';
}
} catch (e) {
// Fallback to default locale if window/location access fails
}
const currentLocale = pathname?.startsWith('/en') ? 'en' : 'de';
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/${currentLocale === 'de' ? 'contact' : 'contact'}`;
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
return (
<div className="relative w-full h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{videoUrl ? (
<video
key={videoUrl}
key={`${pathname}-${videoUrl}`}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
autoPlay
@@ -67,6 +60,7 @@ export function HeroVideo(props: HeroVideoProps) {
/>
) : (
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
alt={posterAlt}
fill
@@ -88,13 +82,14 @@ export function HeroVideo(props: HeroVideoProps) {
<div className="container relative z-[4] text-center px-4">
<motion.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] }}
>
<h1
className="font-heading font-bold text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
dangerouslySetInnerHTML={{ __html: title.replace('KABELTIEFBAU', '<span class="text-primary-light">KABELTIEFBAU</span>').replace(/\\n|\n/g, '<br />') }}
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
dangerouslySetInnerHTML={{ __html: title.replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/\\n|\n/g, '<br />') }}
/>
<p className="text-lg md:text-xl lg:text-2xl text-white/90 max-w-3xl mx-auto mb-12 drop-shadow">

View File

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

View File

@@ -71,13 +71,13 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<div className="py-12 md:py-24">
<div className="container mx-auto">
{showFairs && (
<div className="mb-24 relative">
<div className="mb-12 md:mb-24 relative">
<motion.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-4xl text-neutral-dark mb-12 flex items-center gap-4"
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
>
<span className="w-12 h-1.5 bg-primary rounded-full" />
{fairsTitle}
@@ -182,7 +182,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
className="font-heading font-extrabold text-4xl text-neutral-dark mb-8"
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
>
{title}
</motion.h2>

View File

@@ -1,8 +1,7 @@
import * as React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { ReferencesSlider, Reference } from './ReferencesSlider';
import { NextIntlClientProvider } from 'next-intl';
// Mock next/link to render standard <a> tag
vi.mock('next/link', () => ({
@@ -35,7 +34,7 @@ describe('ReferencesSlider TDD', () => {
id: '1',
title: 'Referenz Projekt Eins',
slug: 'referenz-projekt-eins',
category: 'Kabeltiefbau',
category: 'Kabelnetzbau',
},
{
id: '2',
@@ -52,45 +51,14 @@ describe('ReferencesSlider TDD', () => {
expect(screen.getByText('Referenz Projekt Zwei')).toBeTruthy();
});
it('wraps each tile in a link to /referenzen/[slug]', () => {
it('renders each tile as a static block and NOT wrapped in a link', () => {
render(<ReferencesSlider references={mockReferences} />);
const links = screen.getAllByTestId('reference-tile-link');
expect(links).toHaveLength(2);
expect(links[0].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-eins');
expect(links[1].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-zwei');
});
const links = screen.queryAllByTestId('reference-tile-link');
expect(links).toHaveLength(0);
it('allows navigation on simple click (no drag)', () => {
render(<ReferencesSlider references={mockReferences} />);
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
// Simulate simple click sequence
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
fireEvent.mouseUp(firstLink);
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
firstLink.dispatchEvent(clickEvent);
expect(clickEvent.defaultPrevented).toBe(false);
});
it('prevents navigation if mouse was dragged', () => {
render(<ReferencesSlider references={mockReferences} />);
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
// Simulate drag sequence
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
// Simulate mouse move of 20px
fireEvent.mouseMove(firstLink, { pageX: 120, clientX: 120 });
fireEvent.mouseUp(firstLink);
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
firstLink.dispatchEvent(clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
const tiles = screen.getAllByTestId('reference-tile');
expect(tiles).toHaveLength(2);
});
});

View File

@@ -92,27 +92,23 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
dragDistanceRef.current = Math.abs(x - startXRef.current);
};
const handleTileClick = (e: React.MouseEvent) => {
if (dragDistanceRef.current > 10) {
e.preventDefault();
}
};
return (
<section id="referenzen" className="py-24 bg-neutral-dark text-white relative overflow-hidden">
<section id="referenzen" className="py-16 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-4xl md:text-5xl font-extrabold">{title}</h3>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
</div>
<p className="text-white/60 max-w-md">
<div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
{description}
</p>
</div>
</div>
<div className="relative z-10">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<div
ref={containerRef}
onMouseDown={onMouseDown}
@@ -135,13 +131,12 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
>
<Link
href={`/${locale}/referenzen/${ref.slug}`}
onClick={handleTileClick}
data-testid="reference-tile-link"
className="block relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
<div
data-testid="reference-tile"
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
>
<Image
draggable={false}
src={imgSrc}
alt={ref.title}
fill
@@ -158,7 +153,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
</span>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</Link>
</div>
</motion.div>
);
})}

View File

@@ -23,7 +23,7 @@ export function ScaleOfImpact() {
return (
<div
ref={containerRef}
className="relative w-full min-h-[70vh] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
className="relative w-full min-h-[40vh] md:min-h-[70vh] bg-[#050B14] py-12 md:py-0 flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
>
{/* Massive Watermark Numbers */}
<motion.div
@@ -31,14 +31,14 @@ export function ScaleOfImpact() {
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none overflow-hidden mix-blend-overlay"
>
<div className="font-heading font-black text-[20vw] leading-none text-white opacity-20 whitespace-nowrap">
<span className="font-mono tracking-tighter">474.932</span>
<span className="font-mono tracking-tighter">1.435.223</span>
</div>
</motion.div>
{/* Foreground Content */}
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
<h2 className="text-3xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
<h2 className="text-2xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
{locale === 'en' ? (
<>An infrastructure <br className="md:hidden"/> that connects the country.</>
) : (
@@ -46,7 +46,7 @@ export function ScaleOfImpact() {
)}
</h2>
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-16">
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-4 md:mb-16">
{locale === 'en'
? "We don't just talk about the energy transition. We build it. In the last 36 months alone, we have completed a route that stretches from our headquarters in Guben all the way to Munich."
: "Wir sprechen nicht nur über die Energiewende. Wir bauen sie. Allein in den letzten 36 Monaten haben wir eine Strecke realisiert, die von unserem Hauptsitz in Guben bis nach München reicht."
@@ -54,7 +54,7 @@ export function ScaleOfImpact() {
</p>
{/* Abstract SVG Line Animation */}
<div className="w-full max-w-4xl h-32 relative">
<div className="w-full max-w-4xl h-12 md:h-32 relative">
<svg
width="100%"
height="100%"
@@ -73,26 +73,30 @@ export function ScaleOfImpact() {
<motion.path
d="M0,50 Q250,0 500,50 T1000,50"
fill="none"
stroke="#82ED20" // primary color
stroke="#ffc107" // warning/yellow color
strokeWidth="4"
style={{ pathLength }}
className="drop-shadow-[0_0_15px_rgba(130,237,32,0.8)]"
className="drop-shadow-[0_0_15px_rgba(255,193,7,0.8)]"
/>
</svg>
</div>
{/* Static Data Grid (No jumping text) */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-16 w-full max-w-4xl mt-12">
<div className="grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-2 md:gap-12 w-full max-w-5xl mt-4 md:mt-12">
<div className="flex flex-col items-center">
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">100<span className="text-primary">+</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Major Projects' : 'Großprojekte'}</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-5xl md:text-6xl font-black text-white tracking-tighter mb-2">372<span className="text-primary text-3xl">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-5xl md:text-6xl font-black text-white tracking-tighter mb-2">102<span className="text-primary text-3xl">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 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

@@ -105,17 +105,17 @@ export function ServiceDetailGrid({
panels
}: ServiceDetailGridProps) {
return (
<section className="bg-neutral-50 py-24 border-b border-neutral-100 relative overflow-hidden">
<section className="bg-neutral-50 py-16 md:py-24 lg:py-32 border-b border-neutral-100 relative overflow-hidden">
<div className="container relative z-10 px-4 max-w-7xl mx-auto">
<div className="max-w-3xl mx-auto text-center mb-20">
<div className="max-w-3xl mb-12 md:mb-20 text-left">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
{title && (
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
{title}
</h3>
)}
<div className="w-20 h-2 bg-primary mx-auto mb-8"></div>
<div className="w-20 h-2 bg-primary mb-8"></div>
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
{descriptionParagraphs?.map((p, i) => {
@@ -141,7 +141,7 @@ export function ServiceDetailGrid({
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
>
{problemStatement && (
<div className="p-8 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
<div className="p-6 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
<div className="flex items-center mb-6 text-red-600">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
<circle cx="12" cy="12" r="10"></circle>
@@ -154,7 +154,7 @@ export function ServiceDetailGrid({
</div>
)}
{solutionStatement && (
<div className="p-8 md:p-12 bg-primary/5">
<div className="p-6 md:p-12 bg-primary/5">
<div className="flex items-center mb-6 text-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
@@ -183,7 +183,7 @@ export function ServiceDetailGrid({
<motion.div
key={panel.id}
variants={itemVariants}
className={`group relative overflow-hidden bg-white border border-neutral-200 rounded-3xl p-8 md:p-12 transition-all duration-500 hover:border-primary/40 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1 select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
className={`group relative overflow-hidden bg-white border border-neutral-200 rounded-3xl p-6 md:p-12 transition-all duration-500 hover:border-primary/40 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1 select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
>
{/* Premium Watermark Number */}
<div className="absolute -right-8 -bottom-12 text-[12rem] font-heading font-black text-neutral-50 z-0 pointer-events-none select-none transition-all duration-700 group-hover:scale-105 group-hover:-translate-y-4 group-hover:text-primary/[0.03]">
@@ -193,9 +193,9 @@ export function ServiceDetailGrid({
{/* On a white card, a very subtle primary/grey shine works best */}
<HoverShineOverlay shineColor="via-primary/5" />
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
<div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
<div className="pt-2 flex-1 min-w-0 pr-4">
<h4 className="text-2xl md:text-3xl font-extrabold text-neutral-dark uppercase tracking-wider break-words hyphens-auto" lang="de">{panel.title}</h4>
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
</div>

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { motion, Variants } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
@@ -67,28 +68,31 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})) || defaultCompanies;
return (
<section id="unternehmen" className="py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<section id="unternehmen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4">
{(badge || title) && (
<div className="text-center max-w-3xl mx-auto mb-16">
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
{title && <h3 className="font-heading text-4xl md:text-5xl font-extrabold text-white">{title}</h3>}
{title && <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-white">{title}</h3>}
</div>
)}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
<div className="relative -mx-4 md:mx-0">
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
<motion.div
className="flex overflow-x-auto md:overflow-visible snap-x snap-mandatory pb-6 md:pb-0 md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 px-4 md:px-0 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
>
{companiesData.map((company: any, index: number) => {
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
const hasUrl = !!company.url;
const CardWrapper = hasUrl ? 'a' : 'div';
const wrapperProps = hasUrl ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
const isExternal = hasUrl && company.url.startsWith('http');
const CardWrapper = hasUrl ? (isExternal ? 'a' : Link) : 'div';
const wrapperProps = hasUrl ? (isExternal ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : { href: company.url }) : {};
// Generate logo representation based on title
const isEtib = company.title.includes('E-TIB');
@@ -112,33 +116,21 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
const animationClass = animationClasses[index % animationClasses.length];
return (
<motion.div
key={index}
variants={itemVariants}
className="h-full"
>
<CardWrapper
{...wrapperProps}
className={`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 ${
isCurrent
? 'border-2 border-primary shadow-[0_0_30px_rgba(17,124,97,0.15)] cursor-default'
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
}`}
>
<div className="absolute inset-0 bg-[#0a192f] z-0">
<Image
src={company.backgroundImage}
alt="Background"
fill
className={`object-cover transition-all duration-1000 ease-out ${
isCurrent
? 'opacity-30 grayscale-0 scale-105'
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
}`}
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
const CardContent = (
<>
<div className="absolute inset-0 bg-[#0a192f] z-0">
<Image
src={company.backgroundImage}
alt="Background"
fill
className={`object-cover transition-all duration-1000 ease-out ${
isCurrent
? 'opacity-30 grayscale-0 scale-105'
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
}`}
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
{/* Heavy dark gradient overlay to make logos pop */}
<div className={`absolute inset-0 z-0 transition-colors duration-700 mix-blend-multiply ${
@@ -182,7 +174,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{/* LOGO RENDERING */}
{isEtib ? (
<div className="flex flex-col items-center">
<div className="relative w-48 h-14 mb-2">
<div className="relative w-32 md:w-40 lg:w-44 h-10 md:h-12 lg:h-14 mb-2 max-w-[85%]">
<Image
src="/assets/logo-white.png"
alt="E-TIB Logo"
@@ -191,17 +183,17 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
/>
</div>
{isIng && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Ingenieurgesellschaft
</div>
)}
{isVerwaltung && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Verwaltung GmbH
</div>
)}
{isBohrtechnik && (
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
Bohrtechnik GmbH
</div>
)}
@@ -227,17 +219,47 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{company.url && (
<div className="absolute bottom-6 left-0 right-0 flex justify-center opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-500 z-20">
<div className="flex items-center text-white font-bold uppercase text-xs tracking-widest bg-white/10 backdrop-blur-md px-4 py-2 rounded-full border border-white/20">
Website Besuchen
{isExternal ? (isDe ? 'Website Besuchen' : 'Visit Website') : (isDe ? 'Zum Standort' : 'View Location')}
<svg className="w-4 h-4 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</div>
</div>
)}
</CardWrapper>
</>
);
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 ${
isCurrent
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
}`;
return (
<motion.div
key={index}
variants={itemVariants}
className="h-full flex-shrink-0 snap-center w-[85%] sm:w-[70%] md:w-full"
>
{hasUrl ? (
isExternal ? (
<a href={company.url} target="_blank" rel="noopener noreferrer" className={sharedClass}>
{CardContent}
</a>
) : (
<Link href={company.url as string} className={sharedClass}>
{CardContent}
</Link>
)
) : (
<div className={sharedClass}>
{CardContent}
</div>
)}
</motion.div>
);
})}
</motion.div>
</motion.div>
</div>
</div>
</section>
);

View File

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

View File

@@ -28,87 +28,18 @@ export function TeamGrid({ members }: TeamGridProps) {
if (!members || members.length === 0) return null;
return (
<section className="py-24 bg-white relative overflow-hidden">
<section className="py-16 md:py-24 lg:py-32 bg-white relative overflow-hidden">
<div className="container relative z-10">
<div className="mb-12">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{t('badge')}</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
<h3 className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
<p className="text-text-secondary max-w-2xl text-lg">
{t('subtitle')}
</p>
</div>
{members.length > 0 && (
<div className="mb-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="relative rounded-[2.5rem] bg-neutral-dark overflow-hidden shadow-2xl border border-white/10"
>
{/* Premium Background Effects */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(238,114,3,0.15),transparent_50%)] pointer-events-none" />
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/20 blur-[120px] mix-blend-screen rounded-full pointer-events-none" />
<div className="absolute inset-0 opacity-20 pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'40\\' height=\\'40\\' viewBox=\\'0 0 40 40\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Cpath d=\\'M0 0h40v40H0V0zm20 20h20v20H20V20zM0 20h20v20H0V20z\\' fill=\\'%23ffffff\\' fill-opacity=\\'0.02\\' fill-rule=\\'evenodd\\'/%3E%3C/svg%3E')" }} />
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between p-8 md:p-12 lg:p-20 gap-12 lg:gap-16">
{/* Text Content */}
<div className="w-full lg:w-[55%] flex flex-col justify-center text-white">
<div className="inline-flex items-center space-x-2 px-4 py-1.5 mb-8 rounded-full border border-primary/30 bg-primary/10 text-primary text-xs font-bold uppercase tracking-widest self-start backdrop-blur-md shadow-[0_0_20px_rgba(238,114,3,0.2)]">
<span className="relative flex h-2 w-2 mr-1">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
{t('management')}
</div>
<h3 className="font-heading font-black text-5xl md:text-6xl lg:text-7xl mb-4 leading-[1.1] tracking-tight">
{members[0].name}
</h3>
<p className="text-xl md:text-2xl text-white/70 font-medium mb-10 leading-relaxed max-w-xl">
{members[0].position}
</p>
<div className="flex flex-col sm:flex-row gap-4">
{members[0].email && (
<a href={`mailto:${members[0].email}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-primary border border-white/10 hover:border-primary px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].email}</span>
</a>
)}
{members[0].phone && (
<a href={`tel:${members[0].phone}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-white/10 border border-white/10 px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].phone}</span>
</a>
)}
</div>
</div>
{/* Premium Image Block */}
<div className="w-full lg:w-[45%] flex justify-center lg:justify-end perspective-1000">
<div className="relative w-64 h-64 md:w-80 md:h-80 lg:w-[400px] lg:h-[400px] rounded-[3rem] overflow-hidden border-4 border-white/10 shadow-[0_30px_60px_rgba(0,0,0,0.6)] transform md:rotate-3 hover:rotate-0 transition-all duration-700 bg-neutral-800 group">
{members[0].image && (typeof members[0].image === 'string' ? members[0].image : members[0].image.url) ? (
<Image
src={typeof members[0].image === 'string' ? members[0].image : members[0].image.url}
alt={members[0].name}
fill
sizes="400px"
className="object-cover scale-110 group-hover:scale-100 transition-transform duration-1000 ease-out"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-white/20">
<svg className="w-20 h-20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</div>
)}
</div>
</div>
</div>
</motion.div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 xl:gap-10">
{members.slice(1).map((member, i) => (
{members.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}

View File

@@ -78,7 +78,7 @@ export function CorporateBackground() {
const y5 = useTransform(scrollYProgress, [0, 1], [0, -500]);
return (
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none" aria-hidden="true">
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none hidden md:block" aria-hidden="true">
{/* 1. Top Right - Medium, rotating moderately */}
<motion.div
style={{

View File

@@ -0,0 +1,41 @@
'use client';
import { motion } from 'framer-motion';
import Image from 'next/image';
export function EUFundingBadge() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1] }}
className="absolute top-0 right-0 w-[260px] md:w-[380px] h-[260px] md:h-[380px] z-0 pointer-events-none select-none overflow-hidden"
style={{
// Perfekter weicher Fade, der sicherstellt, dass harte Kanten komplett unsichtbar werden
maskImage: 'radial-gradient(100% 100% at top right, black 10%, rgba(0,0,0,0.8) 30%, transparent 70%)',
WebkitMaskImage: 'radial-gradient(100% 100% at top right, black 10%, rgba(0,0,0,0.8) 30%, transparent 70%)',
}}
>
{/* Background Flag */}
<div className="absolute inset-0 w-full h-full opacity-95">
<Image
src="/assets/eu-flag.webp"
alt="European Union Background"
fill
className="object-cover object-top"
priority
/>
</div>
{/* Text overlaid on the flag */}
<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>
<span className="block text-[11px] md:text-[13px] font-extrabold text-white tracking-wider leading-snug uppercase drop-shadow-md">
Europäischen Union
</span>
</div>
</motion.div>
);
}

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-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-32 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,13 +24,13 @@ export default function GallerySection({ data }: { data?: any }) {
const lightboxIndex = photoParam ? parseInt(photoParam, 10) : 0;
return (
<Section className="bg-white text-white py-32">
<Section className="bg-white text-white">
<Container>
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} align="center">
{data?.title || t('title')}
</Heading>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8">
{images.map((src, idx) => (
<button
key={idx}

View File

@@ -8,6 +8,7 @@ import { useAnalytics } from '../analytics/useAnalytics';
import { AnalyticsEvents } from '../analytics/analytics-events';
import { useState, useEffect, useRef, useCallback } from 'react';
import { ChevronRight } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { AISearchResults } from '../search/AISearchResults';
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false });
@@ -15,6 +16,7 @@ const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false });
export default function Hero({ data }: { data?: any }) {
const t = useTranslations('Home.hero');
const locale = useLocale();
const pathname = usePathname();
const { trackEvent } = useAnalytics();
const [searchQuery, setSearchQuery] = useState('');
@@ -85,7 +87,7 @@ export default function Hero({ data }: { data?: any }) {
return (
<>
<Section className="relative min-h-[85vh] md:h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
<Section key={`home-hero-${pathname}`} className="relative min-h-[75vh] md:min-h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
<Container className="relative z-10 text-center md:text-left text-white w-full order-2 md:order-none">
<div className="max-w-5xl mx-auto md:mx-0">
<div>
@@ -197,7 +199,7 @@ export default function Hero({ data }: { data?: any }) {
</div>
</Container>
<div className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none animate-in fade-in zoom-in-95 duration-1000 ease-out fill-mode-both">
<div className="relative md:absolute inset-0 z-0 w-full h-[35vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none animate-in fade-in zoom-in-95 duration-1000 ease-out fill-mode-both">
<HeroIllustration />
</div>

View File

@@ -9,7 +9,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
const locale = useLocale();
return (
<Section className="relative py-32 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"
@@ -31,7 +31,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
<div className="relative mb-12">
<div className="absolute -left-8 top-0 bottom-0 w-1 bg-accent rounded-full" />
<p className="text-xl md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
<p className="text-lg md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
"{data?.description || t('description')}"
</p>
</div>

View File

@@ -27,7 +27,7 @@ export default function VideoSection({ data }: { data?: any }) {
}, []);
return (
<section ref={sectionRef} className="relative h-[70vh] overflow-hidden bg-primary">
<section ref={sectionRef} className="relative h-[50vh] md:h-[70vh] overflow-hidden bg-primary">
{isVisible && (
<video className="w-full h-full object-cover opacity-60" autoPlay muted loop playsInline>
<source

View File

@@ -4,6 +4,7 @@ import { useLocale } from 'next-intl';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap, Mail, Phone } from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { Tooltip } from '@/components/ui/Tooltip';
import { EUFundingBadge } from '@/components/decorations/EUFundingBadge';
interface CompanyInfo {
contactEmail: string;
contactPhone: string;
@@ -18,7 +19,7 @@ export function Footer({ companyInfo }: FooterProps) {
const locale = useLocale();
return (
<footer className="bg-[#050B14] text-neutral-light py-10 md:py-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" />
@@ -26,9 +27,11 @@ export function Footer({ companyInfo }: FooterProps) {
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/40 to-transparent" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-[200px] bg-primary/20 blur-[100px] rounded-full pointer-events-none opacity-50" />
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-8 md:mb-12">
<EUFundingBadge />
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-4 md:mb-12">
{/* Brand Column Bento */}
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
<div className="space-y-4 md:space-y-6 relative z-10">
@@ -42,8 +45,8 @@ export function Footer({ companyInfo }: FooterProps) {
</TransitionLink>
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
{locale === 'de'
? 'Die Experten für Kabeltiefbau, 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.'}
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
: 'Experts in cable network construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
</p>
</div>
@@ -95,58 +98,158 @@ export function Footer({ companyInfo }: FooterProps) {
{/* Navigation Bento Grid */}
<div className="col-span-1 md:col-span-4 lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-4 md:gap-6 lg:gap-8">
{/* Company Column */}
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
{/* Links Column */}
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
<div className="absolute inset-0 bg-gradient-to-bl from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
{locale === 'de' ? 'Unternehmen' : 'Company'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
</h4>
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
{[
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
{ label: locale === 'de' ? 'Über uns' : 'About us', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
{ 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'}` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
].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>
{/* Desktop View */}
<div className="hidden md:block relative z-10 flex-grow">
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 text-white">
{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">
{[
{ 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'}` },
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
].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>
<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) */}
<div className="md:hidden relative z-10 flex-grow space-y-4">
<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' ? 'Navigation' : 'Navigation'}
<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' ? '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'}` },
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
].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 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>
{/* Legal Column */}
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
<div className="absolute inset-0 bg-gradient-to-tr from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
{locale === 'de' ? 'Rechtliches' : 'Legal'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
</h4>
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
{[
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
].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>
{/* Desktop View */}
<div className="hidden md:block relative z-10 flex-grow">
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 text-white">
{locale === 'de' ? 'Rechtliches' : 'Legal'}
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
</h4>
<ul className="space-y-2">
{[
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
].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>
{/* Built Info */}
{/* Legal Info removed from here */}
{/* Mobile View */}
<div className="md:hidden relative z-10 flex-grow">
<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' ? 'Rechtliches' : 'Legal'}
<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' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
].map((link) => (
<li key={link.href}>
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
<span className="font-medium">{link.label}</span>
</TransitionLink>
</li>
))}
</ul>
</div>
</details>
</div>
</div>
</div>

View File

@@ -11,6 +11,7 @@ export interface NavLink {
label: string;
url: string;
children?: NavLink[];
isGroupLabel?: boolean;
}
interface HeaderProps {
@@ -21,6 +22,7 @@ export function Header({ navLinks }: HeaderProps) {
const [isScrolled, setIsScrolled] = React.useState(false);
const [forceSolid, setForceSolid] = React.useState(false);
const [headerVariant, setHeaderVariant] = React.useState<'capsule' | 'standard'>('capsule');
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const pathname = usePathname() || '/';
// Scroll bound shine effect (continuous looping as user scrolls)
@@ -54,6 +56,7 @@ export function Header({ navLinks }: HeaderProps) {
// Re-check when pathname changes
checkHeaderState();
setIsMobileMenuOpen(false);
return () => {
window.removeEventListener('scroll', handleScroll);
@@ -71,7 +74,7 @@ export function Header({ navLinks }: HeaderProps) {
className={`fixed top-0 left-0 right-0 z-50 flex justify-center transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${
isStandard
? 'bg-[#050B14]/95 backdrop-blur-xl border-b border-white/10'
: (isSolidMode ? 'pt-4 px-4' : 'pt-0 px-0')
: (isSolidMode ? 'pt-4 px-4 sm:px-6' : 'pt-0 px-0')
}`}
>
<div
@@ -79,7 +82,7 @@ export function Header({ navLinks }: HeaderProps) {
isStandard
? 'py-4 px-6 md:px-8'
: isSolidMode
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-6 md:px-8'
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-8 md:px-10'
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
}`}
>
@@ -153,10 +156,82 @@ export function Header({ navLinks }: HeaderProps) {
{/* Sleek shine sweep effect */}
<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>
</>
);
}
@@ -164,10 +239,20 @@ export function Header({ navLinks }: HeaderProps) {
function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink, currentLocale: string, pathname: string, isSolidMode: boolean }) {
const [isHovered, setIsHovered] = React.useState(false);
// Close dropdown on navigation
React.useEffect(() => {
setIsHovered(false);
}, [pathname]);
// Close dropdown on scroll
React.useEffect(() => {
const handleScroll = () => {
if (isHovered) setIsHovered(false);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [isHovered]);
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
? `/${currentLocale}${link.url}`
: link.url;
@@ -222,15 +307,25 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
{/* Top glowing accent line */}
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-70" />
{link.children.map((child) => {
{link.children.map((child, idx) => {
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
? `/${currentLocale}${child.url}`
: child.url;
const isChildActive = pathname === childUrl;
if (child.isGroupLabel) {
return (
<div key={`group-${idx}`} className={`mt-2 mb-1 px-4 text-[11px] font-bold uppercase tracking-wider ${isSolidMode ? 'text-neutral-400' : 'text-neutral-500'}`}>
{child.label}
</div>
);
}
return (
<TransitionLink
key={child.url}
href={childUrl}
onClick={() => setIsHovered(false)}
className={`group/dropitem relative block px-4 py-3.5 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
isSolidMode
? isChildActive ? 'bg-primary/10 text-primary' : 'text-neutral-600 hover:text-primary hover:bg-neutral-50/80'

View File

@@ -56,7 +56,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
}, [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">
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-2 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
<nav className="flex justify-around items-center max-w-md mx-auto">
{items.map((item) => {
// Determine if active

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

@@ -32,9 +32,9 @@ export function Heading({
const effectiveSize = size || String(level) as HeadingSize;
const sizes: Record<string, string> = {
'hero': 'font-heading text-4xl md:text-5xl lg:text-6xl font-black tracking-tight !leading-[0.9]',
'hero': 'font-heading text-3xl md:text-5xl lg:text-6xl font-black tracking-tight !leading-[0.9]',
'section': 'font-heading text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight !leading-[0.95]',
'subsection': 'font-heading text-2xl md:text-3xl lg:text-4xl font-bold tracking-tight leading-tight',
'subsection': 'font-heading text-xl md:text-3xl lg:text-4xl font-bold tracking-tight leading-tight',
'card': 'text-xl md:text-2xl font-bold leading-snug',
'small': 'text-lg font-semibold leading-snug',
// Legacy support

View File

@@ -3,7 +3,7 @@ import { cn } from './utils';
export function Section({ className, children, ...props }: React.HTMLAttributes<HTMLElement>) {
return (
<section className={cn('py-16 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

@@ -24,6 +24,11 @@ export function TransitionLink({ children, href, onClick, transitionMessage, ...
return;
}
// Hash links on the same page should not trigger a transition
if (href.toString().startsWith('#')) {
return;
}
// Default Link Navigation blockieren
e.preventDefault();

File diff suppressed because one or more lines are too long

1
contact-page-debug.html Normal file

File diff suppressed because one or more lines are too long

BIN
contact.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
contact_sheet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,22 +1,59 @@
---
title: "Allgemeine Geschäftsbedingungen"
date: "2024-03-20"
date: "2026-05-28"
excerpt: "Die Allgemeinen Geschäftsbedingungen der E-TIB GmbH für unsere Leistungen und Lieferungen."
---
# Allgemeine Geschäftsbedingungen (AGB)
## 1. Geltungsbereich
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge, Lieferungen und sonstigen Leistungen der E-TIB GmbH gegenüber ihren Kunden.
## 1. Geltungsbereich und Vertragsabschluss
1. Diese Allgemeinen Geschäftsbedingungen (AGB) gelten ausschließlich für alle Verträge, Lieferungen und sonstigen Leistungen (einschließlich Planungs-, Ingenieur-, Kabelnetzbau-, Horizontalspülbohrungs- und Glasfasermontageleistungen) der E-TIB GmbH (nachfolgend „E-TIB“ genannt) gegenüber ihren Vertragspartnern (nachfolgend „Kunde“ genannt).
2. Diese AGB gelten ausschließlich im B2B-Verkehr. Sie richten sich ausschließlich an Unternehmer im Sinne des § 14 BGB, juristische Personen des öffentlichen Rechts oder öffentlich-rechtliche Sondervermögen.
3. Entgegenstehende, abweichende oder ergänzende Bedingungen des Kunden werden nicht Vertragsbestandteil, es sei denn, E-TIB stimmt ihrer Geltung ausdrücklich schriftlich zu.
4. Ein Vertrag kommt erst durch die schriftliche Auftragsbestätigung von E-TIB oder durch den tatsächlichen Beginn der Ausführung der Leistungen durch E-TIB zustande.
## 2. Vertragsabschluss
Ein Vertrag kommt erst durch unsere schriftliche Auftragsbestätigung oder durch die Ausführung des Auftrags zustande.
## 2. Leistungsumfang und Mitwirkungspflichten des Kunden
1. Der genaue Umfang der von E-TIB zu erbringenden Leistungen ergibt sich aus dem jeweiligen Angebot bzw. der schriftlichen Auftragsbestätigung und den darin vereinbarten Spezifikationen.
2. Der Kunde ist verpflichtet, E-TIB bei der Erbringung der Leistungen vollumfänglich und auf eigene Kosten zu unterstützen. Dies umfasst insbesondere:
- Die Bereitstellung aller für die Planung und Ausführung erforderlichen Unterlagen, Pläne (insbesondere Sparten- und Leitungspläne), Genehmigungen und behördlichen Freigaben rechtzeitig vor Baubeginn.
- Die genaue und verbindliche Einweisung in die örtlichen Gegebenheiten sowie die genaue Kennzeichnung und Einmessung aller im Arbeitsbereich verlaufenden unterirdischen Leitungen, Rohre, Kanäle und sonstigen Hindernisse.
- Die Sicherstellung des freien und ungehinderten Zugangs zum Baustellengelände sowie die Bereitstellung eventuell erforderlicher Lager- und Einrichtungsflächen.
- Die Bereitstellung von Bauwasser und Baustrom in der erforderlichen Menge und Qualität an den Arbeitsstellen auf Kosten des Kunden.
3. Versäumt der Kunde eine Mitwirkungspflicht oder erteilt fehlerhafte Auskünfte (insbesondere bezüglich der Lage unterirdischer Leitungen), haftet er E-TIB für alle daraus resultierenden Verzögerungen, Mehrkosten und Schäden. E-TIB ist in diesem Fall von der Haftung für Beschädigungen an nicht oder fehlerhaft eingezeichneten Leitungen freigestellt.
## 3. Preise und Zahlungsbedingungen
Alle Preise verstehen sich in Euro zzgl. der gesetzlichen Mehrwertsteuer. Zahlungen sind innerhalb von 14 Tagen nach Rechnungsstellung ohne Abzug zu leisten.
## 3. Ausführungsfristen, Behinderung und Bauverzögerungen
1. Vereinbarte Ausführungsfristen und Termine sind nur dann verbindlich, wenn sie von E-TIB ausdrücklich schriftlich als fix bestätigt wurden.
2. Die Einhaltung der Fristen setzt die rechtzeitige Erfüllung aller Mitwirkungspflichten des Kunden sowie den Eingang vereinbarter Anzahlungen voraus.
3. Leistungsverzögerungen aufgrund höherer Gewalt, Streiks, Aussperrungen, behördlicher Anordnungen oder unvorhersehbarer, von E-TIB nicht zu vertretender Erschwernisse (z. B. extreme Witterungsverhältnisse, unvorhergesehene Bodenklassen wie massiver Fels, unvollständige oder fehlerhafte Spartenpläne des Netzbetreibers) verlängern die Ausführungsfristen um die Dauer der Behinderung zzgl. einer angemessenen Anlaufzeit.
4. Führen unvorhergesehene geologische Hindernisse oder Abweichungen der tatsächlichen Bodenklasse von den vertraglich zugrunde gelegten Bodenklassen zu einem erheblichen Mehraufwand (z. B. bei Horizontalspülbohrungen), ist E-TIB berechtigt, eine angemessene Anpassung der Vergütung sowie der Ausführungsfristen zu verlangen.
## 4. Haftung
Wir haften für Schäden nur bei Vorsatz oder grober Fahrlässigkeit. Die Haftung für leichte Fahrlässigkeit ist ausgeschlossen, sofern keine wesentlichen Vertragspflichten verletzt wurden.
## 4. Abnahme, Gefahrübergang und Gewährleistung
1. Die Abnahme der Leistungen erfolgt unverzüglich nach Fertigstellung. Auf Verlangen von E-TIB sind auch in sich abgeschlossene Teile der Leistung (z. B. fertiggestellte Kabelgräben oder einzelne Bohrabschnitte) separat abzunehmen (Teilabnahme).
2. Nimmt der Kunde die Leistung nicht innerhalb von 12 Werktagen nach schriftlicher Anzeige der Fertigstellung ab, oder nimmt er die Leistung in Benutzung (z. B. durch Inbetriebnahme der verlegten Leitungen), gilt die Abnahme nach Ablauf dieser Frist als erfolgt.
3. Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung geht mit der Abnahme bzw. Teilabnahme auf den Kunden über.
4. E-TIB leistet Gewähr nach den gesetzlichen Bestimmungen, soweit nachfolgend nichts anderes vereinbart ist. Die Verjährungsfrist für Mängelansprüche bei Bauwerken und damit zusammenhängenden Planungs- und Überwachungsleistungen beträgt 5 Jahre ab Abnahme. Bei sonstigen Werkleistungen beträgt sie 1 Jahr ab Abnahme.
5. Offensichtliche Mängel müssen E-TIB unverzüglich, spätestens jedoch innerhalb von 5 Werktagen nach Abnahme, schriftlich angezeigt werden. Spätere Mängelrügen sind ausgeschlossen.
## 5. Gerichtsstand
Gerichtsstand für alle Streitigkeiten aus dem Vertragsverhältnis ist Cottbus, sofern der Kunde Kaufmann ist.
## 5. Preise, Abschlagszahlungen und Zahlungsbedingungen
1. Alle Preise verstehen sich in Euro netto zuzüglich der jeweils gültigen gesetzlichen Umsatzsteuer.
2. E-TIB ist berechtigt, dem Baufortschritt entsprechend angemessene Abschlagszahlungen (z. B. nach erbrachten Trassenmetern, Bohrmetern oder abgeschlossenen Planungsphasen) in Rechnung zu stellen. Diese sind innerhalb von 7 Tagen ab Rechnungsdatum ohne Abzug zur Zahlung fällig.
3. Schlussrechnungen sind innerhalb von 14 Tagen nach Rechnungsstellung und Abnahme ohne Abzug zur Zahlung fällig.
4. Bei Zahlungsverzug des Kunden ist E-TIB berechtigt, Verzugszinsen in gesetzlicher Höhe (derzeit 9 Prozentpunkte über dem jeweiligen Basiszinssatz) zu verlangen. Zudem behält sich E-TIB das Recht vor, weitere Arbeiten bis zur vollständigen Begleichung der fälligen Forderungen einzustellen (Leistungsverweigerungsrecht).
## 6. Eigentumsvorbehalt
1. E-TIB behält sich das Eigentum an allen gelieferten und eingebauten Materialien, Kabeln, Rohren und sonstigen Komponenten (Vorbehaltsware) bis zur vollständigen Erfüllung sämtlicher Forderungen aus dem jeweiligen Vertrag vor.
2. Der Kunde ist verpflichtet, die Vorbehaltsware pfleglich zu behandeln und E-TIB unverzüglich über Zugriffe Dritter (z. B. Pfändungen) schriftlich zu informieren.
3. Bei Verbindung oder Vermischung der Vorbehaltsware mit anderen Gegenständen erwirbt E-TIB Miteigentum an der neuen Sache im Verhältnis des Rechnungswertes der Vorbehaltsware zum Wert der anderen verbundenen Gegenstände zum Zeitpunkt der Verbindung.
## 7. Haftung und Haftungsbeschränkungen
1. E-TIB haftet unbeschränkt bei Vorsatz, grober Fahrlässigkeit sowie bei Verletzung von Leben, Körper oder Gesundheit.
2. Bei leicht fahrlässiger Verletzung wesentlicher Vertragspflichten (Kardinalpflichten) ist die Haftung von E-TIB auf den vertragstypischen, vorhersehbaren Schaden begrenzt. Wesentliche Vertragspflichten sind solche, deren Erfüllung die ordnungsgemäße Durchführung des Vertrages überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf.
3. Die Haftung für leichte Fahrlässigkeit bei der Verletzung nicht wesentlicher Vertragspflichten ist ausgeschlossen.
4. E-TIB haftet nicht für Schäden, die auf unvollständigen, veralteten oder ungenauen Spartenplänen des Kunden oder der örtlichen Netzbetreiber beruhen, sofern E-TIB die Unvollständigkeit nicht vorsätzlich oder grob fahrlässig hätte erkennen müssen.
5. Die vorstehenden Haftungsbeschränkungen gelten auch zugunsten der Angestellten, Vertreter und Erfüllungsgehilfen von E-TIB.
## 8. Gerichtsstand und Schlussbestimmungen
1. Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG).
2. Erfüllungsort für alle Lieferungen und Leistungen sowie Zahlungsort ist der Sitz von E-TIB in Guben.
3. Ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertragsverhältnis ist Cottbus, sofern der Kunde Kaufmann, juristische Person des öffentlichen Rechts oder ein öffentlich-rechtliches Sondervermögen ist. E-TIB ist jedoch berechtigt, den Kunden auch an dessen allgemeinem Gerichtsstand zu verklagen.
4. Sollte eine Bestimmung dieser AGB unwirksam oder undurchführbar sein oder werden, so wird dadurch die Wirksamkeit der übrigen Bestimmungen nicht berührt. Die unwirksame Bestimmung ist durch eine wirksame Bestimmung zu ersetzen, die dem wirtschaftlichen Zweck der unwirksamen Bestimmung am nächsten kommt.

View File

@@ -1,5 +1,5 @@
---
title: "Grabenlose Leitungsverlegung"
title: "Bohrtechnik"
date: "2024-03-20"
excerpt: "Präzise Horizontalspülbohrungen und Erdraketen in allen Bodenklassen für eine absolut oberflächenschonende und effiziente Leitungsverlegung."
layout: "fullBleed"
@@ -7,9 +7,9 @@ layout: "fullBleed"
<HeroSection
badge="Kernkompetenz"
title="Grabenlose Verlegung"
subtitle="Oberflächenschonende Horizontalspülbohrungen und Erdraketen"
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
title="Bohrtechnik"
subtitle="Grabenlose Leitungsverlegung, Horizontalspülbohrungen und Erdraketen"
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg' }}
alignment="center"
ctaLabel="Projekt anfragen"
ctaHref="/de/kontakt"
@@ -26,7 +26,7 @@ layout: "fullBleed"
## HDD-Spülbohrungen und Erdraketen
Mit unserem spezialisierten Gerätepark beherrschen wir die unterschiedlichsten Verfahren. Beim **Horizontalspülbohrverfahren (HDD)** steuern wir den Bohrkopf punktgenau unter der Erde hindurch. Wir realisieren dabei Bohrlängen von bis zu 250 Metern am Stück und können Schutzrohre mit einem Durchmesser von bis zu 400 mm einziehen.
Mit unserem spezialisierten Gerätepark beherrschen wir die unterschiedlichsten Verfahren. Beim **Horizontalspülbohrverfahren (HDD)** steuern wir den Bohrkopf punktgenau unter der Erde hindurch. Wir können bis zu 400 Meter am Stück bohren und, je nach Bohrlänge, Schutzrohre mit einem Durchmesser von bis zu 600 mm einziehen.
Für kürzere Distanzen, wie beispielsweise schnelle und schonende Hausanschlüsse, setzen wir unsere **Erdraketen** (Bodenverdrängungsverfahren) ein. Hierbei erreichen wir Durchörterungslängen von bis zu 15 Metern für Rohre bis 160 mm Durchmesser ideal um Vorgärten und Einfahrten komplett unversehrt zu lassen.
@@ -47,8 +47,8 @@ layout: "fullBleed"
fullWidth: false,
lists: [
[
"Max. Bohrlänge: 250 m",
"Max. Durchmesser: 400er Rohr",
"Max. Bohrlänge: 400 m",
"Max. Durchmesser: 600er Rohr",
"Zentimetergenaue Steuerbarkeit",
"Querung von Bahn, Autobahn & Gewässern"
]
@@ -71,11 +71,5 @@ layout: "fullBleed"
]}
/>
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Bohr-Projekte" description="Erfolgreiche HDD-Bohrungen und grabenlose Leitungsverlegungen unter komplexesten Bedingungen." />
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Erfolgreiche HDD-Bohrungen und grabenlose Leitungsverlegungen unter komplexesten Bedingungen." />
<CallToAction
title="Suchen Sie nach einer grabenlosen Lösung?"
text="Wir beraten Sie gerne zu den Möglichkeiten der Horizontalspülbohrung für Ihr Vorhaben."
buttonText="Kontakt aufnehmen"
buttonLink="/de/kontakt"
/>

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"
---
@@ -69,11 +69,5 @@ layout: "fullBleed"
]}
/>
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Breitband-Projekte" description="Wir haben bereits hunderte Kilometer FTTX-Netze erfolgreich realisiert. Entdecken Sie unsere Referenzen." />
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Wir haben bereits hunderte Kilometer FTTX-Netze erfolgreich realisiert. Entdecken Sie unsere Referenzen." />
<CallToAction
title="Sie planen einen FTTX-Ausbau?"
text="Wir beraten Sie gerne vollumfänglich rund um den Bereich FTTX und den Glasfaserausbau."
buttonText="Kontakt aufnehmen"
buttonLink="/de/kontakt"
/>

View File

@@ -1,12 +1,12 @@
---
title: "Startseite - Experten für Kabeltiefbau & Horizontalspülbohrungen"
title: "Startseite - Experten für Kabelnetzbau & Horizontalspülbohrungen"
description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabeltrassen, Horizontalspülbohrungen und Infrastrukturnetze in ganz Deutschland."
---
<HomeHero
title={"Die Experten für\nKabeltiefbau"}
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabeltiefbau und der Bohrtechnik."
title={"Die Experten für\nKabelnetzbau"}
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabelnetzbau und der Bohrtechnik."
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
linkText="Unsere Leistungen"
linkHref="/de/kompetenzen"
@@ -18,27 +18,30 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
companies={[
{
title: "E-TIB GMBH",
description: "Das Unternehmen hat sich voll und ganz auf den Kabelleitungstiefbau spezialisiert. Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
description: "Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
backgroundImage: "/assets/photos/DJI_0243.JPG"
url: "/de/standorte/guben",
backgroundImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg"
},
{
title: "E-TIB Ingenieurgesellschaft mbH",
description: "Unsere Ingenieure planen für Sie die Energie- und Telekommunikationsnetze der Zukunft - Vermessung und Dokumentation inklusive.",
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
url: "https://www.etib-ing.com/",
backgroundImage: "/assets/photos/DSC01137.JPG"
backgroundImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg"
},
{
title: "E-TIB Verwaltung GmbH",
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
url: "/de/standorte/buelstedt",
backgroundImage: "/assets/photos/DJI_0048.JPG"
},
{
title: "E-TIB Bohrtechnik GmbH",
description: "Präzise Horizontalspülbohrungen in allen Bodenklassen für eine oberflächenschonende und grabenlose Leitungsverlegung.",
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
url: "/de/bohrtechnik",
backgroundImage: "/assets/photos/DSC08653.JPG"
}
]}
@@ -59,21 +62,25 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
subtitle="Über E-TIB"
badgeStyle="industrial"
className="mb-6 md:mb-8"
>Ihr <span className="text-primary relative whitespace-nowrap">starker<svg className="absolute -bottom-2 left-0 w-full h-3 text-primary/20" viewBox="0 0 100 10" preserveAspectRatio="none"><path d="M0 5 Q 50 10 100 5" stroke="currentColor" strokeWidth="4" fill="none"/></svg></span> Partner <br/>für Infrastruktur</Heading>
>Ihr <span className="text-primary relative whitespace-nowrap">starker<svg className="absolute -bottom-2 left-0 w-full h-3 text-warning/60" viewBox="0 0 100 10" preserveAspectRatio="none"><path d="M0 5 Q 50 10 100 5" stroke="currentColor" strokeWidth="4" fill="none"/></svg></span> Partner <br/>für Infrastruktur</Heading>
<div className="text-xl text-text-secondary leading-relaxed mb-8">
Die E-TIB GmbH ist ein leistungsfähiger und moderner Anbieter von technischen Dienstleistungen. Wir haben uns auf zukunftsweisende Kernbereiche der Infrastruktur spezialisiert.
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-6 pt-8 border-t border-neutral-200">
<div className="grid grid-cols-3 gap-6 pt-8 border-t border-neutral-200">
<div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">2015</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Gründung</div>
</div>
<div>
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">50+</div>
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeiter</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">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>
</div>
</div>
@@ -110,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
ctaHref="/de/kompetenzen"
items={[
{
title: "Kabeltiefbau",
description: "Verlegung von Mittel- und Niederspannungskabeln",
title: "Kabelleitungsnetzbau",
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
tag: "Energie",
size: "large",
href: "/de/kabeltiefbau",
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabeltiefbau" }
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
},
{
title: "Bohrtechnik",
@@ -123,7 +130,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
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: "Planung",
@@ -131,14 +138,15 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
tag: "Präzision",
size: "small",
href: "/de/planung",
image: { url: "/assets/photos/DJI_0048.JPG", alt: "Planung" }
image: { url: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg", alt: "Planung" }
},
{
title: "Vermessung",
description: "Digitale Bestandsdokumentation",
tag: "Digital",
size: "accent",
href: "/de/vermessung"
href: "/de/vermessung",
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie_Vermessung_(26_von_99).jpg", alt: "Vermessung" }
}
]}
/>

View File

@@ -1,14 +1,14 @@
---
title: "Kabelleitungstiefbau"
title: "Kabelleitungsnetzbau"
date: "2024-03-20"
excerpt: "Professioneller Kabeltiefbau für Mittel- und Niederspannungsnetze. 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"
subtitle="Komplette Infrastruktur-Lösungen für Mittel- und Niederspannungsnetze"
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"
ctaLabel="Projekt anfragen"
@@ -24,13 +24,11 @@ layout: "fullBleed"
Auf vielen Baustellen werden zunehmend Billiganbieter aus dem Ausland eingesetzt, was häufig dazu führt, dass **Sicherheitsstandards und Dokumentationsanforderungen missachtet** werden. Die Folgen sind gravierend: Unfälle, defekte Fremdleitungen (wie Strom, Wasser und Gas) sowie massive Bauverzögerungen und teure Nachträge. Ein vermeintliches Schnäppchen entpuppt sich so schnell als finanzielle Belastung.
Besonders bei geförderten Infrastruktur-Projekten gestaltet sich der Abruf der Fördergelder ohne eine absolut lückenlose, normgerechte Dokumentation äußerst problematisch.
## Alles aus einer Hand
Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten für unsere Auftraggeber im Vorfeld sichere und zufriedenstellende Lösungen. Die E-TIB GmbH hat sich voll und ganz auf den Kabelleitungstiefbau spezialisiert.
Die E-TIB GmbH hat sich voll und ganz auf den Kabelnetzbau spezialisiert. Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten für unsere Auftraggeber im Vorfeld sichere und zufriedenstellende Lösungen.
Wir bieten Ihnen das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand mit eigenem, hochmodernem Maschinenpark und festangestelltem Fachpersonal aus der Region. In direkter Zusammenarbeit mit unserem Partnerbüro, der E-TIB Ingenieurgesellschaft mbH, können wir zudem frühzeitig Planungsfehler ausschließen und selbst die komplexesten Dokumentationsanforderungen souverän erfüllen.
Wir bieten Ihnen das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand mit eigenem, hochmodernem Maschinenpark und festangestelltem Fachpersonal. In direkter Zusammenarbeit mit unserem Partnerbüro, der E-TIB Ingenieurgesellschaft mbH, können wir zudem frühzeitig Planungsfehler ausschließen und selbst die komplexesten Dokumentationsanforderungen souverän erfüllen.
<DataGridPulse />
</div>
@@ -39,7 +37,7 @@ layout: "fullBleed"
badge="Leistungsspektrum"
title="Alles für Ihr Netz"
descriptionParagraphs={[
"Wir bieten höchste Präzision, modernste Maschinen und zertifizierte Experten für jedes Tiefbau-Projekt von der Kabelverlegung bis zur Transformatorstation."
"Wir bieten höchste Präzision, modernste Maschinen und zertifizierte Experten für Ihr Projekt."
]}
panels={[
{
@@ -51,7 +49,7 @@ layout: "fullBleed"
[
"Herstellung von Kabel- und Rohrgräben",
"Kabelverlegung von 0,4 kV bis 110 kV",
"Kabelmontagen von 0,4 kV bis 30 kV",
"Kabelmontagen bis 110 kV",
"Kabelpflugarbeiten für schnelle Verlegung"
]
]
@@ -73,11 +71,5 @@ layout: "fullBleed"
]}
/>
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Tiefbau-Projekte" description="Wir haben über hunderte Kilometer Kabel verlegt. Sehen Sie sich unsere Referenz-Projekte an." />
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Wir haben über hunderte Kilometer Kabel verlegt. Sehen Sie sich unsere Referenz-Projekte an." />
<CallToAction
title="Sie haben ein konkretes Projekt?"
text="Wir beraten Sie gerne zu allen Fragen rund um Kabeltiefbau und Leitungsverlegung."
buttonText="Kontakt aufnehmen"
buttonLink="/de/kontakt"
/>

View File

@@ -1,25 +1,26 @@
---
title: "Karriere bei E-TIB - Jobs im Kabeltiefbau & Bohrtechnik"
title: "Karriere bei E-TIB - Jobs im Kabelnetzbau & Bohrtechnik"
date: "2024-03-20"
excerpt: "Werden Sie Teil eines dynamischen Teams und bauen Sie mit uns die Infrastruktur der Zukunft."
layout: "fullBleed"
---
<HomeHero
<HeroSection
badge="Karriere"
title="Zukunft bauen"
description="Suchen Sie eine neue Herausforderung in einem zukunftssicheren Markt? Bei E-TIB arbeiten wir täglich daran, Deutschland zu vernetzen und die Energiewende voranzutreiben."
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
linkText="Offene Stellen"
linkHref="#stellen"
subtitle="Suchen Sie eine neue Herausforderung in einem zukunftssicheren Markt? Bei E-TIB arbeiten wir täglich daran, Deutschland zu vernetzen und die Energiewende voranzutreiben."
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-28.jpg' }}
alignment="center"
ctaLabel="Offene Stellen"
ctaHref="#stellen"
/>
<JobListingBlock showFairs={false} showJobs={true} />
<BenefitGrid
badge="E-TIB Standard"
title="Wir bieten unseren Mitarbeitern"
description="Die E-TIB GmbH wurde aus der Motivation gegründet, hochwertige Leistungen sowie faire Entlohnung zu ermöglichen. Neben der Entlohnung steht die Wertschätzung jedes einzelnen Mitarbeiters bei uns an oberster Stelle."
title="Wir bieten unseren Mitarbeitenden"
description="Die E-TIB GmbH wurde aus der Motivation gegründet, hochwertige Leistungen sowie faire Entlohnung zu ermöglichen. Neben der Entlohnung steht die Wertschätzung jedes einzelnen Mitarbeitenden bei uns an oberster Stelle."
benefits={[
{
id: "region",
@@ -96,10 +97,3 @@ layout: "fullBleed"
]}
/>
<CallToAction
title="Initiativbewerbung (m/w/d)"
description="Sie können sich vorstellen, eine langfristige Herausforderung in unserem Unternehmen anzunehmen, finden aber kein passendes Stellenangebot? Bewerben Sie sich initiativ."
ctaLabel="Zum Kontaktformular"
ctaHref="/de/kontakt"
theme="dark"
/>

View File

@@ -1,14 +1,14 @@
---
title: "Kompetenzen - Kabeltiefbau & Spülbohrtechnik"
title: "Kompetenzen - Kabelnetzbau & Spülbohrtechnik"
date: "2024-03-20"
excerpt: "Von Kabeltiefbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
excerpt: "Von Kabelnetzbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
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: "Kabeltiefbau",
description: "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
title: "Kabelleitungsnetzbau",
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
tag: "Energie",
size: "large",
href: "/de/kabeltiefbau",
image: { url: "/assets/photos/DSC01123.JPG", alt: "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,13 +70,13 @@ layout: "fullBleed"
panels={[
{
id: "energy",
title: "Kabelleitungstiefbau (Mittel-/Niederspannung)",
title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)",
icon: "energy",
lists: [
[
"Herstellung von Kabel- und Rohrgräben",
"Kabelverlegung (0,4kV - 110kV)",
"Kabelmontagen 0,4 kV - 30 kV",
"Kabelmontagen bis 110 kV",
"Errichtung von Transformatorstationen",
"Erstellung von Erdungsanlagen",
"Errichtung von Straßenbeleuchtung",
@@ -86,7 +86,7 @@ layout: "fullBleed"
},
{
id: "fiber",
title: "Kabelleitungstiefbau (Telekommunikation)",
title: "Kabelleitungsnetzbau (Telekommunikation)",
icon: "fiber",
lists: [
[
@@ -143,8 +143,8 @@ layout: "fullBleed"
<JsonLd data={[
{
"@type": "Service",
"name": "Kabeltiefbau",
"description": "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
"name": "Kabelleitungsnetzbau",
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
},
{

View File

@@ -5,13 +5,14 @@ excerpt: "Treffen Sie das Team der E-TIB Gruppe persönlich auf Fachmessen und B
layout: "fullBleed"
---
<HomeHero
<HeroSection
badge="Messen & Events"
title="Treffen Sie uns persönlich"
description="Lernen Sie die E-TIB Gruppe auf den führenden Branchenmessen kennen."
videoUrl="/assets/videos/web/hero-bahnkreuzung.mp4"
linkText="Kontakt aufnehmen"
linkHref="/de/kontakt"
subtitle="Lernen Sie die E-TIB Gruppe auf den führenden Branchenmessen kennen."
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg' }}
alignment="center"
ctaLabel="Kontakt aufnehmen"
ctaHref="/de/kontakt"
/>
<div id="termine">
@@ -19,11 +20,10 @@ layout: "fullBleed"
showFairs={true}
showJobs={false}
fairs={[
{ name: 'Intersolar München', date: '2024 / 2025', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
{ name: 'Windenergietage Linstow', date: '2024 / 2025', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
{ name: 'Kabel-Workshop Wiesbaden', date: '2024 / 2025', type: 'Fachmesse', location: 'Wiesbaden' }
{ name: 'Intersolar München', date: '23. - 25. Juni 2026', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
{ name: 'Windenergietage Linstow', date: '10. - 12. November 2026', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
{ name: 'Werkstatt Kabel 2026', date: '24. - 25. November 2026', type: 'Fachtagung', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' }
]}
/>
</div>
<CallToAction />

View File

@@ -9,7 +9,7 @@ layout: "fullBleed"
badge="Kernkompetenz"
title="Planung & Beratung"
subtitle="Kompetente Begleitung für Energie- und Telekommunikationsnetze"
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
backgroundImage={{ url: '/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg' }}
alignment="center"
ctaLabel="Projekt anfragen"
ctaHref="/de/kontakt"
@@ -17,7 +17,7 @@ layout: "fullBleed"
<div className="container px-4 max-w-7xl mx-auto py-16">
<blockquote>
Wir bieten unseren Kunden alles aus einer Hand von der Grobplanung Ihrer Projektidee über den Kabelleitungstiefbau bis zum Bau Ihres erneuerbaren Energieprojektes.
Wir bieten unseren Kunden alles aus einer Hand von der Grobplanung Ihrer Projektidee über den Kabelleitungsnetzbau bis zum Bau Ihres erneuerbaren Energieprojektes.
</blockquote>
## Fundament für den Projekterfolg
@@ -71,9 +71,3 @@ layout: "fullBleed"
<HomeReferencesSlider badge="Praxisbeispiele" title="Planung in der Praxis" description="Erfolgreich geplante und begleitete Infrastrukturprojekte unserer Unternehmensgruppe." />
<CallToAction
title="Benötigen Sie Planungssicherheit?"
text="Lassen Sie uns über Ihr Vorhaben sprechen. Wir stehen Ihnen mit unserem Ingenieurwissen zur Seite."
buttonText="Kontakt aufnehmen"
buttonLink="/de/kontakt"
/>

View File

@@ -1,14 +0,0 @@
---
title: "Breitbandausbau FTTH, Landkreis Börde"
client: "EKN Breitband GmbH & Co. KG"
date: "2019-05-01"
dateString: "Seit 05/2019"
location: "Landkreis Börde"
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
---
- 109.000 m Leitungstrasse herstellen (Tiefbau/HDD)
- 63 Stück Aufstellung Netzverteiler
- 747 Stück Herstellung Hausanschlüsse
- Koordination und Realisierung der gesamten Trassenführung
- Dokumentation und Abnahme der Hausanschlüsse

View File

@@ -1,13 +0,0 @@
---
title: "Breitbandausbau FTTH, Bomlitz / Schwarmstedt"
client: "inexio Informationstechnologie und Telekommunikation GmbH"
date: "2018-08-01"
dateString: "08/2018 bis 03/2021"
location: "29699 Bomlitz / 29690 Schwarmstedt"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-60.jpg"
---
- 88.890 m Leitungstrasse herstellen (Tiefbau/HDD)
- 23 Stück Aufstellung Multifunktionsgehäuse
- 52 Stück Aufstellung Netzverteiler
- 571 Stück Herstellung Hausanschlüsse

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