Compare commits
20 Commits
fix/qa-wor
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
| a9f16b49cc | |||
| 21cf0725ab | |||
| 2c162f5314 | |||
| be3b2e6a7b | |||
| f2e59fa52f | |||
| 16d494689f | |||
| d24b8a8694 | |||
| 8d42946711 | |||
| b15217b9be | |||
| b138025380 | |||
| 18d2ace03b | |||
| 89ed1faa6b | |||
| 7307e812a4 | |||
| 20d6194a7e | |||
| 86e2fd7097 | |||
| bf8073f5bf | |||
| e8adb87b02 | |||
| 067e34c19b | |||
| e6017e7436 | |||
| 1955bfcd1b |
2895
.lighthouseci/lhr-1782911226698.html
Normal file
2895
.lighthouseci/lhr-1782911226698.html
Normal file
File diff suppressed because one or more lines are too long
11796
.lighthouseci/lhr-1782911226698.json
Normal file
11796
.lighthouseci/lhr-1782911226698.json
Normal file
File diff suppressed because one or more lines are too long
@@ -21,8 +21,7 @@ import { DynamicInitialLoader as InitialLoader } from '@/components/providers/Dy
|
|||||||
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
|
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
|
||||||
|
|
||||||
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
|
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
|
||||||
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
|
const corporateBackground = null; // Removed to prevent line shifting
|
||||||
import { DynamicFramerMotion as FramerMotionProvider } from '@/components/providers/DynamicFramerMotion';
|
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -191,28 +190,26 @@ export default async function Layout(props: {
|
|||||||
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white" suppressHydrationWarning>
|
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white" suppressHydrationWarning>
|
||||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
<TransitionProvider>
|
<TransitionProvider>
|
||||||
<FramerMotionProvider>
|
<CorporateBackground />
|
||||||
<CorporateBackground />
|
{!hasSeenLoader && <InitialLoader shouldShowLoader={!hasSeenLoader} />}
|
||||||
<InitialLoader shouldShowLoader={!hasSeenLoader} />
|
{/* PageTransitionShutter loaded dynamically in TransitionProvider */}
|
||||||
<PageTransitionShutter />
|
<SkipLink />
|
||||||
<SkipLink />
|
<Header navLinks={navLinks} />
|
||||||
<Header navLinks={navLinks} />
|
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
||||||
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
|
||||||
|
<main
|
||||||
<main
|
id="main-content"
|
||||||
id="main-content"
|
className="flex-grow overflow-visible"
|
||||||
className="flex-grow overflow-visible"
|
tabIndex={-1}
|
||||||
tabIndex={-1}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</main>
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer companyInfo={companyInfo} />
|
<Footer companyInfo={companyInfo} />
|
||||||
<JsonLd />
|
<JsonLd />
|
||||||
<AnalyticsShell />
|
<AnalyticsShell />
|
||||||
|
|
||||||
<FeedbackClientWrapper />
|
<FeedbackClientWrapper />
|
||||||
</FramerMotionProvider>
|
|
||||||
</TransitionProvider>
|
</TransitionProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -132,7 +132,12 @@ export default async function Home(props: { params: Promise<{ locale: string }>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
|
preload('/assets/videos/web/hero-kabelpflug-poster.webp', {
|
||||||
|
as: 'image',
|
||||||
|
fetchPriority: 'high',
|
||||||
|
imageSrcSet: '/assets/videos/web/hero-kabelpflug-poster-mobile.webp 800w, /assets/videos/web/hero-kabelpflug-poster.webp 1920w',
|
||||||
|
imageSizes: '100vw'
|
||||||
|
});
|
||||||
|
|
||||||
const mdx = await getMdxContent(locale, 'home');
|
const mdx = await getMdxContent(locale, 'home');
|
||||||
const allReferences = await getAllReferences(locale);
|
const allReferences = await getAllReferences(locale);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
|
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
|
||||||
}}
|
}}
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
quality={50}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
{/* Cinematic Color Grading Overlay */}
|
{/* Cinematic Color Grading Overlay */}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
|
|
||||||
let defaultPoster = "/assets/photos/DJI_0048.JPG";
|
let defaultPoster = "/assets/photos/DJI_0048.JPG";
|
||||||
if (videoUrl && videoUrl.endsWith('.mp4')) {
|
if (videoUrl && videoUrl.endsWith('.mp4')) {
|
||||||
defaultPoster = videoUrl.replace('.mp4', '-poster.jpg');
|
defaultPoster = videoUrl.replace('.mp4', '-poster.webp');
|
||||||
}
|
}
|
||||||
|
|
||||||
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
|
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
|
||||||
@@ -80,16 +80,14 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
{/* 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" />
|
||||||
|
|
||||||
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
|
<img
|
||||||
<Image
|
|
||||||
key={`img-${pathname}-${posterSrc}`}
|
key={`img-${pathname}-${posterSrc}`}
|
||||||
src={posterSrc}
|
src={posterSrc}
|
||||||
|
srcSet={posterSrc.includes('-poster.webp') ? `${posterSrc.replace('-poster.webp', '-poster-mobile.webp')} 800w, ${posterSrc} 1920w` : undefined}
|
||||||
|
sizes={posterSrc.includes('-poster.webp') ? "100vw" : undefined}
|
||||||
alt={posterAlt}
|
alt={posterAlt}
|
||||||
fill
|
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
|
||||||
className="object-cover z-[1] pointer-events-none"
|
decoding="sync"
|
||||||
sizes="100vw"
|
|
||||||
quality={25}
|
|
||||||
priority
|
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
/>
|
/>
|
||||||
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
|
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
|
|||||||
alt="European Union Background"
|
alt="European Union Background"
|
||||||
fill
|
fill
|
||||||
className="object-cover object-top"
|
className="object-cover object-top"
|
||||||
priority
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
export const DynamicFramerMotion = dynamic(
|
|
||||||
() => import('./FramerMotionProvider').then((mod) => mod.FramerMotionProvider),
|
|
||||||
{ ssr: true }
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { LazyMotion } from 'framer-motion';
|
|
||||||
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
|
|
||||||
|
|
||||||
export function FramerMotionProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<LazyMotion features={loadFeatures} strict>
|
|
||||||
{children}
|
|
||||||
</LazyMotion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
|||||||
{/* Sweeping intense white light */}
|
{/* Sweeping intense white light */}
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||||
initial={{ left: '-150%' }}
|
initial={{ x: '-150%' }}
|
||||||
animate={{ left: '250%' }}
|
animate={{ x: '250%' }}
|
||||||
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
|
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
|||||||
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
|
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
|
||||||
>
|
>
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
|
className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
|
||||||
initial={{ width: "0%" }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ width: "100%" }}
|
animate={{ scaleX: 1 }}
|
||||||
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
|
||||||
/>
|
/>
|
||||||
</m.div>
|
</m.div>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isTransitioning && (
|
{isTransitioning && (
|
||||||
<m.div
|
<m.div
|
||||||
key="shutter"
|
key="page-transition-shutter"
|
||||||
initial={{ y: '-100%' }}
|
initial={{ y: '-100%' }}
|
||||||
animate={{ y: '0%' }}
|
animate={{ y: 0 }}
|
||||||
exit={{ y: '100%' }}
|
exit={{ y: '100%' }}
|
||||||
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
|
||||||
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
|
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
top: '0',
|
top: '0',
|
||||||
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
|
|||||||
{/* Sweeping intense white light */}
|
{/* Sweeping intense white light */}
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||||
initial={{ left: '-150%' }}
|
initial={{ x: '-150%' }}
|
||||||
animate={{ left: '250%' }}
|
animate={{ x: '250%' }}
|
||||||
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
|
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||||
|
import { LazyMotion } from 'framer-motion';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
|
||||||
|
|
||||||
|
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
|
||||||
|
const DynamicPageTransitionShutter = dynamic(
|
||||||
|
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
interface TransitionContextType {
|
interface TransitionContextType {
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
@@ -14,8 +24,12 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
|
|||||||
export function TransitionProvider({ children }: { children: ReactNode }) {
|
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 [hasMountedShutter, setHasMountedShutter] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// We no longer eagerly load the shutter in the background.
|
||||||
|
// It will be loaded precisely when the user initiates a transition.
|
||||||
|
|
||||||
// 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);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,6 +55,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
setTransitionMessage(message);
|
setTransitionMessage(message);
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
|
setHasMountedShutter(true);
|
||||||
|
|
||||||
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -68,7 +83,10 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
setTransitionMessage={setTransitionMessage}
|
setTransitionMessage={setTransitionMessage}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
{children}
|
<LazyMotion features={loadFeatures}>
|
||||||
|
{hasMountedShutter && <DynamicPageTransitionShutter />}
|
||||||
|
{children}
|
||||||
|
</LazyMotion>
|
||||||
</TransitionContext.Provider>
|
</TransitionContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungsnetzbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabeltiefbau",
|
href: "/de/kabeltiefbau",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
|||||||
<HeroSection
|
<HeroSection
|
||||||
badge="Kernkompetenz"
|
badge="Kernkompetenz"
|
||||||
title="Kabelleitungsnetzbau"
|
title="Kabelleitungsnetzbau"
|
||||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"
|
||||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||||
alignment="center"
|
alignment="center"
|
||||||
ctaLabel="Projekt anfragen"
|
ctaLabel="Projekt anfragen"
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const nextConfig = {
|
|||||||
pagesBufferLength: 2,
|
pagesBufferLength: 2,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
inlineCss: true,
|
|
||||||
staleTimes: {
|
staleTimes: {
|
||||||
dynamic: 0,
|
dynamic: 0,
|
||||||
static: 30,
|
static: 30,
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.94",
|
"version": "2.4.5",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -39,7 +39,7 @@ describe('Task 6 Content Updates', () => {
|
|||||||
|
|
||||||
// DE Home
|
// DE Home
|
||||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)"');
|
||||||
expect(deContent).toContain('100+');
|
expect(deContent).toContain('100+');
|
||||||
expect(deContent).toContain('Mitarbeitende');
|
expect(deContent).toContain('Mitarbeitende');
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||||
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
||||||
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
|
|
||||||
// EN Kabelnetzbau
|
// EN Kabelnetzbau
|
||||||
|
|||||||
Reference in New Issue
Block a user