Compare commits
50 Commits
v2.2.13-rc
...
perf/light
| Author | SHA1 | Date | |
|---|---|---|---|
| c4e5b44d5e | |||
| 0378dde328 | |||
| 3202139b1e | |||
| dc5489706d | |||
| e8b8493740 | |||
| ef44ebcf4f | |||
| 816aef924e | |||
| 1bd9f26356 | |||
| 156ae66dec | |||
| b2d1865f40 | |||
| dd363d0a55 | |||
| 84f10694bb | |||
| 3e2b20ce0e | |||
| 4cd919545e | |||
| e5891e8214 | |||
| 4e4e44a5fd | |||
| 3e37c37863 | |||
| 40ec588d52 | |||
| b2c38fca7d | |||
| 1b6f1d9405 | |||
| 6cc9ccdea8 | |||
| aafc705f0e | |||
| 691f02a5b8 | |||
| 4b04178f01 | |||
| f76940dc34 | |||
| 2653bd2569 | |||
| adbf2855d8 | |||
| 87942dff41 | |||
| 38bd728393 | |||
| 547e62f9d4 | |||
| 27fc34e186 | |||
| d068d68d7a | |||
| 908f2b0b69 | |||
| 272b7a397b | |||
| 9edfe24509 | |||
| de8f41cc6d | |||
| 57cc429b54 | |||
| 01cf4013df | |||
| caf3fe8c1e | |||
| 51eac9fb50 | |||
| 6426512192 | |||
| a63ee1b9f5 | |||
| 43667e7d68 | |||
| 0191fdb770 | |||
| afaa5e038c | |||
| e0d6ea0d6c | |||
| 34839ad5a5 | |||
| a8d6a45aad | |||
| 6de8ad8516 | |||
| 023fef5343 |
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# KLZ Cables - Production Environment Configuration
|
# E-TIB - Production Environment Configuration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# This file contains runtime environment variables for the production deployment.
|
# This file contains runtime environment variables for the production deployment.
|
||||||
# It should be placed on the production server at: /home/deploy/sites/e-tib.com/.env
|
# It should be placed on the production server at: /home/deploy/sites/e-tib.com/.env
|
||||||
@@ -12,7 +12,7 @@ NODE_ENV=production
|
|||||||
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
||||||
|
|
||||||
# Analytics (Umami)
|
# Analytics (Umami)
|
||||||
UMAMI_WEBSITE_ID=
|
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
|
||||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||||
|
|
||||||
# Error Tracking (GlitchTip/Sentry)
|
# Error Tracking (GlitchTip/Sentry)
|
||||||
@@ -23,7 +23,7 @@ MAIL_HOST=smtp.eu.mailgun.org
|
|||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USERNAME=
|
MAIL_USERNAME=
|
||||||
MAIL_PASSWORD=
|
MAIL_PASSWORD=
|
||||||
MAIL_FROM=KLZ Cables <noreply@e-tib.com>
|
MAIL_FROM=E-TIB GmbH <noreply@e-tib.com>
|
||||||
MAIL_RECIPIENTS=info@e-tib.com
|
MAIL_RECIPIENTS=info@e-tib.com
|
||||||
|
|
||||||
# Varnish Cache Size (optional)
|
# Varnish Cache Size (optional)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/v') && 'production' || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -235,7 +235,7 @@ jobs:
|
|||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET: ${{ needs.prepare.outputs.target }}
|
||||||
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
|
NEXT_PUBLIC_APP_VERSION: ${{ needs.prepare.outputs.image_tag }}
|
||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
|
||||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
|
NPM_TOKEN: ${{ steps.auth.outputs.working_token }}
|
||||||
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
|
NPM_DOMAIN: ${{ steps.auth.outputs.working_domain }}
|
||||||
@@ -282,14 +282,18 @@ jobs:
|
|||||||
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }}
|
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@e-tib.com' }}
|
||||||
|
|
||||||
# Monitoring
|
# Monitoring
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN || 'https://dcb81958-dbf2-4a3d-b422-875f4672c14b@glitchtip.infra.mintel.me/5' }}
|
||||||
|
|
||||||
# Gatekeeper
|
# Gatekeeper
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || 'd773ea10-a3b3-4ccf-9024-987e14c4d669' }}
|
||||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -350,6 +354,10 @@ jobs:
|
|||||||
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
||||||
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "# Notifications"
|
||||||
|
echo "GOTIFY_URL=$GOTIFY_URL"
|
||||||
|
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
|
||||||
|
echo ""
|
||||||
echo "TARGET=$TARGET"
|
echo "TARGET=$TARGET"
|
||||||
echo "SENTRY_ENVIRONMENT=$TARGET"
|
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||||
echo "PROJECT_NAME=$PROJECT_NAME"
|
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||||
@@ -392,10 +400,19 @@ jobs:
|
|||||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
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 system prune -f"
|
||||||
ssh root@alpha.mintel.me "docker rm -f \$(docker ps -a -q --filter name=${SLUG}-etib-) 2>/dev/null || true"
|
|
||||||
|
# ── Deterministic cleanup: stop the EXACT compose project we're about to redeploy ──
|
||||||
|
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' down --remove-orphans 2>/dev/null || true"
|
||||||
|
|
||||||
|
# ── Safety net: kill ANY container whose Traefik host rule matches our target domain ──
|
||||||
|
# This catches ghost containers from prior deployments that used a different project name
|
||||||
|
# (e.g. 'e-tibcom' vs 'etib-production' due to Docker Compose defaulting to dir name)
|
||||||
|
ssh root@alpha.mintel.me "docker ps -q --filter label=traefik.http.routers.${PROJECT_NAME}.rule 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true"
|
||||||
|
ssh root@alpha.mintel.me "docker ps --format '{{.Names}}' | grep -E '^e-tibcom-' | xargs -r docker rm -f 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 && 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 '${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"
|
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${PROJECT_NAME}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
2895
.lighthouseci/lhr-1782063683063.html
Normal file
2895
.lighthouseci/lhr-1782063683063.html
Normal file
File diff suppressed because one or more lines are too long
13346
.lighthouseci/lhr-1782063683063.json
Normal file
13346
.lighthouseci/lhr-1782063683063.json
Normal file
File diff suppressed because one or more lines are too long
2895
.lighthouseci/lhr-1782063711742.html
Normal file
2895
.lighthouseci/lhr-1782063711742.html
Normal file
File diff suppressed because one or more lines are too long
13179
.lighthouseci/lhr-1782063711742.json
Normal file
13179
.lighthouseci/lhr-1782063711742.json
Normal file
File diff suppressed because one or more lines are too long
2895
.lighthouseci/lhr-1782063741217.html
Normal file
2895
.lighthouseci/lhr-1782063741217.html
Normal file
File diff suppressed because one or more lines are too long
13289
.lighthouseci/lhr-1782063741217.json
Normal file
13289
.lighthouseci/lhr-1782063741217.json
Normal file
File diff suppressed because one or more lines are too long
3
.lighthouseci/links.json
Normal file
3
.lighthouseci/links.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"https://e-tib.com/en": "https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1782063743188-8000.report.html"
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPageBySlug } from '@/lib/pages';
|
import { getPageBySlug } from '@/lib/pages';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -20,12 +20,16 @@ export default async function Image({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const bg = getOgBackground();
|
||||||
|
const logo = getOgLogo();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
title={pageData.frontmatter.title}
|
title="Die Experten für Kabelnetzbau"
|
||||||
description={pageData.frontmatter.excerpt}
|
description={`${pageData.frontmatter.title}`}
|
||||||
label="Information"
|
label="Information"
|
||||||
|
image={bg}
|
||||||
|
logo={logo}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPostBySlug } from '@/lib/blog';
|
import { getPostBySlug } from '@/lib/blog';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
@@ -52,13 +52,16 @@ export default async function Image({
|
|||||||
if (featuredImage) {
|
if (featuredImage) {
|
||||||
base64Image = await fetchImageAsBase64(featuredImage);
|
base64Image = await fetchImageAsBase64(featuredImage);
|
||||||
}
|
}
|
||||||
|
const bg = getOgBackground();
|
||||||
|
const logo = getOgLogo();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
title={post.frontmatter.title}
|
title="Die Experten für Kabelnetzbau"
|
||||||
description={post.frontmatter.excerpt}
|
description={`${post.frontmatter.title}`}
|
||||||
label={post.frontmatter.category || 'Blog'}
|
label={post.frontmatter.category || 'Blog'}
|
||||||
image={base64Image || featuredImage}
|
image={base64Image || featuredImage || bg}
|
||||||
|
logo={logo}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,9 +11,17 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const bg = getOgBackground();
|
||||||
|
const logo = getOgLogo();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
|
<OGImageTemplate
|
||||||
|
title="Die Experten für Kabelnetzbau"
|
||||||
|
description={t('title')}
|
||||||
|
label="Blog"
|
||||||
|
image={bg}
|
||||||
|
logo={logo}
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,12 +11,22 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const bg = getOgBackground();
|
||||||
|
const logo = getOgLogo();
|
||||||
|
|
||||||
const title = t('meta.title') || t('title');
|
let title = t('meta.title') || t('title');
|
||||||
const description = t('meta.description') || t('subtitle');
|
if (title.startsWith('E-TIB GmbH | ')) {
|
||||||
|
title = title.substring('E-TIB GmbH | '.length);
|
||||||
|
}
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Contact" />,
|
<OGImageTemplate
|
||||||
|
title="Die Experten für Kabelnetzbau"
|
||||||
|
description={title}
|
||||||
|
label="Contact"
|
||||||
|
image={bg}
|
||||||
|
logo={logo}
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function ContactPage({ params }: ContactPageProps) {
|
export default async function ContactPage({ params }: ContactPageProps) {
|
||||||
console.log('--- RENDERING CONTACT PAGE FOR LOCALE ---');
|
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getMessages } from 'next-intl/server';
|
|||||||
import '../../styles/globals.css';
|
import '../../styles/globals.css';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
|
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
|
||||||
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
|
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
@@ -175,9 +175,7 @@ export default async function Layout(props: {
|
|||||||
|
|
||||||
// Read directly from process.env — bypasses all abstraction to guarantee correctness
|
// Read directly from process.env — bypasses all abstraction to guarantee correctness
|
||||||
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||||
|
const hasSeenLoader = true; // Disabled to allow static generation and instant LCP (Lighthouse 100)
|
||||||
const cookieStore = await cookies();
|
|
||||||
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -186,13 +184,10 @@ export default async function Layout(props: {
|
|||||||
data-scroll-behavior="smooth"
|
data-scroll-behavior="smooth"
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
||||||
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
|
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
|
||||||
{/* Preload critical hero video */}
|
|
||||||
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
|
|
||||||
</head>
|
</head>
|
||||||
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
||||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
@@ -215,8 +210,7 @@ export default async function Layout(props: {
|
|||||||
<Footer companyInfo={companyInfo} />
|
<Footer companyInfo={companyInfo} />
|
||||||
<JsonLd />
|
<JsonLd />
|
||||||
<AnalyticsShell />
|
<AnalyticsShell />
|
||||||
{/* Annotator vorübergehend deaktiviert auf Staging laut Anforderung */}
|
|
||||||
{/* process.env.TARGET !== 'production' && <AnnotatorClientWrapper /> */}
|
|
||||||
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
||||||
</TransitionProvider>
|
</TransitionProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, getOgBackground, getOgLogo, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -10,13 +10,23 @@ export const runtime = 'nodejs';
|
|||||||
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||||
|
|
||||||
|
let pageTitle = t('title');
|
||||||
|
if (pageTitle.startsWith('E-TIB GmbH | ')) {
|
||||||
|
pageTitle = pageTitle.substring('E-TIB GmbH | '.length);
|
||||||
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const bg = getOgBackground();
|
||||||
|
const logo = getOgLogo();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
title={t('title')}
|
title="Die Experten für Kabelnetzbau"
|
||||||
description={t('description')}
|
description={pageTitle}
|
||||||
label="Reliable Energy Infrastructure"
|
label="Kabelnetzbau & Infrastruktur"
|
||||||
|
image={bg}
|
||||||
|
logo={logo}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
|
|||||||
@@ -4,23 +4,23 @@ import { notFound } from 'next/navigation';
|
|||||||
import { getMdxContent } from '@/lib/mdx';
|
import { getMdxContent } from '@/lib/mdx';
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
// Import components used in MDX
|
import nextDynamic from 'next/dynamic';
|
||||||
|
|
||||||
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
||||||
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
|
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
|
||||||
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
|
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
|
||||||
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
|
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
|
||||||
import { FaqBlock } from '@/components/blocks/FaqBlock';
|
const FaqBlock = nextDynamic(() => import('@/components/blocks/FaqBlock').then(mod => mod.FaqBlock));
|
||||||
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
|
const CertificatesBlock = nextDynamic(() => import('@/components/blocks/CertificatesBlock').then(mod => mod.CertificatesBlock));
|
||||||
import { HeroSection } from '@/components/blocks/HeroSection';
|
import { HeroSection } from '@/components/blocks/HeroSection';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Heading } from '@/components/ui/Heading';
|
import { Heading } from '@/components/ui/Heading';
|
||||||
import { AnimatedCounter } from '@/components/ui';
|
const AnimatedCounter = nextDynamic(() => import('@/components/ui').then(mod => mod.AnimatedCounter));
|
||||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
const InteractiveGermanyMap = nextDynamic(() => import('@/components/blocks/InteractiveGermanyMap').then(mod => mod.InteractiveGermanyMap));
|
||||||
import { ScaleOfImpact } from '@/components/blocks/ScaleOfImpact';
|
const ScaleOfImpact = nextDynamic(() => import('@/components/blocks/ScaleOfImpact').then(mod => mod.ScaleOfImpact));
|
||||||
import { getAllReferences } from '@/lib/references';
|
import { getAllReferences } from '@/lib/references';
|
||||||
|
|
||||||
const mdxComponents = (references: any[]) => ({
|
const mdxComponents = (references: any[]) => ({
|
||||||
@@ -37,6 +37,10 @@ const mdxComponents = (references: any[]) => ({
|
|||||||
AnimatedCounter,
|
AnimatedCounter,
|
||||||
InteractiveGermanyMap,
|
InteractiveGermanyMap,
|
||||||
ScaleOfImpact,
|
ScaleOfImpact,
|
||||||
|
img: (props: any) => {
|
||||||
|
// We proxy it through Next.js image optimizer
|
||||||
|
return <img {...props} src={`/_next/image?url=${encodeURIComponent(props.src)}&w=1920&q=75`} loading="lazy" decoding="async" />;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function generateMetadata(props: any): Promise<Metadata> {
|
export async function generateMetadata(props: any): Promise<Metadata> {
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ export async function POST(request: NextRequest) {
|
|||||||
website: websiteId,
|
website: websiteId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const umamiEndpoint = config.analytics.umami.apiEndpoint;
|
let umamiEndpoint = config.analytics.umami.apiEndpoint || 'https://analytics.infra.mintel.me';
|
||||||
|
if (!umamiEndpoint.startsWith('http')) {
|
||||||
|
umamiEndpoint = `https://${umamiEndpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Log the event (internal only)
|
// Log the event (internal only)
|
||||||
logger.debug('Forwarding analytics event', {
|
logger.debug('Forwarding analytics event', {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
logo?: string;
|
||||||
mode?: 'dark' | 'light' | 'image';
|
mode?: 'dark' | 'light' | 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +14,11 @@ export function OGImageTemplate({
|
|||||||
description,
|
description,
|
||||||
label,
|
label,
|
||||||
image,
|
image,
|
||||||
|
logo,
|
||||||
mode = 'dark',
|
mode = 'dark',
|
||||||
}: OGImageTemplateProps) {
|
}: OGImageTemplateProps) {
|
||||||
const primaryBlue = '#001a4d';
|
const backgroundDark = '#1a1a1a';
|
||||||
const accentGreen = '#10a379';
|
const primaryGreen = '#0e7a5c';
|
||||||
const saturatedBlue = '#011dff';
|
|
||||||
|
|
||||||
const containerStyle: React.CSSProperties = {
|
const containerStyle: React.CSSProperties = {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -26,7 +27,7 @@ export function OGImageTemplate({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
|
backgroundColor: mode === 'light' ? '#ffffff' : backgroundDark,
|
||||||
padding: '80px',
|
padding: '80px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
@@ -65,7 +66,7 @@ export function OGImageTemplate({
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))',
|
background: 'linear-gradient(135deg, rgba(26,26,26,0.98) 0%, rgba(26,26,26,0.85) 40%, rgba(14,122,92,0.4) 100%)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,101 +76,127 @@ export function OGImageTemplate({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-150px',
|
top: '-300px',
|
||||||
right: '-150px',
|
right: '-200px',
|
||||||
width: '600px',
|
width: '800px',
|
||||||
height: '600px',
|
height: '1200px',
|
||||||
borderRadius: '300px',
|
backgroundColor: `${primaryGreen}1A`,
|
||||||
backgroundColor: `${accentGreen}15`,
|
transform: 'rotate(25deg)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 10 }}>
|
{/* Main Content Wrapper */}
|
||||||
{/* Label / Category */}
|
|
||||||
{label && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: accentGreen,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.3em',
|
|
||||||
marginBottom: '32px',
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: title.length > 40 ? '64px' : '82px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'white',
|
|
||||||
lineHeight: '1.05',
|
|
||||||
maxWidth: '950px',
|
|
||||||
marginBottom: '40px',
|
|
||||||
display: 'flex',
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
{description && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '32px',
|
|
||||||
color: 'rgba(255,255,255,0.7)',
|
|
||||||
maxWidth: '850px',
|
|
||||||
lineHeight: '1.4',
|
|
||||||
display: 'flex',
|
|
||||||
fontWeight: 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description.length > 160 ? description.substring(0, 157) + '...' : description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Brand Footer */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
bottom: '80px',
|
|
||||||
left: '80px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Top: Text Content */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{/* Label / Category */}
|
||||||
|
{label && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: primaryGreen,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.25em',
|
||||||
|
marginBottom: '24px',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '72px',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'white',
|
||||||
|
lineHeight: '1.1',
|
||||||
|
maxWidth: '900px',
|
||||||
|
marginBottom: '24px',
|
||||||
|
display: 'flex',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{description && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '36px',
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
maxWidth: '850px',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom: Brand Footer */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '80px',
|
|
||||||
height: '6px',
|
|
||||||
backgroundColor: accentGreen,
|
|
||||||
borderRadius: '3px',
|
|
||||||
marginRight: '24px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'white',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.15em',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
KLZ Cables
|
{logo ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="E-TIB GmbH"
|
||||||
|
height="56"
|
||||||
|
style={{ marginRight: '24px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: primaryGreen,
|
||||||
|
borderRadius: '3px',
|
||||||
|
marginRight: '24px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!logo && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'white',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.15em',
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
E-TIB GmbH
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Saturated Blue Brand Strip */}
|
{/* Primary Green Brand Strip */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -177,7 +204,7 @@ export function OGImageTemplate({
|
|||||||
right: 0,
|
right: 0,
|
||||||
width: '12px',
|
width: '12px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
backgroundColor: saturatedBlue,
|
backgroundColor: primaryGreen,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/components/ui';
|
import { cn } from '@/components/ui';
|
||||||
|
|
||||||
interface RevealProps {
|
interface RevealProps {
|
||||||
@@ -10,40 +10,12 @@ interface RevealProps {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Reveal({ children, className, threshold = 0.1, delay = 0 }: RevealProps) {
|
export default function Reveal({ children, className }: RevealProps) {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
// Reveal animation disabled due to flickering issues on mobile/hydration
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setIsVisible(true);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold }
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentRef = ref.current;
|
|
||||||
if (currentRef) {
|
|
||||||
observer.observe(currentRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (currentRef) {
|
|
||||||
observer.unobserve(currentRef);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [threshold]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn(className)}>
|
||||||
ref={ref}
|
|
||||||
className={cn('reveal-on-scroll', isVisible && 'is-visible', className)}
|
|
||||||
style={{ transitionDelay: `${delay}ms` }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const containerVariants: Variants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
const itemVariants: Variants = {
|
||||||
hidden: { opacity: 0, y: 40 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
|||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 40 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
show: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="show"
|
whileInView="show"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
>
|
>
|
||||||
{certificates.map((cert, index) => {
|
{certificates.map((cert, index) => {
|
||||||
const isIso = cert.type === 'iso';
|
const isIso = cert.type === 'iso';
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function CompanyTimeline({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={i}
|
key={i}
|
||||||
initial={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button href={ctaHref} variant="ghost" className="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
|
|
||||||
{ctaLabel}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px] px-4 md:px-0">
|
||||||
@@ -109,7 +105,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={idx}
|
key={idx}
|
||||||
initial={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
||||||
@@ -136,7 +132,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={idx}
|
key={idx}
|
||||||
initial={{ opacity: 0, y: 40, scale: 0.98 }}
|
initial={{ opacity: 0, y: 20, scale: 0.98 }}
|
||||||
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ delay: idx * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
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.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-3xl md: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>
|
||||||
@@ -104,7 +104,7 @@ export const ContactSection: React.FC<ContactSectionProps> = (props) => {
|
|||||||
variants={formVariants}
|
variants={formVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
>
|
>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function DeepDrillAnimation() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 0.8 }}
|
||||||
className="flex flex-col items-center"
|
className="flex flex-col items-center"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function GrowthChart() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
whileInView={{ width: `${kabelWidth}%` }}
|
whileInView={{ width: `${kabelWidth}%` }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 1.5, delay: 0.1 * index, ease: "easeOut" }}
|
transition={{ duration: 1.5, delay: 0.1 * index, ease: "easeOut" }}
|
||||||
className="absolute top-0 left-0 h-full bg-primary rounded-full"
|
className="absolute top-0 left-0 h-full bg-primary rounded-full"
|
||||||
/>
|
/>
|
||||||
@@ -61,7 +61,7 @@ export function GrowthChart() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
whileInView={{ width: `${offenerWidth}%` }}
|
whileInView={{ width: `${offenerWidth}%` }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 1.5, delay: 0.15 + 0.1 * index, ease: "easeOut" }}
|
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"
|
className="absolute top-0 left-0 h-full bg-neutral-300 rounded-full"
|
||||||
/>
|
/>
|
||||||
@@ -77,7 +77,7 @@ export function GrowthChart() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
whileInView={{ width: `${hddWidth}%` }}
|
whileInView={{ width: `${hddWidth}%` }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 1.5, delay: 0.2 + 0.1 * index, ease: "easeOut" }}
|
transition={{ duration: 1.5, delay: 0.2 + 0.1 * index, ease: "easeOut" }}
|
||||||
className="absolute top-0 left-0 h-full bg-neutral-dark rounded-full"
|
className="absolute top-0 left-0 h-full bg-neutral-dark rounded-full"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
@@ -42,31 +41,60 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
||||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
||||||
|
|
||||||
|
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let interactionTriggered = false;
|
||||||
|
|
||||||
|
const handleInteraction = () => {
|
||||||
|
if (interactionTriggered) return;
|
||||||
|
interactionTriggered = true;
|
||||||
|
setVideoSrcLoaded(true);
|
||||||
|
|
||||||
|
// Cleanup listeners
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for any user interaction
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.addEventListener(event, handleInteraction, { passive: true, once: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-[85svh] md: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 */}
|
{/* Background color while video loads */}
|
||||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||||
|
|
||||||
{videoUrl ? (
|
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
|
||||||
|
<Image
|
||||||
|
key={`img-${pathname}-${posterSrc}`}
|
||||||
|
src={posterSrc}
|
||||||
|
alt={posterAlt}
|
||||||
|
fill
|
||||||
|
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||||
|
sizes="100vw"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Render video on top if available, but defer src to avoid blocking LCP */}
|
||||||
|
{videoUrl && (
|
||||||
<video
|
<video
|
||||||
key={`${pathname}-${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"
|
className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||||
src={videoUrl}
|
src={videoSrcLoaded ? videoUrl : undefined}
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="auto"
|
preload="none"
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
key={`img-${pathname}-${posterSrc}`}
|
|
||||||
src={posterSrc}
|
|
||||||
alt={posterAlt}
|
|
||||||
fill
|
|
||||||
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
|
||||||
sizes="100vw"
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -81,11 +109,9 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
|
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
|
||||||
|
|
||||||
<div className="container relative z-[4] text-center px-4">
|
<div className="container relative z-[4] text-center px-4">
|
||||||
<motion.div
|
<div
|
||||||
key={`hero-content-${pathname}`}
|
key={`hero-content-${pathname}`}
|
||||||
initial={{ opacity: 0, y: 40 }}
|
className="animate-in fade-in slide-in-from-bottom-10 duration-1000 ease-out fill-mode-both"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
|
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className="font-heading font-bold text-3xl sm:text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
|
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"
|
||||||
@@ -114,7 +140,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
{secondaryCtaLabel}
|
{secondaryCtaLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const containerVariants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 40 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -75,7 +75,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
|||||||
<motion.h3
|
<motion.h3
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
||||||
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
|
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-8 md:mb-12 flex items-center gap-4"
|
||||||
>
|
>
|
||||||
@@ -86,7 +86,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||||
>
|
>
|
||||||
{fairs.map((messe, idx) => {
|
{fairs.map((messe, idx) => {
|
||||||
@@ -180,7 +180,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
|||||||
<motion.h2
|
<motion.h2
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
||||||
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
|
className="font-heading font-extrabold text-3xl md:text-4xl text-neutral-dark mb-6 md:mb-8"
|
||||||
>
|
>
|
||||||
@@ -192,7 +192,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
className="grid gap-6"
|
className="grid gap-6"
|
||||||
>
|
>
|
||||||
{jobs.map((job: any) => (
|
{jobs.map((job: any) => (
|
||||||
@@ -221,7 +221,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }}
|
||||||
className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center"
|
className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={ref.id}
|
key={ref.id}
|
||||||
initial={{ opacity: 0, x: 50 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const containerVariants: Variants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants: Variants = {
|
const itemVariants: Variants = {
|
||||||
hidden: { opacity: 0, y: 40 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
visible: { opacity: 1, y: 0, transition: { duration: 1.0, ease: [0.16, 1, 0.3, 1] } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, amount: 0.1 }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
>
|
>
|
||||||
{companiesData.map((company: any, index: number) => {
|
{companiesData.map((company: any, index: number) => {
|
||||||
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
className="mt-12 md:mt-24 p-6 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group"
|
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="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" />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function TeamGrid({ members }: TeamGridProps) {
|
|||||||
{members.map((member, i) => (
|
{members.map((member, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={member.id}
|
key={member.id}
|
||||||
initial={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function Header({ navLinks }: HeaderProps) {
|
|||||||
fill
|
fill
|
||||||
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
|
className="object-contain object-left group-hover:scale-105 transition-transform duration-500 origin-left"
|
||||||
priority
|
priority
|
||||||
|
fetchPriority="high"
|
||||||
sizes="192px"
|
sizes="192px"
|
||||||
/>
|
/>
|
||||||
{/* Masked sweep overlay on hover */}
|
{/* Masked sweep overlay on hover */}
|
||||||
|
|||||||
@@ -4,51 +4,29 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
const PRELOAD_VIDEOS = [
|
|
||||||
'/assets/videos/web/hero-bohrung.mp4'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
|
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
|
||||||
const [isLoading, setIsLoading] = useState(shouldShowLoader);
|
const [isLoading, setIsLoading] = useState(shouldShowLoader);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shouldShowLoader) return;
|
|
||||||
|
|
||||||
// Fallback Timeout (4 Sekunden), falls Videos hängen
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
finishLoading();
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
let videosLoaded = 0;
|
|
||||||
|
|
||||||
const checkAllLoaded = () => {
|
|
||||||
videosLoaded++;
|
|
||||||
if (videosLoaded === PRELOAD_VIDEOS.length) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
// Kleine Verzögerung für die smootheness
|
|
||||||
setTimeout(finishLoading, 800);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
PRELOAD_VIDEOS.forEach((src) => {
|
|
||||||
const video = document.createElement('video');
|
|
||||||
video.preload = 'auto';
|
|
||||||
video.oncanplaythrough = checkAllLoaded;
|
|
||||||
video.onerror = checkAllLoaded; // bei Fehler trotzdem weitermachen
|
|
||||||
video.src = src;
|
|
||||||
video.load();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [shouldShowLoader]);
|
|
||||||
|
|
||||||
const finishLoading = () => {
|
const finishLoading = () => {
|
||||||
// Set a session cookie so the server knows not to render the loader again
|
// Set a session cookie so the server knows not to render the loader again
|
||||||
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
|
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldShowLoader) return;
|
||||||
|
|
||||||
|
// Just show the animation for a short time to allow UI to mount,
|
||||||
|
// we don't preload 10MB videos anymore to save pagespeed!
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
finishLoading();
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [shouldShowLoader]);
|
||||||
|
|
||||||
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
|
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
|
||||||
if (!shouldShowLoader) return null;
|
if (!shouldShowLoader) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
// We use a ref to track transition state without triggering the route change effect when it turns true
|
// We use a ref to track transition state without triggering the route change effect when it turns true
|
||||||
const isTransitioningRef = React.useRef(isTransitioning);
|
const isTransitioningRef = React.useRef(isTransitioning);
|
||||||
@@ -24,18 +22,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
isTransitioningRef.current = isTransitioning;
|
isTransitioningRef.current = isTransitioning;
|
||||||
}, [isTransitioning]);
|
}, [isTransitioning]);
|
||||||
|
|
||||||
// Watch for route changes to complete the transition
|
|
||||||
useEffect(() => {
|
|
||||||
if (isTransitioningRef.current) {
|
|
||||||
// The route has actually changed in the browser, or at least the RSC payload arrived
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsTransitioning(false);
|
|
||||||
setTimeout(() => setTransitionMessage(null), 300);
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [pathname, searchParams]); // Run when pathname or search parameters change
|
|
||||||
|
|
||||||
const startTransition = useCallback((href: string, message: string | null = null) => {
|
const startTransition = useCallback((href: string, message: string | null = null) => {
|
||||||
// Wenn wir bereits navigieren, nichts tun
|
// Wenn wir bereits navigieren, nichts tun
|
||||||
if (isTransitioning) return;
|
if (isTransitioning) return;
|
||||||
@@ -75,11 +61,35 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
|
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<TransitionRouteWatcher
|
||||||
|
isTransitioningRef={isTransitioningRef}
|
||||||
|
setIsTransitioning={setIsTransitioning}
|
||||||
|
setTransitionMessage={setTransitionMessage}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
{children}
|
{children}
|
||||||
</TransitionContext.Provider>
|
</TransitionContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTransitioningRef.current) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
setTimeout(() => setTransitionMessage(null), 300);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function useTransition() {
|
export function useTransition() {
|
||||||
const context = useContext(TransitionContext);
|
const context = useContext(TransitionContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
{/* Right Column: Imagery & Mission */}
|
{/* Right Column: Imagery & Mission */}
|
||||||
<div className="lg:col-span-7 relative">
|
<div className="lg:col-span-7 relative">
|
||||||
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
||||||
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB im Einsatz" className="w-full h-full object-cover" />
|
<img src="/_next/image?url=%2Fassets%2Fphotos%2FEtib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg&w=1920&q=75" alt="E-TIB im Einsatz" className="w-full h-full object-cover" loading="lazy" decoding="async" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
|
|||||||
|
|
||||||
- 500 m Kabelgraben ausheben und verfüllen
|
- 500 m Kabelgraben ausheben und verfüllen
|
||||||
- 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160
|
- 4.500 m Einpflügen von 3 x PE-Schutzrohr d 160
|
||||||
- 3.200 m Horizontalspülbohrung d 160
|
- 12.000 m Horizontalspülbohrung d 160
|
||||||
- 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV
|
- 8.500 m Verlegung HS-Kabel 1.000 mm² 64/110 kV
|
||||||
- 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV
|
- 11 Satz Verbindungsmuffen 1.000 mm² 64/110 kV
|
||||||
- 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV
|
- 2 Satz Freiluftendverschlüsse 1.000 mm² 64/110 kV
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ layout: "fullBleed"
|
|||||||
{
|
{
|
||||||
id: "martin-polan",
|
id: "martin-polan",
|
||||||
name: "Martin Polan",
|
name: "Martin Polan",
|
||||||
position: "Bauleiter",
|
position: "Bauleitung",
|
||||||
email: "m.polan@e-tib.com",
|
email: "m.polan@e-tib.com",
|
||||||
phone: "+49 171 3868167",
|
phone: "+49 171 3868167",
|
||||||
image: "/assets/photos/team/martin.jpg"
|
image: "/assets/photos/team/martin.jpg"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
|||||||
{/* Right Column: Imagery & Mission */}
|
{/* Right Column: Imagery & Mission */}
|
||||||
<div className="lg:col-span-7 relative">
|
<div className="lg:col-span-7 relative">
|
||||||
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
||||||
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB in action" className="w-full h-full object-cover" />
|
<img src="/_next/image?url=%2Fassets%2Fphotos%2FEtib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg&w=1920&q=75" alt="E-TIB in action" className="w-full h-full object-cover" loading="lazy" decoding="async" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ featuredImage: "/assets/photos/DJI_0037.JPG"
|
|||||||
|
|
||||||
- Excavation and backfilling of 500 m cable trench
|
- Excavation and backfilling of 500 m cable trench
|
||||||
- 4,500 m plowing of 3 x PE protective pipe d 160
|
- 4,500 m plowing of 3 x PE protective pipe d 160
|
||||||
- 3,200 m HDD directional drilling d 160
|
- 12,000 m HDD directional drilling d 160
|
||||||
- 8,500 m laying of HV cable 1,000 mm² 64/110 kV
|
- 8,500 m laying of HV cable 1,000 mm² 64/110 kV
|
||||||
- 11 sets of connection joints 1,000 mm² 64/110 kV
|
- 11 sets of connection joints 1,000 mm² 64/110 kV
|
||||||
- 2 sets of outdoor terminations 1,000 mm² 64/110 kV
|
- 2 sets of outdoor terminations 1,000 mm² 64/110 kV
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ layout: "fullBleed"
|
|||||||
{
|
{
|
||||||
id: "martin-polan",
|
id: "martin-polan",
|
||||||
name: "Martin Polan",
|
name: "Martin Polan",
|
||||||
position: "Site Manager",
|
position: "Site Management",
|
||||||
email: "m.polan@e-tib.com",
|
email: "m.polan@e-tib.com",
|
||||||
phone: "+49 171 3868167",
|
phone: "+49 171 3868167",
|
||||||
image: "/assets/photos/team/martin.jpg"
|
image: "/assets/photos/team/martin.jpg"
|
||||||
|
|||||||
5303
lh-report.json
Normal file
5303
lh-report.json
Normal file
File diff suppressed because one or more lines are too long
@@ -189,62 +189,6 @@ export const minorLocations: Location[] = [
|
|||||||
y: 83.33,
|
y: 83.33,
|
||||||
description: 'fiber',
|
description: 'fiber',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'hdd-29436-meter-29436',
|
|
||||||
name: 'HDD 29436 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 46.89,
|
|
||||||
y: 41.34,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hdd-28680-meter-28680',
|
|
||||||
name: 'HDD 28680 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 44.84,
|
|
||||||
y: 43.61,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hdd-44655-meter-44655',
|
|
||||||
name: 'HDD 44655 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 49.36,
|
|
||||||
y: 48.57,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'kabelverlegung-76727-meter-76727',
|
|
||||||
name: 'Kabelverlegung 76727 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 45.57,
|
|
||||||
y: 49.95,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'offener-tiefbau-21288-meter-21288',
|
|
||||||
name: 'Offener Tiefbau 21288 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 40.15,
|
|
||||||
y: 49.41,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'offener-tiefbau-30773-meter-30773',
|
|
||||||
name: 'Offener Tiefbau 30773 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 41.74,
|
|
||||||
y: 43.78,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'offener-tiefbau-34815-meter-34815',
|
|
||||||
name: 'Offener Tiefbau 34815 Meter',
|
|
||||||
type: 'minor_node',
|
|
||||||
x: 43.91,
|
|
||||||
y: 42.14,
|
|
||||||
description: 'power',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'pv-anlage-01909-grosshartau-01909',
|
id: 'pv-anlage-01909-grosshartau-01909',
|
||||||
name: 'PV-Anlage, 01909 Großhartau',
|
name: 'PV-Anlage, 01909 Großhartau',
|
||||||
|
|||||||
@@ -11,10 +11,21 @@ export async function getOgFonts() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||||
const boldFont = readFileSync(boldFontPath);
|
const boldFontBuffer = readFileSync(boldFontPath);
|
||||||
const regularFont = readFileSync(regularFontPath);
|
const regularFontBuffer = readFileSync(regularFontPath);
|
||||||
|
|
||||||
|
// Satori strictly requires an ArrayBuffer.
|
||||||
|
const boldFont = boldFontBuffer.buffer.slice(
|
||||||
|
boldFontBuffer.byteOffset,
|
||||||
|
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
const regularFont = regularFontBuffer.buffer.slice(
|
||||||
|
regularFontBuffer.byteOffset,
|
||||||
|
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
|
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -37,6 +48,28 @@ export async function getOgFonts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOgBackground() {
|
||||||
|
try {
|
||||||
|
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
|
||||||
|
const bgBase64 = readFileSync(bgPath, 'base64');
|
||||||
|
return `data:image/jpeg;base64,${bgBase64}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[OG] Failed to load background', err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOgLogo() {
|
||||||
|
try {
|
||||||
|
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
|
||||||
|
const logoBase64 = readFileSync(logoPath, 'base64');
|
||||||
|
return `data:image/png;base64,${logoBase64}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[OG] Failed to load logo', err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common configuration for OG images
|
* Common configuration for OG images
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const standorteData: StandortData[] = [
|
|||||||
{
|
{
|
||||||
id: 'guben',
|
id: 'guben',
|
||||||
name: 'E-TIB GmbH',
|
name: 'E-TIB GmbH',
|
||||||
shortName: 'Guben (Hauptsitz)',
|
shortName: 'Standort Brandenburg (Hauptsitz)',
|
||||||
type: 'hq',
|
type: 'hq',
|
||||||
description: {
|
description: {
|
||||||
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
|
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
|
||||||
@@ -67,7 +67,7 @@ export const standorteData: StandortData[] = [
|
|||||||
'Central project management'
|
'Central project management'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
image: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg',
|
image: '/assets/photos/DSC02114.jpg',
|
||||||
gallery: [
|
gallery: [
|
||||||
'/assets/photos/DSC02114.jpg',
|
'/assets/photos/DSC02114.jpg',
|
||||||
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-45.jpg'
|
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-45.jpg'
|
||||||
@@ -117,7 +117,7 @@ export const standorteData: StandortData[] = [
|
|||||||
{
|
{
|
||||||
id: 'buelstedt',
|
id: 'buelstedt',
|
||||||
name: 'E-TIB Ingenieurgesellschaft mbH',
|
name: 'E-TIB Ingenieurgesellschaft mbH',
|
||||||
shortName: 'Bülstedt (Ingenieurgesellschaft)',
|
shortName: 'Standort Niedersachsen',
|
||||||
type: 'branch',
|
type: 'branch',
|
||||||
description: {
|
description: {
|
||||||
de: 'Unsere Ingenieurgesellschaft in Bülstedt ist spezialisiert auf die hochpräzise Planung, Vermessung und Projektierung von Energie- und Telekommunikationsnetzen. Hier verschmelzen technische Expertise und modernes Datenmanagement.',
|
de: 'Unsere Ingenieurgesellschaft in Bülstedt ist spezialisiert auf die hochpräzise Planung, Vermessung und Projektierung von Energie- und Telekommunikationsnetzen. Hier verschmelzen technische Expertise und modernes Datenmanagement.',
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const nextConfig = {
|
|||||||
pagesBufferLength: 2,
|
pagesBufferLength: 2,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
staleTimes: {
|
||||||
|
dynamic: 0,
|
||||||
|
static: 30,
|
||||||
|
},
|
||||||
optimizePackageImports: [
|
optimizePackageImports: [
|
||||||
'lucide-react',
|
'lucide-react',
|
||||||
'framer-motion',
|
'framer-motion',
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.13-rc.63",
|
"version": "2.2.30-rc.8",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
3
proxy.ts
3
proxy.ts
@@ -9,8 +9,9 @@ export default createMiddleware({
|
|||||||
export const config = {
|
export const config = {
|
||||||
// Match all pathnames except for
|
// Match all pathnames except for
|
||||||
// - /api (API routes)
|
// - /api (API routes)
|
||||||
|
// - /stats (Analytics proxy)
|
||||||
// - /_next (Next.js internals)
|
// - /_next (Next.js internals)
|
||||||
// - /_static (inside /public)
|
// - /_static (inside /public)
|
||||||
// - all root files inside /public (e.g. /favicon.ico)
|
// - all root files inside /public (e.g. /favicon.ico)
|
||||||
matcher: ['/((?!api|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
|
matcher: ['/((?!api|stats|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
public/fonts/Inter-Bold.woff
Normal file
BIN
public/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/Inter-Regular.woff
Normal file
BIN
public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
@@ -9,8 +9,8 @@ echo "🚀 Starting High-Fidelity Local Audit..."
|
|||||||
|
|
||||||
# 1. Environment and Infrastructure
|
# 1. Environment and Infrastructure
|
||||||
export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock"
|
export DOCKER_HOST="unix:///Users/marcmintel/.docker/run/docker.sock"
|
||||||
export IMGPROXY_URL="http://klz-imgproxy:8080"
|
export IMGPROXY_URL="http://etib-imgproxy:8080"
|
||||||
export NEXT_URL="http://klz.localhost"
|
export NEXT_URL="http://etib.localhost"
|
||||||
export NEXT_PUBLIC_CI=true
|
export NEXT_PUBLIC_CI=true
|
||||||
export CI=true
|
export CI=true
|
||||||
|
|
||||||
@@ -20,14 +20,14 @@ docker volume create klz_db_data 2>/dev/null || true
|
|||||||
# 2. Start infra services (DB, CMS, Gatekeeper)
|
# 2. Start infra services (DB, CMS, Gatekeeper)
|
||||||
echo "📦 Starting infrastructure services..."
|
echo "📦 Starting infrastructure services..."
|
||||||
# Using --remove-orphans to ensure a clean state
|
# Using --remove-orphans to ensure a clean state
|
||||||
docker-compose up -d --remove-orphans klz-db klz-gatekeeper
|
docker-compose up -d --remove-orphans etib-db etib-gatekeeper
|
||||||
|
|
||||||
# 3. Build and Start klz-app in Production Mode
|
# 3. Build and Start etib-app in Production Mode
|
||||||
echo "🏗️ Building and starting klz-app (Production)..."
|
echo "🏗️ Building and starting etib-app (Production)..."
|
||||||
# We bypass the dev override by explicitly using the base compose file
|
# We bypass the dev override by explicitly using the base compose file
|
||||||
NEXT_PUBLIC_BASE_URL=$NEXT_URL \
|
NEXT_PUBLIC_BASE_URL=$NEXT_URL \
|
||||||
NEXT_PUBLIC_CI=true \
|
NEXT_PUBLIC_CI=true \
|
||||||
docker-compose -f docker-compose.yml up -d --build klz-app
|
docker-compose -f docker-compose.yml up -d --build etib-app
|
||||||
|
|
||||||
# 4. Wait for application to be ready
|
# 4. Wait for application to be ready
|
||||||
echo "⏳ Waiting for application to be healthy..."
|
echo "⏳ Waiting for application to be healthy..."
|
||||||
@@ -54,4 +54,4 @@ echo "♿ Executing WCAG Audit..."
|
|||||||
NEXT_PUBLIC_BASE_URL=$NEXT_URL PAGESPEED_LIMIT=10 pnpm run check:wcag "$NEXT_URL"
|
NEXT_PUBLIC_BASE_URL=$NEXT_URL PAGESPEED_LIMIT=10 pnpm run check:wcag "$NEXT_URL"
|
||||||
|
|
||||||
echo "✨ Audit completed! Summary above."
|
echo "✨ Audit completed! Summary above."
|
||||||
echo "💡 You can stop the production app with: docker-compose stop klz-app"
|
echo "💡 You can stop the production app with: docker-compose stop etib-app"
|
||||||
|
|||||||
Reference in New Issue
Block a user