Compare commits
121 Commits
fix/header
...
v2.2.13-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ae1e6257d | |||
| 1419bab7cf | |||
| ac6b121685 | |||
| 4a9a7adf93 | |||
| 3b297aae3f | |||
| c6b81a3d77 | |||
| c8f3420a5c | |||
| a6559eb7bb | |||
| 8d75c2ec12 | |||
| 6a18eb4ee1 | |||
| d91eb220be | |||
| 5c40d55b21 | |||
| a512bbe226 | |||
| 8b3851f45c | |||
| e2667ed750 | |||
| 0253956bd9 | |||
| 42cd4394c7 | |||
| 276ceed010 | |||
| cdf10bd670 | |||
| a1ea41b42f | |||
| 561d75d2b8 | |||
| 685c0ffff6 | |||
| 17b001a97d | |||
| 275a857554 | |||
| f766c9c9db | |||
| c851a4f95e | |||
| 1dca7f79fe | |||
| d3eee45112 | |||
| c308035e77 | |||
| 44d8c22c97 | |||
| 9d3f918aef | |||
| a34bee6272 | |||
| 373017059e | |||
| 309a53624e | |||
| e994d79895 | |||
| 88e88c8b99 | |||
| e2a29b41e9 | |||
| 62727a7231 | |||
| 3a6189e0d8 | |||
| 01e2fdbb7d | |||
| fa7182d351 | |||
| e0d43f7f5f | |||
| d09b576137 | |||
| 04599827f9 | |||
| 5368f1a64d | |||
| 241c97e346 | |||
| 3334e327bf | |||
| 145bff90b6 | |||
| d2bf48fc60 | |||
| 5e7ee84dc9 | |||
| b42e53495d | |||
| 8813ed3910 | |||
| 1d3c6a0f70 | |||
| 8fb3b96bd3 | |||
| 67dc56d3ee | |||
| e5933bad69 | |||
| 11b9e4630b | |||
| a47d378521 | |||
| 30047cb1c4 | |||
| 394ac5a19c | |||
| d63f196693 | |||
| 6092f1d827 | |||
| 766c385704 | |||
| 6c9d2b6832 | |||
| 7a9d4d7f18 | |||
| 0b9929c2ec | |||
| aa0dfdf331 | |||
| 4c9028f58b | |||
| 9de5f2938b | |||
| de41a0e29d | |||
| 6689dd3013 | |||
| 31fbcb32dd | |||
| 68eca73df2 | |||
| 3bb6eb3447 | |||
| a3c732dd13 | |||
| aaa1a8793c | |||
| 233509db67 | |||
| 17a807c48b | |||
| d8634efa9a | |||
| 0f2aa500e9 | |||
| 2c465bc16b | |||
| 68717bf0ce | |||
| 6444281c6f | |||
| 4b98fa9b4c | |||
| fa4385af4c | |||
| d98eb20430 | |||
| 3cfe8ff379 | |||
| 2abcd386f0 | |||
| d7ff6a3415 | |||
| d8f5d9bfc6 | |||
| f81a85fdd7 | |||
| 4968315ebf | |||
| eaaa3b7251 | |||
| bbc8716da6 | |||
| f4d65452a6 | |||
| 1b2e0793d3 | |||
| b378bb168f | |||
| 9134d6b070 | |||
| 0105c8c9a3 | |||
| 0138064bcc | |||
| f74df4e8a2 | |||
| cf29d0c45a | |||
| 4aebd226f1 | |||
| 528bf83a67 | |||
| 05ca944828 | |||
| f2209c4815 | |||
| ded2aee989 | |||
| 37ecb81444 | |||
| 343e5a976c | |||
| 8817b5d586 | |||
| fb1e7efd30 | |||
| 3a7100d8b0 | |||
| 50d3d827e8 | |||
| 79813f5d8a | |||
| 4c0ee13d97 | |||
| a95fddca56 | |||
| f110ce9d46 | |||
| 68c4a056b7 | |||
| 57a67ffdf3 | |||
| ed802d03e7 | |||
| 68b0ed7ba0 |
8
.env
8
.env
@@ -1,5 +1,5 @@
|
||||
# ============================================================================
|
||||
# KLZ Cables - Environment Configuration
|
||||
# E-TIB GmbH - Environment Configuration
|
||||
# ============================================================================
|
||||
# Copy this file to .env for local development
|
||||
# For production, use .env.production as a template
|
||||
@@ -20,14 +20,14 @@ NEXT_PUBLIC_RECORD_MODE_ENABLED=true
|
||||
# Analytics (Umami)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Optional: Leave empty to disable analytics
|
||||
UMAMI_WEBSITE_ID=
|
||||
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
|
||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Error Tracking (GlitchTip/Sentry)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Optional: Leave empty to disable error tracking
|
||||
SENTRY_DSN=
|
||||
SENTRY_DSN=https://dcb81958-dbf2-4a3d-b422-875f4672c14b@glitchtip.infra.mintel.me/5
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Email Configuration (SMTP)
|
||||
@@ -37,7 +37,7 @@ MAIL_HOST=smtp.eu.mailgun.org
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_FROM=KLZ Cables <noreply@e-tib.com>
|
||||
MAIL_FROM=E-TIB GmbH <noreply@mg.mintel.me>
|
||||
MAIL_RECIPIENTS=info@e-tib.com
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -17,9 +17,6 @@ env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
||||
|
||||
concurrency:
|
||||
group: deploy-pipeline
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -64,10 +61,17 @@ jobs:
|
||||
echo "Pruning Buildkit inside container: $container"
|
||||
docker exec -i "$container" buildctl prune --all || true
|
||||
done
|
||||
echo "Removing buildkit builder containers to clear overlay storage..."
|
||||
containers=$(docker ps -a --filter "name=buildx_buildkit_builder" -q)
|
||||
if [ -n "$containers" ]; then
|
||||
docker rm -f $containers || true
|
||||
fi
|
||||
echo "=== Truncating host Docker container log files ==="
|
||||
docker run --rm -v /:/host-root alpine sh -c "truncate -s 0 /host-root/mnt/HC_Volume_104796416/docker/containers/*/*.log" || true
|
||||
echo "Purging old build layers, unused volumes and images..."
|
||||
docker builder prune -a -f || true
|
||||
docker volume prune -f || true
|
||||
docker system prune -a -f || true
|
||||
docker system prune -a -f --volumes || true
|
||||
echo "=== System Disk Usage After Prune ==="
|
||||
df -h || true
|
||||
|
||||
@@ -139,6 +143,13 @@ jobs:
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🚦 Priority Gate
|
||||
env:
|
||||
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||
CURRENT_RUN_ID: ${{ github.run_id }}
|
||||
CURRENT_TARGET: ${{ steps.determine.outputs.target }}
|
||||
run: bash scripts/priority-gate.sh
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -213,28 +224,33 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: bash scripts/registry-auth.sh
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
- name: 🏗️ Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
provenance: false
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
NEXT_PUBLIC_APP_VERSION=${{ needs.prepare.outputs.image_tag }}
|
||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
NPM_TOKEN=${{ steps.auth.outputs.working_token }}
|
||||
NPM_DOMAIN=${{ steps.auth.outputs.working_domain }}
|
||||
tags: registry.infra.mintel.me/mintel/e-tib.com:${{ needs.prepare.outputs.image_tag }}
|
||||
secrets: |
|
||||
"NPM_TOKEN=${{ steps.auth.outputs.working_token }}"
|
||||
- name: 🏗️ Build and Push (Native Docker Buildkit)
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
|
||||
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
|
||||
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
run: |
|
||||
echo "$NPM_TOKEN" > npm_token.txt
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--secret id=NPM_TOKEN,src=npm_token.txt \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
|
||||
--build-arg NEXT_PUBLIC_TARGET="$NEXT_PUBLIC_TARGET" \
|
||||
--build-arg NEXT_PUBLIC_APP_VERSION="$IMAGE_TAG" \
|
||||
--build-arg UMAMI_WEBSITE_ID="$UMAMI_WEBSITE_ID" \
|
||||
--build-arg UMAMI_API_ENDPOINT="$UMAMI_API_ENDPOINT" \
|
||||
--build-arg NPM_TOKEN="$NPM_TOKEN" \
|
||||
--build-arg NPM_DOMAIN="$NPM_DOMAIN" \
|
||||
-t registry.infra.mintel.me/mintel/e-tib.com:"$IMAGE_TAG" \
|
||||
.
|
||||
docker push registry.infra.mintel.me/mintel/e-tib.com:"$IMAGE_TAG"
|
||||
rm -f npm_token.txt
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 4: Deploy
|
||||
@@ -294,6 +310,19 @@ jobs:
|
||||
|
||||
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
|
||||
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
INTERNAL_SUBNET="10.199.10.0/24"
|
||||
elif [[ "$TARGET" == "testing" ]]; then
|
||||
INTERNAL_SUBNET="10.199.11.0/24"
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
INTERNAL_SUBNET="10.199.12.0/24"
|
||||
else
|
||||
HEX_SHA="${{ needs.prepare.outputs.short_sha }}"
|
||||
DEC_SHA=$((16#$HEX_SHA))
|
||||
SUBNET_NUM=$(( 13 + (DEC_SHA % 230) ))
|
||||
INTERNAL_SUBNET="10.199.${SUBNET_NUM}.0/24"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Generated by CI - $TARGET"
|
||||
echo "IMAGE_TAG=$IMAGE_TAG"
|
||||
@@ -330,6 +359,7 @@ jobs:
|
||||
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
|
||||
echo "AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE"
|
||||
echo "AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED"
|
||||
echo "INTERNAL_SUBNET=$INTERNAL_SUBNET"
|
||||
} > .env.deploy
|
||||
|
||||
- name: 🚀 SSH Deploy
|
||||
@@ -337,6 +367,7 @@ jobs:
|
||||
env:
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
SLUG=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
@@ -349,7 +380,6 @@ jobs:
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/staging.e-tib.com"
|
||||
else
|
||||
SLUG=$(echo "${{ github.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
SITE_DIR="/home/deploy/sites/branch.e-tib.com/$SLUG"
|
||||
fi
|
||||
|
||||
@@ -357,6 +387,8 @@ jobs:
|
||||
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f"
|
||||
ssh root@alpha.mintel.me "docker rm -f \$(docker ps -a -q --filter name=${SLUG}-etib-) 2>/dev/null || true"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||
|
||||
BIN
.tmp_vikunja/design_reference.png
Normal file
BIN
.tmp_vikunja/design_reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
.tmp_vikunja/mengen_2016_2018.docx
Normal file
BIN
.tmp_vikunja/mengen_2016_2018.docx
Normal file
Binary file not shown.
BIN
.tmp_vikunja/mengen_2019_2021.docx
Normal file
BIN
.tmp_vikunja/mengen_2019_2021.docx
Normal file
Binary file not shown.
21
.tmp_vikunja/mengen_2019_2021.txt
Normal file
21
.tmp_vikunja/mengen_2019_2021.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
2019
|
||||
Offener Tiefbau 32445 Meter
|
||||
HDD 8505Meter
|
||||
Kabelverlegung 87468 Meter
|
||||
|
||||
|
||||
|
||||
|
||||
2020
|
||||
Offener Tiefbau 31305 Meter
|
||||
HDD 9050 Meter
|
||||
Kabelverlegung 93556 Meter
|
||||
|
||||
|
||||
|
||||
|
||||
2021
|
||||
Offener Tiefbau 38756 Meter
|
||||
HDD 10957 Meter
|
||||
Kabelverlegung 110127 Meter
|
||||
|
||||
BIN
.tmp_vikunja/mengen_2022.docx
Normal file
BIN
.tmp_vikunja/mengen_2022.docx
Normal file
Binary file not shown.
8
.tmp_vikunja/mengen_2022.txt
Normal file
8
.tmp_vikunja/mengen_2022.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
2022
|
||||
Offener Tiefbau 41445 Meter
|
||||
HDD 9505Meter
|
||||
Kabelverlegung 102468 Meter
|
||||
|
||||
|
||||
|
||||
|
||||
1
.turbo/0864e4a10f76dcaf-manifest.json
Normal file
1
.turbo/0864e4a10f76dcaf-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":11972,"mtime_nanos":1779970374061729177,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo/0864e4a10f76dcaf-meta.json
Normal file
1
.turbo/0864e4a10f76dcaf-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"0864e4a10f76dcaf","duration":6769,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
||||
BIN
.turbo/0864e4a10f76dcaf.tar.zst
Normal file
BIN
.turbo/0864e4a10f76dcaf.tar.zst
Normal file
Binary file not shown.
1
.turbo/41698960411f6bf6-manifest.json
Normal file
1
.turbo/41698960411f6bf6-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-test.log":{"size":2776,"mtime_nanos":1781469605328811961,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
||||
1
.turbo/41698960411f6bf6-meta.json
Normal file
1
.turbo/41698960411f6bf6-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"41698960411f6bf6","duration":4168,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/41698960411f6bf6.tar.zst
Normal file
BIN
.turbo/41698960411f6bf6.tar.zst
Normal file
Binary file not shown.
1
.turbo/7668a73054ba889d-manifest.json
Normal file
1
.turbo/7668a73054ba889d-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1779970373499305257,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
||||
1
.turbo/7668a73054ba889d-meta.json
Normal file
1
.turbo/7668a73054ba889d-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"7668a73054ba889d","duration":6206,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
||||
BIN
.turbo/7668a73054ba889d.tar.zst
Normal file
BIN
.turbo/7668a73054ba889d.tar.zst
Normal file
Binary file not shown.
1
.turbo/7d27d7d8e3c30797-manifest.json
Normal file
1
.turbo/7d27d7d8e3c30797-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-typecheck.log":{"size":90,"mtime_nanos":1781782738306875884,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
||||
1
.turbo/7d27d7d8e3c30797-meta.json
Normal file
1
.turbo/7d27d7d8e3c30797-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"7d27d7d8e3c30797","duration":14130,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
||||
BIN
.turbo/7d27d7d8e3c30797.tar.zst
Normal file
BIN
.turbo/7d27d7d8e3c30797.tar.zst
Normal file
Binary file not shown.
1
.turbo/9c3fa4d9e1993aff-manifest.json
Normal file
1
.turbo/9c3fa4d9e1993aff-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-test.log":{"size":2150,"mtime_nanos":1779970370282819916,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
||||
1
.turbo/9c3fa4d9e1993aff-meta.json
Normal file
1
.turbo/9c3fa4d9e1993aff-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"9c3fa4d9e1993aff","duration":2989,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
||||
BIN
.turbo/9c3fa4d9e1993aff.tar.zst
Normal file
BIN
.turbo/9c3fa4d9e1993aff.tar.zst
Normal file
Binary file not shown.
1
.turbo/bb46018d4a7bc516-manifest.json
Normal file
1
.turbo/bb46018d4a7bc516-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":12987,"mtime_nanos":1781469609469080321,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo/bb46018d4a7bc516-meta.json
Normal file
1
.turbo/bb46018d4a7bc516-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"bb46018d4a7bc516","duration":8307,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/bb46018d4a7bc516.tar.zst
Normal file
BIN
.turbo/bb46018d4a7bc516.tar.zst
Normal file
Binary file not shown.
1
.turbo/c3b4dbd60e2d475a-manifest.json
Normal file
1
.turbo/c3b4dbd60e2d475a-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":15001,"mtime_nanos":1781782739446599688,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo/c3b4dbd60e2d475a-meta.json
Normal file
1
.turbo/c3b4dbd60e2d475a-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"c3b4dbd60e2d475a","duration":15270,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
||||
BIN
.turbo/c3b4dbd60e2d475a.tar.zst
Normal file
BIN
.turbo/c3b4dbd60e2d475a.tar.zst
Normal file
Binary file not shown.
1
.turbo/ea9126c55a797120-manifest.json
Normal file
1
.turbo/ea9126c55a797120-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-test.log":{"size":3396,"mtime_nanos":1781782731048784172,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
||||
1
.turbo/ea9126c55a797120-meta.json
Normal file
1
.turbo/ea9126c55a797120-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"ea9126c55a797120","duration":6873,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
||||
BIN
.turbo/ea9126c55a797120.tar.zst
Normal file
BIN
.turbo/ea9126c55a797120.tar.zst
Normal file
Binary file not shown.
1
.turbo/eb4eeeb717c1af0c-manifest.json
Normal file
1
.turbo/eb4eeeb717c1af0c-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1781469608726900998,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
||||
1
.turbo/eb4eeeb717c1af0c-meta.json
Normal file
1
.turbo/eb4eeeb717c1af0c-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"eb4eeeb717c1af0c","duration":7565,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/eb4eeeb717c1af0c.tar.zst
Normal file
BIN
.turbo/eb4eeeb717c1af0c.tar.zst
Normal file
Binary file not shown.
@@ -1,119 +1,5 @@
|
||||
|
||||
|
||||
> 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.
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 lint /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> eslint .
|
||||
|
||||
ELIFECYCLE Command failed.
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
|
||||
|
||||
> 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
|
||||
|
||||
> e-tib-nextjs@2.2.12 test /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> vitest run --passWithNoTests
|
||||
|
||||
|
||||
[1m[46m RUN [49m[22m [36mv4.1.2 [39m[90m/Volumes/Alpha SSD/Coding/e-tib.com[39m
|
||||
|
||||
(node:64961) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
[32m✓[39m tests/mdx-links.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 29[2mms[22m[39m
|
||||
[32m✓[39m tests/homepage-and-competence-content.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
||||
[31m❯[39m tests/legal-pages.test.ts [2m([22m[2m2 tests[22m[2m | [22m[31m1 failed[39m[2m)[22m[32m 9[2mms[22m[39m
|
||||
[32m✓[39m should contain highly professional B2B sections in German AGB[32m 5[2mms[22m[39m
|
||||
[31m [31m×[31m should contain professional corresponding B2B sections in English Terms[39m[32m 2[2mms[22m[39m
|
||||
[32m✓[39m tests/task-12-links.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
||||
[32m✓[39m tests/messen-content.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
||||
[32m✓[39m lib/mail/mailer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
||||
[90mstderr[2m | components/layout/LanguageSwitcher.test.tsx[2m > [22m[2mLanguageSwitcher TDD[2m > [22m[2mtranslates /en/trade-fairs to /de/messen when switching to German
|
||||
[22m[39mReact does not recognize the `transitionMessage` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `transitionmessage` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
|
||||
|
||||
[32m✓[39m components/layout/LanguageSwitcher.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 25[2mms[22m[39m
|
||||
[32m✓[39m components/search/AISearchResults.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 52[2mms[22m[39m
|
||||
[90mstderr[2m | components/blocks/ReferencesSlider.test.tsx[2m > [22m[2mReferencesSlider TDD[2m > [22m[2mrenders reference tiles correctly
|
||||
[22m[39mReceived `true` for a non-boolean attribute `fill`.
|
||||
|
||||
If you want to write it to the DOM, pass a string instead: fill="true" or fill={value.toString()}.
|
||||
|
||||
[32m✓[39m components/blocks/ReferencesSlider.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
||||
[32m✓[39m lib/env.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
||||
[90mstdout[2m | app/[locale]/referenzen/[slug]/page.test.tsx
|
||||
[22m[39m[Config] Initializing Toggles: { feedbackEnabled: [33mfalse[39m }
|
||||
|
||||
[32m✓[39m app/[locale]/referenzen/[slug]/page.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 26[2mms[22m[39m
|
||||
|
||||
[31m⎯⎯⎯⎯⎯⎯⎯[39m[1m[41m Failed Tests 1 [49m[22m[31m⎯⎯⎯⎯⎯⎯⎯[39m
|
||||
|
||||
[41m[1m FAIL [22m[49m tests/legal-pages.test.ts[2m > [22mLegal Pages Content Validation[2m > [22mshould contain professional corresponding B2B sections in English Terms
|
||||
[31m[1mError[22m: ENOENT: no such file or directory, open '/Volumes/Alpha SSD/Coding/e-tib.com/content/en/terms.mdx'[39m
|
||||
[36m [2m❯[22m tests/legal-pages.test.ts:[2m25:24[22m[39m
|
||||
[90m 23|[39m
|
||||
[90m 24|[39m it('should contain professional corresponding B2B sections in Englis…
|
||||
[90m 25|[39m [35mconst[39m content [33m=[39m fs[33m.[39m[34mreadFileSync[39m(enTermsPath[33m,[39m [32m'utf8'[39m)[33m;[39m
|
||||
[90m |[39m [31m^[39m
|
||||
[90m 26|[39m
|
||||
[90m 27|[39m [34mexpect[39m(content)[33m.[39m[34mtoContain[39m([32m'title: "Terms and Conditions"'[39m)[33m;[39m
|
||||
|
||||
[31m[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯[22m[39m
|
||||
|
||||
|
||||
[2m Test Files [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m10 passed[39m[22m[90m (11)[39m
|
||||
[2m Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m26 passed[39m[22m[90m (27)[39m
|
||||
[2m Start at [22m 14:54:33
|
||||
[2m Duration [22m 5.94s[2m (transform 5.27s, setup 0ms, import 9.43s, tests 222ms, environment 13.80s)[22m
|
||||
|
||||
ELIFECYCLE Test failed. See above for more details.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> tsc --noEmit
|
||||
|
||||
|
||||
> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
|
||||
> tsc --noEmit
|
||||
|
||||
ELIFECYCLE Command failed.
|
||||
|
||||
25
INSTRUCTIONS.md
Normal file
25
INSTRUCTIONS.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Antigravity Instructions & Project Rules
|
||||
|
||||
This document contains critical project-specific rules for the e-tib.com codebase. Antigravity must read and follow these rules at all times.
|
||||
|
||||
## 1. Bilingual Site (German & English) - STRICT
|
||||
- **Always update BOTH languages!** The website is bilingual.
|
||||
- When changing content, adding pages, or updating text, you MUST always check and update both `content/de/` and `content/en/` simultaneously.
|
||||
- When adding new components with text, ensure they are built to support both languages or update the respective translation files/mechanisms. Do NOT just hardcode German text and forget the English version.
|
||||
|
||||
## 2. No Dummy Data / No Hallucinations
|
||||
- Do not invent or hallucinate placeholder text (e.g., "Nachweis liegt vor" for certificates that don't exist).
|
||||
- Only implement what is explicitly requested or what matches provided assets/data.
|
||||
- If data is missing, ask the user instead of inventing placeholders.
|
||||
|
||||
## 3. Git Workflow & RC Tagging
|
||||
- When instructed to "commit and tag" (or similar), always follow this process:
|
||||
1. Add and commit the changes with conventional commits (`feat:`, `fix:`, `chore:`, etc.).
|
||||
2. Check the latest tag using `git describe --tags --abbrev=0`.
|
||||
3. Increment the RC (Release Candidate) number of the current version (e.g., if latest is `v2.2.13-rc19`, the new tag is `v2.2.13-rc20`).
|
||||
4. Create the tag: `git tag vX.Y.Z-rcN`.
|
||||
5. Push the commit and the tag: `git push origin main --tags`.
|
||||
|
||||
## 4. Work Exclusively on the Correct Branch
|
||||
- Follow the `USER_GLOBAL` directive: If on `main`, create a branch unless explicitly told to work on `main`.
|
||||
- For e-tib.com, often small fixes are done on `main` if the user commands it ("auf main arbeiten"), but always verify context.
|
||||
BIN
after-login.png
Normal file
BIN
after-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
@@ -10,7 +10,6 @@ import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { getAllReferences } from '@/lib/references';
|
||||
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
|
||||
// Import components used in MDX
|
||||
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
||||
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
|
||||
@@ -18,7 +17,6 @@ import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blo
|
||||
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
|
||||
import { CompanyTimeline } from '@/components/blocks/CompanyTimeline';
|
||||
import { JobListingBlock } from '@/components/blocks/JobListingBlock';
|
||||
import { CallToAction } from '@/components/blocks/CallToAction';
|
||||
import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
|
||||
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
@@ -41,7 +39,6 @@ const mdxComponents = {
|
||||
HomeReferencesSlider,
|
||||
CompanyTimeline,
|
||||
JobListingBlock,
|
||||
CallToAction,
|
||||
ServiceDetailGrid,
|
||||
BenefitGrid,
|
||||
InteractiveGermanyMap,
|
||||
@@ -51,6 +48,7 @@ const mdxComponents = {
|
||||
TeamGrid,
|
||||
JsonLd,
|
||||
Button,
|
||||
Badge,
|
||||
AnimatedCounter,
|
||||
GrowthChart,
|
||||
DeepDrillAnimation,
|
||||
@@ -187,7 +185,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
|
||||
<div className={`flex flex-col min-h-screen ${isFullBleed ? '' : 'bg-white'}`}>
|
||||
{/* Generic Hero Section (only for standard pages) */}
|
||||
{!isFullBleed && (
|
||||
<section className="bg-primary-dark text-white pt-40 pb-12 md:pt-56 md:pb-16 min-h-[40vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
||||
<section key={`hero-section-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
||||
|
||||
{/* Featured Image Header */}
|
||||
{post.frontmatter.featuredImage ? (
|
||||
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
||||
<div className="relative w-full h-[50vh] md:h-[70vh] min-h-[350px] md:min-h-[500px] overflow-hidden group">
|
||||
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
|
||||
<Image
|
||||
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
<div className="bg-neutral-light min-h-screen">
|
||||
{/* Hero Section - Immersive Magazine Feel */}
|
||||
<Reveal>
|
||||
<article className="relative h-[50vh] md:h-[70vh] min-h-[400px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
|
||||
<article className="relative h-[40vh] md:h-[70vh] min-h-[300px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
|
||||
{featuredPost && featuredPost.frontmatter.featuredImage && (
|
||||
<>
|
||||
<Image
|
||||
|
||||
@@ -19,7 +19,6 @@ import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
||||
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
|
||||
import { InitialLoader } from '@/components/providers/InitialLoader';
|
||||
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
@@ -36,7 +35,7 @@ export async function generateMetadata(props: {
|
||||
return {
|
||||
title: {
|
||||
template: '%s | E-TIB',
|
||||
default: 'E-TIB | Die Experten für Kabeltiefbau',
|
||||
default: 'E-TIB | Die Experten für Kabelnetzbau',
|
||||
},
|
||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||
metadataBase: new URL(baseUrl),
|
||||
@@ -93,13 +92,18 @@ export default async function Layout(props: {
|
||||
children: [
|
||||
{ label: safeLocale === 'de' ? 'Firma' : 'Company', url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}` },
|
||||
{ label: safeLocale === 'de' ? 'Unser Team' : 'Our Team', url: `/${safeLocale}/team` },
|
||||
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` }
|
||||
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` },
|
||||
{ label: safeLocale === 'de' ? 'Standorte' : 'Locations', url: `/${safeLocale}/standorte` }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: safeLocale === 'de' ? 'Referenzen' : 'References',
|
||||
url: `/${safeLocale}/referenzen`
|
||||
},
|
||||
{
|
||||
label: safeLocale === 'de' ? 'Karriere' : 'Career',
|
||||
url: `/${safeLocale}/${safeLocale === 'de' ? 'karriere' : 'career'}`
|
||||
},
|
||||
{
|
||||
label: safeLocale === 'de' ? 'Messen' : 'Events',
|
||||
url: `/${safeLocale}/${await mapFileSlugToTranslated('messen', safeLocale)}`
|
||||
@@ -130,6 +134,7 @@ export default async function Layout(props: {
|
||||
'CompanyTimeline',
|
||||
'TeamGrid',
|
||||
'AISearch',
|
||||
'GrowthChart',
|
||||
];
|
||||
const clientMessages: Record<string, any> = {};
|
||||
for (const key of clientKeys) {
|
||||
|
||||
@@ -67,22 +67,48 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
|
||||
const mdxComponents = {
|
||||
Heading,
|
||||
h1: (props: any) => <Heading level={1} size="2" className="hidden" {...props} />, // Hidden because Hero handles H1
|
||||
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4" {...props} />,
|
||||
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4 text-neutral-dark" {...props} />,
|
||||
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
|
||||
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
|
||||
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
|
||||
ul: (props: any) => <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 my-12" {...props} />,
|
||||
ul: (props: any) => <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 my-12" {...props} />,
|
||||
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
|
||||
li: (props: any) => (
|
||||
<li className="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>
|
||||
),
|
||||
li: (props: any) => {
|
||||
// Smart Metric Card Logic
|
||||
let text = '';
|
||||
if (typeof props.children === 'string') {
|
||||
text = props.children;
|
||||
} else if (Array.isArray(props.children)) {
|
||||
text = props.children.map((c: any) => typeof c === 'string' ? c : '').join('');
|
||||
}
|
||||
|
||||
const match = text.match(/^([\d.,]+)\s*(m|Satz|Stück|km|kV|Fasern)\b(.*)/);
|
||||
|
||||
if (match && typeof props.children === 'string') {
|
||||
const [, value, unit, rest] = match;
|
||||
return (
|
||||
<li className="bg-white p-6 md:p-8 rounded-3xl border border-neutral-100 flex flex-col justify-center hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:border-primary/30 transition-all duration-500 group hover:-translate-y-1 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-neutral-50 to-transparent rounded-bl-full -z-10 group-hover:from-primary/5 transition-colors duration-500" />
|
||||
<div className="flex items-baseline gap-2 mb-3">
|
||||
<span className="text-4xl md:text-5xl font-bold font-heading text-primary tracking-tight">{value}</span>
|
||||
<span className="text-lg md:text-xl font-bold text-neutral-400">{unit}</span>
|
||||
</div>
|
||||
<span className="text-neutral-600 font-semibold text-sm md:text-base leading-snug">{rest.replace(/^[\s-]*\s/, '')}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="bg-neutral-50 p-6 rounded-3xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-all duration-300 group hover:-translate-y-1">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-primary/20 transition-colors">
|
||||
<svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-neutral-700 font-semibold leading-relaxed">{props.children}</span>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
|
||||
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
|
||||
blockquote: (props: any) => (
|
||||
@@ -95,62 +121,62 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-white">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-[#050B14] text-white pt-40 pb-16 md:pt-48 md:pb-20 min-h-[35vh] flex flex-col justify-end relative overflow-hidden">
|
||||
<section className="bg-[#050B14] text-white pt-28 pb-10 md:pt-52 md:pb-24 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
||||
{reference.frontmatter.featuredImage && (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={reference.frontmatter.featuredImage}
|
||||
alt={reference.frontmatter.title}
|
||||
fill
|
||||
className="object-cover opacity-25 mix-blend-luminosity scale-105"
|
||||
className="object-cover opacity-30 mix-blend-luminosity scale-105"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/60 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-transparent to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/70 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-[#050B14]/40 to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 opacity-20 z-0">
|
||||
<div className="absolute inset-0 opacity-20 z-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/30 via-transparent to-transparent" />
|
||||
</div>
|
||||
<Container className="relative z-10">
|
||||
<div className="mb-12">
|
||||
<TrackedLink
|
||||
href={`/${locale}/referenzen`}
|
||||
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||||
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-xs font-bold"
|
||||
eventProperties={{ location: 'reference_back_btn' }}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
{t('backToOverview')}
|
||||
</TrackedLink>
|
||||
</div>
|
||||
<div className="max-w-4xl">
|
||||
<Badge variant="accent" className="mb-4 md:mb-6">
|
||||
<div className="max-w-5xl">
|
||||
<Badge variant="accent" className="mb-6">
|
||||
{t('projectReference')}
|
||||
</Badge>
|
||||
<Heading level={1} variant="white" className="mb-8 leading-tight">
|
||||
<Heading level={1} variant="white" className="mb-10 leading-[1.1] md:text-6xl lg:text-7xl drop-shadow-lg">
|
||||
{reference.frontmatter.title}
|
||||
</Heading>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12">
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<MapPin className="w-8 h-8 text-primary shrink-0" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mt-12 md:mt-16">
|
||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
||||
<MapPin className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('location')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.location}</p>
|
||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('location')}</p>
|
||||
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<Briefcase className="w-8 h-8 text-primary shrink-0" />
|
||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
||||
<Briefcase className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('client')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.client}</p>
|
||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('client')}</p>
|
||||
<p className="font-semibold text-lg md:text-xl text-white/90 line-clamp-2">{reference.frontmatter.client}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
|
||||
<Calendar className="w-8 h-8 text-primary shrink-0" />
|
||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
||||
<Calendar className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
||||
<div>
|
||||
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('period')}</p>
|
||||
<p className="font-semibold text-lg">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
|
||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('period')}</p>
|
||||
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { getAllReferences } from '@/lib/references';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { MapPin, Calendar, Briefcase, ArrowUpRight } from 'lucide-react';
|
||||
import { MapPin, Calendar, Briefcase } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
import { HeroSection } from '@/components/blocks/HeroSection';
|
||||
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
|
||||
interface PageProps {
|
||||
@@ -22,7 +23,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
|
||||
return {
|
||||
title: 'Referenzen | E-TIB Gruppe',
|
||||
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabeltiefbau, Spülbohrtechnik und Netzinfrastruktur.',
|
||||
description: 'Erfolgreich abgeschlossene Projekte der E-TIB Gruppe im Bereich Kabelnetzbau, Spülbohrtechnik und Netzinfrastruktur.',
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${locale}/referenzen`,
|
||||
languages: {
|
||||
@@ -72,13 +73,26 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||
{/* Map Hero Section */}
|
||||
{/* Hero Section */}
|
||||
<HeroSection
|
||||
badge="Referenzen"
|
||||
title="Erfolgreich umgesetzte Projekte."
|
||||
subtitle="Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben."
|
||||
alignment="center"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
/>
|
||||
|
||||
{/* Map Section */}
|
||||
<InteractiveGermanyMap
|
||||
isHero={true}
|
||||
badge={locale === 'en' ? 'Our References' : 'Unsere Referenzen'}
|
||||
title={locale === 'en' ? 'Successfully realized projects.' : 'Erfolgreich umgesetzte Projekte.'}
|
||||
description={locale === 'en' ? 'From broadband expansion to complex 110kV lines: A selection of our nationwide projects where we have created infrastructure for the future. Discover our locations.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben. Entdecken Sie unsere Standorte.'}
|
||||
description={locale === 'en' ? 'From broadband expansion to complex 110kV lines: A selection of our nationwide projects where we have created infrastructure for the future.' : 'Vom Breitbandausbau bis zur komplexen 110kV-Trasse: Ein Auszug unserer bundesweiten Projekte, bei denen wir Infrastruktur für die Zukunft geschaffen haben.'}
|
||||
locations={enrichedLocations}
|
||||
stats={[
|
||||
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
|
||||
{ value: '200', suffix: '+', label: locale === 'en' ? 'Completed Projects' : 'Abgeschlossene Projekte' }
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
@@ -88,11 +102,8 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
<TrackedLink
|
||||
key={ref.slug}
|
||||
href={`/${locale}/referenzen/${ref.slug}`}
|
||||
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
|
||||
eventProperties={{
|
||||
location: 'referenzen_grid',
|
||||
target_slug: ref.slug,
|
||||
}}
|
||||
eventProperties={{ location: 'reference_list_card', slug: ref.slug }}
|
||||
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
|
||||
>
|
||||
<div className="flex flex-col h-full relative">
|
||||
{/* Image Section */}
|
||||
@@ -102,14 +113,14 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
src={ref.frontmatter.featuredImage}
|
||||
alt={ref.frontmatter.title}
|
||||
fill
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:grayscale-0"
|
||||
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%]"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[#f4f4f5] flex items-center justify-center">
|
||||
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/5 group-hover:bg-transparent transition-colors duration-500" />
|
||||
<div className="absolute inset-0 bg-black/5 transition-colors duration-500" />
|
||||
|
||||
{/* Location Badge */}
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
@@ -122,12 +133,12 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 group-hover:text-primary transition-colors line-clamp-3 leading-[1.2]">
|
||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 transition-colors line-clamp-3 leading-[1.2] group-hover:text-primary">
|
||||
{ref.frontmatter.title}
|
||||
</h3>
|
||||
|
||||
{/* Technical Meta Data */}
|
||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 pr-16 md:pr-20 relative z-10">
|
||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||
<div className="min-w-0">
|
||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">{t('client')}</span>
|
||||
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
||||
@@ -144,11 +155,6 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industrial Hover Indicator */}
|
||||
<div className="absolute right-6 bottom-6 md:right-8 md:bottom-8 w-10 h-10 bg-neutral-50 border border-neutral-200 rounded flex items-center justify-center transform group-hover:bg-primary group-hover:border-primary transition-colors duration-300">
|
||||
<ArrowUpRight className="w-5 h-5 text-neutral-dark group-hover:text-white transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
|
||||
</div>
|
||||
</div>
|
||||
</TrackedLink>
|
||||
))}
|
||||
|
||||
187
app/[locale]/standorte/[slug]/page.tsx
Normal file
187
app/[locale]/standorte/[slug]/page.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Container, Badge, Heading } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { standorteData, getStandortById } from '@/lib/standorte-data';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { MapPin, Phone, Mail, Navigation, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const locales = ['de', 'en'];
|
||||
const params: { locale: string; slug: string }[] = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
for (const standort of standorteData) {
|
||||
params.push({ locale, slug: standort.id });
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { locale, slug } = await params;
|
||||
if (locale !== 'de' && locale !== 'en') return {};
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) return {};
|
||||
|
||||
return {
|
||||
title: `${standort.name} | E-TIB Gruppe`,
|
||||
description: standort.description[locale],
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${locale}/standorte/${slug}`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/standorte/${slug}`,
|
||||
en: `${SITE_URL}/en/standorte/${slug}`,
|
||||
'x-default': `${SITE_URL}/en/standorte/${slug}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StandortDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
|
||||
const { locale, slug } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-white">
|
||||
{/* Generic Hero Section to match standard pages */}
|
||||
<section key={`standort-hero-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
|
||||
</div>
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-4xl">
|
||||
<Badge variant="accent" className="mb-4 md:mb-6 shadow-sm flex inline-flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{standort.type === 'hq' ? (safeLocale === 'de' ? 'Hauptsitz' : 'Headquarters') : (safeLocale === 'de' ? 'Niederlassung' : 'Branch')}
|
||||
</Badge>
|
||||
<Heading level={1} variant="white" className="mb-0 drop-shadow-sm">
|
||||
{standort.name}
|
||||
</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Container className="py-16 md:py-24">
|
||||
{/* Excerpt/Lead paragraph */}
|
||||
<div className="mb-16 max-w-4xl mx-auto">
|
||||
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
|
||||
{standort.description[safeLocale]}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main content in prose style */}
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="prose prose-lg md:prose-xl prose-neutral max-w-none prose-headings:font-heading prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:border-b prose-h2:border-neutral-200 prose-h2:pb-4 prose-h3:text-2xl prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:font-medium prose-a:underline prose-a:underline-offset-4 prose-a:decoration-primary/30 hover:prose-a:decoration-primary prose-strong:text-text-primary prose-strong:font-semibold prose-li:text-text-secondary prose-ul:list-disc prose-ol:list-decimal">
|
||||
|
||||
<img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" src={standort.image} alt={standort.name} />
|
||||
|
||||
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">{safeLocale === 'de' ? 'Unsere Leistungen vor Ort' : 'Our Local Services'}</h2>
|
||||
<ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl">
|
||||
{standort.keyFeatures[safeLocale].map((feature, idx) => (
|
||||
<li key={idx} className="relative pl-6 before:content-[''] before:absolute before:left-0 before:top-[0.6em] before:w-2 before:h-2 before:bg-primary/50 before:rounded-sm">
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">{safeLocale === 'de' ? 'Kontakt & Anfahrt' : 'Contact & Directions'}</h2>
|
||||
|
||||
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
|
||||
<strong>{safeLocale === 'de' ? 'Adresse:' : 'Address:'}</strong><br/>
|
||||
{standort.address.street}<br/>
|
||||
{standort.address.city}
|
||||
</p>
|
||||
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
|
||||
<TrackedLink
|
||||
href={`https://maps.google.com/?q=${encodeURIComponent(`${standort.address.street}, ${standort.address.city}`)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold flex items-center gap-1 w-fit"
|
||||
eventProperties={{ location: 'standort_maps_link', slug: standort.id }}
|
||||
>
|
||||
{safeLocale === 'de' ? 'In Google Maps öffnen' : 'Open in Google Maps'}
|
||||
<ArrowUpRight className="w-4 h-4 inline-block" />
|
||||
</TrackedLink>
|
||||
</p>
|
||||
|
||||
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
|
||||
<strong>{safeLocale === 'de' ? 'Telefon:' : 'Phone:'}</strong><br/>
|
||||
<a href={`tel:${standort.contact.phone.replace(/[^0-9+]/g, '')}`} className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold">
|
||||
{standort.contact.phone}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl">
|
||||
<strong>Email:</strong><br/>
|
||||
<a href={`mailto:${standort.contact.email}`} className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold">
|
||||
{standort.contact.email}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{standort.gallery && standort.gallery.length > 0 && (
|
||||
<>
|
||||
<h2 className="mt-16 mb-6 border-b border-neutral-100 pb-4">Galerie</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose my-12">
|
||||
{standort.gallery.map((img, idx) => (
|
||||
<div key={idx} className="relative aspect-[4/3] rounded-2xl overflow-hidden bg-neutral-100 border border-neutral-200 shadow-md hover:shadow-xl transition-shadow duration-300">
|
||||
<Image src={img} alt={`Gallery ${idx + 1}`} fill className="object-cover transition-transform duration-700 hover:scale-105" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
{/* Support Section */}
|
||||
<Container>
|
||||
<div className="mt-12 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
|
||||
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
|
||||
<TrackedLink
|
||||
href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className={getButtonClasses('accent', 'lg')}
|
||||
eventProperties={{
|
||||
location: 'standort_page_support_cta',
|
||||
page_slug: slug,
|
||||
}}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
|
||||
{t('contactUs')}
|
||||
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
<ButtonOverlay variant="accent" />
|
||||
</TrackedLink>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
app/[locale]/standorte/page.tsx
Normal file
142
app/[locale]/standorte/page.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Container } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { MapPin, Navigation, ArrowUpRight } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { standorteData, standorteLocations } from '@/lib/standorte-data';
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
|
||||
return {
|
||||
title: safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte',
|
||||
description: safeLocale === 'en' ? 'Our operational locations across Germany.' : 'Unsere operativen Standorte deutschlandweit.',
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${safeLocale}/standorte`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/standorte`,
|
||||
en: `${SITE_URL}/en/standorte`,
|
||||
'x-default': `${SITE_URL}/en/standorte`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||
{/* Map Hero Section */}
|
||||
<InteractiveGermanyMap
|
||||
isHero={true}
|
||||
locations={standorteLocations}
|
||||
badge={safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte'}
|
||||
title={safeLocale === 'en' ? 'Nationwide operational for you.' : 'Deutschlandweit für Sie im Einsatz.'}
|
||||
description={safeLocale === 'en'
|
||||
? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.'
|
||||
: 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.'}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Container className="relative z-20 mt-16 md:mt-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
||||
{standorteData.map((standort) => (
|
||||
<TrackedLink
|
||||
key={standort.id}
|
||||
href={`/${safeLocale}/standorte/${standort.id}`}
|
||||
eventProperties={{ location: 'standorte_list_card', slug: standort.id }}
|
||||
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
|
||||
>
|
||||
<div className="flex flex-col h-full relative">
|
||||
{/* Image Section */}
|
||||
<div className="relative h-64 w-full bg-[#050B14] overflow-hidden shrink-0 border-b border-neutral-100">
|
||||
<Image
|
||||
src={standort.image}
|
||||
alt={standort.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
|
||||
|
||||
{/* Location Badge */}
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<span className="bg-white/95 backdrop-blur-md text-neutral-dark px-3 py-1.5 rounded-sm text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 shadow-sm border border-neutral-200">
|
||||
<MapPin className="w-3 h-3 text-primary" />
|
||||
{standort.address.city}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
|
||||
{standort.name}
|
||||
</h3>
|
||||
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
||||
</div>
|
||||
|
||||
<p className="text-neutral-500 text-sm leading-relaxed mb-8 line-clamp-3">
|
||||
{standort.description[safeLocale]}
|
||||
</p>
|
||||
|
||||
{/* Meta Data */}
|
||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||
<div className="min-w-0">
|
||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">
|
||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||
</span>
|
||||
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
||||
<Navigation className="w-3.5 h-3.5 text-primary shrink-0 mt-[2px]" />
|
||||
<span className="line-clamp-2">{standort.address.street}, {standort.address.city}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TrackedLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Support Section */}
|
||||
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>
|
||||
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
|
||||
<TrackedLink
|
||||
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className={getButtonClasses('accent', 'lg')}
|
||||
eventProperties={{
|
||||
location: 'standorte_support_cta',
|
||||
}}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
|
||||
{t('contactUs')}
|
||||
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
<ButtonOverlay variant="accent" />
|
||||
</TrackedLink>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
const confirmationHtml = await render(
|
||||
React.createElement(ConfirmationMessage, {
|
||||
name,
|
||||
clientName: 'KLZ Cables',
|
||||
clientName: 'E-TIB GmbH',
|
||||
// brandColor: '#82ed20', // Optional: could be KLZ specific
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
display: none !important; /* Chrome, Safari and Opera */
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
@keyframes bg-pulse-15 {
|
||||
|
||||
@@ -13,11 +13,13 @@ const LeafletMap = dynamic(() => import('@/components/LeafletMap'), {
|
||||
});
|
||||
|
||||
interface ContactMapProps {
|
||||
address: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
address?: string;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
locations?: Array<{name: string, address: string, lat: number, lng: number}>;
|
||||
}
|
||||
|
||||
export default function ContactMap({ address, lat, lng }: ContactMapProps) {
|
||||
return <LeafletMap locations={[{ name: 'E-TIB Gruppe', address, lat, lng }]} />;
|
||||
export default function ContactMap({ address, lat, lng, locations }: ContactMapProps) {
|
||||
const mapLocations = locations || (address && lat && lng ? [{ name: 'E-TIB Gruppe', address, lat, lng }] : []);
|
||||
return <LeafletMap locations={mapLocations} />;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ interface TrackedLinkProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +27,8 @@ export default function TrackedLink({
|
||||
className,
|
||||
children,
|
||||
onClick,
|
||||
target,
|
||||
rel,
|
||||
}: TrackedLinkProps) {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
@@ -41,7 +45,7 @@ export default function TrackedLink({
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={href} className={className} onClick={handleClick}>
|
||||
<Link href={href} className={className} onClick={handleClick} target={target} rel={rel}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -75,13 +75,13 @@ const Icons = {
|
||||
|
||||
export function BenefitGrid({ badge, title, description, benefits }: BenefitGridProps) {
|
||||
return (
|
||||
<section className="bg-white py-24 relative overflow-hidden border-t border-neutral-100">
|
||||
<section className="bg-white py-12 md:py-24 relative overflow-hidden border-t border-neutral-100">
|
||||
<div className="container px-4 max-w-7xl mx-auto relative z-10">
|
||||
|
||||
<div className="max-w-3xl mx-auto text-center mb-16">
|
||||
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
|
||||
{title && (
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
|
||||
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export interface CallToActionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
ctaLabel?: string;
|
||||
ctaHref?: string;
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
|
||||
},
|
||||
};
|
||||
|
||||
export const CallToAction: React.FC<CallToActionProps> = (props) => {
|
||||
const { theme = 'light' } = props;
|
||||
const isDark = theme === 'dark';
|
||||
const t = useTranslations('CallToAction');
|
||||
|
||||
const title = props.title || t('title');
|
||||
const description = props.description || t('description');
|
||||
const ctaLabel = props.ctaLabel || t('ctaLabel');
|
||||
const ctaHref = props.ctaHref || t('ctaHref');
|
||||
|
||||
return (
|
||||
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>
|
||||
<motion.div
|
||||
className="container max-w-3xl mx-auto px-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
>
|
||||
<motion.h2 variants={itemVariants} className={`font-heading text-4xl font-extrabold mb-6 ${isDark ? 'text-white' : 'text-neutral-dark'}`}>
|
||||
{title}
|
||||
</motion.h2>
|
||||
<motion.p variants={itemVariants} className={`text-xl mb-10 leading-relaxed ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>
|
||||
{description}
|
||||
</motion.p>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Button
|
||||
href={ctaHref}
|
||||
size="xl"
|
||||
variant={isDark ? 'white' : 'primary'}
|
||||
>
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -68,31 +68,7 @@ const defaultCertificates: Certificate[] = [
|
||||
type: 'tax',
|
||||
date: '13.02.2025',
|
||||
},
|
||||
{
|
||||
title: 'Unbedenklichkeit IHK',
|
||||
description: 'Industrie- und Handelskammer',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Unbedenklichkeit HWK',
|
||||
description: 'Handwerkskammer',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Handelsregisterauszug',
|
||||
description: 'Amtsgericht Cottbus',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Gewerbe-Anmeldung',
|
||||
description: 'Stadt Guben',
|
||||
type: 'general',
|
||||
},
|
||||
{
|
||||
title: 'Gewerbezentralregister',
|
||||
description: 'Auskunft aus dem Register',
|
||||
type: 'general',
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export function CertificatesBlock({ badge, title, description, certificates = defaultCertificates, hideHeader = false }: CertificatesBlockProps) {
|
||||
@@ -123,12 +99,12 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
||||
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<section className="py-24 bg-neutral-50 relative overflow-hidden">
|
||||
<section className="py-12 md: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>
|
||||
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
|
||||
<p className="text-lg text-neutral-600">{descriptionText}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -138,7 +114,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-neutral-50 relative overflow-hidden">
|
||||
<section className="py-12 md:py-24 bg-neutral-50 relative overflow-hidden">
|
||||
{/* Background Accents */}
|
||||
<div className="absolute top-0 right-0 w-1/3 h-full bg-primary/5 -skew-x-12 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-accent/5 skew-y-12 -translate-x-1/2 blur-3xl rounded-full" />
|
||||
@@ -153,7 +129,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badgeText}</h2>
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
|
||||
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">{titleText}</h3>
|
||||
<p className="text-lg text-neutral-600">{descriptionText}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -180,7 +156,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
||||
key={index}
|
||||
{...wrapperProps as any}
|
||||
variants={itemVariants}
|
||||
className={`group relative rounded-2xl p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
|
||||
className={`group relative rounded-2xl p-6 md:p-8 flex flex-col justify-between overflow-hidden transition-all duration-500 hover:shadow-xl hover:-translate-y-1 border ${
|
||||
isIso
|
||||
? 'bg-neutral-dark text-white border-neutral-800 shadow-lg'
|
||||
: 'bg-white text-neutral-dark border-neutral-200 shadow-md'
|
||||
|
||||
@@ -80,18 +80,18 @@ export function CompanyTimeline({
|
||||
const lineHeight = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
|
||||
|
||||
return (
|
||||
<section className="py-24 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
|
||||
<section className="py-12 md:py-32 bg-[#FAFAFA] relative overflow-hidden">
|
||||
{/* Background Decor */}
|
||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
|
||||
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
|
||||
|
||||
<div className="container max-w-5xl relative z-10">
|
||||
<div className="text-center mb-24">
|
||||
<div className="text-center mb-12 md:mb-24">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-neutral-200 text-neutral-dark text-xs font-bold uppercase tracking-wider mb-6 shadow-sm">
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
|
||||
{finalBadge}
|
||||
</div>
|
||||
<h3 className="font-heading text-4xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
|
||||
<h3 className="font-heading text-3xl md:text-5xl lg:text-6xl font-extrabold text-neutral-dark tracking-tight">
|
||||
{finalTitle}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -62,12 +62,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
// Static fallback for SSR
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
|
||||
<section className="py-12 md:py-24 bg-neutral text-neutral-dark overflow-hidden relative">
|
||||
<Container>
|
||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
|
||||
@@ -79,12 +79,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12 px-4 md:px-0">
|
||||
<div className="max-w-2xl">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-12 px-4 md:px-0">
|
||||
<div className="max-w-2xl text-left">
|
||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||
</div>
|
||||
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
|
||||
{ctaLabel}
|
||||
@@ -92,7 +92,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
|
||||
{items.map((item, idx) => {
|
||||
const isLarge = item.size === 'large';
|
||||
const isMedium = item.size === 'medium';
|
||||
@@ -177,7 +177,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
{item.tag}
|
||||
</span>
|
||||
)}
|
||||
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-3xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
|
||||
<h4 className={`font-heading font-extrabold mb-2 ${isLarge ? 'text-2xl md:text-5xl' : 'text-xl md:text-2xl'}`}>
|
||||
{item.title}
|
||||
</h4>
|
||||
{item.description && (
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
>
|
||||
<motion.h2 variants={itemVariants} className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Direktkontakt</motion.h2>
|
||||
<motion.h3 variants={itemVariants} className="font-heading text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
|
||||
<motion.h3 variants={itemVariants} className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-6">Wir sind für Sie da.</motion.h3>
|
||||
<motion.p variants={itemVariants} className="text-lg text-text-secondary mb-8 leading-relaxed">
|
||||
Haben Sie Fragen zu unseren Leistungen oder möchten Sie ein konkretes Projekt besprechen? Rufen Sie uns an oder schreiben Sie uns eine E-Mail.
|
||||
</motion.p>
|
||||
@@ -129,9 +129,10 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
|
||||
fallback={<div className="h-full w-full bg-neutral-medium animate-pulse" />}
|
||||
>
|
||||
<ContactMap
|
||||
address="Gewerbestraße 22\n03172 Guben"
|
||||
lat={51.9547}
|
||||
lng={14.7214}
|
||||
locations={[
|
||||
{ name: 'E-TIB GmbH (Hauptsitz)', address: 'Gewerbestraße 22\n03172 Guben', lat: 51.9547, lng: 14.7214 },
|
||||
{ name: 'E-TIB Bohrtechnik GmbH', address: 'Bahnhofstraße 180a\n99947 Kirchheilingen', lat: 51.1852, lng: 10.6865 }
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.section>
|
||||
|
||||
@@ -7,13 +7,13 @@ import { useLocale } from 'next-intl';
|
||||
export function DataGridPulse() {
|
||||
const locale = useLocale();
|
||||
return (
|
||||
<div className="relative w-full py-32 bg-[#050B14] overflow-hidden flex flex-col items-center border-y border-primary/20 -mx-[50vw] px-[50vw] mb-16">
|
||||
<div className="relative w-full py-12 md:py-24 bg-neutral-50 border border-neutral-100/80 overflow-hidden flex flex-col items-center rounded-3xl mb-16 shadow-sm">
|
||||
|
||||
{/* Blueprint grid background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 pointer-events-none"
|
||||
className="absolute inset-0 opacity-[0.03] pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(#82ED20 1px, transparent 1px), linear-gradient(90deg, #82ED20 1px, transparent 1px)',
|
||||
backgroundImage: 'linear-gradient(#117C61 1px, transparent 1px), linear-gradient(90deg, #117C61 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px'
|
||||
}}
|
||||
/>
|
||||
@@ -21,25 +21,25 @@ export function DataGridPulse() {
|
||||
{/* Pulsing horizontal lines (Data Flow) */}
|
||||
<motion.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-50 blur-[2px]"
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-1/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/30 to-transparent blur-[1px]"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-white to-transparent opacity-30 blur-[2px]"
|
||||
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-2/3 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary/25 to-transparent blur-[1px]"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-full max-w-4xl mx-auto flex flex-col items-center text-center">
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-primary mb-6">
|
||||
<div className="relative z-10 w-full max-w-2xl px-6 mx-auto flex flex-col items-center text-center">
|
||||
<span className="text-xs md:text-sm font-bold uppercase tracking-widest text-primary mb-4 bg-primary/10 px-3 py-1 rounded-full">
|
||||
{locale === 'en' ? 'Network Expansion in Numbers' : 'Netzausbau in Zahlen'}
|
||||
</h3>
|
||||
</span>
|
||||
|
||||
<div className="bg-black/40 backdrop-blur-md p-12 md:p-16 rounded-3xl border border-white/10 shadow-2xl">
|
||||
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4 leading-none" style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
372.161<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
|
||||
<div className="bg-white/80 backdrop-blur-md p-8 md:p-12 rounded-2xl border border-neutral-200/50 shadow-lg w-full">
|
||||
<div className="font-mono text-4xl md:text-7xl font-black text-neutral-900 tracking-tighter mb-4 leading-none animate-pulse-slow" style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
372.161<span className="text-primary text-2xl md:text-4xl ml-1 font-sans">m</span>
|
||||
</div>
|
||||
<p className="text-white/70 text-lg md:text-xl font-medium max-w-lg mx-auto">
|
||||
<p className="text-text-secondary text-base md:text-lg font-medium max-w-md mx-auto leading-relaxed">
|
||||
{locale === 'en'
|
||||
? 'Cable routes laid since 2023. A massive infrastructure achievement, driven by our own machinery.'
|
||||
: 'Verlegte Kabeltrassen seit 2023. Eine massive Infrastrukturleistung, getragen von unserem eigenen Maschinenpark.'}
|
||||
|
||||
@@ -16,13 +16,13 @@ export function DeepDrillAnimation() {
|
||||
const drillDepth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative w-full py-32 flex flex-col items-center">
|
||||
<div ref={containerRef} className="relative w-full py-12 md:py-32 flex flex-col items-center">
|
||||
|
||||
<div className="absolute inset-0 bg-[#050B14] -mx-[50vw] px-[50vw]" />
|
||||
|
||||
<div className="relative z-10 w-full max-w-3xl mx-auto flex flex-col items-center text-center text-white">
|
||||
|
||||
<h3 className="text-3xl font-heading font-extrabold mb-8 tracking-tight">
|
||||
<h3 className="text-2xl md:text-3xl font-heading font-extrabold mb-8 tracking-tight">
|
||||
{locale === 'en' ? 'Precision in Depth' : 'Präzision in der Tiefe'}
|
||||
</h3>
|
||||
<p className="text-white/60 text-lg mb-16 max-w-xl">
|
||||
@@ -50,8 +50,8 @@ export function DeepDrillAnimation() {
|
||||
transition={{ duration: 0.8 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className="font-mono text-6xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
102.771<span className="text-primary text-3xl md:text-5xl ml-2">m</span>
|
||||
<div className="font-mono text-5xl md:text-8xl font-black text-white tracking-tighter mb-4" style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
102.771<span className="text-primary text-2xl md:text-5xl ml-2">m</span>
|
||||
</div>
|
||||
<div className="text-sm font-bold uppercase tracking-widest text-primary">
|
||||
{locale === 'en' ? 'Drilled distance since 2023' : 'Gebohrte Strecke seit 2023'}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const FaqBlock: React.FC<FaqBlockProps> = ({
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-text-primary">
|
||||
<h2 className="text-2xl md:text-4xl font-bold text-text-primary">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -2,27 +2,38 @@
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function GrowthChart() {
|
||||
const t = useTranslations('GrowthChart');
|
||||
|
||||
const data = [
|
||||
{ year: '2023', hdd: 29436, kabel: 76727 },
|
||||
{ year: '2024', hdd: 28680, kabel: 131964 },
|
||||
{ year: '2025', hdd: 44655, kabel: 163470 },
|
||||
{ year: '2016', offener: 5000, hdd: 1200, kabel: 16064 },
|
||||
{ year: '2017', offener: 33401, hdd: 8363, kabel: 84675 },
|
||||
{ year: '2018', offener: 30773, hdd: 10225, kabel: 108127 },
|
||||
{ year: '2019', offener: 32445, hdd: 8505, kabel: 87468 },
|
||||
{ year: '2020', offener: 31305, hdd: 9050, kabel: 93556 },
|
||||
{ year: '2021', offener: 38756, hdd: 10957, kabel: 110127 },
|
||||
{ year: '2022', offener: 41445, hdd: 9505, kabel: 102468 },
|
||||
{ year: '2023', offener: 17908, hdd: 29436, kabel: 76727 },
|
||||
{ year: '2024', offener: 30801, hdd: 28680, kabel: 131964 },
|
||||
{ year: '2025', offener: 38166, hdd: 44655, kabel: 163470 },
|
||||
];
|
||||
|
||||
const maxVal = 163470; // Highest value for scaling
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white rounded-3xl p-8 md:p-12 shadow-sm border border-neutral-100 my-16">
|
||||
<div className="w-full bg-white rounded-3xl p-6 md:p-12 shadow-sm border border-neutral-100 my-8 md:my-16">
|
||||
<div className="mb-10">
|
||||
<h3 className="text-3xl font-heading font-extrabold text-neutral-dark mb-2">Bewiesene Leistungsfähigkeit</h3>
|
||||
<p className="text-neutral-500 font-medium">Unsere verlässliche Baukapazität der letzten drei Jahre (in Metern)</p>
|
||||
<h3 className="text-2xl md:text-3xl font-heading font-extrabold text-neutral-dark mb-2">{t('title')}</h3>
|
||||
<p className="text-neutral-500 font-medium">{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{data.map((item, index) => {
|
||||
const hddWidth = (item.hdd / maxVal) * 100;
|
||||
const kabelWidth = (item.kabel / maxVal) * 100;
|
||||
const offenerWidth = (item.offener / maxVal) * 100;
|
||||
const hddWidth = (item.hdd / maxVal) * 100;
|
||||
|
||||
return (
|
||||
<div key={item.year} className="relative">
|
||||
@@ -40,7 +51,23 @@ export function GrowthChart() {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
|
||||
Kabel: {new Intl.NumberFormat('de-DE').format(item.kabel)}m
|
||||
{t('kabel')}: {new Intl.NumberFormat('de-DE').format(item.kabel)}m
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offener Tiefbau Bar */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 bg-neutral-100 rounded-full h-5 overflow-hidden relative">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: `${offenerWidth}%` }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
transition={{ duration: 1.5, delay: 0.15 + 0.1 * index, ease: "easeOut" }}
|
||||
className="absolute top-0 left-0 h-full bg-neutral-300 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
|
||||
{t('offener')}: {new Intl.NumberFormat('de-DE').format(item.offener)}m
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +83,7 @@ export function GrowthChart() {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24 text-right text-xs font-bold text-neutral-500 uppercase">
|
||||
HDD: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
|
||||
{t('hdd')}: {new Intl.NumberFormat('de-DE').format(item.hdd)}m
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,14 +92,18 @@ export function GrowthChart() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex gap-6 border-t border-neutral-100 pt-6">
|
||||
<div className="mt-8 flex flex-wrap gap-6 border-t border-neutral-100 pt-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-primary" />
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">Kabelverlegung</span>
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">{t('kabel')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-neutral-300" />
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">{t('offener')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-neutral-dark" />
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">HDD Spülbohrung</span>
|
||||
<span className="text-xs font-bold text-neutral-500 uppercase">{t('hdd')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Badge, Container, Heading } from '@/components/ui';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -39,13 +40,15 @@ const itemVariants = {
|
||||
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
|
||||
const bgSrc = backgroundImage?.sizes?.card?.url || backgroundImage?.url;
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`relative min-h-[45vh] md:min-h-[55vh] flex items-center pt-32 pb-20 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
|
||||
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
|
||||
>
|
||||
{bgSrc && (
|
||||
<motion.div
|
||||
key={`hero-bg-${pathname}`}
|
||||
className="absolute inset-0 z-0"
|
||||
initial={{ scale: 1.05, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
@@ -75,6 +78,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||
)}
|
||||
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
|
||||
<motion.div
|
||||
key={`hero-content-${pathname}`}
|
||||
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import Image from 'next/image';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
interface HeroVideoProps {
|
||||
data?: any;
|
||||
@@ -24,10 +25,11 @@ interface HeroVideoProps {
|
||||
|
||||
export function HeroVideo(props: HeroVideoProps) {
|
||||
const { data } = props;
|
||||
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELTIEFBAU';
|
||||
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELNETZBAU';
|
||||
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
|
||||
const explicitNoVideo = props.videoUrl === 'none' || props.videoUrl === '';
|
||||
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
|
||||
const pathname = usePathname();
|
||||
|
||||
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
|
||||
const posterAlt = props.backgroundImage?.alt || data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
|
||||
@@ -35,28 +37,19 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
|
||||
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
|
||||
|
||||
let currentLocale = 'de';
|
||||
try {
|
||||
// We can't unconditionally call hooks if they might fail outside context, but HeroVideo is usually in context.
|
||||
// However, to be safe and avoid adding new imports, we can use window.location or default to 'de' if not available.
|
||||
if (typeof window !== 'undefined') {
|
||||
currentLocale = window.location.pathname.startsWith('/en') ? 'en' : 'de';
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to default locale if window/location access fails
|
||||
}
|
||||
const currentLocale = pathname?.startsWith('/en') ? 'en' : 'de';
|
||||
|
||||
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/${currentLocale === 'de' ? 'contact' : 'contact'}`;
|
||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
||||
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
||||
{/* Background color while video loads */}
|
||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||
|
||||
{videoUrl ? (
|
||||
<video
|
||||
key={videoUrl}
|
||||
key={`${pathname}-${videoUrl}`}
|
||||
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||
src={videoUrl}
|
||||
autoPlay
|
||||
@@ -67,6 +60,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
key={`img-${pathname}-${posterSrc}`}
|
||||
src={posterSrc}
|
||||
alt={posterAlt}
|
||||
fill
|
||||
@@ -88,13 +82,14 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
|
||||
<div className="container relative z-[4] text-center px-4">
|
||||
<motion.div
|
||||
key={`hero-content-${pathname}`}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<h1
|
||||
className="font-heading font-bold text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
|
||||
dangerouslySetInnerHTML={{ __html: title.replace('KABELTIEFBAU', '<span class="text-primary-light">KABELTIEFBAU</span>').replace(/\\n|\n/g, '<br />') }}
|
||||
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
|
||||
dangerouslySetInnerHTML={{ __html: title.replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/KABELNETZBAU|Kabelnetzbau/g, '<span class="text-primary-light">Kabelnetzbau</span>').replace(/\\n|\n/g, '<br />') }}
|
||||
/>
|
||||
|
||||
<p className="text-lg md:text-xl lg:text-2xl text-white/90 max-w-3xl mx-auto mb-12 drop-shadow">
|
||||
|
||||
@@ -4,17 +4,18 @@ import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
import { Location, defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
import { Location, defaultLocations, minorLocations, projectLocations } from '@/lib/map-data';
|
||||
import { standorteLocations } from '@/lib/standorte-data';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
const allLocations = [...defaultLocations, ...minorLocations];
|
||||
|
||||
interface Stat {
|
||||
value: string;
|
||||
suffix?: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface InteractiveGermanyMapProps {
|
||||
@@ -24,6 +25,7 @@ interface InteractiveGermanyMapProps {
|
||||
stats?: Stat[];
|
||||
locations?: Location[];
|
||||
isHero?: boolean;
|
||||
hideStandorte?: boolean;
|
||||
}
|
||||
|
||||
export function InteractiveGermanyMap({
|
||||
@@ -31,35 +33,59 @@ export function InteractiveGermanyMap({
|
||||
title,
|
||||
description,
|
||||
stats,
|
||||
locations = allLocations,
|
||||
isHero = false
|
||||
locations = projectLocations,
|
||||
isHero = false,
|
||||
hideStandorte = false
|
||||
}: InteractiveGermanyMapProps) {
|
||||
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
||||
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = (loc: Location) => {
|
||||
if (hoverTimeout) clearTimeout(hoverTimeout);
|
||||
setActiveLocation(loc);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
const timeout = setTimeout(() => {
|
||||
setActiveLocation(null);
|
||||
}, 200);
|
||||
setHoverTimeout(timeout);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('InteractiveGermanyMap');
|
||||
const tStandard = useTranslations('StandardPage');
|
||||
|
||||
const finalStats = stats || [
|
||||
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
|
||||
{ value: '2', suffix: '+', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte' },
|
||||
{ value: '3', suffix: '', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte', href: `/${locale}/standorte` },
|
||||
];
|
||||
|
||||
const finalBadge = badge || tStandard('badge');
|
||||
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
|
||||
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||
|
||||
const hq = locations.find((l) => l.type === 'hq');
|
||||
const branch = locations.find((l) => l.type === 'branch');
|
||||
const projects = locations.filter((l) => l.type === 'project');
|
||||
const minorNodes = locations.filter((l) => l.type === 'minor_node');
|
||||
const finalLocations = React.useMemo(() => {
|
||||
if (hideStandorte) return locations;
|
||||
|
||||
// Ensure the 3 standorte are always included, without duplicating if already present by ID
|
||||
const base = [...locations];
|
||||
standorteLocations.forEach(sl => {
|
||||
if (!base.some(l => l.id === sl.id)) {
|
||||
base.push(sl as Location);
|
||||
}
|
||||
});
|
||||
return base;
|
||||
}, [locations, hideStandorte]);
|
||||
|
||||
return (
|
||||
<div className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-16 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
||||
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
||||
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
||||
{/* Animated Border Glow */}
|
||||
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
||||
<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`} />
|
||||
@@ -75,11 +101,11 @@ export function InteractiveGermanyMap({
|
||||
</div>
|
||||
|
||||
{isHero ? (
|
||||
<h1 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
|
||||
<h1 className="font-heading text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
|
||||
{finalTitle}
|
||||
</h1>
|
||||
) : (
|
||||
<h3 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
|
||||
<h3 className="font-heading text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 text-balance">
|
||||
{finalTitle}
|
||||
</h3>
|
||||
)}
|
||||
@@ -90,17 +116,32 @@ export function InteractiveGermanyMap({
|
||||
|
||||
{/* Industrial Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{finalStats.map((stat, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10">
|
||||
<div className="text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
|
||||
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
|
||||
{finalStats.map((stat, i) => {
|
||||
const StatCard = () => (
|
||||
<div className={`bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group h-full ${stat.href ? 'cursor-pointer' : ''}`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10 flex flex-col h-full justify-between">
|
||||
<div className="text-3xl md:text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
|
||||
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
|
||||
</div>
|
||||
<div className={`text-sm text-white/50 font-medium flex items-center ${stat.href ? 'group-hover:text-primary transition-colors' : ''}`}>
|
||||
{stat.label}
|
||||
{stat.href && <ArrowUpRight className="w-4 h-4 ml-1 opacity-0 -translate-y-1 translate-x-1 group-hover:opacity-100 group-hover:translate-y-0 group-hover:translate-x-0 transition-all duration-300" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-white/50 font-medium">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
|
||||
return stat.href ? (
|
||||
<Link key={i} href={stat.href} className="block group/link hover:-translate-y-1 transition-transform duration-300">
|
||||
<StatCard />
|
||||
</Link>
|
||||
) : (
|
||||
<div key={i}>
|
||||
<StatCard />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +162,7 @@ export function InteractiveGermanyMap({
|
||||
</div>
|
||||
|
||||
{/* Minor Location Markers (The 100+ generic projects) */}
|
||||
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||
<div
|
||||
key={`minor-${idx}`}
|
||||
className="absolute z-10 group/minor cursor-pointer w-10 h-10 flex items-center justify-center"
|
||||
@@ -130,16 +171,17 @@ export function InteractiveGermanyMap({
|
||||
top: `${loc.y}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
onMouseEnter={() => setActiveLocation(loc)}
|
||||
onMouseLeave={() => setActiveLocation(null)}
|
||||
onMouseEnter={() => handleMouseEnter(loc)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => handleMouseEnter(loc)}
|
||||
>
|
||||
<div className="w-1.5 h-1.5 bg-primary/80 rounded-full group-hover/minor:bg-white group-hover/minor:scale-150 transition-all duration-300" />
|
||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Major & Reference Location Markers (HQ, Branch, Project) */}
|
||||
{locations.filter(l => l.type !== 'minor_node').map((loc, idx) => {
|
||||
{/* Major Location Markers (HQ, Branch, Project) */}
|
||||
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => {
|
||||
const isHQ = loc.type === 'hq';
|
||||
const isBranch = loc.type === 'branch';
|
||||
const isActive = activeLocation?.id === loc.id;
|
||||
@@ -149,14 +191,9 @@ export function InteractiveGermanyMap({
|
||||
key={loc.id}
|
||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20'}`}
|
||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
||||
onMouseEnter={() => setActiveLocation(loc)}
|
||||
onMouseLeave={() => setActiveLocation(null)}
|
||||
onClick={() => {
|
||||
if (loc.href) {
|
||||
const href = loc.href.startsWith('/') ? `/${locale}${loc.href}` : loc.href;
|
||||
router.push(href);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => handleMouseEnter(loc)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => handleMouseEnter(loc)}
|
||||
>
|
||||
{/* Ping Animation for HQ / Branch */}
|
||||
{(isHQ || isBranch) && (
|
||||
@@ -191,12 +228,14 @@ export function InteractiveGermanyMap({
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute z-[9999] pointer-events-none"
|
||||
className="absolute z-[9999] pointer-events-auto"
|
||||
style={{
|
||||
left: `${activeLocation.x}%`,
|
||||
top: `${activeLocation.y}%`,
|
||||
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||
}}
|
||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl min-w-[200px] w-max max-w-[300px] text-white overflow-hidden">
|
||||
|
||||
@@ -256,11 +295,15 @@ export function InteractiveGermanyMap({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{activeLocation.href && (
|
||||
<div className="inline-flex items-center text-xs font-bold text-primary">
|
||||
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
|
||||
<Link
|
||||
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
|
||||
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{t('learnMore')}
|
||||
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,13 +71,13 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
||||
<div className="py-12 md:py-24">
|
||||
<div className="container mx-auto">
|
||||
{showFairs && (
|
||||
<div className="mb-24 relative">
|
||||
<div className="mb-12 md:mb-24 relative">
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
||||
className="font-heading font-extrabold text-4xl text-neutral-dark mb-12 flex items-center gap-4"
|
||||
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
|
||||
>
|
||||
<span className="w-12 h-1.5 bg-primary rounded-full" />
|
||||
{fairsTitle}
|
||||
@@ -182,7 +182,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
||||
className="font-heading font-extrabold text-4xl text-neutral-dark mb-8"
|
||||
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
|
||||
>
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ReferencesSlider, Reference } from './ReferencesSlider';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
|
||||
// Mock next/link to render standard <a> tag
|
||||
vi.mock('next/link', () => ({
|
||||
@@ -35,7 +34,7 @@ describe('ReferencesSlider TDD', () => {
|
||||
id: '1',
|
||||
title: 'Referenz Projekt Eins',
|
||||
slug: 'referenz-projekt-eins',
|
||||
category: 'Kabeltiefbau',
|
||||
category: 'Kabelnetzbau',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
@@ -52,45 +51,14 @@ describe('ReferencesSlider TDD', () => {
|
||||
expect(screen.getByText('Referenz Projekt Zwei')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('wraps each tile in a link to /referenzen/[slug]', () => {
|
||||
it('renders each tile as a static block and NOT wrapped in a link', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const links = screen.getAllByTestId('reference-tile-link');
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-eins');
|
||||
expect(links[1].getAttribute('href')).toBe('/en/referenzen/referenz-projekt-zwei');
|
||||
});
|
||||
const links = screen.queryAllByTestId('reference-tile-link');
|
||||
expect(links).toHaveLength(0);
|
||||
|
||||
it('allows navigation on simple click (no drag)', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
|
||||
|
||||
// Simulate simple click sequence
|
||||
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
|
||||
fireEvent.mouseUp(firstLink);
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||
|
||||
firstLink.dispatchEvent(clickEvent);
|
||||
|
||||
expect(clickEvent.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('prevents navigation if mouse was dragged', () => {
|
||||
render(<ReferencesSlider references={mockReferences} />);
|
||||
|
||||
const firstLink = screen.getAllByTestId('reference-tile-link')[0];
|
||||
|
||||
// Simulate drag sequence
|
||||
fireEvent.mouseDown(firstLink, { pageX: 100, clientX: 100 });
|
||||
// Simulate mouse move of 20px
|
||||
fireEvent.mouseMove(firstLink, { pageX: 120, clientX: 120 });
|
||||
fireEvent.mouseUp(firstLink);
|
||||
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
|
||||
firstLink.dispatchEvent(clickEvent);
|
||||
|
||||
expect(clickEvent.defaultPrevented).toBe(true);
|
||||
const tiles = screen.getAllByTestId('reference-tile');
|
||||
expect(tiles).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -92,27 +92,23 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
dragDistanceRef.current = Math.abs(x - startXRef.current);
|
||||
};
|
||||
|
||||
const handleTileClick = (e: React.MouseEvent) => {
|
||||
if (dragDistanceRef.current > 10) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="referenzen" className="py-24 bg-neutral-dark text-white relative overflow-hidden">
|
||||
<section id="referenzen" className="py-12 md: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">
|
||||
<div>
|
||||
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||
<h3 className="font-heading text-4xl md:text-5xl font-extrabold">{title}</h3>
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
||||
</div>
|
||||
<p className="text-white/60 max-w-md">
|
||||
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseDown={onMouseDown}
|
||||
@@ -137,11 +133,18 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
>
|
||||
<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"
|
||||
data-testid="reference-tile"
|
||||
draggable={false}
|
||||
onClick={(e) => {
|
||||
if (dragDistanceRef.current > 15) {
|
||||
e.preventDefault();
|
||||
}
|
||||
dragDistanceRef.current = 0;
|
||||
}}
|
||||
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
|
||||
>
|
||||
<Image
|
||||
draggable={false}
|
||||
src={imgSrc}
|
||||
alt={ref.title}
|
||||
fill
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ScaleOfImpact() {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full min-h-[70vh] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
|
||||
className="relative w-full min-h-[40vh] md:min-h-[70vh] bg-[#050B14] py-12 md:py-0 flex flex-col items-center justify-center overflow-hidden border-y border-neutral-800"
|
||||
>
|
||||
{/* Massive Watermark Numbers */}
|
||||
<motion.div
|
||||
@@ -31,14 +31,14 @@ export function ScaleOfImpact() {
|
||||
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none overflow-hidden mix-blend-overlay"
|
||||
>
|
||||
<div className="font-heading font-black text-[20vw] leading-none text-white opacity-20 whitespace-nowrap">
|
||||
<span className="font-mono tracking-tighter">474.932</span>
|
||||
<span className="font-mono tracking-tighter">1.435.223</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Foreground Content */}
|
||||
<div className="relative z-10 container max-w-7xl mx-auto px-4 flex flex-col items-center text-center">
|
||||
|
||||
<h2 className="text-3xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
|
||||
<h2 className="text-2xl md:text-5xl font-heading font-extrabold text-white mb-6 tracking-tight">
|
||||
{locale === 'en' ? (
|
||||
<>An infrastructure <br className="md:hidden"/> that connects the country.</>
|
||||
) : (
|
||||
@@ -46,7 +46,7 @@ export function ScaleOfImpact() {
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-16">
|
||||
<p className="text-white/60 text-lg md:text-xl max-w-2xl font-medium mb-4 md:mb-16">
|
||||
{locale === 'en'
|
||||
? "We don't just talk about the energy transition. We build it. In the last 36 months alone, we have completed a route that stretches from our headquarters in Guben all the way to Munich."
|
||||
: "Wir sprechen nicht nur über die Energiewende. Wir bauen sie. Allein in den letzten 36 Monaten haben wir eine Strecke realisiert, die von unserem Hauptsitz in Guben bis nach München reicht."
|
||||
@@ -54,7 +54,7 @@ export function ScaleOfImpact() {
|
||||
</p>
|
||||
|
||||
{/* Abstract SVG Line Animation */}
|
||||
<div className="w-full max-w-4xl h-32 relative">
|
||||
<div className="w-full max-w-4xl h-12 md:h-32 relative">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -73,26 +73,30 @@ export function ScaleOfImpact() {
|
||||
<motion.path
|
||||
d="M0,50 Q250,0 500,50 T1000,50"
|
||||
fill="none"
|
||||
stroke="#82ED20" // primary color
|
||||
stroke="#ffc107" // warning/yellow color
|
||||
strokeWidth="4"
|
||||
style={{ pathLength }}
|
||||
className="drop-shadow-[0_0_15px_rgba(130,237,32,0.8)]"
|
||||
className="drop-shadow-[0_0_15px_rgba(255,193,7,0.8)]"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Static Data Grid (No jumping text) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-16 w-full max-w-4xl mt-12">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-2 md:gap-12 w-full max-w-5xl mt-4 md:mt-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">100<span className="text-primary">+</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Major Projects' : 'Großprojekte'}</div>
|
||||
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">200 <span className="text-primary">+</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Projects' : 'Projekte'}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">372<span className="text-primary text-3xl">km</span></div>
|
||||
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">974 <span className="text-primary text-xl md:text-2xl">km</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Cable Laying' : 'Kabelverlegung'}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-5xl md:text-6xl font-black text-white tracking-tighter mb-2">102<span className="text-primary text-3xl">km</span></div>
|
||||
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">300 <span className="text-primary text-xl md:text-2xl">km</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'Open Trenching' : 'Offener Tiefbau'}</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="font-mono text-3xl md:text-5xl font-black text-white tracking-tighter mb-2">160 <span className="text-primary text-xl md:text-2xl">km</span></div>
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-white/50">{locale === 'en' ? 'HDD Drilling' : 'Spülbohrung'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,17 +105,17 @@ export function ServiceDetailGrid({
|
||||
panels
|
||||
}: ServiceDetailGridProps) {
|
||||
return (
|
||||
<section className="bg-neutral-50 py-24 border-b border-neutral-100 relative overflow-hidden">
|
||||
<section className="bg-neutral-50 py-12 md:py-24 border-b border-neutral-100 relative overflow-hidden">
|
||||
|
||||
<div className="container relative z-10 px-4 max-w-7xl mx-auto">
|
||||
<div className="max-w-3xl mx-auto text-center mb-20">
|
||||
<div className="max-w-3xl mb-12 md:mb-20 text-left">
|
||||
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-4">{badge}</h2>}
|
||||
{title && (
|
||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-6">
|
||||
<h3 className="font-heading text-2xl md:text-5xl font-extrabold text-neutral-dark mb-6">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<div className="w-20 h-2 bg-primary mx-auto mb-8"></div>
|
||||
<div className="w-20 h-2 bg-primary mb-8"></div>
|
||||
|
||||
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
|
||||
{descriptionParagraphs?.map((p, i) => {
|
||||
@@ -141,7 +141,7 @@ export function ServiceDetailGrid({
|
||||
className="max-w-4xl mx-auto mb-20 grid md:grid-cols-2 gap-0 bg-white rounded-2xl overflow-hidden shadow-lg border border-neutral-200 text-left"
|
||||
>
|
||||
{problemStatement && (
|
||||
<div className="p-8 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
|
||||
<div className="p-6 md:p-12 border-b md:border-b-0 md:border-r border-neutral-100 bg-red-50/50">
|
||||
<div className="flex items-center mb-6 text-red-600">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
@@ -154,7 +154,7 @@ export function ServiceDetailGrid({
|
||||
</div>
|
||||
)}
|
||||
{solutionStatement && (
|
||||
<div className="p-8 md:p-12 bg-primary/5">
|
||||
<div className="p-6 md:p-12 bg-primary/5">
|
||||
<div className="flex items-center mb-6 text-primary">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
@@ -183,7 +183,7 @@ export function ServiceDetailGrid({
|
||||
<motion.div
|
||||
key={panel.id}
|
||||
variants={itemVariants}
|
||||
className={`group relative overflow-hidden bg-white border border-neutral-200 rounded-3xl p-8 md:p-12 transition-all duration-500 hover:border-primary/40 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1 select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
|
||||
className={`group relative overflow-hidden bg-white border border-neutral-200 rounded-3xl p-6 md:p-12 transition-all duration-500 hover:border-primary/40 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1 select-none ${isFullWidth ? 'md:col-span-2' : ''}`}
|
||||
>
|
||||
{/* Premium Watermark Number */}
|
||||
<div className="absolute -right-8 -bottom-12 text-[12rem] font-heading font-black text-neutral-50 z-0 pointer-events-none select-none transition-all duration-700 group-hover:scale-105 group-hover:-translate-y-4 group-hover:text-primary/[0.03]">
|
||||
@@ -195,7 +195,7 @@ export function ServiceDetailGrid({
|
||||
|
||||
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
|
||||
<div className="pt-2 flex-1 min-w-0 pr-4">
|
||||
<h4 className="text-2xl md:text-3xl font-extrabold text-neutral-dark uppercase tracking-wider break-words hyphens-auto" lang="de">{panel.title}</h4>
|
||||
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
|
||||
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
|
||||
@@ -67,28 +68,31 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
})) || defaultCompanies;
|
||||
|
||||
return (
|
||||
<section id="unternehmen" className="py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
|
||||
<section id="unternehmen" className="py-12 md:py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
|
||||
<div className="container relative z-10 px-4">
|
||||
|
||||
{(badge || title) && (
|
||||
<div className="text-center max-w-3xl mx-auto mb-16">
|
||||
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
|
||||
{badge && <h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>}
|
||||
{title && <h3 className="font-heading text-4xl md:text-5xl font-extrabold text-white">{title}</h3>}
|
||||
{title && <h3 className="font-heading text-3xl md:text-5xl font-extrabold text-white">{title}</h3>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
>
|
||||
<div className="relative -mx-4 md:mx-0">
|
||||
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
|
||||
<motion.div
|
||||
className="flex overflow-x-auto md:overflow-visible snap-x snap-mandatory pb-6 md:pb-0 md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 px-4 md:px-0 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
>
|
||||
{companiesData.map((company: any, index: number) => {
|
||||
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
||||
const hasUrl = !!company.url;
|
||||
const CardWrapper = hasUrl ? 'a' : 'div';
|
||||
const wrapperProps = hasUrl ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
|
||||
const isExternal = hasUrl && company.url.startsWith('http');
|
||||
const CardWrapper = hasUrl ? (isExternal ? 'a' : Link) : 'div';
|
||||
const wrapperProps = hasUrl ? (isExternal ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : { href: company.url }) : {};
|
||||
|
||||
// Generate logo representation based on title
|
||||
const isEtib = company.title.includes('E-TIB');
|
||||
@@ -112,33 +116,21 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
|
||||
const animationClass = animationClasses[index % animationClasses.length];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="h-full"
|
||||
>
|
||||
<CardWrapper
|
||||
{...wrapperProps}
|
||||
className={`group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
|
||||
isCurrent
|
||||
? 'border-2 border-primary shadow-[0_0_30px_rgba(17,124,97,0.15)] cursor-default'
|
||||
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[#0a192f] z-0">
|
||||
<Image
|
||||
src={company.backgroundImage}
|
||||
alt="Background"
|
||||
fill
|
||||
className={`object-cover transition-all duration-1000 ease-out ${
|
||||
isCurrent
|
||||
? 'opacity-30 grayscale-0 scale-105'
|
||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||
}`}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
const CardContent = (
|
||||
<>
|
||||
<div className="absolute inset-0 bg-[#0a192f] z-0">
|
||||
<Image
|
||||
src={company.backgroundImage}
|
||||
alt="Background"
|
||||
fill
|
||||
className={`object-cover transition-all duration-1000 ease-out ${
|
||||
isCurrent
|
||||
? 'opacity-30 grayscale-0 scale-105'
|
||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||
}`}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Heavy dark gradient overlay to make logos pop */}
|
||||
<div className={`absolute inset-0 z-0 transition-colors duration-700 mix-blend-multiply ${
|
||||
@@ -182,7 +174,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
{/* LOGO RENDERING */}
|
||||
{isEtib ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative w-48 h-14 mb-2">
|
||||
<div className="relative w-32 md:w-40 lg:w-44 h-10 md:h-12 lg:h-14 mb-2 max-w-[85%]">
|
||||
<Image
|
||||
src="/assets/logo-white.png"
|
||||
alt="E-TIB Logo"
|
||||
@@ -191,17 +183,17 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
/>
|
||||
</div>
|
||||
{isIng && (
|
||||
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
|
||||
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
|
||||
Ingenieurgesellschaft
|
||||
</div>
|
||||
)}
|
||||
{isVerwaltung && (
|
||||
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
|
||||
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
|
||||
Verwaltung GmbH
|
||||
</div>
|
||||
)}
|
||||
{isBohrtechnik && (
|
||||
<div className="text-white/80 tracking-[0.2em] text-[10px] sm:text-xs font-semibold uppercase mt-1 text-center">
|
||||
<div className="text-white/80 tracking-[0.15em] sm:tracking-[0.2em] text-[9px] sm:text-[10px] lg:text-[11px] font-semibold uppercase mt-1 text-center">
|
||||
Bohrtechnik GmbH
|
||||
</div>
|
||||
)}
|
||||
@@ -227,17 +219,47 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
{company.url && (
|
||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-500 z-20">
|
||||
<div className="flex items-center text-white font-bold uppercase text-xs tracking-widest bg-white/10 backdrop-blur-md px-4 py-2 rounded-full border border-white/20">
|
||||
Website Besuchen
|
||||
{isExternal ? (isDe ? 'Website Besuchen' : 'Visit Website') : (isDe ? 'Zum Standort' : 'View Location')}
|
||||
<svg className="w-4 h-4 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CardWrapper>
|
||||
|
||||
</>
|
||||
);
|
||||
|
||||
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
|
||||
isCurrent
|
||||
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="h-full flex-shrink-0 snap-center w-[85%] sm:w-[70%] md:w-full"
|
||||
>
|
||||
{hasUrl ? (
|
||||
isExternal ? (
|
||||
<a href={company.url} target="_blank" rel="noopener noreferrer" className={sharedClass}>
|
||||
{CardContent}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={company.url as string} className={sharedClass}>
|
||||
{CardContent}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<div className={sharedClass}>
|
||||
{CardContent}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
|
||||
className="mt-12 md:mt-24 p-6 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform duration-700 group-hover:translate-x-1/3" />
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
|
||||
@@ -28,87 +28,18 @@ export function TeamGrid({ members }: TeamGridProps) {
|
||||
if (!members || members.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-white relative overflow-hidden">
|
||||
<section className="py-12 md: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">{t('badge')}</h2>
|
||||
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
|
||||
<h3 className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>
|
||||
<p className="text-text-secondary max-w-2xl text-lg">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{members.length > 0 && (
|
||||
<div className="mb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative rounded-[2.5rem] bg-neutral-dark overflow-hidden shadow-2xl border border-white/10"
|
||||
>
|
||||
{/* Premium Background Effects */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(238,114,3,0.15),transparent_50%)] pointer-events-none" />
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/20 blur-[120px] mix-blend-screen rounded-full pointer-events-none" />
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'40\\' height=\\'40\\' viewBox=\\'0 0 40 40\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Cpath d=\\'M0 0h40v40H0V0zm20 20h20v20H20V20zM0 20h20v20H0V20z\\' fill=\\'%23ffffff\\' fill-opacity=\\'0.02\\' fill-rule=\\'evenodd\\'/%3E%3C/svg%3E')" }} />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between p-8 md:p-12 lg:p-20 gap-12 lg:gap-16">
|
||||
{/* Text Content */}
|
||||
<div className="w-full lg:w-[55%] flex flex-col justify-center text-white">
|
||||
<div className="inline-flex items-center space-x-2 px-4 py-1.5 mb-8 rounded-full border border-primary/30 bg-primary/10 text-primary text-xs font-bold uppercase tracking-widest self-start backdrop-blur-md shadow-[0_0_20px_rgba(238,114,3,0.2)]">
|
||||
<span className="relative flex h-2 w-2 mr-1">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
</span>
|
||||
{t('management')}
|
||||
</div>
|
||||
<h3 className="font-heading font-black text-5xl md:text-6xl lg:text-7xl mb-4 leading-[1.1] tracking-tight">
|
||||
{members[0].name}
|
||||
</h3>
|
||||
<p className="text-xl md:text-2xl text-white/70 font-medium mb-10 leading-relaxed max-w-xl">
|
||||
{members[0].position}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{members[0].email && (
|
||||
<a href={`mailto:${members[0].email}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-primary border border-white/10 hover:border-primary px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].email}</span>
|
||||
</a>
|
||||
)}
|
||||
{members[0].phone && (
|
||||
<a href={`tel:${members[0].phone}`} className="group flex items-center justify-center sm:justify-start gap-4 bg-white/5 hover:bg-white/10 border border-white/10 px-6 py-4 rounded-2xl transition-all duration-300 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white/70 group-hover:text-white transition-colors" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
|
||||
<span className="font-semibold text-white/90 group-hover:text-white transition-colors">{members[0].phone}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Premium Image Block */}
|
||||
<div className="w-full lg:w-[45%] flex justify-center lg:justify-end perspective-1000">
|
||||
<div className="relative w-64 h-64 md:w-80 md:h-80 lg:w-[400px] lg:h-[400px] rounded-[3rem] overflow-hidden border-4 border-white/10 shadow-[0_30px_60px_rgba(0,0,0,0.6)] transform md:rotate-3 hover:rotate-0 transition-all duration-700 bg-neutral-800 group">
|
||||
{members[0].image && (typeof members[0].image === 'string' ? members[0].image : members[0].image.url) ? (
|
||||
<Image
|
||||
src={typeof members[0].image === 'string' ? members[0].image : members[0].image.url}
|
||||
alt={members[0].name}
|
||||
fill
|
||||
sizes="400px"
|
||||
className="object-cover scale-110 group-hover:scale-100 transition-transform duration-1000 ease-out"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white/20">
|
||||
<svg className="w-20 h-20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 xl:gap-10">
|
||||
{members.slice(1).map((member, i) => (
|
||||
{members.map((member, i) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function CorporateBackground() {
|
||||
const y5 = useTransform(scrollYProgress, [0, 1], [0, -500]);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none hidden md:block" aria-hidden="true">
|
||||
{/* 1. Top Right - Medium, rotating moderately */}
|
||||
<motion.div
|
||||
style={{
|
||||
|
||||
41
components/decorations/EUFundingBadge.tsx
Normal file
41
components/decorations/EUFundingBadge.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function EUFundingBadge() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute top-0 right-0 w-[260px] md:w-[380px] h-[260px] md:h-[380px] z-0 pointer-events-none select-none overflow-hidden"
|
||||
style={{
|
||||
// Perfekter weicher Fade, der sicherstellt, dass harte Kanten komplett unsichtbar werden
|
||||
maskImage: 'radial-gradient(100% 100% at top right, black 10%, rgba(0,0,0,0.8) 30%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(100% 100% at top right, black 10%, rgba(0,0,0,0.8) 30%, transparent 70%)',
|
||||
}}
|
||||
>
|
||||
{/* Background Flag */}
|
||||
<div className="absolute inset-0 w-full h-full opacity-95">
|
||||
<Image
|
||||
src="/assets/eu-flag.webp"
|
||||
alt="European Union Background"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text overlaid on the flag */}
|
||||
<div className="absolute top-0 right-0 p-5 md:p-8 flex flex-col items-end text-right z-10 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]">
|
||||
<span className="block text-[9px] md:text-[11px] font-bold text-white/90 tracking-wide leading-snug drop-shadow-md">
|
||||
Kofinanziert von der
|
||||
</span>
|
||||
<span className="block text-[11px] md:text-[13px] font-extrabold text-white tracking-wider leading-snug uppercase drop-shadow-md">
|
||||
Europäischen Union
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default function CTA({ data }: { data?: any }) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<Section className="bg-primary text-white py-32 relative overflow-hidden">
|
||||
<Section className="bg-primary text-white py-16 md:py-32 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-1/3 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-primary/10 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function Experience({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.experience');
|
||||
|
||||
return (
|
||||
<Section className="relative py-32 md:py-48 overflow-hidden text-white">
|
||||
<Section className="relative py-16 md:py-48 overflow-hidden text-white">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/uploads/2024/12/1694273920124-copy-2.webp"
|
||||
|
||||
@@ -24,13 +24,13 @@ export default function GallerySection({ data }: { data?: any }) {
|
||||
const lightboxIndex = photoParam ? parseInt(photoParam, 10) : 0;
|
||||
|
||||
return (
|
||||
<Section className="bg-white text-white py-32">
|
||||
<Section className="bg-white text-white py-16 md:py-32">
|
||||
<Container>
|
||||
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} align="center">
|
||||
{data?.title || t('title')}
|
||||
</Heading>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8">
|
||||
{images.map((src, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAnalytics } from '../analytics/useAnalytics';
|
||||
import { AnalyticsEvents } from '../analytics/analytics-events';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { AISearchResults } from '../search/AISearchResults';
|
||||
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
|
||||
const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false });
|
||||
@@ -15,6 +16,7 @@ const AIOrb = dynamic(() => import('../search/AIOrb'), { ssr: false });
|
||||
export default function Hero({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.hero');
|
||||
const locale = useLocale();
|
||||
const pathname = usePathname();
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -85,7 +87,7 @@ export default function Hero({ data }: { data?: any }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section className="relative min-h-[85vh] md:h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
|
||||
<Section key={`home-hero-${pathname}`} className="relative min-h-[75vh] md:min-h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
|
||||
<Container className="relative z-10 text-center md:text-left text-white w-full order-2 md:order-none">
|
||||
<div className="max-w-5xl mx-auto md:mx-0">
|
||||
<div>
|
||||
@@ -197,7 +199,7 @@ export default function Hero({ data }: { data?: any }) {
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none animate-in fade-in zoom-in-95 duration-1000 ease-out fill-mode-both">
|
||||
<div className="relative md:absolute inset-0 z-0 w-full h-[35vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none animate-in fade-in zoom-in-95 duration-1000 ease-out fill-mode-both">
|
||||
<HeroIllustration />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<Section className="relative py-32 md:py-48 overflow-hidden">
|
||||
<Section className="relative py-16 md:py-48 overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/uploads/2024/12/DSC08036-Large.webp"
|
||||
@@ -31,7 +31,7 @@ export default function MeetTheTeam({ data }: { data?: any }) {
|
||||
|
||||
<div className="relative mb-12">
|
||||
<div className="absolute -left-8 top-0 bottom-0 w-1 bg-accent rounded-full" />
|
||||
<p className="text-xl md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
|
||||
<p className="text-lg md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
|
||||
"{data?.description || t('description')}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function VideoSection({ data }: { data?: any }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative h-[70vh] overflow-hidden bg-primary">
|
||||
<section ref={sectionRef} className="relative h-[50vh] md:h-[70vh] overflow-hidden bg-primary">
|
||||
{isVisible && (
|
||||
<video className="w-full h-full object-cover opacity-60" autoPlay muted loop playsInline>
|
||||
<source
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useLocale } from 'next-intl';
|
||||
import { ShieldCheck, Leaf, Lock, Accessibility, Zap, Mail, Phone } from 'lucide-react';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { Tooltip } from '@/components/ui/Tooltip';
|
||||
import { EUFundingBadge } from '@/components/decorations/EUFundingBadge';
|
||||
interface CompanyInfo {
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
@@ -18,7 +19,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<footer className="bg-[#050B14] text-neutral-light py-10 md:py-16 relative overflow-hidden border-t border-white/5">
|
||||
<footer className="bg-[#050B14] text-neutral-light pt-40 pb-10 md:pt-16 md:pb-16 relative overflow-hidden border-t border-white/5">
|
||||
{/* Subtle background tech grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||
|
||||
@@ -26,9 +27,11 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/40 to-transparent" />
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-[200px] bg-primary/20 blur-[100px] rounded-full pointer-events-none opacity-50" />
|
||||
|
||||
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-8 md:mb-12">
|
||||
<EUFundingBadge />
|
||||
|
||||
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-4 md:mb-12">
|
||||
{/* Brand Column Bento */}
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||
|
||||
<div className="space-y-4 md:space-y-6 relative z-10">
|
||||
@@ -42,7 +45,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
</TransitionLink>
|
||||
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
||||
{locale === 'de'
|
||||
? 'Die Experten für Kabeltiefbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
||||
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
||||
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -95,58 +98,158 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
{/* Navigation Bento Grid */}
|
||||
<div className="col-span-1 md:col-span-4 lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-4 md:gap-6 lg:gap-8">
|
||||
|
||||
{/* Company Column */}
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
{/* Links Column */}
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="absolute inset-0 bg-gradient-to-bl from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
|
||||
{locale === 'de' ? 'Unternehmen' : 'Company'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
|
||||
</h4>
|
||||
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
|
||||
{ label: locale === 'de' ? 'Über uns' : 'About us', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
|
||||
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
|
||||
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
|
||||
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
|
||||
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
|
||||
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
|
||||
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Desktop View */}
|
||||
<div className="hidden md:block relative z-10 flex-grow">
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 text-white">
|
||||
{locale === 'de' ? 'Navigation' : 'Navigation'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
|
||||
</h4>
|
||||
<ul className="space-y-2 mb-8">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
|
||||
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
|
||||
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
|
||||
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
|
||||
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-4 text-white">
|
||||
<TransitionLink href={`/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}`} className="hover:text-primary transition-colors cursor-pointer inline-block">
|
||||
{locale === 'de' ? 'Über uns' : 'About us'}
|
||||
</TransitionLink>
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
|
||||
</h4>
|
||||
<ul className="space-y-2 border-l border-white/10 pl-4 ml-1">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
|
||||
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
|
||||
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
|
||||
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
|
||||
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Mobile View (Accordions) */}
|
||||
<div className="md:hidden relative z-10 flex-grow space-y-4">
|
||||
<details className="group/acc">
|
||||
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
|
||||
{locale === 'de' ? 'Navigation' : 'Navigation'}
|
||||
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
|
||||
</summary>
|
||||
<div className="pt-2 pb-4">
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/${locale === 'de' ? 'kompetenzen' : 'competencies'}` },
|
||||
{ label: locale === 'de' ? 'Referenzen' : 'References', href: `/${locale}/referenzen` },
|
||||
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/${locale === 'de' ? 'messen' : 'trade-fairs'}` },
|
||||
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="h-px bg-white/10 w-full"></div>
|
||||
|
||||
<details className="group/acc">
|
||||
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
|
||||
{locale === 'de' ? 'Über uns' : 'About us'}
|
||||
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
|
||||
</summary>
|
||||
<div className="pt-2 pb-4">
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Unternehmen' : 'Company', href: `/${locale}/${locale === 'de' ? 'ueber-uns' : 'about-us'}` },
|
||||
{ label: locale === 'de' ? 'Standorte' : 'Locations', href: `/${locale}/standorte` },
|
||||
{ label: locale === 'de' ? 'Unser Team' : 'Our Team', href: `/${locale}/team` },
|
||||
{ label: locale === 'de' ? 'Zertifikate' : 'Certificates', href: `/${locale}/zertifikate` },
|
||||
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/${locale === 'de' ? 'karriere' : 'career'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legal Column */}
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-6 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 md:mb-8 text-white">
|
||||
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
|
||||
</h4>
|
||||
<ul className="space-y-2 md:space-y-4 relative z-10 flex-grow">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
|
||||
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
|
||||
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
|
||||
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Desktop View */}
|
||||
<div className="hidden md:block relative z-10 flex-grow">
|
||||
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-6 text-white">
|
||||
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
||||
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
|
||||
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
|
||||
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="group/link flex items-center text-neutral-400 hover:text-white transition-colors py-1 cursor-pointer">
|
||||
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Built Info */}
|
||||
|
||||
{/* Legal Info removed from here */}
|
||||
{/* Mobile View */}
|
||||
<div className="md:hidden relative z-10 flex-grow">
|
||||
<details className="group/acc">
|
||||
<summary className="font-heading font-bold uppercase tracking-widest text-sm text-white list-none cursor-pointer flex justify-between items-center py-2 outline-none select-none">
|
||||
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
||||
<svg className="w-4 h-4 text-white/50 transition-transform group-open/acc:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 9l-7 7-7-7"/></svg>
|
||||
</summary>
|
||||
<div className="pt-2 pb-4">
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/${locale === 'de' ? 'impressum' : 'imprint'}` },
|
||||
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/${locale === 'de' ? 'datenschutz' : 'privacy-policy'}` },
|
||||
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
|
||||
].map((link) => (
|
||||
<li key={link.href}>
|
||||
<TransitionLink href={link.href} className="flex items-center text-neutral-400 hover:text-white transition-colors py-1">
|
||||
<span className="font-medium">{link.label}</span>
|
||||
</TransitionLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NavLink {
|
||||
label: string;
|
||||
url: string;
|
||||
children?: NavLink[];
|
||||
isGroupLabel?: boolean;
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -21,6 +22,7 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
const [isScrolled, setIsScrolled] = React.useState(false);
|
||||
const [forceSolid, setForceSolid] = React.useState(false);
|
||||
const [headerVariant, setHeaderVariant] = React.useState<'capsule' | 'standard'>('capsule');
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
const pathname = usePathname() || '/';
|
||||
|
||||
// Scroll bound shine effect (continuous looping as user scrolls)
|
||||
@@ -54,6 +56,7 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
|
||||
// Re-check when pathname changes
|
||||
checkHeaderState();
|
||||
setIsMobileMenuOpen(false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
@@ -71,7 +74,7 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
className={`fixed top-0 left-0 right-0 z-50 flex justify-center transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
||||
isStandard
|
||||
? 'bg-[#050B14]/95 backdrop-blur-xl border-b border-white/10'
|
||||
: (isSolidMode ? 'pt-4 px-4' : 'pt-0 px-0')
|
||||
: (isSolidMode ? 'pt-4 px-4 sm:px-6' : 'pt-0 px-0')
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
@@ -79,7 +82,7 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
isStandard
|
||||
? 'py-4 px-6 md:px-8'
|
||||
: isSolidMode
|
||||
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-6 md:px-8'
|
||||
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-8 md:px-10'
|
||||
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
@@ -153,10 +156,82 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
{/* Sleek shine sweep effect */}
|
||||
<span className="absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg] group-hover/cta:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none" />
|
||||
</TransitionLink>
|
||||
|
||||
{/* Hamburger Menu (Mobile Only) */}
|
||||
<button
|
||||
className={`md:hidden ml-2 p-2 transition-colors focus:outline-none ${isSolidMode && !isMobileMenuOpen ? 'text-text-primary hover:text-primary' : 'text-white hover:text-primary z-[60] relative'}`}
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle Menu"
|
||||
>
|
||||
<div className="w-6 flex flex-col gap-[6px] relative items-center justify-center">
|
||||
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? 'rotate-45 translate-y-[8px]' : ''}`} />
|
||||
<span className={`w-full h-[2px] rounded-full bg-current transition-all duration-300 ease-in-out ${isMobileMenuOpen ? 'opacity-0 translate-x-3' : 'opacity-100'}`} />
|
||||
<span className={`w-full h-[2px] rounded-full bg-current transform transition-all duration-300 ease-in-out origin-center ${isMobileMenuOpen ? '-rotate-45 -translate-y-[8px]' : ''}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed inset-0 z-[40] bg-[#050B14]/95 backdrop-blur-xl md:hidden pt-24 pb-24 px-6 overflow-y-auto"
|
||||
>
|
||||
<nav className="flex flex-col gap-6 mt-8">
|
||||
{navLinks && navLinks.length > 0 && navLinks.map((link) => {
|
||||
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${link.url}`
|
||||
: link.url;
|
||||
const isActive = pathname === mappedUrl || (mappedUrl !== `/${currentLocale}` && pathname?.startsWith(`${mappedUrl}/`));
|
||||
|
||||
return (
|
||||
<div key={link.url} className="flex flex-col">
|
||||
<TransitionLink
|
||||
href={mappedUrl}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`text-2xl font-bold uppercase tracking-widest transition-colors ${isActive ? 'text-primary' : 'text-white'}`}
|
||||
>
|
||||
{link.label}
|
||||
</TransitionLink>
|
||||
|
||||
{link.children && (
|
||||
<div className="flex flex-col gap-3 mt-3 pl-4 border-l-2 border-white/10">
|
||||
{link.children.map((child, idx) => {
|
||||
if (child.isGroupLabel) {
|
||||
return <div key={idx} className="text-sm font-bold text-neutral-500 uppercase tracking-widest mt-2">{child.label}</div>;
|
||||
}
|
||||
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${child.url}`
|
||||
: child.url;
|
||||
const isChildActive = pathname === childUrl;
|
||||
|
||||
return (
|
||||
<TransitionLink
|
||||
key={child.url}
|
||||
href={childUrl}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`text-lg font-medium transition-colors ${isChildActive ? 'text-primary' : 'text-white/70'}`}
|
||||
>
|
||||
{child.label}
|
||||
</TransitionLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -164,10 +239,20 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink, currentLocale: string, pathname: string, isSolidMode: boolean }) {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
// Close dropdown on navigation
|
||||
React.useEffect(() => {
|
||||
setIsHovered(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Close dropdown on scroll
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (isHovered) setIsHovered(false);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [isHovered]);
|
||||
|
||||
const mappedUrl = link.url.startsWith('/') && !link.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${link.url}`
|
||||
: link.url;
|
||||
@@ -222,15 +307,25 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
|
||||
{/* Top glowing accent line */}
|
||||
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-70" />
|
||||
|
||||
{link.children.map((child) => {
|
||||
{link.children.map((child, idx) => {
|
||||
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${child.url}`
|
||||
: child.url;
|
||||
const isChildActive = pathname === childUrl;
|
||||
|
||||
if (child.isGroupLabel) {
|
||||
return (
|
||||
<div key={`group-${idx}`} className={`mt-2 mb-1 px-4 text-[11px] font-bold uppercase tracking-wider ${isSolidMode ? 'text-neutral-400' : 'text-neutral-500'}`}>
|
||||
{child.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TransitionLink
|
||||
key={child.url}
|
||||
href={childUrl}
|
||||
onClick={() => setIsHovered(false)}
|
||||
className={`group/dropitem relative block px-4 py-3.5 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
isSolidMode
|
||||
? isChildActive ? 'bg-primary/10 text-primary' : 'text-neutral-600 hover:text-primary hover:bg-neutral-50/80'
|
||||
|
||||
@@ -32,9 +32,9 @@ export function Heading({
|
||||
const effectiveSize = size || String(level) as HeadingSize;
|
||||
|
||||
const sizes: Record<string, string> = {
|
||||
'hero': 'font-heading text-4xl md:text-5xl lg:text-6xl font-black tracking-tight !leading-[0.9]',
|
||||
'hero': 'font-heading text-3xl md:text-5xl lg:text-6xl font-black tracking-tight !leading-[0.9]',
|
||||
'section': 'font-heading text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight !leading-[0.95]',
|
||||
'subsection': 'font-heading text-2xl md:text-3xl lg:text-4xl font-bold tracking-tight leading-tight',
|
||||
'subsection': 'font-heading text-xl md:text-3xl lg:text-4xl font-bold tracking-tight leading-tight',
|
||||
'card': 'text-xl md:text-2xl font-bold leading-snug',
|
||||
'small': 'text-lg font-semibold leading-snug',
|
||||
// Legacy support
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cn } from './utils';
|
||||
|
||||
export function Section({ className, children, ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||
return (
|
||||
<section className={cn('py-16 md:py-28 lg:py-36 overflow-hidden content-visibility-auto', className)} {...props}>
|
||||
<section className={cn('py-12 md:py-28 lg:py-36 overflow-hidden content-visibility-auto', className)} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
|
||||
1
contact-page-auth-debug.html
Normal file
1
contact-page-auth-debug.html
Normal file
File diff suppressed because one or more lines are too long
1
contact-page-debug.html
Normal file
1
contact-page-debug.html
Normal file
File diff suppressed because one or more lines are too long
BIN
contact.png
Normal file
BIN
contact.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
@@ -1,22 +1,59 @@
|
||||
---
|
||||
title: "Allgemeine Geschäftsbedingungen"
|
||||
date: "2024-03-20"
|
||||
date: "2026-05-28"
|
||||
excerpt: "Die Allgemeinen Geschäftsbedingungen der E-TIB GmbH für unsere Leistungen und Lieferungen."
|
||||
---
|
||||
|
||||
# Allgemeine Geschäftsbedingungen (AGB)
|
||||
|
||||
## 1. Geltungsbereich
|
||||
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge, Lieferungen und sonstigen Leistungen der E-TIB GmbH gegenüber ihren Kunden.
|
||||
## 1. Geltungsbereich und Vertragsabschluss
|
||||
1. Diese Allgemeinen Geschäftsbedingungen (AGB) gelten ausschließlich für alle Verträge, Lieferungen und sonstigen Leistungen (einschließlich Planungs-, Ingenieur-, Kabelnetzbau-, Horizontalspülbohrungs- und Glasfasermontageleistungen) der E-TIB GmbH (nachfolgend „E-TIB“ genannt) gegenüber ihren Vertragspartnern (nachfolgend „Kunde“ genannt).
|
||||
2. Diese AGB gelten ausschließlich im B2B-Verkehr. Sie richten sich ausschließlich an Unternehmer im Sinne des § 14 BGB, juristische Personen des öffentlichen Rechts oder öffentlich-rechtliche Sondervermögen.
|
||||
3. Entgegenstehende, abweichende oder ergänzende Bedingungen des Kunden werden nicht Vertragsbestandteil, es sei denn, E-TIB stimmt ihrer Geltung ausdrücklich schriftlich zu.
|
||||
4. Ein Vertrag kommt erst durch die schriftliche Auftragsbestätigung von E-TIB oder durch den tatsächlichen Beginn der Ausführung der Leistungen durch E-TIB zustande.
|
||||
|
||||
## 2. Vertragsabschluss
|
||||
Ein Vertrag kommt erst durch unsere schriftliche Auftragsbestätigung oder durch die Ausführung des Auftrags zustande.
|
||||
## 2. Leistungsumfang und Mitwirkungspflichten des Kunden
|
||||
1. Der genaue Umfang der von E-TIB zu erbringenden Leistungen ergibt sich aus dem jeweiligen Angebot bzw. der schriftlichen Auftragsbestätigung und den darin vereinbarten Spezifikationen.
|
||||
2. Der Kunde ist verpflichtet, E-TIB bei der Erbringung der Leistungen vollumfänglich und auf eigene Kosten zu unterstützen. Dies umfasst insbesondere:
|
||||
- Die Bereitstellung aller für die Planung und Ausführung erforderlichen Unterlagen, Pläne (insbesondere Sparten- und Leitungspläne), Genehmigungen und behördlichen Freigaben rechtzeitig vor Baubeginn.
|
||||
- Die genaue und verbindliche Einweisung in die örtlichen Gegebenheiten sowie die genaue Kennzeichnung und Einmessung aller im Arbeitsbereich verlaufenden unterirdischen Leitungen, Rohre, Kanäle und sonstigen Hindernisse.
|
||||
- Die Sicherstellung des freien und ungehinderten Zugangs zum Baustellengelände sowie die Bereitstellung eventuell erforderlicher Lager- und Einrichtungsflächen.
|
||||
- Die Bereitstellung von Bauwasser und Baustrom in der erforderlichen Menge und Qualität an den Arbeitsstellen auf Kosten des Kunden.
|
||||
3. Versäumt der Kunde eine Mitwirkungspflicht oder erteilt fehlerhafte Auskünfte (insbesondere bezüglich der Lage unterirdischer Leitungen), haftet er E-TIB für alle daraus resultierenden Verzögerungen, Mehrkosten und Schäden. E-TIB ist in diesem Fall von der Haftung für Beschädigungen an nicht oder fehlerhaft eingezeichneten Leitungen freigestellt.
|
||||
|
||||
## 3. Preise und Zahlungsbedingungen
|
||||
Alle Preise verstehen sich in Euro zzgl. der gesetzlichen Mehrwertsteuer. Zahlungen sind innerhalb von 14 Tagen nach Rechnungsstellung ohne Abzug zu leisten.
|
||||
## 3. Ausführungsfristen, Behinderung und Bauverzögerungen
|
||||
1. Vereinbarte Ausführungsfristen und Termine sind nur dann verbindlich, wenn sie von E-TIB ausdrücklich schriftlich als fix bestätigt wurden.
|
||||
2. Die Einhaltung der Fristen setzt die rechtzeitige Erfüllung aller Mitwirkungspflichten des Kunden sowie den Eingang vereinbarter Anzahlungen voraus.
|
||||
3. Leistungsverzögerungen aufgrund höherer Gewalt, Streiks, Aussperrungen, behördlicher Anordnungen oder unvorhersehbarer, von E-TIB nicht zu vertretender Erschwernisse (z. B. extreme Witterungsverhältnisse, unvorhergesehene Bodenklassen wie massiver Fels, unvollständige oder fehlerhafte Spartenpläne des Netzbetreibers) verlängern die Ausführungsfristen um die Dauer der Behinderung zzgl. einer angemessenen Anlaufzeit.
|
||||
4. Führen unvorhergesehene geologische Hindernisse oder Abweichungen der tatsächlichen Bodenklasse von den vertraglich zugrunde gelegten Bodenklassen zu einem erheblichen Mehraufwand (z. B. bei Horizontalspülbohrungen), ist E-TIB berechtigt, eine angemessene Anpassung der Vergütung sowie der Ausführungsfristen zu verlangen.
|
||||
|
||||
## 4. Haftung
|
||||
Wir haften für Schäden nur bei Vorsatz oder grober Fahrlässigkeit. Die Haftung für leichte Fahrlässigkeit ist ausgeschlossen, sofern keine wesentlichen Vertragspflichten verletzt wurden.
|
||||
## 4. Abnahme, Gefahrübergang und Gewährleistung
|
||||
1. Die Abnahme der Leistungen erfolgt unverzüglich nach Fertigstellung. Auf Verlangen von E-TIB sind auch in sich abgeschlossene Teile der Leistung (z. B. fertiggestellte Kabelgräben oder einzelne Bohrabschnitte) separat abzunehmen (Teilabnahme).
|
||||
2. Nimmt der Kunde die Leistung nicht innerhalb von 12 Werktagen nach schriftlicher Anzeige der Fertigstellung ab, oder nimmt er die Leistung in Benutzung (z. B. durch Inbetriebnahme der verlegten Leitungen), gilt die Abnahme nach Ablauf dieser Frist als erfolgt.
|
||||
3. Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung geht mit der Abnahme bzw. Teilabnahme auf den Kunden über.
|
||||
4. E-TIB leistet Gewähr nach den gesetzlichen Bestimmungen, soweit nachfolgend nichts anderes vereinbart ist. Die Verjährungsfrist für Mängelansprüche bei Bauwerken und damit zusammenhängenden Planungs- und Überwachungsleistungen beträgt 5 Jahre ab Abnahme. Bei sonstigen Werkleistungen beträgt sie 1 Jahr ab Abnahme.
|
||||
5. Offensichtliche Mängel müssen E-TIB unverzüglich, spätestens jedoch innerhalb von 5 Werktagen nach Abnahme, schriftlich angezeigt werden. Spätere Mängelrügen sind ausgeschlossen.
|
||||
|
||||
## 5. Gerichtsstand
|
||||
Gerichtsstand für alle Streitigkeiten aus dem Vertragsverhältnis ist Cottbus, sofern der Kunde Kaufmann ist.
|
||||
## 5. Preise, Abschlagszahlungen und Zahlungsbedingungen
|
||||
1. Alle Preise verstehen sich in Euro netto zuzüglich der jeweils gültigen gesetzlichen Umsatzsteuer.
|
||||
2. E-TIB ist berechtigt, dem Baufortschritt entsprechend angemessene Abschlagszahlungen (z. B. nach erbrachten Trassenmetern, Bohrmetern oder abgeschlossenen Planungsphasen) in Rechnung zu stellen. Diese sind innerhalb von 7 Tagen ab Rechnungsdatum ohne Abzug zur Zahlung fällig.
|
||||
3. Schlussrechnungen sind innerhalb von 14 Tagen nach Rechnungsstellung und Abnahme ohne Abzug zur Zahlung fällig.
|
||||
4. Bei Zahlungsverzug des Kunden ist E-TIB berechtigt, Verzugszinsen in gesetzlicher Höhe (derzeit 9 Prozentpunkte über dem jeweiligen Basiszinssatz) zu verlangen. Zudem behält sich E-TIB das Recht vor, weitere Arbeiten bis zur vollständigen Begleichung der fälligen Forderungen einzustellen (Leistungsverweigerungsrecht).
|
||||
|
||||
## 6. Eigentumsvorbehalt
|
||||
1. E-TIB behält sich das Eigentum an allen gelieferten und eingebauten Materialien, Kabeln, Rohren und sonstigen Komponenten (Vorbehaltsware) bis zur vollständigen Erfüllung sämtlicher Forderungen aus dem jeweiligen Vertrag vor.
|
||||
2. Der Kunde ist verpflichtet, die Vorbehaltsware pfleglich zu behandeln und E-TIB unverzüglich über Zugriffe Dritter (z. B. Pfändungen) schriftlich zu informieren.
|
||||
3. Bei Verbindung oder Vermischung der Vorbehaltsware mit anderen Gegenständen erwirbt E-TIB Miteigentum an der neuen Sache im Verhältnis des Rechnungswertes der Vorbehaltsware zum Wert der anderen verbundenen Gegenstände zum Zeitpunkt der Verbindung.
|
||||
|
||||
## 7. Haftung und Haftungsbeschränkungen
|
||||
1. E-TIB haftet unbeschränkt bei Vorsatz, grober Fahrlässigkeit sowie bei Verletzung von Leben, Körper oder Gesundheit.
|
||||
2. Bei leicht fahrlässiger Verletzung wesentlicher Vertragspflichten (Kardinalpflichten) ist die Haftung von E-TIB auf den vertragstypischen, vorhersehbaren Schaden begrenzt. Wesentliche Vertragspflichten sind solche, deren Erfüllung die ordnungsgemäße Durchführung des Vertrages überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf.
|
||||
3. Die Haftung für leichte Fahrlässigkeit bei der Verletzung nicht wesentlicher Vertragspflichten ist ausgeschlossen.
|
||||
4. E-TIB haftet nicht für Schäden, die auf unvollständigen, veralteten oder ungenauen Spartenplänen des Kunden oder der örtlichen Netzbetreiber beruhen, sofern E-TIB die Unvollständigkeit nicht vorsätzlich oder grob fahrlässig hätte erkennen müssen.
|
||||
5. Die vorstehenden Haftungsbeschränkungen gelten auch zugunsten der Angestellten, Vertreter und Erfüllungsgehilfen von E-TIB.
|
||||
|
||||
## 8. Gerichtsstand und Schlussbestimmungen
|
||||
1. Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG).
|
||||
2. Erfüllungsort für alle Lieferungen und Leistungen sowie Zahlungsort ist der Sitz von E-TIB in Guben.
|
||||
3. Ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertragsverhältnis ist Cottbus, sofern der Kunde Kaufmann, juristische Person des öffentlichen Rechts oder ein öffentlich-rechtliches Sondervermögen ist. E-TIB ist jedoch berechtigt, den Kunden auch an dessen allgemeinem Gerichtsstand zu verklagen.
|
||||
4. Sollte eine Bestimmung dieser AGB unwirksam oder undurchführbar sein oder werden, so wird dadurch die Wirksamkeit der übrigen Bestimmungen nicht berührt. Die unwirksame Bestimmung ist durch eine wirksame Bestimmung zu ersetzen, die dem wirtschaftlichen Zweck der unwirksamen Bestimmung am nächsten kommt.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Grabenlose Leitungsverlegung"
|
||||
title: "Bohrtechnik"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Präzise Horizontalspülbohrungen und Erdraketen in allen Bodenklassen für eine absolut oberflächenschonende und effiziente Leitungsverlegung."
|
||||
layout: "fullBleed"
|
||||
@@ -7,9 +7,9 @@ layout: "fullBleed"
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Grabenlose Verlegung"
|
||||
subtitle="Oberflächenschonende Horizontalspülbohrungen und Erdraketen"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
title="Bohrtechnik"
|
||||
subtitle="Grabenlose Leitungsverlegung, Horizontalspülbohrungen und Erdraketen"
|
||||
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
@@ -26,7 +26,7 @@ layout: "fullBleed"
|
||||
|
||||
## HDD-Spülbohrungen und Erdraketen
|
||||
|
||||
Mit unserem spezialisierten Gerätepark beherrschen wir die unterschiedlichsten Verfahren. Beim **Horizontalspülbohrverfahren (HDD)** steuern wir den Bohrkopf punktgenau unter der Erde hindurch. Wir realisieren dabei Bohrlängen von bis zu 250 Metern am Stück und können Schutzrohre mit einem Durchmesser von bis zu 400 mm einziehen.
|
||||
Mit unserem spezialisierten Gerätepark beherrschen wir die unterschiedlichsten Verfahren. Beim **Horizontalspülbohrverfahren (HDD)** steuern wir den Bohrkopf punktgenau unter der Erde hindurch. Wir können bis zu 400 Meter am Stück bohren und, je nach Bohrlänge, Schutzrohre mit einem Durchmesser von bis zu 600 mm einziehen.
|
||||
|
||||
Für kürzere Distanzen, wie beispielsweise schnelle und schonende Hausanschlüsse, setzen wir unsere **Erdraketen** (Bodenverdrängungsverfahren) ein. Hierbei erreichen wir Durchörterungslängen von bis zu 15 Metern für Rohre bis 160 mm Durchmesser – ideal um Vorgärten und Einfahrten komplett unversehrt zu lassen.
|
||||
|
||||
@@ -47,8 +47,8 @@ layout: "fullBleed"
|
||||
fullWidth: false,
|
||||
lists: [
|
||||
[
|
||||
"Max. Bohrlänge: 250 m",
|
||||
"Max. Durchmesser: 400er Rohr",
|
||||
"Max. Bohrlänge: 400 m",
|
||||
"Max. Durchmesser: 600er Rohr",
|
||||
"Zentimetergenaue Steuerbarkeit",
|
||||
"Querung von Bahn, Autobahn & Gewässern"
|
||||
]
|
||||
@@ -71,11 +71,5 @@ layout: "fullBleed"
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Bohr-Projekte" description="Erfolgreiche HDD-Bohrungen und grabenlose Leitungsverlegungen unter komplexesten Bedingungen." />
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Erfolgreiche HDD-Bohrungen und grabenlose Leitungsverlegungen unter komplexesten Bedingungen." />
|
||||
|
||||
<CallToAction
|
||||
title="Suchen Sie nach einer grabenlosen Lösung?"
|
||||
text="Wir beraten Sie gerne zu den Möglichkeiten der Horizontalspülbohrung für Ihr Vorhaben."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
|
||||
@@ -69,11 +69,5 @@ layout: "fullBleed"
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Breitband-Projekte" description="Wir haben bereits hunderte Kilometer FTTX-Netze erfolgreich realisiert. Entdecken Sie unsere Referenzen." />
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Wir haben bereits hunderte Kilometer FTTX-Netze erfolgreich realisiert. Entdecken Sie unsere Referenzen." />
|
||||
|
||||
<CallToAction
|
||||
title="Sie planen einen FTTX-Ausbau?"
|
||||
text="Wir beraten Sie gerne vollumfänglich rund um den Bereich FTTX und den Glasfaserausbau."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Startseite - Experten für Kabeltiefbau & Horizontalspülbohrungen"
|
||||
title: "Startseite - Experten für Kabelnetzbau & Horizontalspülbohrungen"
|
||||
description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabeltrassen, Horizontalspülbohrungen und Infrastrukturnetze in ganz Deutschland."
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
|
||||
title={"Die Experten für\nKabeltiefbau"}
|
||||
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabeltiefbau und der Bohrtechnik."
|
||||
title={"Die Experten für\nKabelnetzbau"}
|
||||
description="Wir realisieren komplexe Versorgungsleitungen für eine moderne Gesellschaft. Verlässlich, innovativ und mit höchster Präzision im Kabelnetzbau und der Bohrtechnik."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="Unsere Leistungen"
|
||||
linkHref="/de/kompetenzen"
|
||||
@@ -18,27 +18,30 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
companies={[
|
||||
{
|
||||
title: "E-TIB GMBH",
|
||||
description: "Das Unternehmen hat sich voll und ganz auf den Kabelleitungstiefbau spezialisiert. Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
|
||||
description: "Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
|
||||
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
||||
backgroundImage: "/assets/photos/DJI_0243.JPG"
|
||||
url: "/de/standorte/guben",
|
||||
backgroundImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg"
|
||||
},
|
||||
{
|
||||
title: "E-TIB Ingenieurgesellschaft mbH",
|
||||
description: "Unsere Ingenieure planen für Sie die Energie- und Telekommunikationsnetze der Zukunft - Vermessung und Dokumentation inklusive.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "https://www.etib-ing.com/",
|
||||
backgroundImage: "/assets/photos/DSC01137.JPG"
|
||||
backgroundImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg"
|
||||
},
|
||||
{
|
||||
title: "E-TIB Verwaltung GmbH",
|
||||
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "/de/standorte/buelstedt",
|
||||
backgroundImage: "/assets/photos/DJI_0048.JPG"
|
||||
},
|
||||
{
|
||||
title: "E-TIB Bohrtechnik GmbH",
|
||||
description: "Präzise Horizontalspülbohrungen in allen Bodenklassen für eine oberflächenschonende und grabenlose Leitungsverlegung.",
|
||||
icon: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z",
|
||||
url: "/de/bohrtechnik",
|
||||
backgroundImage: "/assets/photos/DSC08653.JPG"
|
||||
}
|
||||
]}
|
||||
@@ -59,21 +62,25 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
subtitle="Über E-TIB"
|
||||
badgeStyle="industrial"
|
||||
className="mb-6 md:mb-8"
|
||||
>Ihr <span className="text-primary relative whitespace-nowrap">starker<svg className="absolute -bottom-2 left-0 w-full h-3 text-primary/20" viewBox="0 0 100 10" preserveAspectRatio="none"><path d="M0 5 Q 50 10 100 5" stroke="currentColor" strokeWidth="4" fill="none"/></svg></span> Partner <br/>für Infrastruktur</Heading>
|
||||
>Ihr <span className="text-primary relative whitespace-nowrap">starker<svg className="absolute -bottom-2 left-0 w-full h-3 text-warning/60" viewBox="0 0 100 10" preserveAspectRatio="none"><path d="M0 5 Q 50 10 100 5" stroke="currentColor" strokeWidth="4" fill="none"/></svg></span> Partner <br/>für Infrastruktur</Heading>
|
||||
|
||||
<div className="text-xl text-text-secondary leading-relaxed mb-8">
|
||||
Die E-TIB GmbH ist ein leistungsfähiger und moderner Anbieter von technischen Dienstleistungen. Wir haben uns auf zukunftsweisende Kernbereiche der Infrastruktur spezialisiert.
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-6 pt-8 border-t border-neutral-200">
|
||||
<div className="grid grid-cols-3 gap-6 pt-8 border-t border-neutral-200">
|
||||
<div>
|
||||
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">2015</div>
|
||||
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Gründung</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">50+</div>
|
||||
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeiter</div>
|
||||
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div>
|
||||
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Mitarbeitende</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-black text-primary mb-1 tracking-tighter">100+</div>
|
||||
<div className="text-sm font-bold text-text-secondary uppercase tracking-wider">Kunden</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
ctaHref="/de/kompetenzen"
|
||||
items={[
|
||||
{
|
||||
title: "Kabeltiefbau",
|
||||
description: "Verlegung von Mittel- und Niederspannungskabeln",
|
||||
title: "Kabelleitungstiefbau",
|
||||
description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabeltiefbau" }
|
||||
href: "/de/kabelnetzbau",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
@@ -123,7 +130,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
href: "/de/bohrtechnik",
|
||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Bohrtechnik" }
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg", alt: "Bohrtechnik" }
|
||||
},
|
||||
{
|
||||
title: "Planung",
|
||||
@@ -131,14 +138,15 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
tag: "Präzision",
|
||||
size: "small",
|
||||
href: "/de/planung",
|
||||
image: { url: "/assets/photos/DJI_0048.JPG", alt: "Planung" }
|
||||
image: { url: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg", alt: "Planung" }
|
||||
},
|
||||
{
|
||||
title: "Vermessung",
|
||||
description: "Digitale Bestandsdokumentation",
|
||||
tag: "Digital",
|
||||
size: "accent",
|
||||
href: "/de/vermessung"
|
||||
href: "/de/vermessung",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie_Vermessung_(26_von_99).jpg", alt: "Vermessung" }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
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."
|
||||
excerpt: "Professioneller Kabelleitungstiefbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Kabelleitungstiefbau"
|
||||
subtitle="Komplette Infrastruktur-Lösungen für Mittel- und Niederspannungsnetze"
|
||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
@@ -24,13 +24,11 @@ layout: "fullBleed"
|
||||
|
||||
Auf vielen Baustellen werden zunehmend Billiganbieter aus dem Ausland eingesetzt, was häufig dazu führt, dass **Sicherheitsstandards und Dokumentationsanforderungen missachtet** werden. Die Folgen sind gravierend: Unfälle, defekte Fremdleitungen (wie Strom, Wasser und Gas) sowie massive Bauverzögerungen und teure Nachträge. Ein vermeintliches Schnäppchen entpuppt sich so schnell als finanzielle Belastung.
|
||||
|
||||
Besonders bei geförderten Infrastruktur-Projekten gestaltet sich der Abruf der Fördergelder ohne eine absolut lückenlose, normgerechte Dokumentation äußerst problematisch.
|
||||
|
||||
## Alles aus einer Hand
|
||||
|
||||
Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten für unsere Auftraggeber im Vorfeld sichere und zufriedenstellende Lösungen. Die E-TIB GmbH hat sich voll und ganz auf den Kabelleitungstiefbau spezialisiert.
|
||||
Die E-TIB GmbH hat sich voll und ganz auf den Kabelnetzbau spezialisiert. Durch unsere langjährige Erfahrung erkennen wir Problemstellungen frühzeitig und erarbeiten für unsere Auftraggeber im Vorfeld sichere und zufriedenstellende Lösungen.
|
||||
|
||||
Wir bieten Ihnen das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand – mit eigenem, hochmodernem Maschinenpark und festangestelltem Fachpersonal aus der Region. In direkter Zusammenarbeit mit unserem Partnerbüro, der E-TIB Ingenieurgesellschaft mbH, können wir zudem frühzeitig Planungsfehler ausschließen und selbst die komplexesten Dokumentationsanforderungen souverän erfüllen.
|
||||
Wir bieten Ihnen das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand – mit eigenem, hochmodernem Maschinenpark und festangestelltem Fachpersonal. In direkter Zusammenarbeit mit unserem Partnerbüro, der E-TIB Ingenieurgesellschaft mbH, können wir zudem frühzeitig Planungsfehler ausschließen und selbst die komplexesten Dokumentationsanforderungen souverän erfüllen.
|
||||
|
||||
<DataGridPulse />
|
||||
</div>
|
||||
@@ -39,7 +37,7 @@ layout: "fullBleed"
|
||||
badge="Leistungsspektrum"
|
||||
title="Alles für Ihr Netz"
|
||||
descriptionParagraphs={[
|
||||
"Wir bieten höchste Präzision, modernste Maschinen und zertifizierte Experten für jedes Tiefbau-Projekt – von der Kabelverlegung bis zur Transformatorstation."
|
||||
"Wir bieten höchste Präzision, modernste Maschinen und zertifizierte Experten für Ihr Projekt."
|
||||
]}
|
||||
panels={[
|
||||
{
|
||||
@@ -51,7 +49,7 @@ layout: "fullBleed"
|
||||
[
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Kabelverlegung von 0,4 kV bis 110 kV",
|
||||
"Kabelmontagen von 0,4 kV bis 30 kV",
|
||||
"Kabelmontagen bis 110 kV",
|
||||
"Kabelpflugarbeiten für schnelle Verlegung"
|
||||
]
|
||||
]
|
||||
@@ -73,11 +71,5 @@ layout: "fullBleed"
|
||||
]}
|
||||
/>
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Abgeschlossene Tiefbau-Projekte" description="Wir haben über hunderte Kilometer Kabel verlegt. Sehen Sie sich unsere Referenz-Projekte an." />
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Referenzen" description="Wir haben über hunderte Kilometer Kabel verlegt. Sehen Sie sich unsere Referenz-Projekte an." />
|
||||
|
||||
<CallToAction
|
||||
title="Sie haben ein konkretes Projekt?"
|
||||
text="Wir beraten Sie gerne zu allen Fragen rund um Kabeltiefbau und Leitungsverlegung."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
---
|
||||
title: "Karriere bei E-TIB - Jobs im Kabeltiefbau & Bohrtechnik"
|
||||
title: "Karriere bei E-TIB - Jobs im Kabelnetzbau & Bohrtechnik"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Werden Sie Teil eines dynamischen Teams und bauen Sie mit uns die Infrastruktur der Zukunft."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
<HeroSection
|
||||
badge="Karriere"
|
||||
title="Zukunft bauen"
|
||||
description="Suchen Sie eine neue Herausforderung in einem zukunftssicheren Markt? Bei E-TIB arbeiten wir täglich daran, Deutschland zu vernetzen und die Energiewende voranzutreiben."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="Offene Stellen"
|
||||
linkHref="#stellen"
|
||||
subtitle="Suchen Sie eine neue Herausforderung in einem zukunftssicheren Markt? Bei E-TIB arbeiten wir täglich daran, Deutschland zu vernetzen und die Energiewende voranzutreiben."
|
||||
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-28.jpg' }}
|
||||
alignment="center"
|
||||
ctaLabel="Offene Stellen"
|
||||
ctaHref="#stellen"
|
||||
/>
|
||||
|
||||
<JobListingBlock showFairs={false} showJobs={true} />
|
||||
|
||||
<BenefitGrid
|
||||
badge="E-TIB Standard"
|
||||
title="Wir bieten unseren Mitarbeitern"
|
||||
description="Die E-TIB GmbH wurde aus der Motivation gegründet, hochwertige Leistungen sowie faire Entlohnung zu ermöglichen. Neben der Entlohnung steht die Wertschätzung jedes einzelnen Mitarbeiters bei uns an oberster Stelle."
|
||||
title="Wir bieten unseren Mitarbeitenden"
|
||||
description="Die E-TIB GmbH wurde aus der Motivation gegründet, hochwertige Leistungen sowie faire Entlohnung zu ermöglichen. Neben der Entlohnung steht die Wertschätzung jedes einzelnen Mitarbeitenden bei uns an oberster Stelle."
|
||||
benefits={[
|
||||
{
|
||||
id: "region",
|
||||
@@ -96,10 +97,3 @@ layout: "fullBleed"
|
||||
]}
|
||||
/>
|
||||
|
||||
<CallToAction
|
||||
title="Initiativbewerbung (m/w/d)"
|
||||
description="Sie können sich vorstellen, eine langfristige Herausforderung in unserem Unternehmen anzunehmen, finden aber kein passendes Stellenangebot? Bewerben Sie sich initiativ."
|
||||
ctaLabel="Zum Kontaktformular"
|
||||
ctaHref="/de/kontakt"
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Kompetenzen - Kabeltiefbau & Spülbohrtechnik"
|
||||
title: "Kompetenzen - Kabelnetzbau & Spülbohrtechnik"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Von Kabeltiefbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
|
||||
excerpt: "Von Kabelnetzbau bis Elektromontage: Wir bieten ganzheitliche Lösungen für moderne Netzinfrastrukturen."
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
@@ -23,12 +23,12 @@ layout: "fullBleed"
|
||||
ctaHref="/de/kontakt"
|
||||
items={[
|
||||
{
|
||||
title: "Kabeltiefbau",
|
||||
description: "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
|
||||
title: "Kabelleitungstiefbau",
|
||||
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabeltiefbau" }
|
||||
href: "/de/kabelnetzbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungstiefbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
@@ -70,13 +70,13 @@ layout: "fullBleed"
|
||||
panels={[
|
||||
{
|
||||
id: "energy",
|
||||
title: "Kabelleitungstiefbau (Mittel-/Niederspannung)",
|
||||
title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)",
|
||||
icon: "energy",
|
||||
lists: [
|
||||
[
|
||||
"Herstellung von Kabel- und Rohrgräben",
|
||||
"Kabelverlegung (0,4kV - 110kV)",
|
||||
"Kabelmontagen 0,4 kV - 30 kV",
|
||||
"Kabelmontagen bis 110 kV",
|
||||
"Errichtung von Transformatorstationen",
|
||||
"Erstellung von Erdungsanlagen",
|
||||
"Errichtung von Straßenbeleuchtung",
|
||||
@@ -143,8 +143,8 @@ layout: "fullBleed"
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Kabeltiefbau",
|
||||
"description": "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
|
||||
"name": "Kabelleitungstiefbau",
|
||||
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,13 +5,14 @@ excerpt: "Treffen Sie das Team der E-TIB Gruppe persönlich auf Fachmessen und B
|
||||
layout: "fullBleed"
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
<HeroSection
|
||||
badge="Messen & Events"
|
||||
title="Treffen Sie uns persönlich"
|
||||
description="Lernen Sie die E-TIB Gruppe auf den führenden Branchenmessen kennen."
|
||||
videoUrl="/assets/videos/web/hero-bahnkreuzung.mp4"
|
||||
linkText="Kontakt aufnehmen"
|
||||
linkHref="/de/kontakt"
|
||||
subtitle="Lernen Sie die E-TIB Gruppe auf den führenden Branchenmessen kennen."
|
||||
backgroundImage={{ url: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg' }}
|
||||
alignment="center"
|
||||
ctaLabel="Kontakt aufnehmen"
|
||||
ctaHref="/de/kontakt"
|
||||
/>
|
||||
|
||||
<div id="termine">
|
||||
@@ -19,11 +20,10 @@ layout: "fullBleed"
|
||||
showFairs={true}
|
||||
showJobs={false}
|
||||
fairs={[
|
||||
{ name: 'Intersolar München', date: '2024 / 2025', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
|
||||
{ name: 'Windenergietage Linstow', date: '2024 / 2025', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
|
||||
{ name: 'Kabel-Workshop Wiesbaden', date: '2024 / 2025', type: 'Fachmesse', location: 'Wiesbaden' }
|
||||
{ name: 'Intersolar München', date: '23. - 25. Juni 2026', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
|
||||
{ name: 'Windenergietage Linstow', date: '10. - 12. November 2026', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
|
||||
{ name: 'Werkstatt Kabel 2026', date: '24. - 25. November 2026', type: 'Fachtagung', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CallToAction />
|
||||
|
||||
@@ -9,7 +9,7 @@ layout: "fullBleed"
|
||||
badge="Kernkompetenz"
|
||||
title="Planung & Beratung"
|
||||
subtitle="Kompetente Begleitung für Energie- und Telekommunikationsnetze"
|
||||
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
|
||||
backgroundImage={{ url: '/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
ctaHref="/de/kontakt"
|
||||
@@ -71,9 +71,3 @@ layout: "fullBleed"
|
||||
|
||||
<HomeReferencesSlider badge="Praxisbeispiele" title="Planung in der Praxis" description="Erfolgreich geplante und begleitete Infrastrukturprojekte unserer Unternehmensgruppe." />
|
||||
|
||||
<CallToAction
|
||||
title="Benötigen Sie Planungssicherheit?"
|
||||
text="Lassen Sie uns über Ihr Vorhaben sprechen. Wir stehen Ihnen mit unserem Ingenieurwissen zur Seite."
|
||||
buttonText="Kontakt aufnehmen"
|
||||
buttonLink="/de/kontakt"
|
||||
/>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Landkreis Börde"
|
||||
client: "EKN Breitband GmbH & Co. KG"
|
||||
date: "2019-05-01"
|
||||
dateString: "Seit 05/2019"
|
||||
location: "Landkreis Börde"
|
||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
||||
---
|
||||
|
||||
- 109.000 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
- 63 Stück Aufstellung Netzverteiler
|
||||
- 747 Stück Herstellung Hausanschlüsse
|
||||
- Koordination und Realisierung der gesamten Trassenführung
|
||||
- Dokumentation und Abnahme der Hausanschlüsse
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
title: "Breitbandausbau FTTH, Bomlitz / Schwarmstedt"
|
||||
client: "inexio Informationstechnologie und Telekommunikation GmbH"
|
||||
date: "2018-08-01"
|
||||
dateString: "08/2018 bis 03/2021"
|
||||
location: "29699 Bomlitz / 29690 Schwarmstedt"
|
||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-60.jpg"
|
||||
---
|
||||
|
||||
- 88.890 m Leitungstrasse herstellen (Tiefbau/HDD)
|
||||
- 23 Stück Aufstellung Multifunktionsgehäuse
|
||||
- 52 Stück Aufstellung Netzverteiler
|
||||
- 571 Stück Herstellung Hausanschlüsse
|
||||
17
content/de/referenzen/dettmannsdorf.mdx
Normal file
17
content/de/referenzen/dettmannsdorf.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "PV-Anlage Dettmannsdorf"
|
||||
client: "Energiekontor AG"
|
||||
date: "2026-06-01"
|
||||
dateString: "01/2025 bis 06/2026"
|
||||
location: "18334 Dettmannsdorf"
|
||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg"
|
||||
---
|
||||
|
||||
- 12.300 m Kabelgraben ausheben und verfüllen
|
||||
- 14.200 m Horizontalspülbohrung d 225 / d 200 / d 180
|
||||
- 26.500 m Verlegung MS-Kabel 630 mm², 18/30 kV
|
||||
- 7.100 m Verlegung MS-Kabel 1000 mm², 18/30 kV
|
||||
- 26.500 m Verlegung PE-Schutzrohr 50 x 4,6 mm
|
||||
- 26.500 m Einblasen LWL-Kabel, 12 Fasern
|
||||
- sämtliche Mittelspannungsmontagen
|
||||
- VLF/Mantelprüfungen/OTDR
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user