Compare commits

..

112 Commits

Author SHA1 Message Date
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
5977ebf23d fix(ci): fix getTranslations import and clean AISearchResults orphaned syntax
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m9s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 45s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-28 11:48:21 +02:00
ee2bcea42b fix(team,certs): revert Danny Joseph email and add missing text certificates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
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 4s
2026-05-28 11:08:11 +02:00
2f2fcfdf13 fix(typography): prevent ugly line breaks in InteractiveGermanyMap titles
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
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 4s
2026-05-28 10:38:07 +02:00
070f97dd6f fix(i18n): fix reference page translation, fix map zoom, connect contact form to server action, fix english 404 links, fix certificate pdf links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
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 6s
2026-05-28 09:58:34 +02:00
2a46015d0d fix(i18n): localize TeamGrid component
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🧪 QA (push) Successful in 1m12s
Build & Deploy / 🏗️ Build (push) Successful in 2m58s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🔔 Notify (push) Successful in 5s
2026-05-28 09:38:59 +02:00
5329d96e3b fix(links): fix unlocalized and missing hrefs in map, hero, job listing and call to action
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m11s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 47s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-27 17:49:23 +02:00
0c01aa799d fix(contact): update email to info@e-tib.com and fix map zoom level
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m7s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-27 17:41:52 +02:00
75c15fce43 fix(i18n): ensure next-intl client component namespaces are loaded
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m7s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 51s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-27 16:41:17 +02:00
3b20d4087c fix(i18n): ensure 100% translation parity on all pages and components
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 30s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 3m3s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 46s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-27 13:13:55 +02:00
2dbafec633 fix(parity): update footer translations, certificate links, references CTA and trade fairs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 2m43s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-27 13:03:00 +02:00
2b79d18a93 fix: full translation parity, component consistency, and link clickability
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m47s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-27 12:04:51 +02:00
fa747763b8 fix(locale): align all English MDX page links and add automated links validation test to prevent language drift
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-26 15:18:36 +02:00
0d71e2ad24 merge: GermanyMap minorLocations enrichment and full page parity fix
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 47s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-26 15:06:59 +02:00
64d4edc175 fix(map): enrich and render minorLocations alongside defaultLocations on dynamic slug pages to establish 100% map parity with referenzen overview
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-26 15:00:51 +02:00
222c894266 merge: references carousel click-drag fix and complete DE/EN parity alignment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m7s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 44s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-26 14:45:34 +02:00
4a3aee7691 fix(locale): align DE/EN component structures, fix dynamic language switcher slug mapping, and localize referenzen map
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-26 14:40:56 +02:00
fd8dac97d0 fix(blocks): add clickable dynamic navigation links and click-drag gesture prevention to ReferencesSlider
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
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-05-26 14:36:34 +02:00
bc5a78d73e chore(ci): aggressively prune buildkit caches inside host builder containers
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Successful in 1m42s
Build & Deploy / 🏗️ Build (push) Successful in 3m45s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 48s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-26 12:12:39 +02:00
f43b945756 chore(ci): list running host containers using docker ps
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m13s
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-05-26 12:10:41 +02:00
183342cb8c chore(ci): print host docker storage breakdown via du -d 1
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-05-26 12:06:30 +02:00
3728c37dce chore(ci): find host files larger than 100M in docker directory
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
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-05-26 12:05:41 +02:00
f61f1411cb chore(ci): add host diagnostics (df -h, df -i, docker info) to debug runner disk/inode exhaustion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
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-26 12:04:36 +02:00
d2668e36eb chore(ci): run aggressive docker volume and system prune during maintenance to resolve ENOSPC errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 39s
Build & Deploy / 🧪 QA (push) Failing after 1m39s
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 13s
2026-05-26 11:53:31 +02:00
5678ddcfd9 fix: interactive map visuals and docker dev setup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Failing after 2m52s
Build & Deploy / 🔔 Notify (push) Successful in 1s
- Removed confusing SVG lines from map

- Restored distinct clickable markers for major references

- Fixed corepack failing to install pnpm v11 in Docker by explicitly forcing v10

- Added direct port mapping 3001:3001 to bypass proxy issues
2026-05-26 11:31:31 +02:00
06d5178614 feat(ui): add nice informative tooltips to footer quality badges
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 1m11s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 45s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-18 22:18:48 +02:00
a983696153 chore(ci): also purge hidden files in /opt/hostedtoolcache to reclaim disk space
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 46s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-05-18 14:46:27 +02:00
6908964df5 chore(ci): purge /opt/hostedtoolcache in maintenance step to free disk space
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-18 14:45:46 +02:00
159587f545 chore(ci): make docker cleanup robust against hangs and log system disk usage
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
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-18 14:45:05 +02:00
c02b49644e chore(ci): log disk usage and add aggressive docker system volume prune
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-18 14:42:42 +02:00
313fa14c56 chore(ci): use aggressive docker system prune to resolve runner disk space issues
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 17s
Build & Deploy / 🧪 QA (push) Successful in 59s
Build & Deploy / 🏗️ Build (push) Failing after 1m13s
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-18 14:39:36 +02:00
052be7d1c9 fix(referenzen): add cleanLocation helper to resolve compilation and linting errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 59s
Build & Deploy / 🏗️ Build (push) Failing after 1m15s
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-18 14:36:30 +02:00
aef10f706c fix(pipeline): pass dynamic NPM_DOMAIN to docker build to prevent 404s, fix reference badge location cleaning, and enable glowing border on map
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
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 2s
2026-05-18 11:58:35 +02:00
35d9650626 fix(types): fix typescript errors breaking the pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 59s
Build & Deploy / 🏗️ Build (push) Failing after 1m19s
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-17 15:41:49 +02:00
140cb012df chore(content): align career benefits exactly with legacy reference promises
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
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 22:52:45 +02:00
e1c9720eeb refactor: remove Karriere link from header navigation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Failing after 1m16s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-05-15 22:49:32 +02:00
843d61cd5d feat: overhaul competence pages, unify brand colors, and polish site integrity
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Failing after 1m8s
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
- Rewrote all 5 competence pages (Kabeltiefbau, Bohrtechnik, Glasfaser, Planung, Vermessung) with factual data from references
- Unified brand colors: replaced toxic neon green with brand-aligned teal (#10a379)
- Fixed ReferencesSlider injection in dynamic MDX pages
- Enhanced Footer with icon parity, improved branding placement, and unified hover effects
- Refined Header with 3D micro-animations and improved dropdown visibility
- Fixed localized tracking API (rewrites for /stats/api/send)
- Resolved ReferenceErrors in InteractiveGermanyMap and Header state management
- Cleaned reference titles and truncated metadata in lists
2026-05-15 21:07:25 +02:00
4dcd5f511a Revert "fix: remove hallucinated sub-companies and timelines"
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Failing after 57s
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
This reverts commit 62644a0366.
2026-05-15 12:51:20 +02:00
62644a0366 fix: remove hallucinated sub-companies and timelines
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
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-15 12:50:12 +02:00
db9e0fde91 fix: resolve content hallucinations and restore legal parity with legacy site
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 1s
2026-05-15 12:45:41 +02:00
0516dbe743 feat: modernize digital presence, team layout, and hero component parity
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 17s
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-05-15 11:03:01 +02:00
3808339711 chore: add www.nemo-gmbh.de site reference mirror
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
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 10:08:49 +02:00
dd04755690 chore: add www.etib-ing.com site reference mirror
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
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-15 10:08:00 +02:00
5919d5d47a chore: add e-tib.com site reference mirror
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
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-15 10:07:01 +02:00
e327c77e52 feat(ui): add dedicated certificates pages and block component
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
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 09:38:42 +02:00
c48bf18433 chore: update design preview email draft & fix support section margin
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
Build & Deploy / 🧪 QA (push) Successful in 58s
Build & Deploy / 🏗️ Build (push) Failing after 2m48s
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 09:19:30 +02:00
372ef1a418 fix(a11y): resolve color-contrast and link-in-text-block WCAG violations
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m0s
Build & Deploy / 🏗️ Build (push) Failing after 2m46s
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 09:04:37 +02:00
331 changed files with 35838 additions and 1940 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

@@ -43,9 +43,40 @@ jobs:
- name: 🧹 Maintenance (High Density Cleanup)
shell: bash
run: |
echo "Purging old build layers and dangling images..."
docker image prune -f
docker builder prune -f --filter "until=24h"
echo "=== System Disk Usage ==="
df -h || true
echo "=== Tool Cache Usage ==="
du -sh /opt/hostedtoolcache/* || true
du -sh /opt/hostedtoolcache/.[!.]* || true
echo "Purging old tool caches..."
rm -rf /opt/hostedtoolcache/* || true
rm -rf /opt/hostedtoolcache/.[!.]* || true
echo "=== Host Disk Usage ==="
docker run --rm -v /:/host-root alpine df -h || true
echo "=== Host Inode Usage ==="
docker run --rm -v /:/host-root alpine df -i || true
echo "=== Running Host Containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
echo "=== Host Docker Info ==="
docker info || true
echo "Pruning host Buildkit builder caches aggressively..."
for container in $(docker ps --filter "name=buildx_buildkit_builder" --format "{{.Names}}"); do
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 --volumes || true
echo "=== System Disk Usage After Prune ==="
df -h || true
- name: Checkout repository
uses: actions/checkout@v4
@@ -189,27 +220,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 }}
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
@@ -269,6 +306,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"
@@ -305,6 +355,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
@@ -312,6 +363,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
@@ -324,7 +376,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
@@ -332,6 +383,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"

1
.pa11yci/report.json Normal file

File diff suppressed because one or more lines are too long

18
.pa11yci/single-test.json Normal file
View File

@@ -0,0 +1,18 @@
{
"defaults": {
"runners": ["axe"],
"timeout": 60000,
"chromeLaunchConfig": {
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu"
]
},
"ignore": []
},
"urls": [
"http://etib.localhost/de/blog/moderne-verfahren-kabeltiefbau"
]
}

9
.pa11yci/test-html.json Normal file
View File

@@ -0,0 +1,9 @@
{
"defaults": {
"runners": ["axe"],
"ignore": ["color-contrast"]
},
"urls": [
"file:////Volumes/Alpha SSD/Coding/e-tib.com/test.html"
]
}

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

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

View File

@@ -0,0 +1 @@
{"hash":"85340b254e0f10f1","duration":6595,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"9cc3e16c40a10f42f7a963333e800e1384f047b1f387b18b5f37769c34f36609"}

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

BIN
.turbo/cache/32905076da5f4894.tar.zst vendored Normal file

Binary file not shown.

View File

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

View File

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

BIN
.turbo/cache/7e2629942c881f80.tar.zst vendored Normal file

Binary file not shown.

View File

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

View File

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

BIN
.turbo/cache/85340b254e0f10f1.tar.zst vendored Normal file

Binary file not shown.

View File

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

View File

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

Binary file not shown.

5
.turbo/turbo-lint.log Normal file
View File

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

55
.turbo/turbo-test.log Normal file
View File

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

View File

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

View File

@@ -20,13 +20,15 @@ ENV CI=true
COPY pnpm-lock.yaml package.json .npmrc* ./
COPY patches* ./patches/
ARG NPM_DOMAIN=git.infra.mintel.me
# Configure private registry and install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
if [ -f /run/secrets/NPM_TOKEN ]; then \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN); \
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc; \
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}" >> .npmrc; \
echo "@mintel:registry=https://${NPM_DOMAIN}/api/packages/mmintel/npm" > .npmrc; \
echo "//${NPM_DOMAIN}/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}" >> .npmrc; \
fi && \
pnpm install --no-frozen-lockfile && \
rm -f .npmrc

View File

@@ -5,8 +5,8 @@ RUN apk add --no-cache libc6-compat python3 make g++ curl
WORKDIR /app
# Enable corepack for pnpm
RUN corepack enable
# Install pnpm v10 explicitly (corepack defaults to v11 which requires Node 22)
RUN npm install -g pnpm@10.18.3
# Pre-set the pnpm store directory
ENV PNPM_HOME="/pnpm"

BIN
after-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -9,8 +9,7 @@ import { SITE_URL } from '@/lib/schema';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
import { getAllReferences } from '@/lib/references';
import { defaultLocations } from '@/lib/map-data';
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';
@@ -23,8 +22,15 @@ import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
import { HeroSection } from '@/components/blocks/HeroSection';
import { TeamGrid } from '@/components/blocks/TeamGrid';
import { DeepDrillAnimation } from '@/components/blocks/DeepDrillAnimation';
import { DataGridPulse } from '@/components/blocks/DataGridPulse';
import { GrowthChart } from '@/components/blocks/GrowthChart';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { AnimatedCounter } from '@/components/ui';
const mdxComponents = {
@@ -39,8 +45,15 @@ const mdxComponents = {
BenefitGrid,
InteractiveGermanyMap,
FaqBlock,
CertificatesBlock,
HeroSection,
TeamGrid,
JsonLd,
Button,
AnimatedCounter,
GrowthChart,
DeepDrillAnimation,
DataGridPulse,
// Standard HTML element mapping for consistent E-TIB typography
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} />,
@@ -139,7 +152,8 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
// Fetch references to enrich the map if it's used
const references = await getAllReferences(locale);
const enrichedLocations = defaultLocations.map(loc => {
const allLocations = [...defaultLocations, ...minorLocations];
const enrichedLocations = allLocations.map(loc => {
if (loc.type === 'project') {
const ref = references.find(r => r.slug === loc.id);
if (ref) {
@@ -147,7 +161,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
...loc,
featuredImage: ref.frontmatter.featuredImage,
details: [
ref.frontmatter.client || 'Kunde',
ref.frontmatter.client || t('client'),
ref.frontmatter.dateString || new Date(ref.frontmatter.date).getFullYear().toString(),
],
};
@@ -159,6 +173,13 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
const pageMdxComponents = {
...mdxComponents,
InteractiveGermanyMap: (props: any) => <InteractiveGermanyMap locations={enrichedLocations} {...props} />,
HomeReferencesSlider: (props: any) => <HomeReferencesSlider {...props} references={references.map(r => ({
id: r.slug,
slug: r.slug,
title: r.frontmatter.title,
category: Array.isArray(r.frontmatter.category) ? r.frontmatter.category[0] : (r.frontmatter.category || 'Projekt'),
image: r.frontmatter.featuredImage
}))} />,
};
return (
@@ -209,7 +230,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
{/* Support Section */}
<Container>
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="mt-24 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>

View File

@@ -24,13 +24,13 @@ const mdxComponents = {
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="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...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} />,
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-normal max-w-3xl" {...props} />,
ul: (props: any) => <ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-normal max-w-3xl" {...props} />,
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-normal max-w-3xl" {...props} />,
li: (props: any) => (
<li 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" {...props} />
),
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} />,
a: (props: any) => <a className="text-text-secondary underline decoration-primary decoration-2 underline-offset-4 hover:text-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} />
@@ -114,12 +114,12 @@ export default async function BlogPost({ params }: BlogPostProps) {
}}
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/90 via-neutral-dark/40 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/90 via-neutral-dark/70 to-neutral-dark/30" />
{/* Title overlay on image */}
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
<div className="container mx-auto px-4">
<div className="max-w-4xl">
<div className="max-w-4xl bg-neutral-dark/40 backdrop-blur-md p-6 rounded-2xl border border-white/10">
{post.frontmatter.category && (
<div className="overflow-hidden mb-6">
<span className="inline-block px-4 py-1.5 bg-accent text-neutral-dark text-xs font-bold uppercase tracking-[0.2em] rounded-sm">
@@ -127,10 +127,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
</span>
</div>
)}
<Heading level={1} className="text-white mb-8 drop-shadow-2xl">
<Heading level={1} className="text-white mb-8 drop-shadow-md">
{post.frontmatter.title}
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
<div className="flex flex-wrap items-center gap-6 text-white text-sm md:text-base font-medium drop-shadow-sm">
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
year: 'numeric',
@@ -138,7 +138,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
day: 'numeric',
})}
</time>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="w-1 h-1 bg-white/50 rounded-full" />
<span>{readingTime} min read</span>
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
@@ -206,7 +206,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
)}
{/* Main content */}
<div className="w-full">
<div className="w-full [&_a]:text-primary [&_a:hover]:text-primary-dark [&_a]:underline [&_a]:decoration-primary [&_a]:underline-offset-4 [&_a]:transition-all [&_a]:font-bold [&_a_strong]:text-inherit">
<MDXRemote source={post.content} components={mdxComponents} />
</div>

View File

@@ -120,7 +120,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
opens: '07:00',
closes: '17:00',
},
],
@@ -214,6 +214,20 @@ export default async function ContactPage({ params }: ContactPageProps) {
/>
</div>
</div>
<div className="flex items-start gap-4 md:gap-6 group">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
<svg className="w-5 h-5 md:w-7 md:h-7" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
<div>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
{locale === 'de' ? 'Telefon' : 'Phone'}
</h4>
<a href="tel:+4935616857733" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target block">
+49 (0) 3561 / 68577 33
</a>
</div>
</div>
</address>
</div>

View File

@@ -3,8 +3,9 @@
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { getAppServices } from '@/lib/services/create-services';
import { Container, Button, Heading } from '@/components/ui';
import Scribble from '@/components/Scribble';
import { Button, Heading } from '@/components/ui';
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
import { motion } from 'framer-motion';
export default function Error({
error,
@@ -16,6 +17,9 @@ export default function Error({
const t = useTranslations('Error');
useEffect(() => {
// Force standard solid header
document.body.setAttribute('data-header-variant', 'standard');
// Treat "Failed to find Server Action" as a deployment sync issue and reload
if (error?.message?.includes('Failed to find Server Action')) {
window.location.reload();
@@ -30,64 +34,131 @@ export default function Error({
digest: error?.digest,
});
// Force solid header
document.body.setAttribute('data-force-solid-header', 'true');
return () => {
document.body.removeAttribute('data-force-solid-header');
document.body.removeAttribute('data-header-variant');
};
}, [error]);
return (
<>
<div className="relative min-h-[80vh] flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4 mt-16">
{/* Deep Tech Grid Background */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] pointer-events-none" />
{/* Glowing Orb */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-red-500/20 blur-[120px] rounded-full pointer-events-none" />
<div className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4">
{/* Industrial Blueprint Background */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff_1px,transparent_1px),linear-gradient(to_bottom,#ffffff_1px,transparent_1px)] bg-[size:100px_100px]" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff_1px,transparent_1px),linear-gradient(to_bottom,#ffffff_1px,transparent_1px)] bg-[size:20px_20px]" />
</div>
<div className="relative z-10 w-full max-w-2xl mx-auto">
<div className="group relative bg-[#050B14]/80 backdrop-blur-xl rounded-[2.5rem] p-12 md:p-16 border border-white/10 shadow-2xl overflow-hidden text-center">
{/* Inner Shimmer */}
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
<div className="relative mb-8">
<h1 className="text-8xl md:text-9xl font-heading font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-white/20 drop-shadow-[0_0_30px_rgba(255,255,255,0.1)]">
{/* Decorative Technical Annotations */}
<div className="absolute inset-0 pointer-events-none overflow-hidden select-none">
<div className="absolute top-40 left-10 text-[10px] font-mono text-primary/30 uppercase tracking-[0.2em] rotate-90 origin-left">
System_Core_Access :: Restricted
</div>
<div className="absolute bottom-40 right-10 text-[10px] font-mono text-primary/30 uppercase tracking-[0.2em] -rotate-90 origin-right">
Protocol_Failure :: Type_500
</div>
<div className="absolute top-1/4 right-20 w-32 h-px bg-primary/20" />
<div className="absolute top-1/4 right-20 w-px h-32 bg-primary/20" />
</div>
{/* Background Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-red-500/10 blur-[150px] rounded-full pointer-events-none animate-pulse" />
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center">
{/* The "Broken" Machine Visual */}
<div className="relative mb-12">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="relative"
>
<h1 className="text-[12rem] md:text-[18rem] font-heading font-black leading-none select-none">
<span className="relative inline-block text-transparent bg-clip-text bg-gradient-to-b from-white to-white/5 drop-shadow-[0_0_50px_rgba(255,255,255,0.1)]">
500
</h1>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-1 bg-red-500/40 blur-md skew-y-[-10deg]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-px bg-red-500 skew-y-[-10deg] shadow-[0_0_15px_rgba(239,68,68,1)]" />
{/* Glitch Layers */}
<span className="absolute inset-0 text-red-500/30 translate-x-[2px] -translate-y-[1px] mix-blend-screen animate-pulse pointer-events-none">500</span>
<span className="absolute inset-0 text-blue-500/30 -translate-x-[2px] translate-y-[1px] mix-blend-screen animate-pulse pointer-events-none">500</span>
</span>
</h1>
{/* Warning Overlay */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<motion.div
animate={{
opacity: [0.4, 1, 0.4],
scale: [0.95, 1.05, 0.95]
}}
transition={{ duration: 2, repeat: Infinity }}
className="p-4 rounded-2xl bg-red-500/20 border border-red-500/40 backdrop-blur-md shadow-[0_0_30px_rgba(239,68,68,0.3)]"
>
<ShieldAlert className="w-12 h-12 text-red-500" />
</motion.div>
</div>
</motion.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 w-full items-start">
{/* Main Content */}
<div className="md:col-span-7 text-center md:text-left">
<div className="inline-flex items-center gap-3 px-4 py-2 mb-6 rounded-lg bg-red-500/10 border border-red-500/20 text-[10px] font-mono uppercase tracking-widest text-red-400">
<Activity className="w-4 h-4 animate-pulse" />
CRITICAL_SYSTEM_OVERRIDE :: ACTIVE
</div>
<div className="inline-flex items-center gap-3 px-4 py-1.5 mb-6 rounded-full bg-red-500/10 border border-red-500/20 text-xs font-bold uppercase tracking-wider text-red-400">
<span className="w-2 h-2 rounded-full bg-red-500 animate-[ping_2s_infinite] shadow-[0_0_10px_rgba(239,68,68,0.8)]" />
SYSTEM PROTOCOL: CRITICAL ERROR
</div>
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-white">
<Heading level={1} className="text-3xl md:text-5xl font-bold mb-6 text-white leading-tight">
{t('title')}
</Heading>
<p className="text-white/60 mb-10 max-w-md text-lg mx-auto leading-relaxed">
<p className="text-white/50 mb-10 max-w-lg text-lg leading-relaxed">
{t('description')}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button onClick={() => reset()} variant="primary" className="shadow-[0_0_20px_rgba(var(--color-primary),0.3)]">
<div className="flex flex-col sm:flex-row gap-4 justify-center md:justify-start">
<Button
onClick={() => reset()}
variant="primary"
className="group h-14 px-10 shadow-[0_0_30px_rgba(var(--color-primary),0.2)] hover:shadow-primary/40 transition-all"
>
<RefreshCw className="w-5 h-5 mr-3 group-hover:rotate-180 transition-transform duration-700" />
{t('tryAgain')}
</Button>
<Button href="/" variant="outline" className="border-white/20 text-white hover:bg-white/5">
<Button
href="/"
variant="outline"
className="h-14 px-8 border-white/10 text-white hover:bg-white/5"
>
<Home className="w-5 h-5 mr-3" />
{t('goHome')}
</Button>
</div>
</div>
{/* Decorative Base Line */}
<div className="w-full flex justify-center mt-12 opacity-50">
<div className="w-px h-24 bg-gradient-to-t from-red-500/50 to-transparent" />
{/* Technical Log Terminal */}
<div className="md:col-span-5">
<div className="bg-black/40 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
<div className="bg-white/5 px-4 py-2 border-b border-white/10 flex items-center justify-between">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-red-500/50" />
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/50" />
<div className="w-2.5 h-2.5 rounded-full bg-green-500/50" />
</div>
<div className="text-[10px] font-mono text-white/30 tracking-wider">TERMINAL_OUTPUT</div>
</div>
<div className="p-6 font-mono text-xs leading-relaxed overflow-hidden h-[240px]">
<div className="text-primary/70 mb-1 animate-[fade-in_0.5s_ease-out_forwards]">[SYSTEM] Initializing recovery protocol...</div>
<div className="text-red-500/80 mb-1 animate-[fade-in_0.5s_ease-out_0.5s_forwards] opacity-0">[ERROR] Connection timeout at node 0x7F</div>
<div className="text-white/40 mb-1 animate-[fade-in_0.5s_ease-out_1s_forwards] opacity-0">[INFO] Request ID: {error?.digest || '0xDEADBEEF'}</div>
<div className="text-white/40 mb-1 animate-[fade-in_0.5s_ease-out_1.5s_forwards] opacity-0">[INFO] Timestamp: {new Date().toISOString()}</div>
<div className="text-white/40 mb-1 animate-[fade-in_0.5s_ease-out_2s_forwards] opacity-0">[INFO] Trace: {error?.message?.slice(0, 40)}...</div>
<div className="text-primary/70 mt-4 animate-[pulse_1.5s_infinite] opacity-0 animate-[fade-in_0.5s_ease-out_2.5s_forwards]">_ Waiting for user input...</div>
</div>
<div className="bg-red-500/10 p-3 flex items-center gap-3">
<Terminal className="w-4 h-4 text-red-400" />
<div className="text-[10px] font-bold text-red-400 uppercase tracking-tighter">Status: Handshake_Failed</div>
</div>
</div>
</div>
</div>
</div>
</>
</div>
);
}

View File

@@ -19,6 +19,7 @@ import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
import { InitialLoader } from '@/components/providers/InitialLoader';
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
import { EUFundingBadge } from '@/components/decorations/EUFundingBadge';
const inter = Inter({
subsets: ['latin'],
@@ -89,15 +90,21 @@ export default async function Layout(props: {
},
{
label: safeLocale === 'de' ? 'Über uns' : 'About Us',
url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}`
url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}`,
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' ? 'Standorte' : 'Locations', url: `/${safeLocale}/standorte` }
]
},
{
label: safeLocale === 'de' ? 'Referenzen' : 'References',
url: `/${safeLocale}/referenzen`
},
{
label: safeLocale === 'de' ? 'Karriere' : 'Careers',
url: `/${safeLocale}/${await mapFileSlugToTranslated('karriere', safeLocale)}`
label: safeLocale === 'de' ? 'Karriere' : 'Career',
url: `/${safeLocale}/${safeLocale === 'de' ? 'karriere' : 'career'}`
},
{
label: safeLocale === 'de' ? 'Messen' : 'Events',
@@ -122,6 +129,14 @@ export default async function Layout(props: {
'Error',
'StandardPage',
'Brochure',
'JobListingBlock',
'CallToAction',
'InteractiveGermanyMap',
'ReferencesSlider',
'CompanyTimeline',
'TeamGrid',
'AISearch',
'GrowthChart',
];
const clientMessages: Record<string, any> = {};
for (const key of clientKeys) {
@@ -188,6 +203,7 @@ export default async function Layout(props: {
<PageTransitionShutter />
<SkipLink />
<Header navLinks={navLinks} />
<EUFundingBadge />
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
<main

View File

@@ -1,70 +1,145 @@
import { getTranslations } from 'next-intl/server';
import { Container, Button, Heading } from '@/components/ui';
import Scribble from '@/components/Scribble';
'use client';
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Button, Heading } from '@/components/ui';
import { Terminal, Search, Home, Mail, Activity, Cpu } from 'lucide-react';
import { motion } from 'framer-motion';
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
export default async function NotFound() {
console.log('--- NOT FOUND RENDERED ---');
const t = await getTranslations('Error.notFound');
export default function NotFound() {
const t = useTranslations('Error.notFound');
// Suggested URL logic removed along with Payload CMS
let suggestedUrl = null;
let suggestedLang = null;
useEffect(() => {
// Force standard solid header
document.body.setAttribute('data-header-variant', 'standard');
return () => {
document.body.removeAttribute('data-header-variant');
};
}, []);
return (
<>
<script dangerouslySetInnerHTML={{ __html: `document.body.setAttribute('data-force-solid-header', 'true');` }} />
<div className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4">
<ClientNotFoundTracker path="" />
<div className="relative min-h-[80vh] flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4 mt-16">
{/* Deep Tech Grid Background */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] pointer-events-none" />
{/* Glowing Orb */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-primary/20 blur-[120px] rounded-full pointer-events-none" />
{/* Industrial Blueprint Background */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff_1px,transparent_1px),linear-gradient(to_bottom,#ffffff_1px,transparent_1px)] bg-[size:100px_100px]" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff_1px,transparent_1px),linear-gradient(to_bottom,#ffffff_1px,transparent_1px)] bg-[size:20px_20px]" />
</div>
<div className="relative z-10 w-full max-w-2xl mx-auto">
<div className="group relative bg-[#050B14]/80 backdrop-blur-xl rounded-[2.5rem] p-12 md:p-16 border border-white/10 shadow-2xl overflow-hidden text-center">
{/* Inner Shimmer */}
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
<div className="relative mb-8">
<h1 className="text-8xl md:text-9xl font-heading font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-white/20 drop-shadow-[0_0_30px_rgba(255,255,255,0.1)]">
{/* Decorative Technical Annotations */}
<div className="absolute inset-0 pointer-events-none overflow-hidden select-none">
<div className="absolute top-40 left-10 text-[10px] font-mono text-primary/30 uppercase tracking-[0.2em] rotate-90 origin-left">
Discovery_Service :: Failure
</div>
<div className="absolute bottom-40 right-10 text-[10px] font-mono text-primary/30 uppercase tracking-[0.2em] -rotate-90 origin-right">
Address_Resolution :: 0x404
</div>
<div className="absolute top-1/4 left-20 w-32 h-px bg-primary/20" />
<div className="absolute top-1/4 left-20 w-px h-32 bg-primary/20" />
</div>
{/* Background Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/10 blur-[150px] rounded-full pointer-events-none animate-pulse" />
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center">
{/* 404 Visual */}
<div className="relative mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="relative"
>
<h1 className="text-[12rem] md:text-[18rem] font-heading font-black leading-none select-none">
<span className="relative inline-block text-transparent bg-clip-text bg-gradient-to-b from-white to-white/5 drop-shadow-[0_0_50px_rgba(var(--color-primary),0.15)]">
404
</h1>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-1 bg-primary/40 blur-md skew-y-[-10deg]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-px bg-primary skew-y-[-10deg] shadow-[0_0_15px_rgba(var(--color-primary),1)]" />
{/* Glitch Layers */}
<span className="absolute inset-0 text-primary/30 translate-x-[2px] -translate-y-[1px] mix-blend-screen animate-pulse pointer-events-none">404</span>
<span className="absolute inset-0 text-blue-500/20 -translate-x-[2px] translate-y-[1px] mix-blend-screen animate-pulse pointer-events-none">404</span>
</span>
</h1>
{/* Search Scan Overlay */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 1, 0.5]
}}
transition={{ duration: 3, repeat: Infinity }}
className="p-5 rounded-full bg-primary/20 border border-primary/40 backdrop-blur-md shadow-[0_0_40px_rgba(var(--color-primary),0.3)]"
>
<Search className="w-10 h-10 text-primary" />
</motion.div>
</div>
</motion.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 w-full items-start">
{/* Main Content */}
<div className="md:col-span-7 text-center md:text-left">
<div className="inline-flex items-center gap-3 px-4 py-2 mb-6 rounded-lg bg-primary/10 border border-primary/20 text-[10px] font-mono uppercase tracking-widest text-primary">
<Cpu className="w-4 h-4 animate-spin-slow" />
OBJECT_NOT_FOUND :: SCAN_COMPLETE
</div>
<div className="inline-flex items-center gap-3 px-4 py-1.5 mb-6 rounded-full bg-red-500/10 border border-red-500/20 text-xs font-bold uppercase tracking-wider text-red-400">
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse shadow-[0_0_10px_rgba(239,68,68,0.8)]" />
SYSTEM PROTOCOL: NOT FOUND
</div>
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-white">
<Heading level={1} className="text-3xl md:text-5xl font-bold mb-6 text-white leading-tight">
{t('title')}
</Heading>
<p className="text-white/60 mb-10 max-w-md text-lg mx-auto leading-relaxed">
<p className="text-white/50 mb-10 max-w-lg text-lg leading-relaxed">
{t('description')}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button href="/" variant="primary" className="shadow-[0_0_20px_rgba(var(--color-primary),0.3)]">
<div className="flex flex-col sm:flex-row gap-4 justify-center md:justify-start">
<Button
href="/"
variant="primary"
className="group h-14 px-10 shadow-[0_0_30px_rgba(var(--color-primary),0.2)] hover:shadow-primary/40 transition-all"
>
<Home className="w-5 h-5 mr-3" />
{t('cta')}
</Button>
<Button href="/kontakt" variant="outline" className="border-white/20 text-white hover:bg-white/5">
<Button
href="/kontakt"
variant="outline"
className="h-14 px-8 border-white/10 text-white hover:bg-white/5"
>
<Mail className="w-5 h-5 mr-3" />
Support kontaktieren
</Button>
</div>
</div>
{/* Decorative Base Line */}
<div className="w-full flex justify-center mt-12 opacity-50">
<div className="w-px h-24 bg-gradient-to-t from-primary to-transparent" />
{/* Technical Log Terminal */}
<div className="md:col-span-5">
<div className="bg-black/40 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
<div className="bg-white/5 px-4 py-2 border-b border-white/10 flex items-center justify-between">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-primary/50" />
<div className="w-2.5 h-2.5 rounded-full bg-white/20" />
<div className="w-2.5 h-2.5 rounded-full bg-white/10" />
</div>
<div className="text-[10px] font-mono text-white/30 tracking-wider">DISCOVERY_LOG</div>
</div>
<div className="p-6 font-mono text-xs leading-relaxed overflow-hidden h-[220px]">
<div className="text-primary/70 mb-1">[SCAN] Initializing sector sweep...</div>
<div className="text-white/40 mb-1">[INFO] Indexing available nodes...</div>
<div className="text-primary/50 mb-1 animate-pulse">[INFO] Searching for: {typeof window !== 'undefined' ? window.location.pathname : 'unknown_resource'}</div>
<div className="text-red-400/80 mb-1 mt-4">[WARN] Resource 404: Not found in current branch</div>
<div className="text-white/30 mb-1">[INFO] Redirecting to fallback_strategy...</div>
<div className="text-primary/70 mt-4 animate-[pulse_1.5s_infinite]">_ Ready for manual redirect...</div>
</div>
<div className="bg-primary/10 p-3 flex items-center gap-3">
<Activity className="w-4 h-4 text-primary" />
<div className="text-[10px] font-bold text-primary uppercase tracking-tighter">Status: Resolution_Failed</div>
</div>
</div>
</div>
</div>
</div>
</>
</div>
);
}

View File

@@ -12,21 +12,32 @@ import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubC
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
import { FaqBlock } from '@/components/blocks/FaqBlock';
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
import { HeroSection } from '@/components/blocks/HeroSection';
import JsonLd from '@/components/JsonLd';
import { Button } from '@/components/ui/Button';
import { Heading } from '@/components/ui/Heading';
import { AnimatedCounter } from '@/components/ui';
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact';
import { getAllReferences } from '@/lib/references';
const mdxComponents = {
const mdxComponents = (references: any[]) => ({
HomeHero,
HomeSubCompanyTiles,
HomeCompetenceBentoGrid,
HomeReferencesSlider,
HomeReferencesSlider: (props: any) => <HomeReferencesSlider {...props} references={props.references || references} />,
FaqBlock,
CertificatesBlock,
HeroSection,
JsonLd,
Button,
Heading,
};
AnimatedCounter,
InteractiveGermanyMap,
ScaleOfImpact,
});
export async function generateMetadata(props: any): Promise<Metadata> {
const { locale } = await props.params;
@@ -50,6 +61,16 @@ export default async function Home(props: any) {
setRequestLocale(locale);
const mdx = await getMdxContent(locale, 'home');
const allReferences = await getAllReferences(locale);
// Transform ReferenceData to the format expected by ReferencesSlider
const sliderReferences = allReferences.map(ref => ({
id: ref.slug,
title: ref.frontmatter.title,
slug: ref.slug,
category: ref.frontmatter.location, // Using location as category if not specified
image: ref.frontmatter.featuredImage
})).slice(0, 6);
if (!mdx) {
notFound();
@@ -57,7 +78,7 @@ export default async function Home(props: any) {
return (
<main>
<MDXRemote source={mdx.content} components={mdxComponents} />
<MDXRemote source={mdx.content} components={mdxComponents(sliderReferences)} />
</main>
);
}

View File

@@ -0,0 +1,38 @@
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,6 +1,6 @@
import { notFound } from 'next/navigation';
import { Container, Badge, Heading } from '@/components/ui';
import { setRequestLocale } from 'next-intl/server';
import { setRequestLocale, getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getReferenceBySlug, getAllReferences } from '@/lib/references';
import { MDXRemote } from 'next-mdx-remote/rsc';
@@ -57,6 +57,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
setRequestLocale(locale);
const reference = await getReferenceBySlug(slug, locale);
const t = await getTranslations('ReferenceDetail');
if (!reference) {
notFound();
@@ -66,15 +67,48 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
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} />,
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4 text-neutral-dark" {...props} />,
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
ul: (props: any) => <ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
ul: (props: any) => <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 my-12" {...props} />,
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
li: (props: any) => (
<li 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" {...props} />
),
li: (props: any) => {
// Smart Metric Card Logic
let text = '';
if (typeof props.children === 'string') {
text = props.children;
} else if (Array.isArray(props.children)) {
text = props.children.map((c: any) => typeof c === 'string' ? c : '').join('');
}
const match = text.match(/^([\d.,]+)\s*(m|Satz|Stück|km|kV|Fasern)\b(.*)/);
if (match && typeof props.children === 'string') {
const [, value, unit, rest] = match;
return (
<li className="bg-white p-6 md:p-8 rounded-3xl border border-neutral-100 flex flex-col justify-center hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:border-primary/30 transition-all duration-500 group hover:-translate-y-1 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-neutral-50 to-transparent rounded-bl-full -z-10 group-hover:from-primary/5 transition-colors duration-500" />
<div className="flex items-baseline gap-2 mb-3">
<span className="text-4xl md:text-5xl font-bold font-heading text-primary tracking-tight">{value}</span>
<span className="text-lg md:text-xl font-bold text-neutral-400">{unit}</span>
</div>
<span className="text-neutral-600 font-semibold text-sm md:text-base leading-snug">{rest.replace(/^[\s-]*\s/, '')}</span>
</li>
);
}
return (
<li className="bg-neutral-50 p-6 rounded-3xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-all duration-300 group hover:-translate-y-1">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-primary/20 transition-colors">
<svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-neutral-700 font-semibold leading-relaxed">{props.children}</span>
</li>
);
},
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
blockquote: (props: any) => (
@@ -87,62 +121,62 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
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">
<section className="bg-[#050B14] text-white pt-40 pb-16 md:pt-52 md:pb-24 min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
{reference.frontmatter.featuredImage && (
<div className="absolute inset-0 z-0">
<Image
src={reference.frontmatter.featuredImage}
alt={reference.frontmatter.title}
fill
className="object-cover opacity-25 mix-blend-luminosity scale-105"
className="object-cover opacity-30 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 className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/70 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-[#050B14]/40 to-transparent" />
</div>
)}
<div className="absolute inset-0 opacity-20 z-0">
<div className="absolute inset-0 opacity-20 z-0 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/30 via-transparent to-transparent" />
</div>
<Container className="relative z-10">
<div className="mb-12">
<TrackedLink
href={`/${locale}/referenzen`}
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors"
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-xs font-bold"
eventProperties={{ location: 'reference_back_btn' }}
>
<ChevronLeft className="w-5 h-5" />
Zurück zur Übersicht
<ChevronLeft className="w-4 h-4" />
{t('backToOverview')}
</TrackedLink>
</div>
<div className="max-w-4xl">
<Badge variant="accent" className="mb-4 md:mb-6">
Projektreferenz
<div className="max-w-5xl">
<Badge variant="accent" className="mb-6">
{t('projectReference')}
</Badge>
<Heading level={1} variant="white" className="mb-8 leading-tight">
<Heading level={1} variant="white" className="mb-10 leading-[1.1] md:text-6xl lg:text-7xl drop-shadow-lg">
{reference.frontmatter.title}
</Heading>
<div className="grid grid-cols-1 md:grid-cols-3 gap-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 className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mt-12 md:mt-16">
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<MapPin className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Ort</p>
<p className="font-semibold text-lg">{reference.frontmatter.location}</p>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('location')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.location}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Briefcase className="w-8 h-8 text-primary shrink-0" />
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<Briefcase className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Auftraggeber</p>
<p className="font-semibold text-lg">{reference.frontmatter.client}</p>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('client')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90 line-clamp-2">{reference.frontmatter.client}</p>
</div>
</div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Calendar className="w-8 h-8 text-primary shrink-0" />
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
<Calendar className="w-8 h-8 text-primary shrink-0 opacity-80" />
<div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Zeitraum</p>
<p className="font-semibold text-lg">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('period')}</p>
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
</div>
</div>
</div>
@@ -153,7 +187,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
{/* 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">Leistungsumfang & Projektbeschreibung</h2>
<h2 className="text-3xl font-bold text-neutral-dark mb-8">{t('scopeTitle')}</h2>
<div className="w-full">
<MDXRemote source={reference.content} components={mdxComponents} />
@@ -166,7 +200,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
eventProperties={{ location: 'reference_bottom_back_btn' }}
>
<span className="relative z-10 flex items-center justify-center gap-2">
Alle Referenzen ansehen
{t('viewAll')}
</span>
<ButtonOverlay variant="primary" />
</TrackedLink>

View File

@@ -3,12 +3,12 @@ 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 { defaultLocations } from '@/lib/map-data';
import { defaultLocations, minorLocations } from '@/lib/map-data';
interface PageProps {
params: Promise<{
@@ -34,6 +34,17 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
};
}
function cleanLocation(location?: string): string {
if (!location) return '';
// Remove 5 digit postal codes
let cleaned = location.replace(/\b\d{5}\b/g, '');
// Clean up multiple spaces, slashes and commas
cleaned = cleaned.replace(/\s*,\s*/g, ', ').replace(/\s*\/\s*/g, ' / ').trim();
// Remove leading/trailing slash/comma residues
cleaned = cleaned.replace(/^[,/\s]+|[,/\s]+$/g, '').trim();
return cleaned || location;
}
export default async function ReferenzenOverview(props: { params: Promise<{ locale: string }> }) {
const { locale } = await props.params;
setRequestLocale(locale);
@@ -41,8 +52,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
const references = await getAllReferences(locale);
// Enrich the static map locations with dynamic project data (images, details)
const enrichedLocations = defaultLocations.map(loc => {
const allLocations = [...defaultLocations, ...minorLocations];
const enrichedLocations = allLocations.map(loc => {
if (loc.type === 'project') {
const ref = references.find(r => r.slug === loc.id);
if (ref) {
@@ -64,9 +75,9 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{/* Map Hero Section */}
<InteractiveGermanyMap
isHero={true}
badge="Unsere Referenzen"
title={<>Erfolgreich umgesetzte<br/>Projekte.</>}
description="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."
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.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben.'}
locations={enrichedLocations}
/>
@@ -77,11 +88,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
<TrackedLink
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,
}}
eventProperties={{ location: 'reference_list_card', slug: ref.slug }}
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
>
<div className="flex flex-col h-full relative">
{/* Image Section */}
@@ -91,53 +99,48 @@ 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 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" />
{ref.frontmatter.location}
{cleanLocation(ref.frontmatter.location)}
</span>
</div>
</div>
{/* 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 transition-colors line-clamp-3 leading-[1.2] group-hover:text-primary">
{ref.frontmatter.title}
</h3>
{/* Technical Meta Data */}
<div className="grid grid-cols-[1fr_1fr] gap-4 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">Kunde</span>
<span className="flex items-center gap-1.5 text-sm font-semibold text-neutral-700">
<Briefcase className="w-3.5 h-3.5 text-primary shrink-0" />
<span className="truncate">{ref.frontmatter.client}</span>
<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">
<Briefcase className="w-3.5 h-3.5 text-primary shrink-0 mt-[2px]" />
<span className="line-clamp-2">{ref.frontmatter.client || t('client')}</span>
</span>
</div>
<div className="min-w-0">
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">Zeitraum</span>
<span className="flex items-center gap-1.5 text-sm font-semibold text-neutral-700">
<Calendar className="w-3.5 h-3.5 text-primary shrink-0" />
<span className="truncate">{ref.frontmatter.dateString || new Date(ref.frontmatter.date).getFullYear()}</span>
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">{t('period')}</span>
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
<Calendar className="w-3.5 h-3.5 text-primary shrink-0 mt-[2px]" />
<span className="line-clamp-2">{ref.frontmatter.dateString || new Date(ref.frontmatter.date).getFullYear()}</span>
</span>
</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>
))}
@@ -147,8 +150,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
<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">Ihr nächstes Projekt?</h3>
<p className="text-lg text-white/70 mb-8">Lassen Sie uns gemeinsam herausragende Infrastruktur bauen.</p>
<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={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
className={getButtonClasses('accent', 'lg')}

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 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">
<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,25 +0,0 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
export const runtime = 'nodejs';
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Our Team" />,
{
...OG_IMAGE_SIZE,
fonts,
},
);
}

View File

@@ -1,326 +0,0 @@
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge } from '@/components/ui';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
import TrackedButton from '@/components/analytics/TrackedButton';
interface TeamPageProps {
params: Promise<{
locale: string;
}>;
}
export async function generateMetadata({ params }: TeamPageProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return {
title,
description,
alternates: {
canonical: `${SITE_URL}/${locale}/team`,
languages: {
de: `${SITE_URL}/de/team`,
en: `${SITE_URL}/en/team`,
'x-default': `${SITE_URL}/en/team`,
},
},
openGraph: {
title: `${title} | E-TIB`,
description,
url: `${SITE_URL}/${locale}/team`,
},
twitter: {
card: 'summary_large_image',
title: `${title} | E-TIB`,
description,
},
};
}
export default async function TeamPage({ params }: TeamPageProps) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Team' });
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
<JsonLd
id="breadcrumb-team"
data={getBreadcrumbSchema([{ name: t('hero.subtitle'), item: `/team` }])}
/>
<JsonLd
id="person-michael"
data={{
'@context': 'https://schema.org',
'@type': 'Person',
name: t('michael.name'),
jobTitle: t('michael.role'),
worksFor: {
'@type': 'Organization',
name: 'E-TIB',
},
sameAs: ['https://www.linkedin.com/in/michael-bodemer-33b493122/'],
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`,
}}
/>
<JsonLd
id="person-klaus"
data={{
'@context': 'https://schema.org',
'@type': 'Person',
name: t('klaus.name'),
jobTitle: t('klaus.role'),
worksFor: {
'@type': 'Organization',
name: 'E-TIB',
},
sameAs: ['https://www.linkedin.com/in/klaus-mintel-b80a8b193/'],
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`,
}}
/>
{/* Hero Section */}
<Reveal>
<section className="relative flex items-center justify-center overflow-hidden bg-primary-dark pt-32 pb-24 md:pt-[14%] md:pb-[12%]">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC07655-Large.webp"
alt="E-TIB Team"
fill
className="object-cover scale-105 animate-slow-zoom opacity-30 md:opacity-40"
sizes="100vw"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
</div>
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{t('hero.badge')}
</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>
</Container>
</section>
</Reveal>
{/* Michael Bodemer Section - Sticky Narrative Split Layout */}
<article className="relative bg-white overflow-hidden">
<div className="flex flex-col lg:flex-row">
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-primary-dark text-white relative order-2 lg:order-1">
<div className="absolute top-0 right-0 w-32 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
<div className="relative z-10">
<Badge variant="accent" className="mb-4 md:mb-8">
{t('michael.role')}
</Badge>
<Heading level={2} className="text-white mb-6 md:mb-10 text-2xl md:text-4xl">
<span className="text-white">{t('michael.name')}</span>
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-accent rounded-full" />
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-white/90">
{t('michael.quote')}
</p>
</div>
<p className="text-base md:text-xl leading-relaxed text-white/70 mb-6 md:mb-12 max-w-xl">
{t('michael.description')}
</p>
<TrackedButton
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
variant="accent"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
eventProperties={{
type: 'social_linkedin',
person: 'Michael Bodemer',
location: 'team_page',
}}
>
{t('michael.linkedin')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</TrackedButton>
</div>
</Reveal>
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1 lg:order-2">
<Image
src="/uploads/2024/12/DSC07768-Large.webp"
alt={t('michael.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent" />
</Reveal>
</div>
</article>
{/* Legacy Section - Immersive Background */}
<Reveal>
<section className="relative py-16 md:py-48 bg-primary-dark text-white overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy.webp"
alt={t('legacy.subtitle')}
fill
className="object-cover opacity-20 md:opacity-30 scale-110 animate-slow-zoom"
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary-dark/60 mix-blend-multiply" />
</div>
<Container className="relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
<div className="lg:col-span-6">
<Heading
level={2}
subtitle={t('legacy.subtitle')}
className="text-white mb-6 md:mb-10"
>
<span className="text-white">{t('legacy.title')}</span>
</Heading>
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
{t('legacy.p1')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">{t('legacy.p2')}</p>
</div>
</div>
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.expertise')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.expertiseDesc')}
</div>
</div>
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.network')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.networkDesc')}
</div>
</div>
</div>
</div>
</Container>
</section>
</Reveal>
{/* Klaus Mintel Section - Reversed Split Layout */}
<article className="relative bg-white overflow-hidden">
<div className="flex flex-col lg:flex-row">
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1">
<Image
src="/uploads/2024/12/DSC07963-Large.webp"
alt={t('klaus.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent" />
</Reveal>
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
<div className="relative z-10">
<Badge variant="saturated" className="mb-4 md:mb-8">
{t('klaus.role')}
</Badge>
<Heading level={2} className="text-saturated mb-6 md:mb-10 text-2xl md:text-4xl">
{t('klaus.name')}
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-saturated rounded-full" />
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-text-secondary">
{t('klaus.quote')}
</p>
</div>
<p className="text-base md:text-xl leading-relaxed text-text-secondary mb-6 md:mb-12 max-w-xl">
{t('klaus.description')}
</p>
<TrackedButton
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
variant="saturated"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
eventProperties={{
type: 'social_linkedin',
person: 'Klaus Mintel',
location: 'team_page',
}}
>
{t('klaus.linkedin')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</TrackedButton>
</div>
</Reveal>
</div>
</article>
{/* Manifesto Section - Modern Grid */}
<Section className="bg-white text-primary py-16 md:py-28">
<Container>
<div className="sticky-narrative-container">
<div className="sticky-narrative-sidebar mb-8 lg:mb-0">
<div className="lg:sticky lg:top-32">
<Heading level={2} subtitle={t('manifesto.subtitle')} className="mb-4 md:mb-8">
{t('manifesto.title')}
</Heading>
<p className="text-base md:text-xl text-text-secondary leading-relaxed">
{t('manifesto.tagline')}
</p>
{/* Mobile-only progress indicator */}
<div className="flex lg:hidden mt-8 gap-2">
{[0, 1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden"
>
<div className="h-full bg-accent w-0 group-active:w-full transition-all duration-500" />
</div>
))}
</div>
</div>
</div>
<ul className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10 list-none p-0 m-0">
{[0, 1, 2, 3, 4, 5].map((idx) => (
<li
key={idx}
className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none"
>
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">
0{idx + 1}
</span>
</div>
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">
{t(`manifesto.items.${idx}.title`)}
</h3>
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">
{t(`manifesto.items.${idx}.description`)}
</p>
</li>
))}
</ul>
</div>
</Container>
</Section>
<Reveal>
<Gallery />
</Reveal>
</div>
);
}

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

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

@@ -36,7 +36,7 @@ export default function LeafletMap({ locations }: LeafletMapProps) {
const firstLoc = locations[0];
const map = L.map(mapRef.current, {
center: [firstLoc.lat, firstLoc.lng],
zoom: 6, // Zoom out to see both or more
zoom: locations.length === 1 ? 16 : 6,
scrollWheelZoom: false,
});

View File

@@ -16,7 +16,7 @@ export function OGImageTemplate({
mode = 'dark',
}: OGImageTemplateProps) {
const primaryBlue = '#001a4d';
const accentGreen = '#82ed20';
const accentGreen = '#10a379';
const saturatedBlue = '#011dff';
const containerStyle: React.CSSProperties = {

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

@@ -3,12 +3,16 @@
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;
text?: string; // Alias for description
ctaLabel?: string;
buttonText?: string; // Alias for ctaLabel
ctaHref?: string;
buttonLink?: string; // Alias for ctaHref
theme?: 'light' | 'dark';
}
@@ -32,14 +36,15 @@ const itemVariants = {
},
};
export const CallToAction: React.FC<CallToActionProps> = ({
title = 'Bereit für Ihr Projekt?',
description = 'Wir suchen stets nach neuen Herausforderungen und starken Partnern. Kontaktieren Sie uns für eine unverbindliche Beratung zu Ihrem Vorhaben.',
ctaLabel = 'Jetzt Kontakt aufnehmen',
ctaHref = '/de/kontakt',
theme = 'light'
}) => {
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 || props.text || t('description');
const ctaLabel = props.ctaLabel || props.buttonText || t('ctaLabel');
const ctaHref = props.ctaHref || props.buttonLink || t('ctaHref');
return (
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>

View File

@@ -0,0 +1,226 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { FileText, Award, Download, ShieldCheck } from 'lucide-react';
import { LogoArcs } from '@/components/ui/LogoArcs';
interface Certificate {
title: string;
description: string;
pdfUrl?: string;
type: 'iso' | 'tax' | 'general';
date?: string;
}
interface CertificatesBlockProps {
badge?: string;
title?: string;
description?: string;
certificates?: Certificate[];
hideHeader?: boolean;
}
const defaultCertificates: Certificate[] = [
{
title: 'ISO 14001:2015',
description: 'Umweltmanagementsystem',
pdfUrl: '/assets/certificates/iso14001.pdf',
type: 'iso',
date: '14.12.2023',
},
{
title: 'ISO 9001:2015',
description: 'Qualitätsmanagementsystem',
pdfUrl: '/assets/certificates/iso9001.pdf',
type: 'iso',
date: '14.12.2023',
},
{
title: 'DIN EN ISO 45001:2018',
description: 'Arbeits- und Gesundheitsschutz',
pdfUrl: '/assets/certificates/iso45001.pdf',
type: 'iso',
date: '05.12.2025',
},
{
title: 'Freistellungsbescheinigung',
description: 'Nach § 48 b EStG',
pdfUrl: '/assets/certificates/freistellung.pdf',
type: 'tax',
date: '09.02.2024',
},
{
title: 'Nachweis § 13b UStG',
description: 'Steuerschuldnerschaft des Leistungsempfängers',
pdfUrl: '/assets/certificates/nachweis13b.pdf',
type: 'tax',
date: '09.02.2024',
},
{
title: 'Bescheinigung in Steuersachen',
description: 'Zertifikat des Finanzamtes',
pdfUrl: '/assets/certificates/bescheinigung.pdf',
type: 'tax',
date: '13.02.2025',
},
];
export function CertificatesBlock({ badge, title, description, certificates = defaultCertificates, hideHeader = false }: CertificatesBlockProps) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const badgeText = badge || 'Zertifizierungen & Nachweise';
const titleText = title || 'Geprüfte Qualität und Sicherheit';
const descriptionText = description || 'Transparenz und höchste Standards sind das Fundament unserer Arbeit. Hier finden Sie unsere aktuellen Zertifikate und Bescheinigungen als PDF-Download.';
const containerVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' as const } },
};
if (!isMounted) {
return (
<section className="py-24 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>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</div>
)}
</Container>
</section>
);
}
return (
<section className="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" />
<Container className="relative z-10">
{!hideHeader && (
<div className="text-center max-w-3xl mx-auto mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
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>
<p className="text-lg text-neutral-600">{descriptionText}</p>
</motion.div>
</div>
)}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={containerVariants}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-50px" }}
>
{certificates.map((cert, index) => {
const isIso = cert.type === 'iso';
const Wrapper = cert.pdfUrl ? motion.a : motion.div;
const wrapperProps = cert.pdfUrl ? {
href: encodeURI(cert.pdfUrl),
target: "_blank",
rel: "noopener noreferrer"
} : {};
return (
<Wrapper
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 ${
isIso
? 'bg-neutral-dark text-white border-neutral-800 shadow-lg'
: 'bg-white text-neutral-dark border-neutral-200 shadow-md'
}`}
>
{/* Background Decorative Elements */}
<div className={`absolute top-0 right-0 w-32 h-32 blur-3xl rounded-full -mr-10 -mt-10 transition-colors duration-500 ${
isIso ? 'bg-primary/20 group-hover:bg-primary/40' : 'bg-primary/10 group-hover:bg-primary/20'
}`} />
{isIso && (
<div className="absolute -bottom-[20%] -right-[20%] w-[100%] h-[100%] z-0 opacity-10 group-hover:opacity-20 transition-opacity duration-1000 pointer-events-none mix-blend-overlay">
<LogoArcs className="w-full h-full text-white animate-[spin_60s_linear_infinite]" />
</div>
)}
<HoverShineOverlay shineColor={isIso ? 'via-white/20' : 'via-black/5'} />
<AnimatedGlossyBorder color={isIso ? 'primary' : 'primary'} className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={2} />
<div className="relative z-10 flex flex-col h-full">
<div className="flex justify-between items-start mb-8">
<div className={`p-3 rounded-xl inline-flex ${
isIso ? 'bg-primary/20 text-primary-light backdrop-blur-md' : 'bg-primary/10 text-primary'
}`}>
{isIso ? <Award size={28} /> : <ShieldCheck size={28} />}
</div>
{cert.date && (
<span className={`text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full ${
isIso ? 'bg-neutral-800 text-neutral-400 border border-neutral-700' : 'bg-neutral-100 text-neutral-500 border border-neutral-200'
}`}>
{cert.date}
</span>
)}
</div>
<div>
<h4 className={`text-xl font-bold mb-2 ${isIso ? 'text-white' : 'text-neutral-dark'}`}>
{cert.title}
</h4>
<p className={`text-sm mb-6 ${isIso ? 'text-neutral-400' : 'text-neutral-600'}`}>
{cert.description}
</p>
</div>
<div className={`mt-auto inline-flex items-center text-sm font-bold uppercase tracking-wider transition-all ${
cert.pdfUrl ? 'group-hover:gap-3' : ''
} ${
isIso ? 'text-primary-light' : 'text-primary'
}`}>
{cert.pdfUrl ? (
<>
<span>Download PDF</span>
<Download size={16} className="ml-2 group-hover:translate-y-0.5 transition-transform" />
</>
) : (
<span className="text-neutral-400">Nachweis liegt vor</span>
)}
</div>
</div>
</Wrapper>
);
})}
</motion.div>
</Container>
</section>
);
}

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
const iconMap: Record<string, React.ElementType> = {
Building2,
@@ -16,37 +17,6 @@ const iconMap: Record<string, React.ElementType> = {
CheckCircle2
};
const defaultMilestones = [
{
date: '16.12.2015',
year: '2015',
title: 'Gründung E-TIB GmbH',
desc: 'Ausführung elektrischer Infrastrukturprojekte, Kabelbau und Horizontalspülbohrungen.',
iconName: 'Building2',
},
{
date: '04.02.2019',
year: '2019',
title: 'Gründung E-TIB Ingenieurgesellschaft',
desc: 'Genehmigungs- und Ausführungsplanung, komplexe Querungen sowie Netzanschlussplanung.',
iconName: 'Compass',
},
{
date: '14.11.2019',
year: '2019',
title: 'Gründung E-TIB Verwaltung GmbH',
desc: 'Zentrale Dienste, Erwerb, Vermietung, Verpachtung und Verwaltung von Immobilien und Maschinen.',
iconName: 'Layers',
},
{
date: '21.10.2025',
year: '2025',
title: 'Gründung E-TIB Bohrtechnik GmbH',
desc: 'Spezialisierung auf präzise Horizontalspülbohrungen in allen Bodenklassen.',
iconName: 'ArrowDownToLine',
},
];
interface Milestone {
date: string;
year: string;
@@ -62,10 +32,45 @@ interface CompanyTimelineProps {
}
export function CompanyTimeline({
badge = 'Unsere Geschichte',
title = 'Meilensteine der Entwicklung',
milestones = defaultMilestones
badge,
title,
milestones
}: CompanyTimelineProps) {
const t = useTranslations('CompanyTimeline');
const finalBadge = badge || t('badge');
const finalTitle = title || t('title');
const finalMilestones = milestones || [
{
date: '16.12.2015',
year: '2015',
title: t('milestones.2015.title'),
desc: t('milestones.2015.desc'),
iconName: 'Building2',
},
{
date: '04.02.2019',
year: '2019',
title: t('milestones.2019_ing.title'),
desc: t('milestones.2019_ing.desc'),
iconName: 'Compass',
},
{
date: '14.11.2019',
year: '2019',
title: t('milestones.2019_verw.title'),
desc: t('milestones.2019_verw.desc'),
iconName: 'Layers',
},
{
date: '21.10.2025',
year: '2025',
title: t('milestones.2025.title'),
desc: t('milestones.2025.desc'),
iconName: 'ArrowDownToLine',
},
];
const containerRef = React.useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
@@ -84,10 +89,10 @@ export function CompanyTimeline({
<div className="text-center 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" />
{badge}
{finalBadge}
</div>
<h3 className="font-heading text-4xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
{title}
{finalTitle}
</h3>
</div>
@@ -104,7 +109,7 @@ export function CompanyTimeline({
</div>
<div className="space-y-16 md:space-y-24">
{milestones.map((milestone, i) => {
{finalMilestones.map((milestone, i) => {
const isEven = i % 2 === 0;
const Icon = iconMap[milestone.iconName] || Building2;

View File

@@ -19,6 +19,7 @@ interface CompetenceBentoGridProps {
title?: string;
description?: string;
tag?: string;
href?: string;
image?: {
url?: string;
alt?: string;
@@ -34,6 +35,7 @@ interface CompetenceBentoGridProps {
title?: string;
description?: string;
tag?: string;
href?: string;
image?: {
url?: string;
alt?: string;
@@ -111,8 +113,11 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
className={gridClasses}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
<Link href={item.href} className="absolute inset-0 z-30" aria-label={item.title} />
)}
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/30 blur-3xl rounded-full -mr-10 -mt-10 group-hover:bg-primary/50 transition-colors duration-500" />
{/* Subtle Background Logo Arcs */}
@@ -135,8 +140,11 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
className={gridClasses}
className={`${gridClasses} ${item.href ? 'cursor-pointer hover:-translate-y-1 transition-transform duration-300' : ''}`}
>
{item.href && (
<Link href={item.href} className="absolute inset-0 z-30" aria-label={item.title} />
)}
{!imgSrc && (
<div className="absolute inset-0 bg-neutral-dark z-0 transition-colors duration-500 group-hover:bg-primary-dark" />
)}

View File

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

@@ -0,0 +1,52 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DataGridPulse() {
const locale = useLocale();
return (
<div className="relative w-full py-16 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-[0.03] pointer-events-none"
style={{
backgroundImage: 'linear-gradient(#117C61 1px, transparent 1px), linear-gradient(90deg, #117C61 1px, transparent 1px)',
backgroundSize: '40px 40px'
}}
/>
{/* Pulsing horizontal lines (Data Flow) */}
<motion.div
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/30 to-transparent blur-[1px]"
/>
<motion.div
animate={{ x: ["100%", "-100%"] }}
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/25 to-transparent blur-[1px]"
/>
<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'}
</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-5xl 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-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.'}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import React, { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useLocale } from 'next-intl';
export function DeepDrillAnimation() {
const containerRef = useRef<HTMLDivElement>(null);
const locale = useLocale();
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start center", "end center"]
});
const drillDepth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
return (
<div ref={containerRef} className="relative w-full 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">
{locale === 'en' ? 'Precision in Depth' : 'Präzision in der Tiefe'}
</h3>
<p className="text-white/60 text-lg mb-16 max-w-xl">
{locale === 'en'
? 'Horizontal Directional Drilling (HDD) allows us to cross obstacles and rivers with extreme care.'
: 'Horizontalspülbohrverfahren (HDD) erlaubt uns, Hindernisse und Flüsse extrem schonend zu unterqueren.'}
</p>
{/* The Drill Path */}
<div className="relative w-2 h-96 bg-white/10 rounded-full mb-16">
<motion.div
className="absolute top-0 left-0 w-full bg-primary rounded-full"
style={{ height: drillDepth, boxShadow: '0 0 20px rgba(130,237,32,0.6)' }}
>
{/* The Drill Head */}
<div className="absolute -bottom-2 -left-2 w-6 h-6 bg-white rounded-full border-4 border-primary shadow-[0_0_15px_rgba(255,255,255,1)]" />
</motion.div>
</div>
{/* The Reveal Data */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
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>
<div className="text-sm font-bold uppercase tracking-widest text-primary">
{locale === 'en' ? 'Drilled distance since 2023' : 'Gebohrte Strecke seit 2023'}
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,111 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
export function GrowthChart() {
const t = useTranslations('GrowthChart');
const data = [
{ 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="mb-10">
<h3 className="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 kabelWidth = (item.kabel / maxVal) * 100;
const offenerWidth = (item.offener / maxVal) * 100;
const hddWidth = (item.hdd / maxVal) * 100;
return (
<div key={item.year} className="relative">
<div className="text-sm font-extrabold text-neutral-dark mb-2 w-16">{item.year}</div>
<div className="space-y-2">
{/* Kabelverlegung Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-6 overflow-hidden relative">
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${kabelWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-primary rounded-full"
/>
</div>
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
{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>
{/* HDD Bar */}
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-100 rounded-full h-4 overflow-hidden relative">
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${hddWidth}%` }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 1.5, delay: 0.2 + 0.1 * index, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-neutral-dark rounded-full"
/>
</div>
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
{t('hdd')}: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
</div>
</div>
</div>
</div>
);
})}
</div>
<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">{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">{t('hdd')}</span>
</div>
</div>
</div>
);
}

View File

@@ -42,7 +42,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
return (
<section
className={`relative min-h-[50vh] md:min-h-[70vh] flex items-center pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden bg-primary-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
className={`relative min-h-[45vh] md:min-h-[55vh] flex items-center pt-24 pb-16 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
>
{bgSrc && (
<motion.div
@@ -55,14 +55,22 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
src={bgSrc}
alt={title || 'Hero Background'}
fill
className="object-cover opacity-30 md:opacity-40"
className="object-cover filter contrast-110 saturate-110 brightness-95"
style={{
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
}}
sizes="100vw"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
{/* Cinematic Color Grading Overlay */}
<div className="absolute inset-0 bg-[#0a192f]/30 z-[2] mix-blend-multiply" />
{/* Dramatic Vignette & Fade to content */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-transparent via-black/20 to-black/80 z-[2]" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/60 to-transparent z-[3]" />
{/* Top Fade for Header Navigation Readability */}
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
</motion.div>
)}
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
@@ -80,19 +88,19 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
</motion.div>
)}
<motion.div variants={itemVariants}>
<Heading level={1} className="text-white mb-4 md:mb-8">
<Heading level={1} variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title}
</Heading>
</motion.div>
{subtitle && (
<motion.div variants={itemVariants}>
<p className="text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl">
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
{subtitle}
</p>
</motion.div>
)}
{ctaLabel && ctaHref && (
<motion.div variants={itemVariants} className="mt-8">
<motion.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex justify-center' : ''}`}>
<Button
href={ctaHref}
variant="accent"

View File

@@ -12,6 +12,7 @@ interface HeroVideoProps {
description?: string;
videoUrl?: string;
posterImage?: any;
backgroundImage?: any;
ctaLabel?: string;
ctaHref?: string;
linkText?: string;
@@ -25,11 +26,11 @@ export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELTIEFBAU';
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const videoUrl = props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4';
const explicitNoVideo = props.videoUrl === 'none' || props.videoUrl === '';
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
const posterSrc = data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
const posterAlt = data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
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";
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
@@ -53,7 +54,7 @@ export function HeroVideo(props: HeroVideoProps) {
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{videoUrl && (
{videoUrl ? (
<video
key={videoUrl}
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
@@ -64,6 +65,15 @@ export function HeroVideo(props: HeroVideoProps) {
playsInline
preload="auto"
/>
) : (
<Image
src={posterSrc}
alt={posterAlt}
fill
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
sizes="100vw"
priority
/>
)}
{/* Cinematic Color Grading Overlay */}

View File

@@ -2,15 +2,20 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MapPin, Factory, Zap, CheckCircle2 } from 'lucide-react';
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { Location, defaultLocations } from '@/lib/map-data';
import Link from 'next/link';
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
import { Location, defaultLocations, minorLocations, projectLocations } from '@/lib/map-data';
import { standorteLocations } from '@/lib/standorte-data';
import { useLocale, useTranslations } from 'next-intl';
interface Stat {
value: string;
suffix?: string;
label: string;
href?: string;
}
interface InteractiveGermanyMapProps {
@@ -23,26 +28,40 @@ interface InteractiveGermanyMapProps {
}
export function InteractiveGermanyMap({
badge = 'Einsatzgebiete',
title = <>Deutschlandweit<br/>für Sie im Einsatz.</>,
description = 'Von unseren strategischen Standorten in Guben und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.',
stats = [
{ value: '100', suffix: '%', label: 'Überregionale Reichweite' },
{ value: '2', suffix: '+', label: 'Operative Standorte' },
],
locations = defaultLocations,
badge,
title,
description,
stats,
locations = projectLocations,
isHero = false
}: InteractiveGermanyMapProps) {
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
const router = useRouter();
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: '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, 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');
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 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`}>
<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" />
@@ -53,119 +72,106 @@ export function InteractiveGermanyMap({
<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" />
<span>{badge}</span>
<span>{finalBadge}</span>
</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 break-words [hyphens:auto]">
{title}
<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">
{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 break-words [hyphens:auto]">
{title}
<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">
{finalTitle}
</h3>
)}
<p className="text-white/60 text-lg mb-12 leading-relaxed">
{description}
{finalDescription}
</p>
{/* Industrial Stats Grid */}
<div className="grid grid-cols-2 gap-4">
{stats.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-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>
))}
);
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>
{/* Map Right */}
<div className="xl:col-span-7 relative flex items-center justify-center lg:justify-end mt-8 lg:mt-0 z-10">
<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 z-10">
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
{/* Map SVG */}
<div className="absolute inset-0 opacity-[0.15] mix-blend-screen drop-shadow-2xl brightness-150">
<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="Deutschlandkarte"
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
fill
className="object-cover"
priority
/>
</div>
{/* Connections (Lines) */}
<svg className="absolute inset-0 w-full h-full pointer-events-none z-10">
{hq && branch && (
<motion.line
x1={`${hq.x}%`}
y1={`${hq.y}%`}
x2={`${branch.x}%`}
y2={`${branch.y}%`}
stroke="rgba(130,237,32,0.6)"
strokeWidth="2"
strokeDasharray="6 6"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 2, ease: "easeOut" }}
/>
)}
{hq && projects.map((proj, idx) => (
<motion.line
key={proj.id}
x1={`${hq.x}%`}
y1={`${hq.y}%`}
x2={`${proj.x}%`}
y2={`${proj.y}%`}
stroke="rgba(130,237,32,0.3)"
strokeWidth="1.5"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, delay: 0.5 + idx * 0.2 }}
/>
))}
</svg>
{/* 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>
))}
{/* Location Pins */}
{locations.map((loc, idx) => {
{/* 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;
const isNearLeft = loc.x < 50;
const isNearRight = loc.x >= 50;
const isNearTop = loc.y < 55; // 55% threshold to strongly avoid the top header
const tooltipClasses = [
'absolute w-[280px] sm:w-64 bg-white text-neutral-dark p-5 rounded-2xl shadow-2xl z-[9999] border border-neutral-100',
isNearLeft ? 'left-1/2 sm:left-0 -translate-x-1/2 sm:translate-x-0' : 'left-1/2 sm:left-auto sm:right-0 -translate-x-1/2 sm:translate-x-0',
isNearTop
? 'top-full mt-4 before:absolute before:-top-6 before:left-0 before:w-full before:h-6'
: 'bottom-full mb-4 before:absolute before:-bottom-6 before:left-0 before:w-full before:h-6'
].join(' ');
const arrowClasses = [
'absolute border-8 border-transparent drop-shadow-sm hidden sm:block',
isNearLeft ? 'left-6' : 'right-6',
isNearTop ? '-top-4 border-b-white border-t-0' : '-bottom-4 border-t-white border-b-0'
].join(' ');
return (
<div
key={loc.id}
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20'}`}
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) router.push(loc.href);
if (loc.href) {
const href = loc.href.startsWith('/') ? `/${locale}${loc.href}` : loc.href;
router.push(href);
}
}}
>
{/* Ping Animation for HQ / Branch */}
@@ -173,121 +179,112 @@ export function InteractiveGermanyMap({
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
)}
{/* Marker Dot */}
{/* 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.4)] transition-transform duration-300 ${
isActive ? 'scale-125 z-30 ring-4 ring-primary/30' : 'scale-100'
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-6 h-6 bg-primary text-[#050B14]'
? 'w-7 h-7 bg-primary text-[#050B14]'
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
}`}
>
{(isHQ || isBranch) && <MapPin className="w-3.5 h-3.5" />}
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
</motion.div>
{/* Tooltip */}
<AnimatePresence>
{isActive && (
<motion.div
variants={{
hidden: { opacity: 0, y: isNearTop ? -15 : 15, scale: 0.9 },
visible: {
opacity: 1, y: 0, scale: 1,
transition: {
type: "spring", stiffness: 350, damping: 25,
staggerChildren: 0.05, delayChildren: 0.05
}
},
exit: {
opacity: 0, y: isNearTop ? -10 : 10, scale: 0.95,
transition: { duration: 0.15, ease: "easeIn" }
}
}}
initial="hidden"
animate="visible"
exit="exit"
className={`${tooltipClasses} overflow-hidden p-0`}
>
{loc.featuredImage && (
<motion.div
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1 } }}
className="relative w-full h-32 bg-neutral-100 overflow-hidden"
>
<Image
src={loc.featuredImage}
alt={loc.name}
fill
className="object-cover transform transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80" />
<div className="absolute bottom-3 left-4 flex items-center gap-2">
{isHQ ? <Factory className="w-4 h-4 text-primary drop-shadow" /> : <Zap className="w-4 h-4 text-primary drop-shadow" />}
<span className="font-extrabold text-sm leading-tight text-white drop-shadow-md">{loc.name}</span>
</div>
</motion.div>
)}
<div className={loc.featuredImage ? "p-5 pt-4" : "p-5"}>
{!loc.featuredImage && (
<motion.div
variants={{ hidden: { opacity: 0, x: -5 }, visible: { opacity: 1, x: 0 } }}
className="flex items-center gap-2 mb-2"
>
{isHQ ? <Factory className="w-4 h-4 text-primary" /> : <Zap className="w-4 h-4 text-primary" />}
<span className="font-extrabold text-sm leading-tight text-neutral-dark">{loc.name}</span>
</motion.div>
)}
<motion.p
variants={{ hidden: { opacity: 0, y: 5 }, visible: { opacity: 1, y: 0 } }}
className="text-xs text-neutral-500 font-medium"
>
{loc.description}
</motion.p>
{loc.details && (
<ul className="mt-4 space-y-2">
{loc.details.map((detail, i) => (
<motion.li
key={i}
variants={{ hidden: { opacity: 0, x: -5 }, visible: { opacity: 1, x: 0 } }}
className="text-[11px] text-neutral-dark flex items-center gap-2 font-semibold"
>
<div className="w-4 h-4 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
<CheckCircle2 className="w-3 h-3 text-primary" />
</div>
{detail}
</motion.li>
))}
</ul>
)}
{loc.href && (
<motion.div
variants={{ hidden: { opacity: 0, y: 5 }, visible: { opacity: 1, y: 0 } }}
className="mt-4 pt-3 border-t border-neutral-100 text-xs font-bold text-primary flex items-center gap-1 group/link"
>
Zum Projekt
<span className="text-base leading-none translate-y-[1px] transform transition-transform duration-300 group-hover:translate-x-1.5">&rarr;</span>
</motion.div>
)}
</div>
{/* Arrow Down/Up */}
<div className={arrowClasses} />
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
{/* Global Tooltip */}
<AnimatePresence>
{activeLocation && (
<motion.div
key="tooltip"
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="absolute z-[9999] pointer-events-none"
style={{
left: `${activeLocation.x}%`,
top: `${activeLocation.y}%`,
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
}}
>
<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">
{activeLocation.featuredImage && (
<div className="relative w-full h-32 bg-neutral-900">
<Image
src={activeLocation.featuredImage}
alt={activeLocation.name}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
</div>
)}
<div className="p-4">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-bold text-primary uppercase tracking-wider">
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
</span>
</div>
<div className="font-heading font-bold text-lg leading-tight mb-2">
{activeLocation.name}
</div>
{activeLocation.type !== 'minor_node' ? (
<div className="text-white/60 text-sm line-clamp-3 mb-3">
{activeLocation.description}
</div>
) : (
<div className="text-white/60 text-xs mb-3">
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
</div>
)}
{(activeLocation.details || activeLocation.type === 'minor_node') && (
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
{activeLocation.type === 'minor_node' ? (
<>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
</li>
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{t('features.method')}</span>
</li>
</>
) : (
activeLocation.details?.map((detail, i) => (
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
<span>{detail}</span>
</li>
))
)}
</ul>
)}
{activeLocation.href && (
<div className="inline-flex items-center text-xs font-bold text-primary">
{t('learnMore')}
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
</div>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
export interface JobListingBlockProps {
title?: string;
@@ -45,20 +46,23 @@ const itemVariants = {
};
export const JobListingBlock = (props: JobListingBlockProps) => {
const t = useTranslations('JobListingBlock');
const {
title,
showJobs = true,
showFairs,
fairsTitle = 'Nächste Messetermine 2026',
fairsTitle = t('fairsTitle'),
fairs = [
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe', location: 'Messe München', booth: 'Halle A3, Stand 110', url: 'https://www.intersolar.de' },
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress', location: 'Van der Valk Resort Linstow', booth: 'Stand 42', url: 'https://windenergietage.de' },
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse', location: 'RheinMain CongressCenter', booth: 'Halle 1, Stand B20' }
{ name: 'Intersolar München', date: 'Termin folgt', type: 'Energie-Messe', location: 'Messe München' },
{ name: 'Windenergietage Linstow', date: 'Termin folgt', type: 'Fachkongress', location: 'Linstow' },
{ name: 'Kabelwerkstatt Wiesbaden', date: 'Termin folgt', type: 'Fachmesse', location: 'Wiesbaden' }
],
emptyStateMessage = 'Aktuell sind alle Positionen besetzt. Senden Sie uns gerne eine Initiativbewerbung!',
emptyStateLinkText = 'Jetzt Kontakt aufnehmen',
emptyStateLinkHref = '/kontakt'
emptyStateMessage = t('emptyStateMessage'),
emptyStateLinkText = t('emptyStateLinkText'),
emptyStateLinkHref = t('emptyStateLinkHref')
} = props;
const title = props.title || t('title');
// Since Payload is removed, we use an empty array or static data
const jobs: any[] = [];
@@ -118,7 +122,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
</div>
<div>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Datum</p>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">{t('date')}</p>
<p className="text-white/90 font-medium">{messe.date}</p>
</div>
</div>
@@ -129,7 +133,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</div>
<div>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Location</p>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">{t('location')}</p>
<p className="text-white/90 font-medium">{messe.location}</p>
</div>
</div>
@@ -141,7 +145,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
</div>
<div>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Stand</p>
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">{t('booth')}</p>
<p className="text-primary-light font-bold text-sm">{messe.booth}</p>
</div>
</div>
@@ -180,7 +184,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
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"
>
{title || 'Aktuelle Stellenangebote'}
{title}
</motion.h2>
{jobs.length > 0 ? (
@@ -206,7 +210,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
<p className="text-text-secondary mt-1">{job.location}</p>
</div>
<div className="flex items-center text-primary font-bold group-hover:translate-x-2 transition-transform">
Details ansehen
{t('viewDetails')}
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</Link>

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ReferencesSlider, Reference } from './ReferencesSlider';
// Mock next/link to render standard <a> tag
vi.mock('next/link', () => ({
default: vi.fn(({ children, href, onClick, ...props }: any) => {
return (
<a href={href} onClick={onClick} {...props}>
{children}
</a>
);
})
}));
// Mock next/image to render standard img tag
vi.mock('next/image', () => ({
default: (props: any) => {
return <img {...props} />;
}
}));
// Mock next-intl
vi.mock('next-intl', () => ({
useTranslations: () => (key: string) => key,
useLocale: () => 'en',
NextIntlClientProvider: ({ children }: any) => <>{children}</>
}));
describe('ReferencesSlider TDD', () => {
const mockReferences: Reference[] = [
{
id: '1',
title: 'Referenz Projekt Eins',
slug: 'referenz-projekt-eins',
category: 'Kabeltiefbau',
},
{
id: '2',
title: 'Referenz Projekt Zwei',
slug: 'referenz-projekt-zwei',
category: 'Bohrtechnik',
}
];
it('renders reference tiles correctly', () => {
render(<ReferencesSlider references={mockReferences} />);
expect(screen.getByText('Referenz Projekt Eins')).toBeTruthy();
expect(screen.getByText('Referenz Projekt Zwei')).toBeTruthy();
});
it('renders each tile as a static block and NOT wrapped in a link', () => {
render(<ReferencesSlider references={mockReferences} />);
const links = screen.queryAllByTestId('reference-tile-link');
expect(links).toHaveLength(0);
const tiles = screen.getAllByTestId('reference-tile');
expect(tiles).toHaveLength(2);
});
});

View File

@@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import Link from 'next/link';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { useTranslations, useLocale } from 'next-intl';
export interface Reference {
id: string;
@@ -34,18 +35,23 @@ interface ReferencesSliderProps {
}
export function ReferencesSlider(props: ReferencesSliderProps) {
const t = useTranslations('ReferencesSlider');
const locale = useLocale();
const { data } = props;
const references = props.references || [];
const containerRef = React.useRef<HTMLDivElement>(null);
const dragDistanceRef = React.useRef(0);
const startXRef = React.useRef(0);
const scrollLeftRef = React.useRef(0);
const isDraggingRef = React.useRef(false);
const [isDragging, setIsDragging] = React.useState(false);
const [startX, setStartX] = React.useState(0);
const [scrollLeft, setScrollLeft] = React.useState(0);
const badge = props.badge || data?.badge || 'Ausgewählte Projekte';
const title = props.title || data?.title || 'Referenzen & Erfolge';
const description = props.description || data?.description || 'Ein Auszug unserer erfolgreich abgeschlossenen Projekte im Bereich Kabeltiefbau, Bohrtechnik und Netzinfrastruktur.';
const ctaLabel = props.ctaLabel || data?.ctaLabel || 'Alle Referenzen ansehen';
const ctaHref = props.ctaHref || data?.ctaHref || '/referenzen';
const badge = props.badge || data?.badge || t('badge');
const title = props.title || data?.title || t('title');
const description = props.description || data?.description || t('description');
const ctaLabel = props.ctaLabel || data?.ctaLabel || t('ctaLabel');
const ctaHref = props.ctaHref || data?.ctaHref || `/${locale}/referenzen`;
if (!references || references.length === 0) return null;
@@ -60,28 +66,35 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
const onMouseDown = (e: React.MouseEvent) => {
if (!containerRef.current) return;
setIsDragging(true);
setStartX(e.pageX - containerRef.current.offsetLeft);
setScrollLeft(containerRef.current.scrollLeft);
isDraggingRef.current = true;
const startXVal = e.clientX - (containerRef.current.offsetLeft || 0);
startXRef.current = startXVal;
scrollLeftRef.current = containerRef.current.scrollLeft || 0;
dragDistanceRef.current = 0;
};
const onMouseLeave = () => {
setIsDragging(false);
isDraggingRef.current = false;
};
const onMouseUp = () => {
setIsDragging(false);
isDraggingRef.current = false;
};
const onMouseMove = (e: React.MouseEvent) => {
if (!isDragging || !containerRef.current) return;
if (!isDraggingRef.current || !containerRef.current) return;
e.preventDefault();
const x = e.pageX - containerRef.current.offsetLeft;
const walk = (x - startX) * 2; // Scroll-fast
containerRef.current.scrollLeft = scrollLeft - walk;
const x = e.clientX - (containerRef.current.offsetLeft || 0);
const walk = (x - startXRef.current) * 2; // Scroll-fast
containerRef.current.scrollLeft = scrollLeftRef.current - walk;
dragDistanceRef.current = Math.abs(x - startXRef.current);
};
return (
<section className="py-24 bg-neutral-dark text-white relative overflow-hidden">
<section id="referenzen" className="py-24 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">
@@ -117,8 +130,19 @@ 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"
>
<div className="relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl">
<Link
href={`/${locale}/referenzen/${ref.slug}`}
data-testid="reference-tile"
draggable={false}
onClick={(e) => {
if (dragDistanceRef.current > 15) {
e.preventDefault();
}
}}
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
@@ -129,13 +153,13 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
<HoverShineOverlay />
<div className="absolute bottom-6 left-8 right-8 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none">
<span className="inline-block py-1.5 px-4 rounded-full bg-primary/30 backdrop-blur-md text-white text-xs font-bold uppercase tracking-widest mb-3 border border-primary/50">
<div className="absolute bottom-6 left-6 right-6 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none bg-black/60 backdrop-blur-sm p-4 rounded-xl border border-white/10">
<span className="inline-block py-1 px-3 rounded-full bg-primary/20 text-white text-[10px] font-bold uppercase tracking-widest mb-2 border border-primary/30">
{ref.category}
</span>
<h4 className="font-heading text-2xl md:text-3xl font-bold line-clamp-2">{ref.title}</h4>
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
</div>
</div>
</Link>
</motion.div>
);
})}

View File

@@ -0,0 +1,106 @@
'use client';
import React, { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useLocale } from 'next-intl';
export function ScaleOfImpact() {
const containerRef = useRef<HTMLDivElement>(null);
const locale = useLocale();
// Track scroll progress through this container
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"]
});
// Map scroll progress to SVG path length
const pathLength = useTransform(scrollYProgress, [0.2, 0.8], [0, 1]);
// Opacity for the massive background numbers
const bgOpacity = useTransform(scrollYProgress, [0.1, 0.4, 0.8, 1], [0, 0.1, 0.1, 0]);
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"
>
{/* Massive Watermark Numbers */}
<motion.div
style={{ opacity: bgOpacity }}
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none overflow-hidden mix-blend-overlay"
>
<div className="font-heading font-black text-[20vw] leading-none text-white opacity-20 whitespace-nowrap">
<span className="font-mono tracking-tighter">1.435.223</span>
</div>
</motion.div>
{/* 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">
{locale === 'en' ? (
<>An infrastructure <br className="md:hidden"/> that connects the country.</>
) : (
<>Eine Infrastruktur, <br className="md:hidden"/> die das Land verbindet.</>
)}
</h2>
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium 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."
}
</p>
{/* Abstract SVG Line Animation */}
<div className="w-full max-w-4xl h-32 relative">
<svg
width="100%"
height="100%"
viewBox="0 0 1000 100"
preserveAspectRatio="none"
className="absolute inset-0"
>
{/* Background dim line */}
<path
d="M0,50 Q250,0 500,50 T1000,50"
fill="none"
stroke="rgba(255,255,255,0.05)"
strokeWidth="2"
/>
{/* Glowing animated line */}
<motion.path
d="M0,50 Q250,0 500,50 T1000,50"
fill="none"
stroke="#ffc107" // warning/yellow color
strokeWidth="4"
style={{ pathLength }}
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-2 md:grid-cols-4 gap-8 md:gap-12 w-full max-w-5xl mt-12">
<div className="flex flex-col items-center">
<div className="font-mono text-4xl 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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">974<span className="text-primary text-2xl">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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">300<span className="text-primary text-2xl">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-4xl md:text-5xl font-black text-white tracking-tighter mb-2">160<span className="text-primary text-2xl">km</span></div>
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -194,8 +194,8 @@ export function ServiceDetailGrid({
<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="pt-2">
<h4 className="text-2xl md:text-3xl font-extrabold text-neutral-dark uppercase tracking-wider">{panel.title}</h4>
<div className="pt-2 flex-1 min-w-0 pr-4">
<h4 className="text-xl 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';
@@ -78,7 +79,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
)}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
className="flex overflow-x-auto snap-x snap-mandatory pb-6 md:pb-0 md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 hide-scrollbar -mx-4 px-4 md:mx-0 md:px-0"
variants={containerVariants}
initial="hidden"
whileInView="visible"
@@ -87,8 +88,9 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
{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 +114,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 ${
@@ -227,13 +217,42 @@ 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>
);
})}

View File

@@ -3,7 +3,7 @@
import * as React from 'react';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { useTranslations } from 'next-intl';
export interface TeamMember {
id: string;
@@ -15,7 +15,7 @@ export interface TeamMember {
url: string;
alt?: string;
} | string | null;
branch: string;
branch?: string;
}
interface TeamGridProps {
@@ -23,20 +23,22 @@ interface TeamGridProps {
}
export function TeamGrid({ members }: TeamGridProps) {
const t = useTranslations('TeamGrid');
if (!members || members.length === 0) return null;
return (
<section className="py-24 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">Persönliche Beratung</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark mb-4">Ihre Ansprechpartner</h3>
<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>
<p className="text-text-secondary max-w-2xl text-lg">
Sprechen Sie direkt mit unseren Experten für Ihr regionales Projekt.
{t('subtitle')}
</p>
</div>
<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-8 xl:gap-10">
{members.map((member, i) => (
<motion.div
key={member.id}
@@ -44,53 +46,60 @@ export function TeamGrid({ members }: TeamGridProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="group relative bg-neutral-50 rounded-[2rem] p-8 border border-neutral-200/60 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-2 overflow-hidden"
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
<HoverShineOverlay shineColor="via-primary/5" />
<div className="relative z-10 flex flex-col h-full">
<div className="flex items-center gap-6 mb-6">
<div className="relative w-24 h-24 flex-shrink-0 rounded-[1.25rem] overflow-hidden bg-neutral-200 border border-neutral-100 shadow-sm group-hover:shadow-md transition-shadow duration-500">
{member.image && typeof member.image === 'object' && member.image.url ? (
<Image
src={member.image.url}
alt={member.name}
fill
sizes="96px"
className="object-cover group-hover:scale-110 transition-transform duration-700 ease-in-out"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-neutral-400">
<svg className="w-10 h-10" 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>
<h4 className="font-heading font-bold text-2xl text-neutral-dark mb-1 leading-tight group-hover:text-primary transition-colors duration-300">{member.name}</h4>
<p className="text-primary font-medium text-sm mb-1">{member.position}</p>
<p className="inline-block px-2 py-0.5 mt-1 bg-neutral-100 text-neutral-600 rounded-md text-xs uppercase tracking-widest font-semibold border border-neutral-200/60">
{member.branch === 'e-tib' ? 'E-TIB GmbH' : member.branch === 'ing' ? 'Ingenieurgesellschaft' : 'Bohrtechnik'}
</p>
</div>
{/* Card Banner */}
<div className="h-32 bg-neutral-50 relative border-b border-neutral-100 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-neutral-100 to-neutral-200/50" />
<div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'20\\' height=\\'20\\' viewBox=\\'0 0 20 20\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Crect width=\\'1\\' height=\\'1\\' fill=\\'%23000000\\'/%3E%3C/svg%3E')" }} />
</div>
<div className="px-8 pb-8 relative flex-grow flex flex-col">
{/* Overlapping Profile Picture */}
<div className="w-24 h-24 rounded-2xl overflow-hidden border-4 border-white shadow-xl bg-white relative -mt-12 mb-6 group-hover:-translate-y-2 transition-transform duration-500">
{member.image && (typeof member.image === 'string' ? member.image : member.image.url) ? (
<Image
src={typeof member.image === 'string' ? member.image : member.image.url}
alt={member.name}
fill
sizes="96px"
className="object-cover"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-neutral-300 bg-neutral-50">
<svg className="w-10 h-10" 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>
{/* Details */}
<h4 className="font-heading font-extrabold text-2xl text-neutral-dark mb-1 group-hover:text-primary transition-colors duration-300 leading-tight">{member.name}</h4>
<p className="text-primary font-bold text-xs uppercase tracking-widest mb-6">{member.position}</p>
<div className="mt-auto pt-6 border-t border-neutral-200/60 space-y-3">
{member.branch && (
<div className="mb-6">
<span className="inline-block px-3 py-1 bg-neutral-100 text-neutral-600 rounded-md text-xs uppercase tracking-widest font-semibold border border-neutral-200/60">
{member.branch === 'e-tib' ? t('branchETIB') : member.branch === 'ing' ? t('branchIng') : member.branch === 'bohrtechnik' ? t('branchBohr') : member.branch}
</span>
</div>
)}
{/* Contacts (Sticky at bottom) */}
<div className="mt-auto pt-6 border-t border-neutral-100 flex flex-col gap-4">
{member.email && (
<a href={`mailto:${member.email}`} className="group/link flex items-center gap-3 text-text-secondary hover:text-primary transition-colors text-sm font-medium">
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center text-neutral-500 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors">
<a href={`mailto:${member.email}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" 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>
</div>
<span className="truncate">{member.email}</span>
<svg className="w-4 h-4 ml-auto opacity-0 -translate-x-2 group-hover/link:opacity-100 group-hover/link:translate-x-0 transition-all duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</a>
)}
{member.phone && (
<a href={`tel:${member.phone}`} className="group/link flex items-center gap-3 text-text-secondary hover:text-primary transition-colors text-sm font-medium">
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center text-neutral-500 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors">
<a href={`tel:${member.phone}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" 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>
</div>
<span>{member.phone}</span>
<svg className="w-4 h-4 ml-auto opacity-0 -translate-x-2 group-hover/link:opacity-100 group-hover/link:translate-x-0 transition-all duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</a>
)}
</div>

View File

@@ -19,13 +19,13 @@ export default function PowerCTA({ locale }: PowerCTAProps) {
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/20 rounded-full blur-3xl -mr-32 -mt-32 transition-transform group-hover:scale-110 duration-1000" />
<div className="relative z-10">
<div className="inline-block px-4 py-1 bg-accent/20 text-accent text-xs font-bold uppercase tracking-[0.2em] rounded-full mb-8">
<div className="inline-block px-4 py-1 bg-primary-light/20 text-primary-light text-xs font-bold uppercase tracking-[0.2em] rounded-full mb-8">
{isDe ? 'Lösungen' : 'Solutions'}
</div>
<h3 className="text-2xl md:text-4xl font-bold text-white mb-8 leading-tight">
{isDe ? 'Bereit für die' : 'Ready for the'}
<span className="text-accent block">{isDe ? 'Energiewende?' : 'Energy Transition?'}</span>
<span className="text-primary-light block">{isDe ? 'Energiewende?' : 'Energy Transition?'}</span>
</h3>
<p className="text-xl text-white/90 mb-10 leading-relaxed max-w-2xl">
@@ -46,9 +46,9 @@ export default function PowerCTA({ locale }: PowerCTAProps) {
: 'Certified quality according to EU standards',
].map((item, i) => (
<div key={i} className="flex items-center gap-4 text-white/90">
<div className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center flex-shrink-0">
<div className="w-6 h-6 rounded-full bg-primary-light/20 flex items-center justify-center flex-shrink-0">
<svg
className="w-3 h-3 text-accent"
className="w-3 h-3 text-primary-light"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"

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,27 @@
'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: 2, ease: [0.16, 1, 0.3, 1] }}
className="absolute top-0 right-0 w-[350px] md:w-[550px] h-[350px] md:h-[550px] z-0 pointer-events-none select-none"
style={{
maskImage: 'radial-gradient(ellipse at top right, black 10%, transparent 70%)',
WebkitMaskImage: 'radial-gradient(ellipse at top right, black 10%, transparent 70%)',
}}
>
<Image
src="/assets/eu-flag-perspective.png"
alt="European Union Background"
fill
className="object-cover object-top opacity-50 mix-blend-screen"
priority
/>
</motion.div>
);
}

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/Button';
import { sendContactFormAction } from '@/app/actions/contact';
export function ContactForm() {
const [status, setStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>('idle');
@@ -11,10 +12,18 @@ export function ContactForm() {
e.preventDefault();
setStatus('loading');
// Simulate server action using @mintel/mail or API route
setTimeout(() => {
setStatus('success');
}, 1500);
const formData = new FormData(e.currentTarget);
try {
const result = await sendContactFormAction(formData);
if (result.success) {
setStatus('success');
} else {
setStatus('error');
}
} catch (error) {
console.error(error);
setStatus('error');
}
};
return (
@@ -41,13 +50,31 @@ export function ContactForm() {
<p>Vielen Dank für Ihr Interesse an der E-TIB Gruppe. Ein Ansprechpartner wird sich zeitnah mit Ihnen in Verbindung setzen.</p>
</div>
</motion.div>
) : (
) : status === 'error' ? (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-red-500/10 border border-red-500 text-red-600 p-6 rounded-xl flex items-start gap-4 mb-6"
>
<div className="bg-red-500 text-white rounded-full p-1 mt-0.5">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</div>
<div>
<h4 className="font-bold text-lg mb-1">Fehler beim Senden</h4>
<p>Leider ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal oder kontaktieren Sie uns direkt per E-Mail oder Telefon.</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => setStatus('idle')}>Erneut versuchen</Button>
</div>
</motion.div>
) : null}
{status !== 'success' && status !== 'error' && (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label htmlFor="firstName" className="block text-sm font-semibold text-neutral-dark">Vorname</label>
<label htmlFor="name" className="block text-sm font-semibold text-neutral-dark">Vorname</label>
<input
id="firstName"
id="name"
name="name"
required
disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
@@ -57,7 +84,8 @@ export function ContactForm() {
<div className="space-y-2">
<label htmlFor="lastName" className="block text-sm font-semibold text-neutral-dark">Nachname</label>
<input
id="lastName"
id="lastName"
name="lastName"
required
disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
@@ -70,6 +98,7 @@ export function ContactForm() {
<label htmlFor="email" className="block text-sm font-semibold text-neutral-dark">E-Mail Adresse</label>
<input
id="email"
name="email"
type="email"
required
disabled={status === 'loading'}
@@ -82,6 +111,7 @@ export function ContactForm() {
<label htmlFor="company" className="block text-sm font-semibold text-neutral-dark">Unternehmen (Optional)</label>
<input
id="company"
name="company"
disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
placeholder="Firma GmbH"
@@ -92,6 +122,7 @@ export function ContactForm() {
<label htmlFor="message" className="block text-sm font-semibold text-neutral-dark">Ihre Nachricht</label>
<textarea
id="message"
name="message"
required
disabled={status === 'loading'}
rows={5}
@@ -100,6 +131,8 @@ export function ContactForm() {
/>
</div>
<input type="text" name="company_website" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<Button
type="submit"
disabled={status === 'loading'}

View File

@@ -20,23 +20,23 @@ export default function Hero({ data }: { data?: any }) {
const [searchQuery, setSearchQuery] = useState('');
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [heroPlaceholder, setHeroPlaceholder] = useState(
'Projekt beschreiben oder Kabel suchen...',
t('searchPlaceholder')
);
const typingRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const HERO_PLACEHOLDERS = [
'Querschnittsberechnung für 110kV Trasse', // Hochspannung
'Wie schwer ist NAYY 4x150?',
'Ich plane einen Solarpark, was brauche ich?', // Projekt Solar
'Unterschied zwischen N2XSY und NAY2XSY?', // Fach
'Mittelspannungskabel für Windkraftanlage', // Windpark
'Welches Aluminiumkabel für 20kV?', // Mittelspannung
t('placeholder1'),
t('placeholder2'),
t('placeholder3'),
t('placeholder4'),
t('placeholder5'),
t('placeholder6'),
];
// Typing animation for the hero search placeholder
useEffect(() => {
if (searchQuery) {
setHeroPlaceholder('Projekt beschreiben oder Kabel suchen...');
setHeroPlaceholder(t('searchPlaceholder'));
return;
}
@@ -152,7 +152,7 @@ export default function Hero({ data }: { data?: any }) {
size="lg"
className="rounded-xl px-6 py-4 shrink-0 flex items-center shadow-md font-bold cursor-pointer hover:bg-accent hover:brightness-110"
>
Fragen
{t('ask')}
<ChevronRight className="w-5 h-5 ml-2 -mr-1" />
</Button>
</form>
@@ -160,7 +160,7 @@ export default function Hero({ data }: { data?: any }) {
<div className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6">
<div>
<Button
href="/contact"
href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
variant="white"
size="lg"
className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg hover:scale-105 transition-all outline-none"

View File

@@ -1,9 +1,9 @@
import { TransitionLink } from '@/components/ui/TransitionLink';
import Image from 'next/image';
import { useLocale } from 'next-intl';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap, Mail, Phone } from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { Tooltip } from '@/components/ui/Tooltip';
interface CompanyInfo {
contactEmail: string;
contactPhone: string;
@@ -18,19 +18,19 @@ 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-10 pb-28 md:py-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" />
{/* Top glowing edge */}
<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">
{/* 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">
<TransitionLink href={`/${locale}`} className="relative block h-12 md:h-14 w-40 md:w-48 mb-4 md:mb-6 group/logo cursor-pointer">
<Image
@@ -40,8 +40,8 @@ export function Footer({ companyInfo }: FooterProps) {
className="object-contain object-left transition-transform duration-500 group-hover/logo:scale-105 origin-left"
/>
</TransitionLink>
<p className="text-white/60 text-base leading-relaxed max-w-sm">
{locale === 'de'
<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.'}
</p>
@@ -54,19 +54,39 @@ export function Footer({ companyInfo }: FooterProps) {
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</div>
<div>
<p className="text-xs text-white/40 uppercase tracking-wider mb-1 font-heading">{locale === 'de' ? 'Hauptsitz' : 'Headquarters'}</p>
<p className="whitespace-pre-line font-medium text-white/90">{companyInfo.address}</p>
<p className="text-xs text-neutral-400 uppercase tracking-wider mb-1 font-heading">{locale === 'de' ? 'Hauptsitz' : 'Headquarters'}</p>
<p className="whitespace-pre-line font-medium text-neutral-200">{companyInfo.address}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3 md:gap-4">
<a href={`mailto:${companyInfo.contactEmail}`} className="flex flex-col gap-2 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact cursor-pointer">
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">E-Mail</p>
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactEmail}</p>
</a>
<a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} className="flex flex-col gap-2 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact cursor-pointer">
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">{locale === 'de' ? 'Telefon' : 'Phone'}</p>
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactPhone}</p>
</a>
<a href={`mailto:${companyInfo.contactEmail}`} className="flex gap-4 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact cursor-pointer">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0 group-hover/contact:bg-primary/30 transition-colors">
<Mail className="w-5 h-5 text-primary" />
</div>
<div className="min-w-0">
<p className="text-xs text-neutral-400 uppercase tracking-wider mb-1 font-heading">E-Mail</p>
<p className="font-medium text-neutral-200 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactEmail}</p>
</div>
</a>
<a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} className="flex gap-4 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact cursor-pointer">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0 group-hover/contact:bg-primary/30 transition-colors">
<Phone className="w-5 h-5 text-primary" />
</div>
<div className="min-w-0">
<p className="text-xs text-neutral-400 uppercase tracking-wider mb-1 font-heading">{locale === 'de' ? 'Telefon' : 'Phone'}</p>
<p className="font-medium text-neutral-200 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactPhone}</p>
</div>
</a>
<div className="flex gap-4 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.05] transition-colors">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
<svg className="w-5 h-5 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
</div>
<div>
<p className="text-xs text-neutral-400 uppercase tracking-wider mb-1 font-heading">{locale === 'de' ? 'Öffnungszeiten' : 'Opening Hours'}</p>
<div className="text-sm font-medium text-neutral-200">
<p>{locale === 'de' ? 'Mo. Fr.' : 'Mon. Fri.'}: 07:00 17:00 {locale === 'de' ? 'Uhr' : ''}</p>
<p className="text-white/50 mt-0.5">{locale === 'de' ? 'Sa. So.' : 'Sat. Sun.'}: {locale === 'de' ? 'Geschlossen' : 'Closed'}</p>
</div>
</div>
</div>
</div>
)}
@@ -74,9 +94,9 @@ 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">
<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'}
@@ -84,14 +104,17 @@ export function Footer({ companyInfo }: FooterProps) {
</h4>
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
{[
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/kompetenzen` },
{ label: locale === 'de' ? 'Über uns' : 'About us', href: `/${locale}/ueber-uns` },
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/karriere` },
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/messen` },
{ 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' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
{ 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-white/60 hover:text-white transition-colors py-1 cursor-pointer">
<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>
@@ -101,7 +124,7 @@ export function Footer({ companyInfo }: FooterProps) {
</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'}
@@ -109,13 +132,13 @@ export function Footer({ companyInfo }: FooterProps) {
</h4>
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
{[
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/impressum` },
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/datenschutz` },
{ 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-white/50 hover:text-white transition-colors py-1 cursor-pointer">
<span className="w-0 h-px bg-white/30 mr-0 group-hover/link:w-3 group-hover/link:mr-3 transition-all duration-300" />
<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>
@@ -123,7 +146,7 @@ export function Footer({ companyInfo }: FooterProps) {
</ul>
{/* Built Info */}
{/* Legal Info removed from here */}
</div>
@@ -133,41 +156,48 @@ export function Footer({ companyInfo }: FooterProps) {
<div className="container relative z-10">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 md:gap-6 py-4 px-5 md:py-6 md:px-8 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-md text-center md:text-left">
<div className="flex flex-col md:flex-row items-center gap-4 md:gap-6 h-full">
<p className="text-white/50 text-xs font-medium m-0">&copy; {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
<div className="hidden md:block w-px h-3 bg-white/10" />
<div className="flex items-center gap-3 text-white/30 text-xs">
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-white/60 transition-colors flex items-center gap-2">
<span>Designed & Engineered by Mintel</span>
</a>
{process.env.NEXT_PUBLIC_APP_VERSION && (
<>
<span className="w-1 h-1 rounded-full bg-white/20" />
<div className="flex items-center gap-2 opacity-60">
<span className="font-mono text-[10px]">v{process.env.NEXT_PUBLIC_APP_VERSION.replace(/^v/, '')}</span>
{process.env.NEXT_PUBLIC_TARGET && process.env.NEXT_PUBLIC_TARGET !== 'production' && (
<span className="px-1.5 py-0.5 rounded-sm bg-white/10 text-[9px] uppercase tracking-wider">
{process.env.NEXT_PUBLIC_TARGET}
</span>
)}
</div>
</>
)}
</div>
<p className="text-neutral-400 text-xs font-medium m-0">&copy; {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
</div>
<div className="flex items-center gap-6">
<LanguageSwitcher isSolidMode={false} />
<LanguageSwitcher isSolidMode={false} />
</div>
</div>
{/* Quality Badges */}
<div className="mt-4 md:mt-6 flex flex-wrap justify-center gap-x-4 gap-y-3 md:gap-x-8 md:gap-y-4 text-white/30 text-[10px] md:text-xs font-medium uppercase tracking-wider px-2">
<div className="flex items-center gap-2"><ShieldCheck className="w-4 h-4 text-white/40" /><span>SSL Secured</span></div>
<div className="flex items-center gap-2"><Leaf className="w-4 h-4 text-primary/60" /><span>Green Hosting</span></div>
<div className="flex items-center gap-2"><Lock className="w-4 h-4 text-white/40" /><span>DSGVO Compliant</span></div>
<div className="flex items-center gap-2"><Zap className="w-4 h-4 text-white/40" /><span>High Performance</span></div>
<div className="mt-4 md:mt-6 flex flex-wrap justify-center gap-x-4 gap-y-3 md:gap-x-8 md:gap-y-4 text-neutral-400 text-[10px] md:text-xs font-medium uppercase tracking-wider px-2">
<Tooltip content={locale === 'de' ? 'Ihre Daten werden sicher und verschlüsselt übertragen.' : 'Your data is transmitted securely and encrypted.'}>
<div className="flex items-center gap-2 cursor-help"><ShieldCheck className="w-4 h-4 text-neutral-400" /><span>SSL Secured</span></div>
</Tooltip>
<Tooltip content={locale === 'de' ? 'Wir nutzen 100% Ökostrom für den Betrieb unserer Website.' : 'We use 100% green energy to run our website.'}>
<div className="flex items-center gap-2 cursor-help"><Leaf className="w-4 h-4 text-primary" /><span>Green Hosting</span></div>
</Tooltip>
<Tooltip content={locale === 'de' ? 'Ihre Privatsphäre ist uns wichtig. Wir halten uns streng an den Datenschutz.' : 'Your privacy is important to us. We strictly adhere to data protection laws.'}>
<div className="flex items-center gap-2 cursor-help"><Lock className="w-4 h-4 text-neutral-400" /><span>DSGVO Compliant</span></div>
</Tooltip>
<Tooltip content={locale === 'de' ? 'Blitzschnelle Ladezeiten für ein optimales Nutzererlebnis.' : 'Lightning-fast loading times for an optimal user experience.'}>
<div className="flex items-center gap-2 cursor-help"><Zap className="w-4 h-4 text-neutral-400" /><span>High Performance</span></div>
</Tooltip>
</div>
{/* Mintel Design & Version */}
<div className="mt-6 flex justify-center items-center gap-3 text-neutral-500 text-[10px] md:text-xs opacity-60 hover:opacity-100 transition-opacity">
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-neutral-300 transition-colors flex items-center gap-2">
<span>Designed & Engineered by Marc Mintel</span>
</a>
{process.env.NEXT_PUBLIC_APP_VERSION && (
<>
<span className="w-1 h-1 rounded-full bg-white/20" />
<div className="flex items-center gap-2">
<span className="font-mono text-[10px]">v{process.env.NEXT_PUBLIC_APP_VERSION.replace(/^v/, '')}</span>
{process.env.NEXT_PUBLIC_TARGET && process.env.NEXT_PUBLIC_TARGET !== 'production' && (
<span className="px-1.5 py-0.5 rounded-sm bg-white/10 text-[9px] uppercase tracking-wider">
{process.env.NEXT_PUBLIC_TARGET}
</span>
)}
</div>
</>
)}
</div>
</div>
</footer>

View File

@@ -4,12 +4,14 @@ import * as React from 'react';
import { TransitionLink } from '@/components/ui/TransitionLink';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion, useScroll, useTransform } from 'framer-motion';
import { motion, useScroll, useTransform, AnimatePresence } from 'framer-motion';
import { LanguageSwitcher } from './LanguageSwitcher';
export interface NavLink {
label: string;
url: string;
children?: NavLink[];
isGroupLabel?: boolean;
}
interface HeaderProps {
@@ -19,6 +21,7 @@ interface HeaderProps {
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 pathname = usePathname() || '/';
// Scroll bound shine effect (continuous looping as user scrolls)
@@ -40,17 +43,18 @@ export function Header({ navLinks }: HeaderProps) {
window.addEventListener('scroll', handleScroll);
// Check for forced solid header (used by error pages, legal pages without hero, etc.)
const checkSolid = () => {
const checkHeaderState = () => {
setForceSolid(document.body.getAttribute('data-force-solid-header') === 'true');
setHeaderVariant((document.body.getAttribute('data-header-variant') as 'capsule' | 'standard') || 'capsule');
};
checkSolid();
checkHeaderState();
// Set up observer for client-side navigation changes
const observer = new MutationObserver(checkSolid);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-force-solid-header'] });
const observer = new MutationObserver(checkHeaderState);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-force-solid-header', 'data-header-variant'] });
// Re-check when pathname changes
checkSolid();
checkHeaderState();
return () => {
window.removeEventListener('scroll', handleScroll);
@@ -58,23 +62,26 @@ export function Header({ navLinks }: HeaderProps) {
};
}, [pathname]);
// Check for force solid initially
const isSolidMode = isScrolled || forceSolid;
const isSolidMode = isScrolled || forceSolid || headerVariant === 'standard';
const isStandard = headerVariant === 'standard';
return (
<>
<header
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)] ${
isSolidMode ? 'pt-4 px-4' : 'pt-0 px-0'
isStandard
? 'bg-[#050B14]/95 backdrop-blur-xl border-b border-white/10'
: (isSolidMode ? 'pt-4 px-4 sm:px-6' : 'pt-0 px-0')
}`}
>
<div
className={`relative overflow-hidden w-full max-w-7xl mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
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-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
className={`relative w-full max-w-7xl mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
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-8 md:px-10'
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
}`}
>
{/* Scroll bound shine effect */}
@@ -114,40 +121,15 @@ export function Header({ navLinks }: HeaderProps) {
<div className="flex items-center gap-2 md:gap-4 ml-auto md:ml-0">
<nav className="relative z-10 hidden md:flex items-center gap-8 lg:gap-10">
{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 (
<TransitionLink
key={link.url}
href={mappedUrl}
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group py-2 select-none ${
isSolidMode
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
}`}
>
{link.label}
{/* Framer Motion Active Indicator */}
{isActive && (
<motion.div
layoutId="header-active-indicator"
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
{/* Subtle hover underline */}
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
</TransitionLink>
);
})}
{navLinks && navLinks.length > 0 && navLinks.map((link) => (
<NavItem
key={link.url}
link={link}
currentLocale={currentLocale}
pathname={pathname}
isSolidMode={isSolidMode}
/>
))}
</nav>
{/* Right side elements (Language Switcher always visible, Contact button desktop only) */}
@@ -180,3 +162,119 @@ 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;
const isActive = pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`));
return (
<div
className="relative group py-2"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<TransitionLink
href={mappedUrl}
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group flex items-center gap-1.5 select-none ${
isSolidMode
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
}`}
>
{link.label}
{link.children && (
<svg className={`w-3.5 h-3.5 transition-transform duration-300 ${isHovered ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" />
</svg>
)}
{/* Framer Motion Active Indicator */}
{isActive && !link.children && (
<motion.div
layoutId="header-active-indicator"
className={`absolute -bottom-1 left-0 right-0 h-0.5 rounded-full ${isSolidMode ? 'bg-primary' : 'bg-white'}`}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
{/* Subtle hover underline */}
<span className={`absolute -bottom-1 left-1/2 w-0 h-0.5 -translate-x-1/2 rounded-full transition-all duration-300 ease-out group-hover:w-full ${isSolidMode ? 'bg-primary/30' : 'bg-white/30'} ${isActive ? 'opacity-0' : 'opacity-100'}`} />
</TransitionLink>
<AnimatePresence>
{isHovered && link.children && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
exit={{ opacity: 0, y: 10, scale: 0.95, rotateX: -10 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
style={{ perspective: 1000, transformOrigin: 'top center' }}
className="absolute top-full left-1/2 -translate-x-1/2 pt-6 w-64 z-50"
>
<div className={`p-2.5 rounded-3xl shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] border ${isSolidMode ? 'bg-white/95 border-neutral-200/60' : 'bg-[#050B14]/95 border-white/[0.08] shadow-[0_0_40px_rgba(0,0,0,0.4)]'} backdrop-blur-2xl flex flex-col gap-1 relative overflow-hidden`}>
{/* Top glowing accent line */}
<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, 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'
: isChildActive ? 'bg-white/10 text-white' : 'text-neutral-300 hover:text-white hover:bg-white/[0.06]'
}`}
>
<div className="relative z-10 flex items-center justify-between">
<span className="relative">
{child.label}
<span className={`absolute -bottom-0.5 left-0 w-0 h-0.5 rounded-full transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover/dropitem:w-full ${isSolidMode ? 'bg-primary/40' : 'bg-primary/60'} ${isChildActive ? 'opacity-0' : 'opacity-100'}`} />
</span>
<svg className={`w-4 h-4 transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] -translate-x-3 opacity-0 group-hover/dropitem:translate-x-0 group-hover/dropitem:opacity-100 ${isSolidMode ? 'text-primary' : 'text-white'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</TransitionLink>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { usePathname } from 'next/navigation';
// Mock next/navigation
vi.mock('next/navigation', () => ({
usePathname: vi.fn(),
}));
// Mock TransitionLink to render standard anchor tag and forward all props
vi.mock('@/components/ui/TransitionLink', () => ({
TransitionLink: ({ children, href, onClick, ...props }: any) => (
<a href={href} onClick={onClick} {...props}>
{children}
</a>
),
}));
// Mock HoverShineOverlay
vi.mock('@/components/ui/HoverShineOverlay', () => ({
HoverShineOverlay: () => <div data-testid="hover-shine" />,
}));
describe('LanguageSwitcher TDD', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('translates /en/trade-fairs to /de/messen when switching to German', () => {
(usePathname as any).mockReturnValue('/en/trade-fairs');
render(<LanguageSwitcher />);
const deLink = screen.getByLabelText('Switch to Deutsch');
expect(deLink.getAttribute('href')).toBe('/de/messen');
});
it('translates /de/messen to /en/trade-fairs when switching to English', () => {
(usePathname as any).mockReturnValue('/de/messen');
render(<LanguageSwitcher />);
const enLink = screen.getByLabelText('Switch to English');
expect(enLink.getAttribute('href')).toBe('/en/trade-fairs');
});
it('translates /en/about-us to /de/ueber-uns when switching to German', () => {
vi.mocked(usePathname).mockReturnValue('/en/about-us');
render(<LanguageSwitcher />);
const deLink = screen.getByLabelText('Switch to Deutsch');
expect(deLink.getAttribute('href')).toBe('/de/ueber-uns');
});
it('translates /de/ueber-uns to /en/about-us when switching to English', () => {
vi.mocked(usePathname).mockReturnValue('/de/ueber-uns');
render(<LanguageSwitcher />);
const enLink = screen.getByLabelText('Switch to English');
expect(enLink.getAttribute('href')).toBe('/en/about-us');
});
it('preserves nested paths like blog posts', () => {
vi.mocked(usePathname).mockReturnValue('/de/blog/some-post');
render(<LanguageSwitcher />);
const enLink = screen.getByLabelText('Switch to English');
expect(enLink.getAttribute('href')).toBe('/en/blog/some-post');
});
});

View File

@@ -3,6 +3,41 @@
import { TransitionLink } from '@/components/ui/TransitionLink';
import { usePathname } from 'next/navigation';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import enMessages from '@/messages/en.json';
import deMessages from '@/messages/de.json';
const getMessages = (locale: string) => {
return locale === 'de' ? deMessages : enMessages;
};
// Synchronous version of mapSlugToFileSlug
function syncMapSlugToFileSlug(translatedSlug: string, locale: string): string {
const messages = getMessages(locale);
const slugs = messages.Slugs;
if (slugs.pages && translatedSlug in slugs.pages) {
return slugs.pages[translatedSlug as keyof typeof slugs.pages];
}
return translatedSlug;
}
// Synchronous version of mapFileSlugToTranslated
function syncMapFileSlugToTranslated(fileSlug: string, locale: string): string {
const messages = getMessages(locale);
const slugs = messages.Slugs;
const sections = [slugs.pages];
for (const sectionData of sections) {
if (sectionData && typeof sectionData === 'object') {
for (const [translatedSlug, mappedSlug] of Object.entries(sectionData)) {
if (mappedSlug === fileSlug) {
return translatedSlug;
}
}
}
}
return fileSlug;
}
interface LanguageSwitcherProps {
mobile?: boolean;
@@ -14,8 +49,23 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
const getSwitchedUrl = (newLocale: string) => {
const pathWithoutLocale = pathname.replace(/^\/(en|de)/, '');
return `/${newLocale}${pathWithoutLocale === '' ? '' : pathWithoutLocale}`;
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) {
return `/${newLocale}`;
}
const currentLoc = segments[0] === 'en' || segments[0] === 'de' ? segments[0] : 'de';
const hasLocale = segments[0] === 'en' || segments[0] === 'de';
const pathSegments = hasLocale ? segments.slice(1) : segments;
if (pathSegments.length === 1) {
const slug = pathSegments[0];
const fileSlug = syncMapSlugToFileSlug(slug, currentLoc);
const translatedSlug = syncMapFileSlugToTranslated(fileSlug, newLocale);
return `/${newLocale}/${translatedSlug}`;
}
return `/${newLocale}/${pathSegments.join('/')}`;
};
const locales = [
@@ -73,7 +123,7 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
} else {
linkClass = isSolidMode
? 'text-text-secondary hover:text-primary'
: 'text-white/70 hover:text-white';
: 'text-white/90 hover:text-white';
}
const currentLocObj = locales.find(l => l.code === currentLocale) || locales[0];

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import { AISearchResults } from './AISearchResults';
// Mock lucide-react
vi.mock('lucide-react', () => ({
ArrowUp: () => <div data-testid="arrow-up" />,
X: () => <div data-testid="close-icon" />,
Sparkles: () => <div data-testid="sparkles" />,
ChevronRight: () => <div data-testid="chevron-right" />,
RotateCcw: () => <div data-testid="rotate-ccw" />,
Copy: () => <div data-testid="copy" />,
Check: () => <div data-testid="check" />,
}));
// Mock next/link
vi.mock('next/link', () => ({
default: ({ children, href }: any) => <a href={href}>{children}</a>,
}));
// Mock useAnalytics
vi.mock('../analytics/useAnalytics', () => ({
useAnalytics: () => ({
trackEvent: vi.fn(),
}),
}));
// Mock react-markdown
vi.mock('react-markdown', () => ({
default: ({ children }: any) => <div>{children}</div>,
}));
// Mock remark-gfm
vi.mock('remark-gfm', () => ({
default: {},
}));
// Mock next-intl
vi.mock('next-intl', () => ({
useTranslations: () => {
const translate = (key: string) => key;
translate.raw = (key: string) => {
if (key === 'loadingTexts') return ['Lade...', 'Denke nach...'];
if (key === 'prompts') return ['Prompt 1', 'Prompt 2'];
return [];
};
return translate;
},
useLocale: () => 'en',
}));
describe('AISearchResults Component Test (TDD)', () => {
it('renders correctly when open', () => {
const handleClose = vi.fn();
const { container } = render(
<AISearchResults isOpen={true} onClose={handleClose} />
);
expect(container).toBeTruthy();
});
});

View File

@@ -8,14 +8,9 @@ import { AnalyticsEvents } from '../analytics/analytics-events';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import dynamic from 'next/dynamic';
import { useTranslations } from 'next-intl';
const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false });
const LOADING_TEXTS = [
'Durchsuche das Kabelhandbuch... 📖',
'Frage den Senior-Ingenieur... 👴🔧',
'Frage ChatGPTs Cousin 2. Grades... 🤖',
];
interface ProductMatch {
id: string;
title: string;
@@ -51,7 +46,13 @@ export function AISearchResults({
const [error, setError] = useState<string | null>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const [loadingText, setLoadingText] = useState(LOADING_TEXTS[0]);
const t = useTranslations('AISearch');
// We load texts dynamically from translations
const loadingTexts = t.raw('loadingTexts') as string[];
const prompts = t.raw('prompts') as string[];
const [loadingText, setLoadingText] = useState(loadingTexts[0]);
const inputRef = useRef<HTMLInputElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
@@ -141,11 +142,11 @@ export function AISearchResults({
setTimeout(() => {
setIsLoading(true);
// Start rotating loading texts
let textIdx = Math.floor(Math.random() * LOADING_TEXTS.length);
setLoadingText(LOADING_TEXTS[textIdx]);
let textIdx = Math.floor(Math.random() * loadingTexts.length);
setLoadingText(loadingTexts[textIdx]);
loadingIntervalRef.current = setInterval(() => {
textIdx = (textIdx + 1) % LOADING_TEXTS.length;
setLoadingText(LOADING_TEXTS[textIdx]);
textIdx = (textIdx + 1) % loadingTexts.length;
setLoadingText(loadingTexts[textIdx]);
}, 2500);
}, 400);
@@ -191,8 +192,8 @@ export function AISearchResults({
console.error(err);
const msg =
err.name === 'AbortError'
? 'Anfrage hat zu lange gedauert. Bitte versuche es erneut.'
: err.message || 'Ein Fehler ist aufgetreten.';
? t('timeoutError')
: err.message || t('genericError');
// Show error as a system message in the chat instead of a separate error banner
setMessages((prev) => [
@@ -248,7 +249,7 @@ export function AISearchResults({
const handleCopyChat = () => {
const fullChat = messages
.map((m) => `${m.role === 'user' ? 'Du' : 'Ohm'}:\n${m.content}`)
.map((m) => `${m.role === 'user' ? t('you') : 'Ohm'}:\n${m.content}`)
.join('\n\n');
handleCopy(fullChat);
};
@@ -294,7 +295,7 @@ export function AISearchResults({
<div>
<h2 className="text-white font-bold text-sm tracking-wide">Ohm</h2>
<p className="text-[10px] text-white/30 font-medium tracking-wider uppercase">
{isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'}
{isLoading ? t('thinking') : error ? t('errorStatus') : t('online')}
</p>
</div>
</div>
@@ -303,14 +304,14 @@ export function AISearchResults({
<button
onClick={handleCopyChat}
className="flex items-center gap-1.5 text-[10px] font-bold text-white/40 hover:text-white/80 transition-all duration-200 hover:bg-white/5 rounded-full px-3 py-1.5 cursor-pointer uppercase tracking-wider"
title="gesamten Chat kopieren"
title={t('copyChatTitle')}
>
{copiedAll ? (
<Check className="w-3.5 h-3.5 text-accent" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
<span>{copiedAll ? 'Kopiert' : 'Chat kopieren'}</span>
<span>{copiedAll ? t('copied') : t('copyChat')}</span>
</button>
)}
<button
@@ -336,16 +337,15 @@ export function AISearchResults({
</div>
<div>
<p className="text-xl md:text-2xl font-bold text-white/80">
Wie kann ich helfen?
{t('howCanIHelp')}
</p>
<p className="text-sm text-white/30 mt-2 max-w-md">
Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine
Anforderungen.
{t('helpDescription')}
</p>
</div>
{/* Quick prompts */}
<div className="flex flex-wrap justify-center gap-2 mt-4">
{['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel für Solarpark'].map(
{prompts.map(
(prompt) => (
<button
key={prompt}
@@ -384,7 +384,7 @@ export function AISearchResults({
? 'top-2 right-2 bg-primary/10 hover:bg-primary/20 text-primary/60 hover:text-primary'
: 'top-2 right-2 bg-white/5 hover:bg-white/10 text-white/40 hover:text-white'
}`}
title="Nachricht kopieren"
title={t('copyMessage')}
>
{copiedIndex === index ? (
<Check className="w-3.5 h-3.5" />
@@ -431,7 +431,7 @@ export function AISearchResults({
{msg.role === 'assistant' && msg.products && msg.products.length > 0 && (
<div className="mt-4 space-y-2 border-t border-white/[0.06] pt-4">
<h4 className="text-[10px] font-bold tracking-widest uppercase text-white/30 mb-2">
Empfohlene Produkte
{t('recommendedProducts')}
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{msg.products.map((product, idx) => (
@@ -509,7 +509,7 @@ export function AISearchResults({
<AIOrb isThinking={false} hasError={true} />
</div>
<div>
<h3 className="text-sm font-bold text-red-300">Da ist was schiefgelaufen 😬</h3>
<h3 className="text-sm font-bold text-red-300">{t('errorTitle')}</h3>
<p className="text-xs text-red-300/60 mt-1">{error}</p>
<button
onClick={() => {
@@ -519,7 +519,7 @@ export function AISearchResults({
className="flex items-center gap-1.5 text-[10px] font-bold text-red-300/50 hover:text-red-300 mt-2 transition-colors cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
Nochmal versuchen
{t('tryAgain')}
</button>
</div>
</div>
@@ -544,7 +544,7 @@ export function AISearchResults({
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Nachricht eingeben..."
placeholder={t('placeholder')}
className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20"
disabled={isLoading}
tabIndex={1}
@@ -567,20 +567,20 @@ export function AISearchResults({
? 'bg-accent text-primary shadow-lg shadow-accent/20 hover:shadow-accent/40 hover:scale-105 active:scale-95'
: 'bg-white/5 text-white/20'
}`}
aria-label="Nachricht senden"
aria-label={t('send')}
>
<ArrowUp className="w-4 h-4" strokeWidth={2.5} />
</button>
</div>
<div className="flex items-center justify-center gap-3 mt-2.5">
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
Enter zum Senden · Esc zum Schließen
{t('footerShortcuts')}
</span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
·
</span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-accent/40 flex items-center gap-1">
🛡 DSGVO-konform · EU-Server
{t('footerPrivacy')}
</span>
</div>
</div>

View File

@@ -0,0 +1,48 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion, useMotionValue, useTransform, animate, useInView } from 'framer-motion';
interface AnimatedCounterProps {
value: number;
duration?: number;
delay?: number;
suffix?: string;
prefix?: string;
className?: string;
}
export function AnimatedCounter({
value,
duration = 2,
delay = 0,
suffix = '',
prefix = '',
className = '',
}: AnimatedCounterProps) {
const count = useMotionValue(0);
const rounded = useTransform(count, (latest) => {
// Format with thousands separator
return new Intl.NumberFormat('de-DE').format(Math.round(latest));
});
const ref = React.useRef(null);
const inView = useInView(ref, { once: true, margin: "-100px" });
useEffect(() => {
if (inView) {
const controls = animate(count, value, {
duration: duration,
delay: delay,
ease: 'easeOut',
});
return controls.stop;
}
}, [inView, value, duration, delay, count]);
return (
<motion.span ref={ref} className={className}>
{prefix}<motion.span>{rounded}</motion.span>{suffix}
</motion.span>
);
}

19
components/ui/Tooltip.tsx Normal file
View File

@@ -0,0 +1,19 @@
import React, { ReactNode } from 'react';
interface TooltipProps {
children: ReactNode;
content: string;
}
export function Tooltip({ children, content }: TooltipProps) {
return (
<div className="group relative flex items-center justify-center">
{children}
<div className="absolute bottom-full mb-3 invisible opacity-0 translate-y-2 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 w-max max-w-[250px] px-4 py-2.5 text-[11px] md:text-xs text-white bg-neutral-900/95 backdrop-blur-md rounded-xl shadow-2xl border border-white/10 transition-all duration-300 pointer-events-none z-50 text-center leading-relaxed font-normal normal-case tracking-normal">
{content}
{/* Triangle Arrow */}
<div className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-neutral-900/95" />
</div>
</div>
);
}

View File

@@ -7,5 +7,6 @@ export * from './Card';
export * from './Badge';
export * from './Input';
export * from './Textarea';
export * from './AnimatedCounter';
export * from './Label';
export * from './Callout';

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

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