Compare commits
47 Commits
9368840139
...
fix/header
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dc2fe8a25 | |||
| 5977ebf23d | |||
| ee2bcea42b | |||
| 2f2fcfdf13 | |||
| 070f97dd6f | |||
| 2a46015d0d | |||
| 5329d96e3b | |||
| 0c01aa799d | |||
| 75c15fce43 | |||
| 3b20d4087c | |||
| 2dbafec633 | |||
| 2b79d18a93 | |||
| fa747763b8 | |||
| 0d71e2ad24 | |||
| 64d4edc175 | |||
| 222c894266 | |||
| 4a3aee7691 | |||
| fd8dac97d0 | |||
| bc5a78d73e | |||
| f43b945756 | |||
| 183342cb8c | |||
| 3728c37dce | |||
| f61f1411cb | |||
| d2668e36eb | |||
| 5678ddcfd9 | |||
| 06d5178614 | |||
| a983696153 | |||
| 6908964df5 | |||
| 159587f545 | |||
| c02b49644e | |||
| 313fa14c56 | |||
| 052be7d1c9 | |||
| aef10f706c | |||
| 35d9650626 | |||
| 140cb012df | |||
| e1c9720eeb | |||
| 843d61cd5d | |||
| 4dcd5f511a | |||
| 62644a0366 | |||
| db9e0fde91 | |||
| 0516dbe743 | |||
| 3808339711 | |||
| dd04755690 | |||
| 5919d5d47a | |||
| e327c77e52 | |||
| c48bf18433 | |||
| 372ef1a418 |
@@ -43,9 +43,33 @@ 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 "Purging old build layers, unused volumes and images..."
|
||||
docker builder prune -a -f || true
|
||||
docker volume prune -f || true
|
||||
docker system prune -a -f || true
|
||||
echo "=== System Disk Usage After Prune ==="
|
||||
df -h || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -207,6 +231,7 @@ jobs:
|
||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
NPM_TOKEN=${{ steps.auth.outputs.working_token }}
|
||||
NPM_DOMAIN=${{ steps.auth.outputs.working_domain }}
|
||||
tags: registry.infra.mintel.me/mintel/e-tib.com:${{ needs.prepare.outputs.image_tag }}
|
||||
secrets: |
|
||||
"NPM_TOKEN=${{ steps.auth.outputs.working_token }}"
|
||||
|
||||
1
.pa11yci/report.json
Normal file
1
.pa11yci/report.json
Normal file
File diff suppressed because one or more lines are too long
18
.pa11yci/single-test.json
Normal file
18
.pa11yci/single-test.json
Normal 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
9
.pa11yci/test-html.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"defaults": {
|
||||
"runners": ["axe"],
|
||||
"ignore": ["color-contrast"]
|
||||
},
|
||||
"urls": [
|
||||
"file:////Volumes/Alpha SSD/Coding/e-tib.com/test.html"
|
||||
]
|
||||
}
|
||||
1
.turbo-test/85340b254e0f10f1-manifest.json
Normal file
1
.turbo-test/85340b254e0f10f1-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":10746,"mtime_nanos":1779098006288513580,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo-test/85340b254e0f10f1-meta.json
Normal file
1
.turbo-test/85340b254e0f10f1-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"85340b254e0f10f1","duration":6595,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"9cc3e16c40a10f42f7a963333e800e1384f047b1f387b18b5f37769c34f36609"}
|
||||
BIN
.turbo-test/85340b254e0f10f1.tar.zst
Normal file
BIN
.turbo-test/85340b254e0f10f1.tar.zst
Normal file
Binary file not shown.
1
.turbo/cache/32905076da5f4894-manifest.json
vendored
Normal file
1
.turbo/cache/32905076da5f4894-manifest.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-typecheck.log":{"size":92,"mtime_nanos":1779097674383861303,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
||||
1
.turbo/cache/32905076da5f4894-meta.json
vendored
Normal file
1
.turbo/cache/32905076da5f4894-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"32905076da5f4894","duration":4492,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
||||
BIN
.turbo/cache/32905076da5f4894.tar.zst
vendored
Normal file
BIN
.turbo/cache/32905076da5f4894.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/7e2629942c881f80-manifest.json
vendored
Normal file
1
.turbo/cache/7e2629942c881f80-manifest.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-test.log":{"size":818,"mtime_nanos":1779097671747078359,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
||||
1
.turbo/cache/7e2629942c881f80-meta.json
vendored
Normal file
1
.turbo/cache/7e2629942c881f80-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"7e2629942c881f80","duration":1855,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
||||
BIN
.turbo/cache/7e2629942c881f80.tar.zst
vendored
Normal file
BIN
.turbo/cache/7e2629942c881f80.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/85340b254e0f10f1-manifest.json
vendored
Normal file
1
.turbo/cache/85340b254e0f10f1-manifest.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":10746,"mtime_nanos":1779097675143794164,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo/cache/85340b254e0f10f1-meta.json
vendored
Normal file
1
.turbo/cache/85340b254e0f10f1-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"85340b254e0f10f1","duration":5252,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
||||
BIN
.turbo/cache/85340b254e0f10f1.tar.zst
vendored
Normal file
BIN
.turbo/cache/85340b254e0f10f1.tar.zst
vendored
Normal file
Binary file not shown.
119
.turbo/turbo-lint.log
Normal file
119
.turbo/turbo-lint.log
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 lint /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> eslint .
|
||||
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/[slug]/page.tsx
|
||||
1:20 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars
|
||||
67:24 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/blog/[slug]/page.tsx
|
||||
1:20 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars
|
||||
39:24 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/error.tsx
|
||||
7:30 warning 'AlertTriangle' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/not-found.tsx
|
||||
6:10 warning 'Terminal' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/[slug]/page.tsx
|
||||
91:26 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/page.tsx
|
||||
1:21 warning 'Heading' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:30 warning 'Badge' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/JsonLd.tsx
|
||||
1:30 warning 'Graph' is defined but never used @typescript-eslint/no-unused-vars
|
||||
43:31 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
48:29 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CertificatesBlock.tsx
|
||||
6:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
|
||||
9:10 warning 'FileText' is defined but never used @typescript-eslint/no-unused-vars
|
||||
77:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
|
||||
|
||||
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
|
||||
* Update external systems with the latest state from React.
|
||||
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
|
||||
|
||||
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CertificatesBlock.tsx:77:5
|
||||
75 |
|
||||
76 | useEffect(() => {
|
||||
> 77 | setIsMounted(true);
|
||||
| ^^^^^^^^^^^^ Avoid calling setState() directly within an effect
|
||||
78 | }, []);
|
||||
79 |
|
||||
80 | const badgeText = badge || 'Zertifizierungen & Nachweise'; react-hooks/set-state-in-effect
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CompetenceBentoGrid.tsx
|
||||
53:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
|
||||
|
||||
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
|
||||
* Update external systems with the latest state from React.
|
||||
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
|
||||
|
||||
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/CompetenceBentoGrid.tsx:53:5
|
||||
51 |
|
||||
52 | useEffect(() => {
|
||||
> 53 | setIsMounted(true);
|
||||
| ^^^^^^^^^^^^ Avoid calling setState() directly within an effect
|
||||
54 | }, []);
|
||||
55 |
|
||||
56 | const badge = props.badge || data?.badge || 'Leistungsspektrum'; react-hooks/set-state-in-effect
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/HeroVideo.tsx
|
||||
5:18 warning 'AnimatePresence' is defined but never used @typescript-eslint/no-unused-vars
|
||||
6:10 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars
|
||||
6:20 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars
|
||||
45:12 warning 'e' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/InteractiveGermanyMap.tsx
|
||||
5:46 warning 'ArrowUpRight' is defined but never used @typescript-eslint/no-unused-vars
|
||||
147:23 warning 'isNearRight' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/TeamGridBlock.tsx
|
||||
20:39 warning 'title' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
20:46 warning 'department' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
20:58 warning 'showContact' is assigned a value but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/decorations/CorporateBackground.tsx
|
||||
36:13 warning 'distance' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/home/Hero.tsx
|
||||
9:39 warning 'useCallback' is defined but never used @typescript-eslint/no-unused-vars
|
||||
77:6 warning React Hook useEffect has a missing dependency: 'HERO_PLACEHOLDERS'. Either include it or remove the dependency array react-hooks/exhaustive-deps
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/home/MeetTheTeam.tsx
|
||||
8:9 warning 'teamT' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/layout/Footer.tsx
|
||||
4:35 warning 'Accessibility' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/providers/InitialLoader.tsx
|
||||
5:8 warning 'Image' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/providers/PageTransitionShutter.tsx
|
||||
6:8 warning 'Image' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/search/AIOrb.tsx
|
||||
153:13 warning 'breathe' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
212:15 warning 'targetExcite' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/search/AISearchResults.tsx
|
||||
99:6 warning React Hook useEffect has missing dependencies: 'handleSearch' and 'initialQuery'. Either include them or remove the dependency array react-hooks/exhaustive-deps
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/lib/blog.ts
|
||||
6:7 warning 'BLOG_DIR' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
94:7 warning 'next' is never reassigned. Use 'const' instead prefer-const
|
||||
95:7 warning 'prev' is never reassigned. Use 'const' instead prefer-const
|
||||
|
||||
✖ 38 problems (0 errors, 38 warnings)
|
||||
0 errors and 2 warnings potentially fixable with the `--fix` option.
|
||||
|
||||
31
.turbo/turbo-test.log
Normal file
31
.turbo/turbo-test.log
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 test /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> vitest run --passWithNoTests
|
||||
|
||||
[?25l
|
||||
RUN v4.1.2 /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
|
||||
[?2026h
|
||||
❯ lib/mail/mailer.test.ts [queued]
|
||||
|
||||
Test Files 0 passed (2)
|
||||
Tests 0 passed (0)
|
||||
Start at 11:47:51
|
||||
Duration 504ms
|
||||
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K ✓ lib/mail/mailer.test.ts (1 test) 2ms
|
||||
|
||||
❯ lib/env.test.ts 0/6
|
||||
|
||||
Test Files 1 passed (2)
|
||||
Tests 1 passed (7)
|
||||
Start at 11:47:51
|
||||
Duration 605ms
|
||||
[?2026l[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K ✓ lib/env.test.ts (6 tests) 3ms
|
||||
|
||||
Test Files 2 passed (2)
|
||||
Tests 7 passed (7)
|
||||
Start at 11:47:51
|
||||
Duration 672ms (transform 113ms, setup 0ms, import 222ms, tests 5ms, environment 813ms)
|
||||
|
||||
[?25h
|
||||
5
.turbo/turbo-typecheck.log
Normal file
5
.turbo/turbo-typecheck.log
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> tsc --noEmit
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,7 +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';
|
||||
@@ -23,8 +23,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 +46,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 +153,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 +162,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 +174,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 +231,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,16 +89,17 @@ 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' ? 'Referenzen' : 'References',
|
||||
url: `/${safeLocale}/referenzen`
|
||||
},
|
||||
{
|
||||
label: safeLocale === 'de' ? 'Karriere' : 'Careers',
|
||||
url: `/${safeLocale}/${await mapFileSlugToTranslated('karriere', safeLocale)}`
|
||||
},
|
||||
{
|
||||
label: safeLocale === 'de' ? 'Messen' : 'Events',
|
||||
url: `/${safeLocale}/${await mapFileSlugToTranslated('messen', safeLocale)}`
|
||||
@@ -122,6 +123,13 @@ export default async function Layout(props: {
|
||||
'Error',
|
||||
'StandardPage',
|
||||
'Brochure',
|
||||
'JobListingBlock',
|
||||
'CallToAction',
|
||||
'InteractiveGermanyMap',
|
||||
'ReferencesSlider',
|
||||
'CompanyTimeline',
|
||||
'TeamGrid',
|
||||
'AISearch',
|
||||
];
|
||||
const clientMessages: Record<string, any> = {};
|
||||
for (const key of clientKeys) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
38
app/[locale]/referenzen/[slug]/page.test.tsx
Normal file
38
app/[locale]/referenzen/[slug]/page.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
@@ -70,10 +71,17 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
|
||||
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 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 className="bg-neutral-50 p-6 rounded-2xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-shadow group" {...props}>
|
||||
<div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-primary/20 transition-colors">
|
||||
<svg className="w-3.5 h-3.5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-neutral-700 font-semibold leading-snug">{props.children}</span>
|
||||
</li>
|
||||
),
|
||||
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
|
||||
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
|
||||
@@ -112,12 +120,12 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
|
||||
eventProperties={{ location: 'reference_back_btn' }}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
Zurück zur Übersicht
|
||||
{t('backToOverview')}
|
||||
</TrackedLink>
|
||||
</div>
|
||||
<div className="max-w-4xl">
|
||||
<Badge variant="accent" className="mb-4 md:mb-6">
|
||||
Projektreferenz
|
||||
{t('projectReference')}
|
||||
</Badge>
|
||||
<Heading level={1} variant="white" className="mb-8 leading-tight">
|
||||
{reference.frontmatter.title}
|
||||
@@ -127,21 +135,21 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<MapPin className="w-8 h-8 text-primary shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Ort</p>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('location')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<Briefcase className="w-8 h-8 text-primary shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Auftraggeber</p>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('client')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.client}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<Calendar className="w-8 h-8 text-primary shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Zeitraum</p>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('period')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +161,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 +174,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>
|
||||
|
||||
@@ -8,7 +8,7 @@ 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. Discover our locations.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben. Entdecken Sie unsere Standorte.'}
|
||||
locations={enrichedLocations}
|
||||
/>
|
||||
|
||||
@@ -104,7 +115,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
<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>
|
||||
@@ -116,19 +127,19 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
</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 pr-16 md:pr-20 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>
|
||||
@@ -147,8 +158,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')}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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">→</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">→</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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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;
|
||||
@@ -32,14 +33,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 || t('description');
|
||||
const ctaLabel = props.ctaLabel || t('ctaLabel');
|
||||
const ctaHref = props.ctaHref || t('ctaHref');
|
||||
|
||||
return (
|
||||
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>
|
||||
|
||||
250
components/blocks/CertificatesBlock.tsx
Normal file
250
components/blocks/CertificatesBlock.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'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',
|
||||
},
|
||||
{
|
||||
title: 'Unbedenklichkeit IHK',
|
||||
description: 'Industrie- und Handelskammer',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Unbedenklichkeit HWK',
|
||||
description: 'Handwerkskammer',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Handelsregisterauszug',
|
||||
description: 'Amtsgericht Cottbus',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Gewerbe-Anmeldung',
|
||||
description: 'Stadt Guben',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Gewerbezentralregister',
|
||||
description: 'Auskunft aus dem Register',
|
||||
type: 'general',
|
||||
},
|
||||
];
|
||||
|
||||
export function CertificatesBlock({ badge, title, description, certificates = defaultCertificates, hideHeader = false }: CertificatesBlockProps) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
52
components/blocks/DataGridPulse.tsx
Normal file
52
components/blocks/DataGridPulse.tsx
Normal 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-32 bg-[#050B14] overflow-hidden flex flex-col items-center border-y border-primary/20 -mx-[50vw] px-[50vw] mb-16">
|
||||
|
||||
{/* Blueprint grid background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(#82ED20 1px, transparent 1px), linear-gradient(90deg, #82ED20 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pulsing horizontal lines (Data Flow) */}
|
||||
<motion.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-50 blur-[2px]"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-white to-transparent opacity-30 blur-[2px]"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center text-center">
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-primary mb-6">
|
||||
{locale === 'en' ? 'Network Expansion in Numbers' : 'Netzausbau in Zahlen'}
|
||||
</h3>
|
||||
|
||||
<div className="bg-black/40 backdrop-blur-md p-12 md:p-16 rounded-3xl border border-white/10 shadow-2xl">
|
||||
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4 leading-none" style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
372.161<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
|
||||
</div>
|
||||
<p className="text-white/70 text-lg md:text-xl font-medium max-w-lg mx-auto">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
64
components/blocks/DeepDrillAnimation.tsx
Normal file
64
components/blocks/DeepDrillAnimation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
80
components/blocks/GrowthChart.tsx
Normal file
80
components/blocks/GrowthChart.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export function GrowthChart() {
|
||||
const data = [
|
||||
{ year: '2023', hdd: 29436, kabel: 76727 },
|
||||
{ year: '2024', hdd: 28680, kabel: 131964 },
|
||||
{ year: '2025', 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">Bewiesene Leistungsfähigkeit</h3>
|
||||
<p className="text-neutral-500 font-medium">Unsere verlässliche Baukapazität der letzten drei Jahre (in Metern)</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{data.map((item, index) => {
|
||||
const hddWidth = (item.hdd / maxVal) * 100;
|
||||
const kabelWidth = (item.kabel / 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">
|
||||
Kabel: {new Intl.NumberFormat('de-DE').format(item.kabel)}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">
|
||||
HDD: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex gap-6 border-t border-neutral-100 pt-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-primary" />
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">Kabelverlegung</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-neutral-dark" />
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">HDD Spülbohrung</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-32 pb-20 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"
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
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 { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
import { Location, defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
const allLocations = [...defaultLocations, ...minorLocations];
|
||||
|
||||
interface Stat {
|
||||
value: string;
|
||||
@@ -23,26 +27,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 = allLocations,
|
||||
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: '2', suffix: '+', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte' },
|
||||
];
|
||||
|
||||
const finalBadge = badge || tStandard('badge');
|
||||
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
|
||||
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||
|
||||
const 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,26 +71,26 @@ 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) => (
|
||||
{finalStats.map((stat, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10">
|
||||
@@ -87,85 +105,57 @@ export function InteractiveGermanyMap({
|
||||
</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 +163,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">→</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
96
components/blocks/ReferencesSlider.test.tsx
Normal file
96
components/blocks/ReferencesSlider.test.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ReferencesSlider, Reference } from './ReferencesSlider';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
|
||||
// Mock next/link to render standard <a> tag
|
||||
vi.mock('next/link', () => ({
|
||||
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('wraps each tile in a link to /referenzen/[slug]', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const links = screen.getAllByTestId('reference-tile-link');
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-eins');
|
||||
expect(links[1].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-zwei');
|
||||
});
|
||||
|
||||
it('allows navigation on simple click (no drag)', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
|
||||
|
||||
// Simulate simple click sequence
|
||||
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
|
||||
fireEvent.mouseUp(firstLink);
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||
|
||||
firstLink.dispatchEvent(clickEvent);
|
||||
|
||||
expect(clickEvent.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('prevents navigation if mouse was dragged', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
|
||||
|
||||
// Simulate drag sequence
|
||||
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
|
||||
// Simulate mouse move of 20px
|
||||
fireEvent.mouseMove(firstLink, { pageX: 120, clientX: 120 });
|
||||
fireEvent.mouseUp(firstLink);
|
||||
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||
firstLink.dispatchEvent(clickEvent);
|
||||
|
||||
expect(clickEvent.defaultPrevented).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,40 @@ 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);
|
||||
};
|
||||
|
||||
const handleTileClick = (e: React.MouseEvent) => {
|
||||
if (dragDistanceRef.current > 10) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
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,7 +135,12 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
|
||||
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
|
||||
>
|
||||
<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}`}
|
||||
onClick={handleTileClick}
|
||||
data-testid="reference-tile-link"
|
||||
className="block relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
|
||||
>
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt={ref.title}
|
||||
@@ -129,13 +152,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>
|
||||
);
|
||||
})}
|
||||
|
||||
102
components/blocks/ScaleOfImpact.tsx
Normal file
102
components/blocks/ScaleOfImpact.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'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">474.932</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="#82ED20" // primary color
|
||||
strokeWidth="4"
|
||||
style={{ pathLength }}
|
||||
className="drop-shadow-[0_0_15px_rgba(130,237,32,0.8)]"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Static Data Grid (No jumping text) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-16 w-full max-w-4xl mt-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">100<span className="text-primary">+</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Major Projects' : 'Großprojekte'}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">372<span className="text-primary text-3xl">km</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Cable Laying' : 'Kabelverlegung'}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">102<span className="text-primary text-3xl">km</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-2xl md:text-3xl font-extrabold text-neutral-dark uppercase tracking-wider break-words hyphens-auto" 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>
|
||||
|
||||
|
||||
@@ -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,74 +23,152 @@ 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">
|
||||
{members.map((member, i) => (
|
||||
{members.length > 0 && (
|
||||
<div className="mb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative rounded-[2.5rem] bg-neutral-dark overflow-hidden shadow-2xl border border-white/10"
|
||||
>
|
||||
{/* Premium Background Effects */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(238,114,3,0.15),transparent_50%)] pointer-events-none" />
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/20 blur-[120px] mix-blend-screen rounded-full pointer-events-none" />
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'40\\' height=\\'40\\' viewBox=\\'0 0 40 40\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Cpath d=\\'M0 0h40v40H0V0zm20 20h20v20H20V20zM0 20h20v20H0V20z\\' fill=\\'%23ffffff\\' fill-opacity=\\'0.02\\' fill-rule=\\'evenodd\\'/%3E%3C/svg%3E')" }} />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between p-8 md:p-12 lg:p-20 gap-12 lg:gap-16">
|
||||
{/* Text Content */}
|
||||
<div className="w-full lg:w-[55%] flex flex-col justify-center text-white">
|
||||
<div className="inline-flex items-center space-x-2 px-4 py-1.5 mb-8 rounded-full border border-primary/30 bg-primary/10 text-primary text-xs font-bold uppercase tracking-widest self-start backdrop-blur-md shadow-[0_0_20px_rgba(238,114,3,0.2)]">
|
||||
<span className="relative flex h-2 w-2 mr-1">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
</span>
|
||||
{t('management')}
|
||||
</div>
|
||||
<h3 className="font-heading font-black text-5xl md:text-6xl lg:text-7xl mb-4 leading-[1.1] tracking-tight">
|
||||
{members[0].name}
|
||||
</h3>
|
||||
<p className="text-xl md:text-2xl text-white/70 font-medium mb-10 leading-relaxed max-w-xl">
|
||||
{members[0].position}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{members[0].email && (
|
||||
<a href={`mailto:${members[0].email}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-primary border border-white/10 hover:border-primary px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].email}</span>
|
||||
</a>
|
||||
)}
|
||||
{members[0].phone && (
|
||||
<a href={`tel:${members[0].phone}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-white/10 border border-white/10 px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
|
||||
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].phone}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Premium Image Block */}
|
||||
<div className="w-full lg:w-[45%] flex justify-center lg:justify-end perspective-1000">
|
||||
<div className="relative w-64 h-64 md:w-80 md:h-80 lg:w-[400px] lg:h-[400px] rounded-[3rem] overflow-hidden border-4 border-white/10 shadow-[0_30px_60px_rgba(0,0,0,0.6)] transform md:rotate-3 hover:rotate-0 transition-all duration-700 bg-neutral-800 group">
|
||||
{members[0].image && (typeof members[0].image === 'string' ? members[0].image : members[0].image.url) ? (
|
||||
<Image
|
||||
src={typeof members[0].image === 'string' ? members[0].image : members[0].image.url}
|
||||
alt={members[0].name}
|
||||
fill
|
||||
sizes="400px"
|
||||
className="object-cover scale-110 group-hover:scale-100 transition-transform duration-1000 ease-out"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white/20">
|
||||
<svg className="w-20 h-20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 xl:gap-10">
|
||||
{members.slice(1).map((member, i) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -21,7 +21,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<footer className="bg-[#050B14] text-neutral-light py-10 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" />
|
||||
@@ -30,7 +30,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
{/* 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="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,7 +94,7 @@ 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="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" />
|
||||
@@ -84,14 +104,16 @@ 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' ? '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>
|
||||
@@ -109,13 +131,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 +145,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</ul>
|
||||
|
||||
{/* Built Info */}
|
||||
|
||||
|
||||
{/* Legal Info removed from here */}
|
||||
</div>
|
||||
|
||||
@@ -133,41 +155,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">© {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">© {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>
|
||||
|
||||
86
components/layout/Header.test.tsx
Normal file
86
components/layout/Header.test.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
|
||||
import { Header, NavLink } from './Header';
|
||||
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 next/image
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, ...props }: any) => <img src={src} alt={alt} {...props} />,
|
||||
}));
|
||||
|
||||
// Mock LanguageSwitcher
|
||||
vi.mock('./LanguageSwitcher', () => ({
|
||||
LanguageSwitcher: () => <div data-testid="language-switcher" />,
|
||||
}));
|
||||
|
||||
describe('Header Dropdown Navigation TDD', () => {
|
||||
const mockNavLinks: NavLink[] = [
|
||||
{
|
||||
label: 'Home',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
label: 'Leistungen',
|
||||
url: '/leistungen',
|
||||
children: [
|
||||
{ label: 'Kabelbau', url: '/leistungen/kabelbau' },
|
||||
{ label: 'Bohrtechnik', url: '/leistungen/bohrtechnik' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('closes the dropdown menu immediately after a page navigation occurs', async () => {
|
||||
// 1. Set initial pathname
|
||||
let currentPath = '/de';
|
||||
vi.mocked(usePathname).mockImplementation(() => currentPath);
|
||||
|
||||
// 2. Render Header
|
||||
const { rerender } = render(<Header navLinks={mockNavLinks} />);
|
||||
|
||||
// 3. Hover over "Leistungen" menu item to open dropdown
|
||||
const leistungenNavItem = screen.getByText('Leistungen');
|
||||
const navItemContainer = leistungenNavItem.closest('.group');
|
||||
expect(navItemContainer).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(navItemContainer!);
|
||||
});
|
||||
|
||||
// 4. Assert dropdown is open and children are rendered
|
||||
expect(screen.getByText('Kabelbau')).toBeTruthy();
|
||||
expect(screen.getByText('Bohrtechnik')).toBeTruthy();
|
||||
|
||||
// 5. Simulate page navigation by changing the path
|
||||
currentPath = '/de/leistungen/kabelbau';
|
||||
|
||||
act(() => {
|
||||
// Trigger pathname hook change and force rerender
|
||||
rerender(<Header navLinks={mockNavLinks} />);
|
||||
});
|
||||
|
||||
// 6. Assert dropdown is closed (children are not in the document)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Kabelbau')).toBeNull();
|
||||
expect(screen.queryByText('Bohrtechnik')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,12 +4,13 @@ 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[];
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -19,6 +20,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 +42,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 +61,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' : '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-6 md:px-8'
|
||||
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
{/* Scroll bound shine effect */}
|
||||
@@ -114,40 +120,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 +161,99 @@ 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);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsHovered(false);
|
||||
}, [pathname]);
|
||||
|
||||
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) => {
|
||||
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${child.url}`
|
||||
: child.url;
|
||||
const isChildActive = pathname === childUrl;
|
||||
return (
|
||||
<TransitionLink
|
||||
key={child.url}
|
||||
href={childUrl}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
70
components/layout/LanguageSwitcher.test.tsx
Normal file
70
components/layout/LanguageSwitcher.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
61
components/search/AISearchResults.test.tsx
Normal file
61
components/search/AISearchResults.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
48
components/ui/AnimatedCounter.tsx
Normal file
48
components/ui/AnimatedCounter.tsx
Normal 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
19
components/ui/Tooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: "Moderne Verfahren im Kabeltiefbau"
|
||||
date: "2024-03-15T10:00:00.000Z"
|
||||
excerpt: "Erfahren Sie mehr über die neuesten Technologien und Verfahren im modernen Kabelleitungstiefbau, von Horizontalspülbohrungen bis hin zu grabenlosen Verlegetechniken."
|
||||
featuredImage: "/assets/images/blog/tiefbau.jpg"
|
||||
category: "Technik"
|
||||
public: true
|
||||
---
|
||||
|
||||
# Moderne Verfahren im Kabeltiefbau
|
||||
|
||||
Der Kabelleitungstiefbau hat sich in den letzten Jahren rasant entwickelt. Neue Technologien ermöglichen eine effizientere und umweltschonendere Verlegung von Versorgungsleitungen.
|
||||
|
||||
## Grabenlose Verlegetechniken
|
||||
|
||||
Eines der wichtigsten Verfahren ist die **Horizontalspülbohrung (HDD)**. Dieses Verfahren ermöglicht es, Leitungen unter Hindernissen wie Straßen, Flüssen oder Gebäuden zu verlegen, ohne die Oberfläche aufbrechen zu müssen.
|
||||
|
||||
### Vorteile der HDD-Bohrung:
|
||||
- Minimale Beeinträchtigung des Verkehrs
|
||||
- Schutz der Umwelt und vorhandener Vegetation
|
||||
- Kürzere Bauzeiten
|
||||
- Geringere Kosten bei komplexen Unterquerungen
|
||||
|
||||
## Klassischer Tiefbau in neuer Qualität
|
||||
|
||||
Auch der klassische Grabenbau profitiert von modernen Maschinen und präziser Planung. Durch den Einsatz von Saugbaggern und GPS-gestützter Vermessung können wir Schäden an Bestandsleitungen nahezu ausschließen.
|
||||
|
||||
## Fazit
|
||||
|
||||
Die Kombination aus bewährter Handwerkskunst und modernster Technik ist der Schlüssel zu erfolgreichen Infrastrukturprojekten. Wir bei E-TIB setzen konsequent auf Innovation, um unseren Kunden die bestmöglichen Ergebnisse zu liefern.
|
||||
81
content/de/bohrtechnik.mdx
Normal file
81
content/de/bohrtechnik.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Grabenlose Leitungsverlegung"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Präzise Horizontalspülbohrungen und Erdraketen in allen Bodenklassen für eine absolut oberflächenschonende und effiziente Leitungsverlegung."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Grabenlose Verlegung"
|
||||
subtitle="Oberflächenschonende Horizontalspülbohrungen und Erdraketen"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Wenn klassischer Grabenbau aus Platz-, Umwelt- oder Kostengründen nicht in Frage kommt, setzen wir auf modernste grabenlose Techniken. Wir schonen die Natur und minimieren Wiederherstellungskosten.
|
||||
</blockquote>
|
||||
|
||||
## Maximale Effizienz, Minimaler Eingriff
|
||||
|
||||
Die grabenlose Leitungsverlegung ist die intelligenteste Lösung für komplexe Bauvorhaben. Insbesondere bei der Querung von hochfrequentierten Straßen, Bahntrassen, Autobahnen oder sensiblen Gewässern spielt dieses Verfahren seine absoluten Stärken aus. Der Verkehr fließt weiter, die Natur bleibt unberührt und aufwendige Oberflächen wie teures Pflaster oder Asphalt müssen nicht zerstört werden.
|
||||
|
||||
## HDD-Spülbohrungen und Erdraketen
|
||||
|
||||
Mit unserem spezialisierten Gerätepark beherrschen wir die unterschiedlichsten Verfahren. Beim **Horizontalspülbohrverfahren (HDD)** steuern wir den Bohrkopf punktgenau unter der Erde hindurch. Wir realisieren dabei Bohrlängen von bis zu 250 Metern am Stück und können Schutzrohre mit einem Durchmesser von bis zu 400 mm einziehen.
|
||||
|
||||
Für kürzere Distanzen, wie beispielsweise schnelle und schonende Hausanschlüsse, setzen wir unsere **Erdraketen** (Bodenverdrängungsverfahren) ein. Hierbei erreichen wir Durchörterungslängen von bis zu 15 Metern für Rohre bis 160 mm Durchmesser – ideal um Vorgärten und Einfahrten komplett unversehrt zu lassen.
|
||||
|
||||
<DeepDrillAnimation />
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Verfahren"
|
||||
title="Unsere Bohrtechnik im Detail"
|
||||
descriptionParagraphs={[
|
||||
"Wir wählen für jede Bodenklasse und örtliche Gegebenheit das exakt passende Bohrverfahren, um maximale Wirtschaftlichkeit zu garantieren."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "hdd",
|
||||
title: "Horizontalspülbohrung (HDD)",
|
||||
icon: "hdd",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Max. Bohrlänge: 250 m",
|
||||
"Max. Durchmesser: 400er Rohr",
|
||||
"Zentimetergenaue Steuerbarkeit",
|
||||
"Querung von Bahn, Autobahn & Gewässern"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "erdrakete",
|
||||
title: "Bodenverdrängung (Erdrakete)",
|
||||
icon: "hdd",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Max. Durchörterungslänge: 15 m",
|
||||
"Max. Durchmesser: 160er Rohr",
|
||||
"Schnelle Hausanschlüsse ohne Graben",
|
||||
"Kein Aufbruch der Oberfläche nötig"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Bohr-Projekte" description="Erfolgreiche HDD-Bohrungen und grabenlose Leitungsverlegungen unter komplexesten Bedingungen." />
|
||||
|
||||
<CallToAction
|
||||
title="Suchen Sie nach einer grabenlosen Lösung?"
|
||||
text="Wir beraten Sie gerne zu den Möglichkeiten der Horizontalspülbohrung für Ihr Vorhaben."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
79
content/de/glasfaser.mdx
Normal file
79
content/de/glasfaser.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Kabelleitungstiefbau im Bereich Telekommunikation"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungstiefbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Glasfaser & Telekommunikation"
|
||||
subtitle="Komplette Infrastruktur-Lösungen für zukunftssichere Breitbandnetze (FTTX)"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01129.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Ein Leben ohne Strom oder High-Speed Internet ist heute unvorstellbar – dafür verlegen wir Kabel. Wir bauen das Rückgrat der digitalen Gesellschaft.
|
||||
</blockquote>
|
||||
|
||||
## Das Rückgrat der Digitalisierung
|
||||
|
||||
Schnelles Internet und modernste Telekommunikationsnetze erfordern einen absolut reibungslosen und hochprofessionellen Netzausbau. E-TIB sorgt mit hochspezialisierten Montageteams und eigenem, modernen Maschinenpark für höchste Ausführungsqualität im bundesweiten Glasfaserausbau.
|
||||
|
||||
## FTTX-Ausbau aus einer Hand
|
||||
|
||||
Als leistungsfähiger Partner für Telekommunikationsanbieter decken wir jeden Schritt der physischen Netzerrichtung ab. Wir übernehmen die Herstellung kompletter Leerrohrtrassen sowie von Kabel- und Rohrgräben. Darüber hinaus positionieren und errichten wir Multifunktionsgehäuse an strategischen Netzknotenpunkten.
|
||||
|
||||
Unser Service endet nicht beim reinen Tiefbau: Wir kümmern uns um das fachgerechte Einziehen und Einblasen von hochsensiblen LWL- und TK-Kabeln über weite Distanzen und führen alle erforderlichen Kabelmontagen durch. So garantieren wir, dass die digitale Infrastruktur sicher und performant in Betrieb gehen kann.
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Leistungsspektrum"
|
||||
title="Kompletter Glasfaserausbau"
|
||||
descriptionParagraphs={[
|
||||
"Von der Leerrohrtrasse bis zur fertigen LWL-Montage – wir liefern schlüsselfertige Telekommunikationsinfrastruktur auf höchstem Niveau."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "tiefbau",
|
||||
title: "Tiefbau & Trassen",
|
||||
icon: "fiber",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Herstellung kompletter Leerrohrtrassen",
|
||||
"Stellen von Multifunktionsgehäusen (MFG)",
|
||||
"Netzknoten-Anbindung"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "montage",
|
||||
title: "Kabelzug & Montage",
|
||||
icon: "fiber",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Einziehen von TK-/LWL-Kabeln",
|
||||
"Pneumatisches Einblasen von Glasfaserkabeln",
|
||||
"Kabelmontagen TK/LWL",
|
||||
"Spleiß- und Messarbeiten"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Breitband-Projekte" description="Wir haben bereits hunderte Kilometer FTTX-Netze erfolgreich realisiert. Entdecken Sie unsere Referenzen." />
|
||||
|
||||
<CallToAction
|
||||
title="Sie planen einen FTTX-Ausbau?"
|
||||
text="Wir beraten Sie gerne vollumfänglich rund um den Bereich FTTX und den Glasfaserausbau."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
@@ -6,7 +6,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
<HomeHero
|
||||
|
||||
title={"Die Experten für\nKabeltiefbau"}
|
||||
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabelbau und der Bohrtechnik."
|
||||
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabeltiefbau und der Bohrtechnik."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="Unsere Leistungen"
|
||||
linkHref="/de/kompetenzen"
|
||||
@@ -110,17 +110,19 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
ctaHref="/de/kompetenzen"
|
||||
items={[
|
||||
{
|
||||
title: "Kabelbau",
|
||||
title: "Kabeltiefbau",
|
||||
description: "Verlegung von Mittel- und Niederspannungskabeln",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelbau" }
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabeltiefbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
description: "Horizontalspülbohrverfahren für grabenlose Verlegung",
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
href: "/de/bohrtechnik",
|
||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Bohrtechnik" }
|
||||
},
|
||||
{
|
||||
@@ -128,54 +130,20 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
description: "Trassenplanung und Genehmigungsverfahren",
|
||||
tag: "Präzision",
|
||||
size: "small",
|
||||
href: "/de/planung",
|
||||
image: { url: "/assets/photos/DJI_0048.JPG", alt: "Planung" }
|
||||
},
|
||||
{
|
||||
title: "Vermessung",
|
||||
description: "Digitale Bestandsdokumentation",
|
||||
tag: "Digital",
|
||||
size: "accent"
|
||||
size: "accent",
|
||||
href: "/de/vermessung"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider
|
||||
references={[
|
||||
{
|
||||
id: "1",
|
||||
title: "Breitbandausbau FTTC, Planungsregion Spreewald",
|
||||
slug: "breitbandausbau-fttc-spreewald",
|
||||
category: "Telekommunikation",
|
||||
image: "/assets/photos/DSC01123.JPG"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Breitbandausbau FTTH, 29699 Bomlitz / 29690 Schwarmstedt",
|
||||
slug: "breitbandausbau-ftth-bomlitz",
|
||||
category: "Telekommunikation",
|
||||
image: "/assets/photos/DSC00850.JPG"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Neubau Kabeltrasse, 03149 Forst",
|
||||
slug: "neubau-kabeltrasse-forst",
|
||||
category: "Kabelbau",
|
||||
image: "/assets/photos/DSC01129.JPG"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Kabelverbindung U30 - T10, 15890 Eisenhüttenstadt",
|
||||
slug: "kabelverbindung-eisenhuettenstadt",
|
||||
category: "Kabelbau",
|
||||
image: "/assets/photos/DSC00010.JPG"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Einspeisung PV-Anlage 14728 Görne",
|
||||
slug: "einspeisung-pv-goerne",
|
||||
category: "Erneuerbare Energien",
|
||||
image: "/assets/photos/DSC01123.JPG"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ScaleOfImpact />
|
||||
|
||||
<HomeReferencesSlider />
|
||||
|
||||
|
||||
@@ -46,3 +46,17 @@ Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||
## Verbraucherstreitbeilegung/Universalschlichtungsstelle
|
||||
|
||||
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
|
||||
|
||||
## Haftungsausschluss (Disclaimer)
|
||||
|
||||
### Haftung für Inhalte
|
||||
|
||||
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
|
||||
|
||||
### Haftung für Links
|
||||
|
||||
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
|
||||
|
||||
### Urheberrecht
|
||||
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
|
||||
|
||||
83
content/de/kabeltiefbau.mdx
Normal file
83
content/de/kabeltiefbau.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Kabelleitungstiefbau"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Professioneller Kabeltiefbau für Mittel- und Niederspannungsnetze. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Kabelleitungstiefbau"
|
||||
subtitle="Komplette Infrastruktur-Lösungen für Mittel- und Niederspannungsnetze"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Überall wird gebuddelt und gebohrt, aber leider geht auch vieles schief. Wir setzen auf Qualität, höchste Sicherheitsstandards und rechtssichere Dokumentation – damit Ihr Bauprojekt nicht zur Kostenfalle wird.
|
||||
</blockquote>
|
||||
|
||||
## Qualität statt Kostenfalle
|
||||
|
||||
Auf vielen Baustellen werden zunehmend Billiganbieter aus dem Ausland eingesetzt, was häufig dazu führt, dass **Sicherheitsstandards und Dokumentationsanforderungen missachtet** werden. Die Folgen sind gravierend: Unfälle, defekte Fremdleitungen (wie Strom, Wasser und Gas) sowie massive Bauverzögerungen und teure Nachträge. Ein vermeintliches Schnäppchen entpuppt sich so schnell als finanzielle Belastung.
|
||||
|
||||
Besonders bei geförderten Infrastruktur-Projekten gestaltet sich der Abruf der Fördergelder ohne eine absolut lückenlose, normgerechte Dokumentation äußerst problematisch.
|
||||
|
||||
## Alles aus einer Hand
|
||||
|
||||
Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten für unsere Auftraggeber im Vorfeld sichere und zufriedenstellende Lösungen. Die E-TIB GmbH hat sich voll und ganz auf den Kabelleitungstiefbau spezialisiert.
|
||||
|
||||
Wir bieten Ihnen das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand – mit eigenem, hochmodernem Maschinenpark und festangestelltem Fachpersonal aus der Region. In direkter Zusammenarbeit mit unserem Partnerbüro, der E-TIB Ingenieurgesellschaft mbH, können wir zudem frühzeitig Planungsfehler ausschließen und selbst die komplexesten Dokumentationsanforderungen souverän erfüllen.
|
||||
|
||||
<DataGridPulse />
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Leistungsspektrum"
|
||||
title="Alles für Ihr Netz"
|
||||
descriptionParagraphs={[
|
||||
"Wir bieten höchste Präzision, modernste Maschinen und zertifizierte Experten für jedes Tiefbau-Projekt – von der Kabelverlegung bis zur Transformatorstation."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "grabenbau",
|
||||
title: "Grabenbau & Verlegung",
|
||||
icon: "energy",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Kabelverlegung von 0,4 kV bis 110 kV",
|
||||
"Kabelmontagen von 0,4 kV bis 30 kV",
|
||||
"Kabelpflugarbeiten für schnelle Verlegung"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "anlagen",
|
||||
title: "Anlagen & Infrastruktur",
|
||||
icon: "energy",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Errichtung von Transformatorstationen",
|
||||
"Erstellung von komplexen Erdungsanlagen",
|
||||
"Errichtung von Straßenbeleuchtungen",
|
||||
"Zertifizierte Schalthandlungen"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Tiefbau-Projekte" description="Wir haben über hunderte Kilometer Kabel verlegt. Sehen Sie sich unsere Referenz-Projekte an." />
|
||||
|
||||
<CallToAction
|
||||
title="Sie haben ein konkretes Projekt?"
|
||||
text="Wir beraten Sie gerne zu allen Fragen rund um Kabeltiefbau und Leitungsverlegung."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
@@ -24,73 +24,73 @@ layout: "fullBleed"
|
||||
{
|
||||
id: "region",
|
||||
title: "Spannende Bauvorhaben",
|
||||
description: "Einsatz bei interessanten regionalen und überregionalen Projekten.",
|
||||
description: "Einsatz bei regionalen und überregionalen Bauvorhaben.",
|
||||
icon: "region"
|
||||
},
|
||||
{
|
||||
id: "contract",
|
||||
title: "Unbefristeter Vertrag",
|
||||
description: "Sicherer Arbeitsplatz durch einen unbefristeten Arbeitsvertrag in Vollzeit.",
|
||||
description: "Sicherer, unbefristeter Arbeitsvertrag in Vollzeit.",
|
||||
icon: "contract"
|
||||
},
|
||||
{
|
||||
id: "money",
|
||||
title: "Faire Vergütung",
|
||||
description: "Leistungsbezogene, überdurchschnittliche und faire Bezahlung.",
|
||||
description: "Leistungsbezogene und faire Vergütung für Ihre Arbeit.",
|
||||
icon: "money"
|
||||
},
|
||||
{
|
||||
id: "allowance",
|
||||
title: "Tägliche Auslöse",
|
||||
description: "Wir zahlen eine steuerfreie Auslöse von bis zu 30,00 EUR pro Arbeitstag.",
|
||||
description: "Auslöse von bis zu 30,00 EUR pro Arbeitstag.",
|
||||
icon: "allowance"
|
||||
},
|
||||
{
|
||||
id: "vacation",
|
||||
title: "31 Tage Urlaub",
|
||||
description: "31 Tage Jahresurlaub sowie zusätzliches Urlaubsgeld zur Erholung.",
|
||||
description: "31 Tage Jahresurlaub sowie zusätzliches Urlaubsgeld.",
|
||||
icon: "vacation"
|
||||
},
|
||||
{
|
||||
id: "clothes",
|
||||
title: "Premium Arbeitskleidung",
|
||||
description: "Hochwertige Engelbert-Strauss Arbeitskleidung wird komplett gestellt.",
|
||||
title: "Marken-Arbeitskleidung",
|
||||
description: "Bereitstellung von hochwertiger Engelbert-Strauss Arbeitskleidung.",
|
||||
icon: "clothes"
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
title: "Zusatzzahlungen",
|
||||
description: "Monatliche Boni über eine persönliche MasterCard.",
|
||||
description: "Monatliche Zusatzzahlungen über eine persönliche MasterCard.",
|
||||
icon: "card"
|
||||
},
|
||||
{
|
||||
id: "retirement",
|
||||
title: "Altersvorsorge",
|
||||
description: "Attraktive Zuzahlung im Rahmen der betrieblichen Altersvorsorge.",
|
||||
description: "Zuzahlung im Rahmen der betrieblichen Altersvorsorge.",
|
||||
icon: "retirement"
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
title: "Weiterbildung",
|
||||
description: "Individuelle Entwicklungs- und Aufstiegsmöglichkeiten im Unternehmen.",
|
||||
description: "Individuelle Weiterbildungs- und Entwicklungsmöglichkeiten.",
|
||||
icon: "education"
|
||||
},
|
||||
{
|
||||
id: "car",
|
||||
title: "Firmenfahrzeug",
|
||||
description: "Stellung eines Fahrzeugs zur betrieblichen Nutzung und für Baustellen.",
|
||||
description: "Stellung eines Fahrzeugs zur betrieblichen Nutzung und Fahrten zur Baustelle.",
|
||||
icon: "car"
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
title: "Intensive Einarbeitung",
|
||||
description: "Professionelles Onboarding in einem hilfsbereiten und engagierten Team.",
|
||||
description: "Entsprechende Einarbeitung in einem engagierten Team.",
|
||||
icon: "team"
|
||||
},
|
||||
{
|
||||
id: "family",
|
||||
title: "Familiäres Klima",
|
||||
description: "Starker Zusammenhalt durch regelmäßige betrieblich organisierte Events.",
|
||||
description: "Familiäres Betriebsklima mit betrieblich organisierten Veranstaltungen.",
|
||||
icon: "family"
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Kompetenzen - Kabeltiefbau & Spülbohrtechnik"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Von Kabelbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
|
||||
excerpt: "Von Kabeltiefbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
@@ -13,7 +13,7 @@ layout: "fullBleed"
|
||||
linkText="Kontakt aufnehmen"
|
||||
linkHref="/de/kontakt"
|
||||
secondaryCtaLabel="Referenzen ansehen"
|
||||
secondaryCtaHref="/de/kompetenzen#referenzen"
|
||||
secondaryCtaHref="/de/#referenzen"
|
||||
/>
|
||||
|
||||
<HomeCompetenceBentoGrid
|
||||
@@ -23,17 +23,19 @@ layout: "fullBleed"
|
||||
ctaHref="/de/kontakt"
|
||||
items={[
|
||||
{
|
||||
title: "Kabelbau",
|
||||
title: "Kabeltiefbau",
|
||||
description: "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelbau" }
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabeltiefbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
description: "Horizontalspülbohrverfahren (HDD) für grabenlose, oberflächenschonende Verlegung.",
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
href: "/de/bohrtechnik",
|
||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Bohrtechnik" }
|
||||
},
|
||||
{
|
||||
@@ -41,13 +43,15 @@ layout: "fullBleed"
|
||||
description: "Ausbau von zukunftssicheren Breitbandnetzen (FTTX).",
|
||||
tag: "Kommunikation",
|
||||
size: "small",
|
||||
href: "/de/glasfaser",
|
||||
image: { url: "/assets/photos/DSC01129.JPG", alt: "Glasfaser" }
|
||||
},
|
||||
{
|
||||
title: "Planung & Doku",
|
||||
description: "Trassenplanung, Genehmigungsverfahren und präzise GIS-Dokumentation.",
|
||||
tag: "Digital",
|
||||
size: "accent"
|
||||
size: "accent",
|
||||
href: "/de/planung"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -57,50 +61,53 @@ layout: "fullBleed"
|
||||
title="Keine Kompromisse am Bau"
|
||||
problemStatement={{
|
||||
title: "Die Realität auf vielen Baustellen",
|
||||
text: "Auf vielen Baustellen werden zunehmend Billiganbieter eingesetzt. Sicherheitsstandards und Dokumentationsanforderungen werden oft missachtet. Daraus resultieren Unfälle, defekte Fremdleitungen und teure Bauverzögerungen."
|
||||
text: "Auf vielen Baustellen werden zunehmend Billiganbieter eingesetzt. Sicherheitsstandards und Dokumentationsanforderungen werden oft missachtet. Daraus resultieren Unfälle, defekte Fremdleitungen (Strom, Wasser, Gas etc.) und teure Bauverzögerungen, sodass sich ein vermeintliches Schnäppchen als Kostenfalle entpuppt."
|
||||
}}
|
||||
solutionStatement={{
|
||||
title: "Der E-TIB Standard",
|
||||
text: "Wir arbeiten anders. Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten sichere Lösungen. Unterstützt durch unser Partner-Ingenieurbüro bieten wir Planung, Ausführung und Dokumentation auf höchstem Niveau."
|
||||
text: "Durch unsere langjährige Erfahrung werden wir frühzeitig auf Problemstellungen aufmerksam und erarbeiten sichere Lösungen. Unterstützt durch unser Partner-Ingenieurbüro bieten wir Planung, Ausführung und Dokumentation auf höchstem Niveau, um Planungsfehler zu vermeiden."
|
||||
}}
|
||||
panels={[
|
||||
{
|
||||
id: "energy",
|
||||
title: "Energie & Starkstrom",
|
||||
title: "Kabelleitungstiefbau (Mittel-/Niederspannung)",
|
||||
icon: "energy",
|
||||
lists: [
|
||||
[
|
||||
"Kabelbau",
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Kabelverlegung (0,4kV - 110kV)",
|
||||
"Elektromontagen bis 110 kV",
|
||||
"Kabelmontagen 0,4 kV - 30 kV",
|
||||
"Errichtung von Transformatorstationen",
|
||||
"Wartung & Störungsdienst"
|
||||
"Erstellung von Erdungsanlagen",
|
||||
"Errichtung von Straßenbeleuchtung",
|
||||
"Schalthandlungen"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "fiber",
|
||||
title: "Glasfaser & TK",
|
||||
title: "Kabelleitungstiefbau (Telekommunikation)",
|
||||
icon: "fiber",
|
||||
lists: [
|
||||
[
|
||||
"Glasfaser-Kabelmontagen",
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Herstellung von Leerrohrtrassen",
|
||||
"Einziehen/Einblasen von LWL-Kabeln",
|
||||
"Stellen von Multifunktionsgehäusen"
|
||||
"Einziehen/Einblasen von TK-/LWL-Kabeln",
|
||||
"Stellen von Multifunktionsgehäusen",
|
||||
"Kabelmontagen TK/LWL"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "hdd",
|
||||
title: "Bohrtechnik & Kabelpflug",
|
||||
title: "Grabenlose Leitungsverlegung",
|
||||
icon: "hdd",
|
||||
lists: [
|
||||
[
|
||||
"Horizontalspülbohrungen",
|
||||
"Kabelpflugarbeiten",
|
||||
"Komplexe Querung (Bahn, Autobahn, Gewässer)",
|
||||
"Erdrakete (bis 15m / 160er Rohr)"
|
||||
"Horizontalspülbohrverfahren (bis 250m)",
|
||||
"Horizontalspülbohrverfahren (bis 400er Rohr)",
|
||||
"Erdrakete (bis 15m DÖ-Länge)",
|
||||
"Erdrakete (bis 160er Rohr)"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -110,10 +117,10 @@ layout: "fullBleed"
|
||||
icon: "planning",
|
||||
lists: [
|
||||
[
|
||||
"Genehmigungs- und Ausführungsplanung",
|
||||
"Elektro- und Netzanschlussplanung",
|
||||
"Ausschreibung & Bauüberwachung",
|
||||
"Machbarkeitsstudien & Projektprüfung"
|
||||
"Struktur-/Genehmigungs-/Ausführungsplanung",
|
||||
"Ausschreibung, Vergabe & Bauüberwachung",
|
||||
"Zustimmungsplanung",
|
||||
"Projektprüfung, Studien"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -124,11 +131,8 @@ layout: "fullBleed"
|
||||
fullWidth: true,
|
||||
lists: [
|
||||
[
|
||||
"Vermessung & Dokumentation",
|
||||
"Absteckung und Einmessung von Leitungstrassen"
|
||||
],
|
||||
[
|
||||
"Digitale Bestandsdokumentation (GIS)",
|
||||
"Absteckung und Einmessung von Leitungstrassen",
|
||||
"Erstellung von Datenbanken zur Projektabrechnung",
|
||||
"360° Foto-/Video-Erfassung des Baufeldes"
|
||||
]
|
||||
]
|
||||
@@ -139,7 +143,7 @@ layout: "fullBleed"
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Kabelbau",
|
||||
"name": "Kabeltiefbau",
|
||||
"description": "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
|
||||
@@ -19,80 +19,11 @@ layout: "fullBleed"
|
||||
showFairs={true}
|
||||
showJobs={false}
|
||||
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: '2024 / 2025', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
|
||||
{ name: 'Windenergietage Linstow', date: '2024 / 2025', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
|
||||
{ name: 'Kabel-Workshop Wiesbaden', date: '2024 / 2025', type: 'Fachmesse', location: 'Wiesbaden' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CallToAction />
|
||||
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Intersolar München",
|
||||
"startDate": "2026-06-19",
|
||||
"endDate": "2026-06-21",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "Messe München",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "München",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Windenergietage Linstow",
|
||||
"startDate": "2026-11-04",
|
||||
"endDate": "2026-11-06",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "Van der Valk Resort Linstow",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Linstow",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Kabelwerkstatt Wiesbaden",
|
||||
"startDate": "2026-12-02",
|
||||
"endDate": "2026-12-03",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "RheinMain CongressCenter",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Wiesbaden",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
}
|
||||
]} />
|
||||
|
||||
79
content/de/planung.mdx
Normal file
79
content/de/planung.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Planung und Beratung"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Professionelle Planung und Beratung im Bereich Energie- und Telekommunikationsnetze: Trassenplanung, Genehmigungsverfahren und Projektsteuerung aus einer Hand."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Planung & Beratung"
|
||||
subtitle="Kompetente Begleitung für Energie- und Telekommunikationsnetze"
|
||||
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Wir bieten unseren Kunden alles aus einer Hand – von der Grobplanung Ihrer Projektidee über den Kabelleitungstiefbau bis zum Bau Ihres erneuerbaren Energieprojektes.
|
||||
</blockquote>
|
||||
|
||||
## Fundament für den Projekterfolg
|
||||
|
||||
Eine solide Planung ist das entscheidende Fundament für jedes erfolgreiche Infrastrukturprojekt. Oft scheitern Projekte oder verzögern sich massiv, weil die Komplexität der Tiefbauplanung und Genehmigungsverfahren unterschätzt wurde. Durch unsere gebündelte Kompetenz vermeiden Sie teure Planungsfehler und Reibungsverluste an den Schnittstellen.
|
||||
|
||||
Sowohl für den hochaktuellen Breitbandausbau als auch für den anspruchsvollen Netzanschluss von Solar- oder Windprojekten wird eine absolut präzise und hochwertige Kabeltrassenplanung benötigt. Dazu besichtigen wir die Trasse vor Ort, analysieren frühzeitig alle potenziellen Baukonflikte und stellen sicher, dass alle erforderlichen Anträge für die Verlegung fristgerecht und vollständig bei den Behörden eingereicht werden.
|
||||
|
||||
## Starke Ingenieurskompetenz
|
||||
|
||||
In enger Zusammenarbeit mit unserem spezialisierten Partner-Ingenieurbüro, der **E-TIB Ingenieurgesellschaft mbH**, begleiten wir Sie durch alle Leistungsphasen. Ob Strukturplanung zur Trassenoptimierung, komplexe Genehmigungsplanung oder detaillierte Ausführungsplanung – wir steuern Ihr Projekt von der ersten Machbarkeitsstudie bis zur erfolgreichen Bauüberwachung und Abnahme.
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Ingenieursdienstleistungen"
|
||||
title="Unsere Planungsleistungen"
|
||||
descriptionParagraphs={[
|
||||
"Minimieren Sie Risiken und Schnittstellen. Wir vereinen planerisches Know-how mit praktischer Tiefbau-Expertise."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "planung",
|
||||
title: "Trassen- & Genehmigungsplanung",
|
||||
icon: "planning",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Strukturplanung (Trassenoptimierung)",
|
||||
"Genehmigungsplanung & Behördenmanagement",
|
||||
"Detaillierte Ausführungsplanung",
|
||||
"Zustimmungsplanung"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "beratung",
|
||||
title: "Steuerung & Überwachung",
|
||||
icon: "planning",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Ausschreibung & Vergabe (AVA)",
|
||||
"Fachbauüberwachung",
|
||||
"Projektprüfung & Controlling",
|
||||
"Fundierte Machbarkeitsstudien"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Planung in der Praxis" description="Erfolgreich geplante und begleitete Infrastrukturprojekte unserer Unternehmensgruppe." />
|
||||
|
||||
<CallToAction
|
||||
title="Benötigen Sie Planungssicherheit?"
|
||||
text="Lassen Sie uns über Ihr Vorhaben sprechen. Wir stehen Ihnen mit unserem Ingenieurwissen zur Seite."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Landkreis Börde"
|
||||
client: "EKN Breitband GmbH & Co. KG"
|
||||
date: "2026-01-01"
|
||||
dateString: "seit 05/2019"
|
||||
date: "2019-05-01"
|
||||
dateString: "Seit 05/2019"
|
||||
location: "Landkreis Börde"
|
||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
||||
---
|
||||
@@ -10,3 +10,5 @@ featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessu
|
||||
- 109.000 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
- 63 Stück Aufstellung Netzverteiler
|
||||
- 747 Stück Herstellung Hausanschlüsse
|
||||
- Koordination und Realisierung der gesamten Trassenführung
|
||||
- Dokumentation und Abnahme der Hausanschlüsse
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Planungsregion Bomlitz"
|
||||
client: "Telekom Deutschland GmbH"
|
||||
date: "2018-12-31"
|
||||
dateString: "04/2018 bis 12/2018"
|
||||
location: "Bomlitz"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
||||
title: "Breitbandausbau FTTH, Bomlitz / Schwarmstedt"
|
||||
client: "inexio Informationstechnologie und Telekommunikation GmbH"
|
||||
date: "2018-08-01"
|
||||
dateString: "08/2018 bis 03/2021"
|
||||
location: "29699 Bomlitz / 29690 Schwarmstedt"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-60.jpg"
|
||||
---
|
||||
|
||||
- 88.890 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "Kabelverbindung U30 - T10, 15890 Eisenhüttenstadt"
|
||||
title: "Kabelverbindung U30 - T10, Eisenhüttenstadt"
|
||||
client: "ArcelorMittal Eisenhüttenstadt GmbH"
|
||||
date: "2019-12-31"
|
||||
date: "2019-07-01"
|
||||
dateString: "07/2019 bis 12/2019"
|
||||
location: "Eisenhüttenstadt"
|
||||
location: "15890 Eisenhüttenstadt"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie_Vermessung_(24_von_99)_groß2.jpg"
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "Neubau Kabeltrasse, 03149 Forst"
|
||||
title: "Neubau Kabeltrasse, Forst"
|
||||
client: "Mitteldeutsche Netzgesellschaft Strom mbH"
|
||||
date: "2020-09-30"
|
||||
date: "2020-03-01"
|
||||
dateString: "03/2020 bis 09/2020"
|
||||
location: "Forst"
|
||||
location: "03149 Forst"
|
||||
featuredImage: "/assets/photos/DSC02156.jpg"
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "Einspeisung PV-Anlage 14728 Görne"
|
||||
title: "Einspeisung PV-Anlage, Görne"
|
||||
client: "EVH GmbH"
|
||||
date: "2020-07-31"
|
||||
date: "2020-05-01"
|
||||
dateString: "05/2020 bis 07/2020"
|
||||
location: "Görne"
|
||||
location: "14728 Görne"
|
||||
featuredImage: "/assets/photos/DSC08744.JPG"
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTC, Planungsregion Spreewald"
|
||||
client: "Telekom Deutschland GmbH"
|
||||
date: "2018-01-31"
|
||||
date: "2017-09-01"
|
||||
dateString: "09/2017 bis 01/2018"
|
||||
location: "Spreewald"
|
||||
location: "Planungsregion Spreewald"
|
||||
featuredImage: "/assets/photos/DSC01129.JPG"
|
||||
---
|
||||
|
||||
@@ -12,8 +12,6 @@ featuredImage: "/assets/photos/DSC01129.JPG"
|
||||
- 13.008 m Horizontalspülbohrung D 50
|
||||
- 94.951 m Einblasen von Kabel/Rohren unbelegt
|
||||
- 42 Stück Stellen von Multifunktionsgehäusen
|
||||
- Aufbau von Energieanschlusssäulen
|
||||
- Aufbau elektrischer Anlagen 230V
|
||||
- Koordination mit Energieversorgungsunternehmen
|
||||
- Inbetriebnahme der elektrischen Anlagen
|
||||
- Aufbau von Energieanschlusssäulen und elektrischen Anlagen 230V
|
||||
- Koordination mit Energieversorgungsunternehmen und Inbetriebnahme der elektrischen Anlagen
|
||||
- Oberflächen- und Tiefbauarbeiten für Multifunktionsgehäuse
|
||||
|
||||
90
content/de/team.mdx
Normal file
90
content/de/team.mdx
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Unser Team"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Lernen Sie die Experten der E-TIB Gruppe kennen."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Unternehmen"
|
||||
title="Unser Team"
|
||||
subtitle="Die Experten hinter der E-TIB Gruppe"
|
||||
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
|
||||
alignment="center"
|
||||
/>
|
||||
|
||||
<TeamGrid
|
||||
members={[
|
||||
{
|
||||
id: "danny-joseph",
|
||||
name: "Danny Joseph",
|
||||
position: "Geschäftsführung",
|
||||
email: "d.joseph@e-tib.com",
|
||||
phone: "+49 1520 7230518",
|
||||
image: "/assets/photos/team/danny.jpg"
|
||||
},
|
||||
{
|
||||
id: "dirk-schmidt",
|
||||
name: "Dirk Schmidt",
|
||||
position: "Projektleitung",
|
||||
email: "d.schmidt@e-tib.com",
|
||||
phone: "+49 151 65069040",
|
||||
image: "/assets/photos/team/dirk.jpg"
|
||||
},
|
||||
{
|
||||
id: "oliver-fiedler",
|
||||
name: "Oliver Fiedler",
|
||||
position: "Bauleitung",
|
||||
email: "o.fiedler@e-tib.com",
|
||||
phone: "+49 151 52264087",
|
||||
image: "/assets/photos/team/oliver.jpg"
|
||||
},
|
||||
{
|
||||
id: "martin-polan",
|
||||
name: "Martin Polan",
|
||||
position: "Polier",
|
||||
email: "m.polan@e-tib.com",
|
||||
phone: "+49 171 3868167",
|
||||
image: "/assets/photos/team/martin.jpg"
|
||||
},
|
||||
{
|
||||
id: "kerstin-joseph",
|
||||
name: "Kerstin Joseph",
|
||||
position: "Sekretariat / Buchhaltung",
|
||||
email: "k.joseph@e-tib.com",
|
||||
phone: "+49 3561 6851692",
|
||||
image: "/assets/photos/team/kerstin.jpg"
|
||||
},
|
||||
{
|
||||
id: "kathrin-lemanski",
|
||||
name: "Kathrin Lemanski",
|
||||
position: "Sachbearbeitung",
|
||||
email: "k.lemanski@e-tib.com",
|
||||
phone: "+49 3561 6857733",
|
||||
image: "/assets/photos/team/kathrin.jpg"
|
||||
},
|
||||
{
|
||||
id: "sven-kirbs",
|
||||
name: "Sven Kirbs",
|
||||
position: "Vermessung / Dokumentation",
|
||||
email: "s.kirbs@e-tib.com",
|
||||
phone: "+49 151 29603652",
|
||||
image: "/assets/photos/team/sven.jpg"
|
||||
},
|
||||
{
|
||||
id: "maik-weichert",
|
||||
name: "Maik Weichert",
|
||||
position: "Fuhrpark / Werkstatt / Lager",
|
||||
email: "m.weichert@e-tib.com",
|
||||
phone: "+49 171 9282872",
|
||||
image: "/assets/photos/team/maik.jpg"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CallToAction
|
||||
title="Werden Sie Teil unseres Teams"
|
||||
text="Wir sind immer auf der Suche nach motivierten Talenten für spannende Infrastrukturprojekte."
|
||||
buttonText="Offene Stellen ansehen"
|
||||
buttonLink="/de/karriere"
|
||||
/>
|
||||
@@ -52,9 +52,9 @@ layout: "fullBleed"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CompanyTimeline />
|
||||
<InteractiveGermanyMap isHero={true} badge="Bundesweite Präsenz" title="Infrastruktur für ein ganzes Land" description="Seit 2023 haben wir in über 100 Projekten quer durch Deutschland bewiesen, dass wir Infrastrukturprojekte jeder Größenordnung verlässlich umsetzen." />
|
||||
|
||||
<InteractiveGermanyMap />
|
||||
<CompanyTimeline />
|
||||
|
||||
<div id="struktur">
|
||||
<HomeSubCompanyTiles
|
||||
|
||||
80
content/de/vermessung.mdx
Normal file
80
content/de/vermessung.mdx
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: "Vermessung und Dokumentation"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Präzise GIS-Vermessung, Absteckung und digitale Dokumentation von Leitungstrassen zur Gewährleistung höchster Bau- und Sicherheitsstandards."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Vermessung & Dokumentation"
|
||||
subtitle="Lückenlose und rechtssichere Erfassung aller Bauphasen"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Wir arbeiten mit hochpräzisem GPS-Equipment zur lückenlosen Dokumentation der Projekte unserer Kunden. Dafür entwickeln wir eigenständig App-Integrationen zur Automatisierung der Vermessung.
|
||||
</blockquote>
|
||||
|
||||
## Höchste Präzision schützt vor Überraschungen
|
||||
|
||||
In der heutigen Zeit wachsen die gesetzlichen Dokumentationsanforderungen und strengen Sicherheitsstandards rasant. Viele herkömmliche Tiefbaufirmen sind kaum noch in der Lage, diese hochkomplexen Anforderungen – insbesondere für den geförderten Breitbandausbau – sauber zu erfüllen. Fehlende oder fehlerhafte Dokumentationen führen unweigerlich zu massiven Problemen bei der Bauabnahme und Projektfinanzierung.
|
||||
|
||||
Wir haben diesen Engpass erkannt und uns proaktiv auf die hochpräzise Vermessung und digitale Dokumentation von Kabeltrassen spezialisiert.
|
||||
|
||||
## Digitale Erfassung auf dem neuesten Stand
|
||||
|
||||
Mit modernster GPS-Technik und eigens entwickelten, digitalen App-Integrationen sorgen wir für eine hochgradig automatisierte, lückenlose und absolut rechtssichere Erfassung aller verlegten Leitungen und Anlagen.
|
||||
Neben der klassischen GPS-Absteckung und Einmessung erstellen wir für unsere Auftraggeber komplette Geodatenbanken zur transparenten Projektabrechnung und bieten eine lückenlose 360° Foto- und Video-Erfassung des Baufeldes an. So behalten Sie jederzeit den vollen Überblick über Ihre unterirdische Infrastruktur.
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Präzision"
|
||||
title="Digitale Sicherheitsstandards"
|
||||
descriptionParagraphs={[
|
||||
"Wir liefern Ihnen nicht nur eine saubere Trasse, sondern auch die perfekten Daten. Rechtssicher, digital und auf den Zentimeter genau."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "vermessung",
|
||||
title: "Klassische Geodäsie",
|
||||
icon: "survey",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Zentimetergenaue Absteckung von Leitungstrassen",
|
||||
"Einmessung von Bestandstrassen und neuen Leitungen",
|
||||
"Hochpräzise GPS-gestützte Geländeaufnahme",
|
||||
"Automatisierte App-basierte Messverfahren"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "dokumentation",
|
||||
title: "Digitale Dokumentation",
|
||||
icon: "survey",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Erstellung von Datenbanken zur Projektabrechnung",
|
||||
"360° Foto- und Video-Erfassung des Baufeldes",
|
||||
"Einbindung und Aufbereitung für Geoinformationssysteme (GIS)",
|
||||
"Revisionssichere Bestandspläne"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Dokumentierte Projekte" description="Wir haben hunderte Kilometer Trassen exakt eingemessen und digital dokumentiert." />
|
||||
|
||||
<CallToAction
|
||||
title="Stehen Sie vor hohen Dokumentationsanforderungen?"
|
||||
text="Kontaktieren Sie uns für professionelle Vermessungs- und GIS-Dienstleistungen."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
15
content/de/zertifikate.mdx
Normal file
15
content/de/zertifikate.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Zertifikate & Nachweise"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Geprüfte Qualität, Arbeitssicherheit und Umweltschutz sind das Fundament unserer täglichen Arbeit."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Die E-TIB Gruppe"
|
||||
title="Zertifikate & Nachweise"
|
||||
subtitle="Geprüfte Qualität, Arbeitssicherheit und Umweltschutz sind das Fundament unserer täglichen Arbeit."
|
||||
backgroundImage={{ url: "/assets/photos/DSC08653.JPG" }}
|
||||
/>
|
||||
|
||||
<CertificatesBlock hideHeader={true} />
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: "Modern Methods in Cable Civil Engineering"
|
||||
date: "2024-03-15T10:00:00.000Z"
|
||||
excerpt: "Learn more about the latest technologies and methods in modern cable civil engineering, from horizontal directional drilling to trenchless installation techniques."
|
||||
featuredImage: "/assets/images/blog/tiefbau.jpg"
|
||||
category: "Technology"
|
||||
public: true
|
||||
---
|
||||
|
||||
# Modern Methods in Cable Civil Engineering
|
||||
|
||||
Cable civil engineering has evolved rapidly in recent years. New technologies enable more efficient and environmentally friendly installation of utility lines.
|
||||
|
||||
## Trenchless Installation Techniques
|
||||
|
||||
One of the most important methods is **Horizontal Directional Drilling (HDD)**. This method allows pipes to be laid under obstacles such as roads, rivers, or buildings without having to break open the surface.
|
||||
|
||||
### Benefits of HDD Drilling:
|
||||
- Minimal impact on traffic
|
||||
- Protection of the environment and existing vegetation
|
||||
- Shorter construction times
|
||||
- Lower costs for complex crossings
|
||||
|
||||
## Classic Civil Engineering in New Quality
|
||||
|
||||
Classic trench construction also benefits from modern machinery and precise planning. By using suction excavators and GPS-supported surveying, we can virtually eliminate damage to existing utility lines.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The combination of proven craftsmanship and state-of-the-art technology is the key to successful infrastructure projects. At E-TIB, we consistently focus on innovation to deliver the best possible results for our customers.
|
||||
81
content/en/bohrtechnik.mdx
Normal file
81
content/en/bohrtechnik.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Trenchless Line Laying"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Precise horizontal directional drilling and earth rockets in all soil classes for surface-friendly and efficient line laying."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Core Competency"
|
||||
title="Trenchless Laying"
|
||||
subtitle="Surface-friendly horizontal directional drilling and earth rockets"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Request a project"
|
||||
ctaHref="/en/contact"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
If classic trench construction is out of the question for space, environmental or cost reasons, we rely on the most modern trenchless techniques. We protect nature and minimize restoration costs.
|
||||
</blockquote>
|
||||
|
||||
## Maximum Efficiency, Minimal Intervention
|
||||
|
||||
Trenchless line laying is the smartest solution for complex construction projects. This process shows its absolute strengths particularly when crossing highly frequented roads, railway lines, highways or sensitive bodies of water. Traffic continues to flow, nature remains untouched and complex surfaces such as expensive paving or asphalt do not have to be destroyed.
|
||||
|
||||
## HDD Boreholes and Earth Rockets
|
||||
|
||||
With our specialized machinery, we master a wide variety of processes. With the **Horizontal Directional Drilling (HDD) process**, we steer the drill head precisely underground. We achieve drilling lengths of up to 250 meters in one piece and can pull in protective pipes with a diameter of up to 400 mm.
|
||||
|
||||
For shorter distances, such as fast and gentle house connections, we use our **earth rockets** (soil displacement method). Here we achieve drilling lengths of up to 15 meters for pipes up to 160 mm in diameter - ideal for leaving front gardens and driveways completely intact.
|
||||
|
||||
<DeepDrillAnimation />
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Procedures"
|
||||
title="Our Drilling Technology in Detail"
|
||||
descriptionParagraphs={[
|
||||
"We select the exactly matching drilling method for each soil class and local condition to guarantee maximum economic efficiency."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "hdd",
|
||||
title: "Horizontal Directional Drilling (HDD)",
|
||||
icon: "hdd",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Max. drilling length: 250 m",
|
||||
"Max. diameter: 400mm pipe",
|
||||
"Precision control and steerability",
|
||||
"Crossing of railways, highways & water bodies"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "erdrakete",
|
||||
title: "Soil Displacement (Earth Rocket)",
|
||||
icon: "hdd",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Max. crossing length: 15 m",
|
||||
"Max. diameter: 160mm pipe",
|
||||
"Fast house connections without trenches",
|
||||
"No surface opening necessary"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Case Studies" title="Completed Drilling Projects" description="Successful HDD drilling and trenchless cable laying under the most complex conditions." />
|
||||
|
||||
<CallToAction
|
||||
title="Looking for a trenchless solution?"
|
||||
text="We would be happy to advise you on the possibilities of horizontal directional drilling for your project."
|
||||
buttonText="Contact us"
|
||||
buttonLink="/en/contact"
|
||||
/>
|
||||
79
content/en/glasfaser.mdx
Normal file
79
content/en/glasfaser.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Cable Civil Engineering in Telecommunications"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Future-proof infrastructure through competent cable civil engineering for fiber optic and telecommunications networks (FTTX)."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Core Competency"
|
||||
title="Fiber Optics & Telecommunications"
|
||||
subtitle="Complete infrastructure solutions for future-proof broadband networks (FTTX)"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01129.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Request a project"
|
||||
ctaHref="/en/contact"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
A life without electricity or high-speed internet is unimaginable today - that is why we lay cables. We build the backbone of the digital society.
|
||||
</blockquote>
|
||||
|
||||
## The Backbone of Digitalization
|
||||
|
||||
Fast internet and state-of-the-art telecommunications networks require an absolutely smooth and highly professional network expansion. With highly specialized assembly teams and our own modern fleet of machinery, E-TIB ensures the highest execution quality in nationwide fiber optic expansion.
|
||||
|
||||
## FTTX Expansion from a Single Source
|
||||
|
||||
As a powerful partner for telecommunications providers, we cover every step of physical network creation. We take over the construction of complete empty conduit routes as well as cable and pipe trenches. In addition, we position and build multi-function enclosures at strategic network nodes.
|
||||
|
||||
Our service does not end with pure civil engineering: We take care of the professional pulling and blowing in of highly sensitive fiber optic and telecommunication cables over long distances and carry out all required cable assemblies. In this way, we guarantee that the digital infrastructure can go into operation safely and performantly.
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Service Portfolio"
|
||||
title="Complete Fiber Optic Expansion"
|
||||
descriptionParagraphs={[
|
||||
"From the empty conduit route to the finished fiber optic assembly - we deliver turnkey telecommunication infrastructure at the highest level."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "tiefbau",
|
||||
title: "Civil Engineering & Routes",
|
||||
icon: "fiber",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Production of cable and pipe trenches",
|
||||
"Production of complete empty conduit routes",
|
||||
"Setting up multi-function enclosures (MFG)",
|
||||
"Network node connection"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "montage",
|
||||
title: "Cable Pulling & Assembly",
|
||||
icon: "fiber",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Pulling of TK/FO cables",
|
||||
"Pneumatic blowing in of fiber optic cables",
|
||||
"Cable assembly TK/FO",
|
||||
"Splicing and measuring work"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Case Studies" title="Completed Broadband Projects" description="We have already successfully realized hundreds of kilometers of FTTX networks. Discover our references." />
|
||||
|
||||
<CallToAction
|
||||
title="Are you planning an FTTX expansion?"
|
||||
text="We are happy to advise you on all aspects of FTTX and fiber optic expansion."
|
||||
buttonText="Contact us"
|
||||
buttonLink="/en/contact"
|
||||
/>
|
||||
@@ -139,42 +139,6 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider
|
||||
references={[
|
||||
{
|
||||
id: "1",
|
||||
title: "Broadband Expansion FTTC, Spreewald Planning Region",
|
||||
slug: "broadband-expansion-fttc-spreewald",
|
||||
category: "Telecommunications",
|
||||
image: "/assets/photos/DSC01123.JPG"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Broadband Expansion FTTH, 29699 Bomlitz / 29690 Schwarmstedt",
|
||||
slug: "broadband-expansion-ftth-bomlitz",
|
||||
category: "Telecommunications",
|
||||
image: "/assets/photos/DSC00850.JPG"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "New Cable Route, 03149 Forst",
|
||||
slug: "new-cable-route-forst",
|
||||
category: "Cable Construction",
|
||||
image: "/assets/photos/DSC01129.JPG"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Cable Connection U30 - T10, 15890 Eisenhüttenstadt",
|
||||
slug: "cable-connection-eisenhuettenstadt",
|
||||
category: "Cable Construction",
|
||||
image: "/assets/photos/DSC00010.JPG"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "PV System Feed-in 14728 Görne",
|
||||
slug: "pv-system-feed-in-goerne",
|
||||
category: "Renewable Energy",
|
||||
image: "/assets/photos/DSC01123.JPG"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ScaleOfImpact />
|
||||
|
||||
<HomeReferencesSlider />
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: "Imprint"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Legal information about E-TIB GmbH."
|
||||
---
|
||||
|
||||
# Imprint
|
||||
|
||||
## Information according to § 5 TMG
|
||||
|
||||
E-TIB GmbH
|
||||
Gewerbestraße 22
|
||||
03172 Guben
|
||||
Germany
|
||||
|
||||
**Represented by:**
|
||||
Danny Joseph
|
||||
|
||||
## Contact
|
||||
|
||||
Phone: +49 (0) 3561 / 68577 33
|
||||
Fax: +49 (0) 3561 / 68577 35
|
||||
Email: info@e-tib.com
|
||||
|
||||
## Register Entry
|
||||
|
||||
Registration in the Commercial Register.
|
||||
Registration Court: District Court Cottbus
|
||||
Registration Number: HRB 14781 CB
|
||||
|
||||
## VAT ID
|
||||
|
||||
VAT identification number according to § 27 a VAT Act:
|
||||
DE332345678 (Example)
|
||||
|
||||
## Responsible for Content
|
||||
|
||||
Danny Joseph
|
||||
Gewerbestraße 22
|
||||
03172 Guben
|
||||
Germany
|
||||
|
||||
## EU Dispute Resolution
|
||||
|
||||
The European Commission provides a platform for online dispute resolution (ODR): [https://ec.europa.eu/consumers/odr/](https://ec.europa.eu/consumers/odr/).
|
||||
Our email address can be found at the top of the imprint.
|
||||
|
||||
## Consumer Dispute Resolution/Universal Arbitration Board
|
||||
|
||||
We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.
|
||||
64
content/en/imprint.mdx
Normal file
64
content/en/imprint.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "Imprint"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Legal information about E-TIB GmbH."
|
||||
---
|
||||
|
||||
# Imprint
|
||||
|
||||
## Information according to § 5 TMG
|
||||
|
||||
E-TIB GmbH
|
||||
Gewerbestraße 22
|
||||
03172 Guben
|
||||
Germany
|
||||
|
||||
**Represented by:**
|
||||
Danny Joseph
|
||||
|
||||
## Contact
|
||||
|
||||
Phone: +49 (0) 3561 / 68577 33
|
||||
Fax: +49 (0) 3561 / 68577 35
|
||||
Email: info@e-tib.com
|
||||
|
||||
## Register Entry
|
||||
|
||||
Registration in the Commercial Register.
|
||||
Registration Court: District Court Cottbus
|
||||
Registration Number: HRB 12403 CB
|
||||
|
||||
## VAT ID
|
||||
|
||||
VAT identification number according to § 27 a VAT Act:
|
||||
DE304799919
|
||||
|
||||
## Responsible for Content
|
||||
|
||||
Danny Joseph
|
||||
Gewerbestraße 22
|
||||
03172 Guben
|
||||
Germany
|
||||
|
||||
## EU Dispute Resolution
|
||||
|
||||
The European Commission provides a platform for online dispute resolution (ODR): [https://ec.europa.eu/consumers/odr/](https://ec.europa.eu/consumers/odr/).
|
||||
Our email address can be found at the top of the imprint.
|
||||
|
||||
## Consumer Dispute Resolution/Universal Arbitration Board
|
||||
|
||||
We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
### Liability for Content
|
||||
|
||||
As a service provider, we are responsible for our own content on these pages in accordance with general legislation pursuant to § 7 Abs.1 TMG (German Telemedia Act). However, according to §§ 8 to 10 TMG, we are not obligated to monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity. Obligations to remove or block the use of information under general laws remain unaffected. However, liability in this regard is only possible from the time of knowledge of a specific infringement. Upon becoming aware of such legal violations, we will remove this content immediately.
|
||||
|
||||
### Liability for Links
|
||||
|
||||
Our website contains links to external third-party websites, over whose content we have no influence. Therefore, we cannot assume any liability for these external contents. The respective provider or operator of the pages is always responsible for the content of the linked pages. The linked pages were checked for possible legal violations at the time of linking. Illegal content was not recognizable at the time of linking. However, permanent monitoring of the content of the linked pages is unreasonable without concrete evidence of a violation of the law. If we become aware of any legal violations, we will remove such links immediately.
|
||||
|
||||
### Copyright
|
||||
|
||||
The content and works created by the site operators on these pages are subject to German copyright law. The duplication, editing, distribution, and any kind of exploitation outside the limits of copyright require the written consent of the respective author or creator. Downloads and copies of this site are only permitted for private, non-commercial use. Insofar as the content on this site was not created by the operator, the copyrights of third parties are respected. In particular, third-party content is marked as such. Should you nevertheless become aware of a copyright infringement, please inform us accordingly. Upon becoming aware of any legal violations, we will remove such content immediately.
|
||||
83
content/en/kabeltiefbau.mdx
Normal file
83
content/en/kabeltiefbau.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Cable Civil Engineering"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Professional cable civil engineering for medium and low voltage networks. We guarantee maximum safety standards and seamless documentation for your project."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Core Competency"
|
||||
title="Cable Civil Engineering"
|
||||
subtitle="Complete infrastructure solutions for medium and low voltage networks"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Request a project"
|
||||
ctaHref="/en/contact"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
Everywhere digging and drilling is happening, but unfortunately a lot also goes wrong. We focus on quality, the highest safety standards and legally compliant documentation - so that your construction project does not turn into a cost trap.
|
||||
</blockquote>
|
||||
|
||||
## Quality instead of a Cost Trap
|
||||
|
||||
On many construction sites, cheap providers from abroad are increasingly being used, which often leads to **safety standards and documentation requirements being ignored**. The consequences are severe: accidents, damaged external lines (such as electricity, water and gas) as well as massive construction delays and expensive supplements. A supposed bargain quickly turns out to be a financial burden.
|
||||
|
||||
Particularly for funded infrastructure projects, retrieving the funding is extremely problematic without absolutely seamless, standard-compliant documentation.
|
||||
|
||||
## Everything from a Single Source
|
||||
|
||||
Thanks to our many years of experience, we identify problems early on and work out safe and satisfactory solutions for our clients in advance. E-TIB GmbH has fully specialized in cable civil engineering.
|
||||
|
||||
We offer you the complete range of services for the construction of cable routes from a single source - with our own ultra-modern machine fleet and permanent specialist staff from the region. In direct cooperation with our partner office, E-TIB Ingenieurgesellschaft mbH, we can also rule out planning errors at an early stage and sovereignly meet even the most complex documentation requirements.
|
||||
|
||||
<DataGridPulse />
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Service Portfolio"
|
||||
title="Everything for Your Network"
|
||||
descriptionParagraphs={[
|
||||
"We offer the highest precision, modern machines and certified experts for every civil engineering project - from cable laying to transformer stations."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "grabenbau",
|
||||
title: "Trench Construction & Laying",
|
||||
icon: "energy",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Production of cable and pipe trenches",
|
||||
"Cable laying from 0.4 kV to 110 kV",
|
||||
"Cable assembly from 0.4 kV to 30 kV",
|
||||
"Cable plowing works for fast laying"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "anlagen",
|
||||
title: "Systems & Infrastructure",
|
||||
icon: "energy",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Construction of transformer stations",
|
||||
"Establishment of complex grounding systems",
|
||||
"Erection of street lighting",
|
||||
"Certified switching operations"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Case Studies" title="Completed Civil Engineering Projects" description="We have laid hundreds of kilometers of cables. Take a look at our reference projects." />
|
||||
|
||||
<CallToAction
|
||||
title="Do you have a specific project?"
|
||||
text="We would be happy to advise you on all questions regarding cable civil engineering and line laying."
|
||||
buttonText="Contact us"
|
||||
buttonLink="/en/contact"
|
||||
/>
|
||||
@@ -7,63 +7,99 @@ layout: "fullBleed"
|
||||
|
||||
<HomeHero
|
||||
badge="Careers"
|
||||
title="Build the Future\nwith Us"
|
||||
description="We are looking for motivated talents who want to shape the digital and energy infrastructure of tomorrow. Join the E-TIB team."
|
||||
title="Build the Future"
|
||||
description="Are you looking for a new challenge in a future-proof market? At E-TIB, we work daily to network Germany and drive the energy transition forward."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="View Open Positions"
|
||||
linkHref="#jobs"
|
||||
linkText="Open Positions"
|
||||
linkHref="#stellen"
|
||||
/>
|
||||
|
||||
<div id="jobs">
|
||||
<JobListingBlock
|
||||
showFairs={false}
|
||||
showJobs={true}
|
||||
title="Current Job Offers"
|
||||
fairsTitle="Upcoming Trade Fairs 2026"
|
||||
fairs={[
|
||||
{ name: 'Intersolar Munich', date: 'June 19 - 21, 2026', type: 'Energy Fair' },
|
||||
{ name: 'Wind Energy Days Linstow', date: 'Nov 04 - 06, 2026', type: 'Specialist Congress' },
|
||||
{ name: 'Cable Workshop Wiesbaden', date: 'Dec 02 - 03, 2026', type: 'Trade Fair' }
|
||||
]}
|
||||
emptyStateMessage="Currently all positions are filled. However, we welcome your speculative application!"
|
||||
emptyStateLinkText="Contact us now"
|
||||
emptyStateLinkHref="/en/kontakt"
|
||||
/>
|
||||
</div>
|
||||
<JobListingBlock showFairs={false} showJobs={true} />
|
||||
|
||||
<div className="bg-white py-24 border-t border-neutral-100">
|
||||
<div className="container max-w-4xl mx-auto px-4 prose prose-lg md:prose-xl prose-headings:font-bold prose-headings:text-neutral-dark prose-p:text-text-secondary prose-li:text-text-secondary prose-ul:space-y-2">
|
||||
<h2>Careers at E-TIB</h2>
|
||||
<div>
|
||||
E-TIB GmbH was founded in 2015 out of the motivation to enable high-quality services for our customers as well as fair remuneration for our employees. In addition to remuneration, the appreciation of each individual employee is our top priority.
|
||||
</div>
|
||||
|
||||
<h3>We offer our employees:</h3>
|
||||
<ul>
|
||||
<li>Deployment in regional and supra-regional construction projects</li>
|
||||
<li>Secure, permanent full-time employment contract</li>
|
||||
<li>Appropriate training in a committed team</li>
|
||||
<li>Performance-related and fair remuneration</li>
|
||||
<li>Daily allowance up to 30.00 EUR per working day</li>
|
||||
<li>31 days of vacation plus vacation pay</li>
|
||||
<li>Work clothing (Engelbert Strauss)</li>
|
||||
<li>Monthly additional payments (personal MasterCard)</li>
|
||||
<li>Contribution to company pension scheme</li>
|
||||
<li>Individual training and development opportunities</li>
|
||||
<li>Provision of vehicle for operational use and trips to the construction site</li>
|
||||
<li>Family atmosphere with company-organized events</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
Our successful corporate development enables us to continuously hire new workers. We are always looking for committed and motivated employees. Become a part of our team. Take a look at our open positions or apply speculatively.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BenefitGrid
|
||||
badge="E-TIB Standard"
|
||||
title="We offer our employees"
|
||||
description="E-TIB GmbH was founded with the motivation to provide high-quality services and fair remuneration. In addition to remuneration, the appreciation of each individual employee is our top priority."
|
||||
benefits={[
|
||||
{
|
||||
id: "region",
|
||||
title: "Exciting Projects",
|
||||
description: "Deployment in interesting regional and supra-regional construction projects.",
|
||||
icon: "region"
|
||||
},
|
||||
{
|
||||
id: "contract",
|
||||
title: "Permanent Contract",
|
||||
description: "Secure job with a permanent full-time employment contract.",
|
||||
icon: "contract"
|
||||
},
|
||||
{
|
||||
id: "money",
|
||||
title: "Fair Remuneration",
|
||||
description: "Performance-related, above-average, and fair payment.",
|
||||
icon: "money"
|
||||
},
|
||||
{
|
||||
id: "allowance",
|
||||
title: "Daily Allowance",
|
||||
description: "We pay a tax-free daily allowance of up to 30.00 EUR per working day.",
|
||||
icon: "allowance"
|
||||
},
|
||||
{
|
||||
id: "vacation",
|
||||
title: "31 Days of Vacation",
|
||||
description: "31 days of annual vacation plus additional vacation pay for relaxation.",
|
||||
icon: "vacation"
|
||||
},
|
||||
{
|
||||
id: "clothes",
|
||||
title: "Premium Workwear",
|
||||
description: "High-quality Engelbert Strauss work clothing is fully provided.",
|
||||
icon: "clothes"
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
title: "Additional Payments",
|
||||
description: "Monthly bonuses via a personal MasterCard.",
|
||||
icon: "card"
|
||||
},
|
||||
{
|
||||
id: "retirement",
|
||||
title: "Pension Scheme",
|
||||
description: "Attractive contribution to the company pension scheme.",
|
||||
icon: "retirement"
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
title: "Further Training",
|
||||
description: "Individual development and promotion opportunities within the company.",
|
||||
icon: "education"
|
||||
},
|
||||
{
|
||||
id: "car",
|
||||
title: "Company Vehicle",
|
||||
description: "Provision of a vehicle for operational use and trips to the construction site.",
|
||||
icon: "car"
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
title: "Intensive Onboarding",
|
||||
description: "Professional onboarding in a helpful and committed team.",
|
||||
icon: "team"
|
||||
},
|
||||
{
|
||||
id: "family",
|
||||
title: "Family Atmosphere",
|
||||
description: "Strong cohesion through regular company-organized events.",
|
||||
icon: "family"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CallToAction
|
||||
title="Speculative Application (m/f/d)"
|
||||
description="Can you imagine taking on a long-term challenge in our company but can't find a suitable job offer? Apply speculatively."
|
||||
ctaLabel="To the contact form"
|
||||
ctaHref="/en/kontakt"
|
||||
ctaHref="/en/contact"
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
@@ -6,12 +6,54 @@ layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
badge="Competencies"
|
||||
title="Everything from a\nsingle source"
|
||||
description="From the first planning sketch to the final documentation – we are your reliable partner for complex infrastructure projects."
|
||||
badge="Our Services"
|
||||
title="Everything from a single source"
|
||||
description="As a full-service partner in the field of cable civil engineering, we cover the entire spectrum of modern infrastructure projects."
|
||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||
linkText="Get in Touch"
|
||||
linkHref="/en/kontakt"
|
||||
linkHref="/en/contact"
|
||||
secondaryCtaLabel="View References"
|
||||
secondaryCtaHref="/en/#referenzen"
|
||||
/>
|
||||
|
||||
<HomeCompetenceBentoGrid
|
||||
badge="Core Competencies"
|
||||
title="Precision & Performance"
|
||||
ctaLabel="Request a project"
|
||||
ctaHref="/en/contact"
|
||||
items={[
|
||||
{
|
||||
title: "Cable Construction",
|
||||
description: "Classic trench construction and professional laying of medium and low voltage cables.",
|
||||
tag: "Energy",
|
||||
size: "large",
|
||||
href: "/en/kabeltiefbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Construction" }
|
||||
},
|
||||
{
|
||||
title: "Drilling Technology",
|
||||
description: "Horizontal directional drilling (HDD) for trenchless, surface-friendly installation.",
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
href: "/en/bohrtechnik",
|
||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Drilling Technology" }
|
||||
},
|
||||
{
|
||||
title: "Fiber Optics",
|
||||
description: "Expansion of future-proof broadband networks (FTTX).",
|
||||
tag: "Communication",
|
||||
size: "small",
|
||||
href: "/en/glasfaser",
|
||||
image: { url: "/assets/photos/DSC01129.JPG", alt: "Fiber Optics" }
|
||||
},
|
||||
{
|
||||
title: "Planning & Docu",
|
||||
description: "Route planning, approval procedures, and precise GIS documentation.",
|
||||
tag: "Digital",
|
||||
size: "accent",
|
||||
href: "/en/planung"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<ServiceDetailGrid
|
||||
@@ -19,50 +61,53 @@ layout: "fullBleed"
|
||||
title="No compromises in construction"
|
||||
problemStatement={{
|
||||
title: "The reality on many construction sites",
|
||||
text: "Everywhere you look, digging and drilling are happening, but unfortunately, many things go wrong. Safety standards and documentation requirements are often ignored. This results in accidents, damaged external lines, and expensive construction delays."
|
||||
text: "Cheap providers are increasingly being used on many construction sites. Safety standards and documentation requirements are often ignored. This results in accidents, damaged external lines (electricity, water, gas, etc.), and expensive construction delays, meaning a supposed bargain turns out to be a cost trap."
|
||||
}}
|
||||
solutionStatement={{
|
||||
title: "The E-TIB Standard",
|
||||
text: "We work differently. Through our many years of experience, we identify problems early and develop safe solutions. Supported by our partner engineering firm, we offer planning, execution, and documentation at the highest level."
|
||||
text: "Through our many years of experience, we identify problems early and develop safe solutions. Supported by our partner engineering firm, we offer planning, execution, and documentation at the highest level to avoid planning errors."
|
||||
}}
|
||||
panels={[
|
||||
{
|
||||
id: "energy",
|
||||
title: "Energy & High Voltage",
|
||||
title: "Cable Civil Engineering (Medium/Low Voltage)",
|
||||
icon: "energy",
|
||||
lists: [
|
||||
[
|
||||
"Cable and pipe trenches",
|
||||
"Production of cable and pipe trenches",
|
||||
"Cable laying (0.4kV - 110kV)",
|
||||
"Cable assembly (0.4kV - 30kV)",
|
||||
"Cable assembly 0.4 kV - 30 kV",
|
||||
"Construction of transformer stations",
|
||||
"Grounding systems & street lighting"
|
||||
"Establishment of grounding systems",
|
||||
"Erection of street lighting",
|
||||
"Switching operations"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "fiber",
|
||||
title: "Fiber Optics & Telecom",
|
||||
title: "Cable Civil Engineering (Telecommunications)",
|
||||
icon: "fiber",
|
||||
lists: [
|
||||
[
|
||||
"Production of cable and pipe trenches",
|
||||
"Production of empty conduit routes",
|
||||
"Pulling/blowing in fiber optic cables",
|
||||
"Pulling/blowing in TK/FO cables",
|
||||
"Setting up multi-function enclosures",
|
||||
"Fiber optic cable assembly"
|
||||
"Cable assembly TK/FO"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "hdd",
|
||||
title: "Trenchless Laying",
|
||||
title: "Trenchless Line Laying",
|
||||
icon: "hdd",
|
||||
lists: [
|
||||
[
|
||||
"Horizontal directional drilling (HDD)",
|
||||
"Max. drilling length: 250m",
|
||||
"Max. diameter: 400mm pipe",
|
||||
"Earth rocket (up to 15m / 160mm pipe)"
|
||||
"Horizontal directional drilling (up to 250m)",
|
||||
"Horizontal directional drilling (up to 400mm pipe)",
|
||||
"Earth rocket (up to 15m crossing length)",
|
||||
"Earth rocket (up to 160mm pipe)"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -72,10 +117,10 @@ layout: "fullBleed"
|
||||
icon: "planning",
|
||||
lists: [
|
||||
[
|
||||
"Structural & execution planning",
|
||||
"Tendering & construction supervision",
|
||||
"Structural/approval/execution planning",
|
||||
"Tendering, awarding & construction supervision",
|
||||
"Approval planning",
|
||||
"Feasibility studies & project review"
|
||||
"Project review, studies"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -86,14 +131,38 @@ layout: "fullBleed"
|
||||
fullWidth: true,
|
||||
lists: [
|
||||
[
|
||||
"Staking out and surveying of cable routes",
|
||||
"Digital as-built documentation (GIS)"
|
||||
],
|
||||
[
|
||||
"360° photo/video recording of the construction site",
|
||||
"Creation of databases for project accounting"
|
||||
"Staking out and surveying of line routes",
|
||||
"Creation of databases for project accounting",
|
||||
"360° photo/video recording of the construction site"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Cable Construction",
|
||||
"description": "Classic trench construction and professional laying of medium and low voltage cables.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Horizontal Directional Drilling",
|
||||
"description": "Horizontal directional drilling (HDD) for trenchless, surface-friendly installation.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Fiber Optic Expansion",
|
||||
"description": "Expansion of future-proof broadband networks (FTTX) and fiber optic cable assemblies.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Planning and Surveying",
|
||||
"description": "Approval and execution planning, electrical and grid connection planning, as well as digital as-built documentation via GIS.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
}
|
||||
]} />
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
---
|
||||
title: "Trade Fairs & Events"
|
||||
title: "Fairs & Events"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Meet the E-TIB Group team in person at trade fairs and industry events."
|
||||
excerpt: "Meet the team of the E-TIB Group personally at trade fairs and industry events."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
badge="Events"
|
||||
title="Meet us\nin person"
|
||||
description="Get in touch with our experts. We are represented at the most important industry trade fairs and look forward to speaking with you personally."
|
||||
videoUrl="/assets/videos/web/hero-messe.mp4"
|
||||
linkText="View dates"
|
||||
linkHref="#dates"
|
||||
badge="Fairs & Events"
|
||||
title="Meet us personally"
|
||||
description="Get to know the E-TIB Group at the leading industry trade fairs."
|
||||
videoUrl="/assets/videos/web/hero-bahnkreuzung.mp4"
|
||||
linkText="Get in Touch"
|
||||
linkHref="/en/contact"
|
||||
/>
|
||||
|
||||
<div id="dates">
|
||||
<div id="termine">
|
||||
<JobListingBlock
|
||||
showFairs={true}
|
||||
showJobs={false}
|
||||
fairsTitle="Upcoming Trade Fairs 2026"
|
||||
showJobs={false}
|
||||
fairs={[
|
||||
{ name: 'Intersolar Munich', date: 'June 19 - 21, 2026', type: 'Energy Fair' },
|
||||
{ name: 'Wind Energy Days Linstow', date: 'Nov 04 - 06, 2026', type: 'Specialist Congress' },
|
||||
{ name: 'Cable Workshop Wiesbaden', date: 'Dec 02 - 03, 2026', type: 'Trade Fair' }
|
||||
{ name: 'Intersolar Munich', date: '2024 / 2025', type: 'Fair', location: 'Munich', url: 'https://www.intersolar.de/' },
|
||||
{ name: 'Wind Energy Days Linstow', date: '2024 / 2025', type: 'Specialist Congress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
|
||||
{ name: 'Cable Workshop Wiesbaden', date: '2024 / 2025', type: 'Trade Fair', location: 'Wiesbaden' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CallToAction
|
||||
title="Ready for Your Project?"
|
||||
description="We are always looking for new challenges and strong partners. Contact us for a non-binding consultation on your project."
|
||||
ctaLabel="Contact Us Now"
|
||||
ctaHref="/en/kontakt"
|
||||
/>
|
||||
<CallToAction />
|
||||
|
||||
79
content/en/planung.mdx
Normal file
79
content/en/planung.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Planning and Consulting"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Professional planning and consulting in the field of energy and telecommunications networks: route planning, approval procedures and project control from a single source."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Core Competency"
|
||||
title="Planning & Consulting"
|
||||
subtitle="Competent support for energy and telecommunications networks"
|
||||
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Request a project"
|
||||
ctaHref="/en/contact"
|
||||
/>
|
||||
|
||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||
<blockquote>
|
||||
We offer our clients everything from a single source - from the rough planning of your project idea and cable civil engineering to the construction of your renewable energy project.
|
||||
</blockquote>
|
||||
|
||||
## Foundation for Project Success
|
||||
|
||||
Solid planning is the crucial foundation for every successful infrastructure project. Projects often fail or are massively delayed because the complexity of civil engineering planning and approval procedures was underestimated. Through our combined competence, you avoid expensive planning errors and frictional losses at the interfaces.
|
||||
|
||||
Both for the highly topical broadband expansion and for the demanding grid connection of solar or wind projects, absolutely precise and high-quality cable route planning is required. To do this, we inspect the route on site, analyze all potential construction conflicts at an early stage and ensure that all necessary applications for the laying are submitted completely and on time to the authorities.
|
||||
|
||||
## Strong Engineering Competence
|
||||
|
||||
In close cooperation with our specialized partner engineering firm, **E-TIB Ingenieurgesellschaft mbH**, we accompany you through all service phases. Whether structural planning for route optimization, complex approval planning or detailed execution planning - we steer your project from the first feasibility study to successful construction supervision and acceptance.
|
||||
</div>
|
||||
|
||||
<ServiceDetailGrid
|
||||
badge="Engineering Services"
|
||||
title="Our Planning Services"
|
||||
descriptionParagraphs={[
|
||||
"Minimize risks and interfaces. We combine planning know-how with practical civil engineering expertise."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
id: "planung",
|
||||
title: "Route & Approval Planning",
|
||||
icon: "planning",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Structural planning (route optimization)",
|
||||
"Approval planning & authority management",
|
||||
"Detailed execution planning",
|
||||
"Consent planning"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "beratung",
|
||||
title: "Control & Supervision",
|
||||
icon: "planning",
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Tendering & awarding (AVA)",
|
||||
"Specialist construction supervision",
|
||||
"Project review & controlling",
|
||||
"Sound feasibility studies"
|
||||
]
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Case Studies" title="Planning in Practice" description="Successfully planned and accompanied infrastructure projects of our group." />
|
||||
|
||||
<CallToAction
|
||||
title="Do you need planning security?"
|
||||
text="Let's talk about your project. We are at your side with our engineering knowledge."
|
||||
buttonText="Contact us"
|
||||
buttonLink="/en/contact"
|
||||
/>
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Landkreis Börde"
|
||||
title: "Broadband Expansion FTTH, Börde District"
|
||||
client: "EKN Breitband GmbH & Co. KG"
|
||||
date: "2026-01-01"
|
||||
dateString: "seit 05/2019"
|
||||
location: "Landkreis Börde"
|
||||
date: "2019-05-01"
|
||||
dateString: "Since 05/2019"
|
||||
location: "Börde District"
|
||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
||||
---
|
||||
|
||||
- 109.000 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
- 63 Stück Aufstellung Netzverteiler
|
||||
- 747 Stück Herstellung Hausanschlüsse
|
||||
- 109,000 m production of cable route (civil engineering/HDD)
|
||||
- 63 units installation of network distributors
|
||||
- 747 units production of house connections
|
||||
- Coordination and realization of the entire route guidance
|
||||
- Documentation and acceptance of the house connections
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Planungsregion Bomlitz"
|
||||
client: "Telekom Deutschland GmbH"
|
||||
date: "2018-12-31"
|
||||
dateString: "04/2018 bis 12/2018"
|
||||
location: "Bomlitz"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
||||
title: "Broadband Expansion FTTH, Bomlitz / Schwarmstedt"
|
||||
client: "inexio Informationstechnologie und Telekommunikation GmbH"
|
||||
date: "2018-08-01"
|
||||
dateString: "08/2018 to 03/2021"
|
||||
location: "29699 Bomlitz / 29690 Schwarmstedt"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-60.jpg"
|
||||
---
|
||||
|
||||
- 88.890 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
- 23 Stück Aufstellung Multifunktionsgehäuse
|
||||
- 52 Stück Aufstellung Netzverteiler
|
||||
- 571 Stück Herstellung Hausanschlüsse
|
||||
- 88,890 m production of cable route (civil engineering/HDD)
|
||||
- 23 units installation of multi-functional cabinets
|
||||
- 52 units installation of network distributors
|
||||
- 571 units production of house connections
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: "Kabelverbindung U30 - T10, 15890 Eisenhüttenstadt"
|
||||
title: "Cable Connection U30 - T10, Eisenhüttenstadt"
|
||||
client: "ArcelorMittal Eisenhüttenstadt GmbH"
|
||||
date: "2019-12-31"
|
||||
dateString: "07/2019 bis 12/2019"
|
||||
location: "Eisenhüttenstadt"
|
||||
date: "2019-07-01"
|
||||
dateString: "07/2019 to 12/2019"
|
||||
location: "15890 Eisenhüttenstadt"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie_Vermessung_(24_von_99)_groß2.jpg"
|
||||
---
|
||||
|
||||
- 2.095 m Kabelgraben ausheben und verfüllen
|
||||
- 180 m Horizontalspülbohrung d 160 + d 50
|
||||
- 5.700 m Lieferung und Verlegung MS-Kabel 240 mm², 18/30 kV
|
||||
- 3.000 m Lieferung und Verlegung PE-Schutzrohr 50 x 4,6 mm
|
||||
- 3.000 m Lieferung und Einblasen LWL-Kabel, 12 Fasern
|
||||
- 3.000 m Lieferung und Verlegung Steuerkabel, 0,6/1 kV
|
||||
- 16 Satz Lieferung und Montage MS-Verbindungsmuffe 240 mm², 18/30 kV
|
||||
- 1 Stück Lieferung und Montage LWL-Verbindungsmuffe, 12 Fasern
|
||||
- 4 Satz Lieferung und Montage MS-T-Stecker 240 mm², 18/30 kV
|
||||
- Hauptprüfung / Mantelprüfung / OTDR-Messung
|
||||
- 2,095 m cable trench excavation and backfilling
|
||||
- 180 m horizontal directional drilling d 160 + d 50
|
||||
- 5,700 m delivery and installation of MV cable 240 mm², 18/30 kV
|
||||
- 3,000 m delivery and installation of PE protection pipe 50 x 4.6 mm
|
||||
- 3,000 m delivery and blowing in of FO cable, 12 fibers
|
||||
- 3,000 m delivery and installation of control cable, 0.6/1 kV
|
||||
- 16 sets delivery and assembly of MV connection joint 240 mm², 18/30 kV
|
||||
- 1 unit delivery and assembly of FO connection joint, 12 fibers
|
||||
- 4 sets delivery and assembly of MV T-connectors 240 mm², 18/30 kV
|
||||
- Main testing / sheath testing / OTDR measurement
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: "Neubau Kabeltrasse, 03149 Forst"
|
||||
title: "New Cable Route, Forst"
|
||||
client: "Mitteldeutsche Netzgesellschaft Strom mbH"
|
||||
date: "2020-09-30"
|
||||
dateString: "03/2020 bis 09/2020"
|
||||
location: "Forst"
|
||||
date: "2020-03-01"
|
||||
dateString: "03/2020 to 09/2020"
|
||||
location: "03149 Forst"
|
||||
featuredImage: "/assets/photos/DSC02156.jpg"
|
||||
---
|
||||
|
||||
- 2984 m Kabelgraben ausheben und verfüllen
|
||||
- 945 m Horizontalspülbohrung d 160 + d 50
|
||||
- 3.929 m Verlegung MS-Kabel 240 mm², 12/20 kV
|
||||
- 3.929 m Verlegung PE-Schutzrohr 50 x 4,6 mm
|
||||
- 11 Satz Montage MS-Verbindungsmuffe 240 mm², 12/20 kV
|
||||
- 1 Satz Montage MS-Endverschluss 240 mm², 12/20 kV
|
||||
- 2.000 m Rückbau Freileitung (inkl. 16 Stück Maste)
|
||||
- 2,984 m cable trench excavation and backfilling
|
||||
- 945 m horizontal directional drilling d 160 + d 50
|
||||
- 3,929 m installation of MV cable 240 mm², 12/20 kV
|
||||
- 3,929 m installation of PE protection pipe 50 x 4.6 mm
|
||||
- 11 sets assembly of MV connection joint 240 mm², 12/20 kV
|
||||
- 1 set assembly of MV end termination 240 mm², 12/20 kV
|
||||
- 2,000 m dismantling of overhead line (incl. 16 poles)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user