Compare commits

..

4 Commits

Author SHA1 Message Date
531e1a49f8 fix: remove inline opacity: 0 from HeroSection and use animation-fill-mode both to prevent invisible text
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 47s
Build & Deploy / 🧪 QA (push) Successful in 1m14s
Build & Deploy / 🏗️ Build (push) Successful in 2m15s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-03 14:45:37 +02:00
324cfb0ca3 fix: ci hang in maintenance, restore hero animations, bump version 2.4.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 13:18:33 +02:00
69d2c42ef7 chore: bump version to 2.4.22
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Failing after 22m7s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 12:22:32 +02:00
6fb46278e0 fix: restore hero animations in tailwind v4 on 2.2.95
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-07-03 12:21:43 +02:00
22 changed files with 92 additions and 14794 deletions

View File

@@ -47,14 +47,7 @@ jobs:
run: | run: |
echo "=== System Disk Usage ===" echo "=== System Disk Usage ==="
df -h || true df -h || true
echo "=== Tool Cache Usage ==="
du -sh /opt/hostedtoolcache/* || true
du -sh /opt/hostedtoolcache/.[!.]* || true
echo "Purging old tool caches skipped to prevent runner corruption." echo "Purging old tool caches skipped to prevent runner corruption."
echo "=== Host Disk Usage ==="
docker run --rm -v /:/host-root alpine df -h || true
echo "=== Host Inode Usage ==="
docker run --rm -v /:/host-root alpine df -i || true
echo "=== Running Host Containers ===" echo "=== Running Host Containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
echo "=== Host Docker Info ===" echo "=== Host Docker Info ==="

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,8 @@ 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';
const corporateBackground = null; // Removed to prevent line shifting import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
import { DynamicFramerMotion as FramerMotionProvider } from '@/components/providers/DynamicFramerMotion';
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
@@ -190,9 +191,10 @@ 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} />
@@ -210,6 +212,7 @@ export default async function Layout(props: {
<AnalyticsShell /> <AnalyticsShell />
<FeedbackClientWrapper /> <FeedbackClientWrapper />
</FramerMotionProvider>
</TransitionProvider> </TransitionProvider>
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>

View File

@@ -132,12 +132,7 @@ export default async function Home(props: { params: Promise<{ locale: string }>
} }
setRequestLocale(locale); setRequestLocale(locale);
preload('/assets/videos/web/hero-kabelpflug-poster.webp', { preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
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);

View File

@@ -88,7 +88,6 @@
} }
/* trigger rebuild */ /* trigger rebuild */
@layer utilities {
@keyframes hero-fade-in-up { @keyframes hero-fade-in-up {
from { from {
opacity: 0; opacity: 0;
@@ -100,12 +99,10 @@
} }
} }
.animate-hero-fade-in-up { @utility animate-hero-fade-in-up {
animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
} }
}
@layer utilities {
@keyframes hero-zoom-out { @keyframes hero-zoom-out {
from { from {
transform: scale(1.05); transform: scale(1.05);
@@ -115,7 +112,6 @@
} }
} }
.animate-hero-zoom-out { @utility animate-hero-zoom-out {
animation: hero-zoom-out 1.5s ease-out forwards; animation: hero-zoom-out 1.5s ease-out forwards;
} }
}

View File

@@ -41,7 +41,6 @@ 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 */}
@@ -61,26 +60,26 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`} className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
> >
{badge && ( {badge && (
<div className="animate-hero-fade-in-up" style={{ animationDelay: '0.1s', opacity: 0, animationFillMode: 'forwards' }}> <div className="animate-hero-fade-in-up" style={{ animationDelay: '0.1s', animationFillMode: 'both' }}>
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg"> <Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{badge} {badge}
</Badge> </Badge>
</div> </div>
)} )}
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.25s' : '0.1s', opacity: 0, animationFillMode: 'forwards' }}> <div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.25s' : '0.1s', animationFillMode: 'both' }}>
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8"> <Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
{title} {title}
</Heading> </Heading>
</div> </div>
{subtitle && ( {subtitle && (
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.4s' : '0.25s', opacity: 0, animationFillMode: 'forwards' }}> <div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.4s' : '0.25s', animationFillMode: 'both' }}>
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}> <p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
{subtitle} {subtitle}
</p> </p>
</div> </div>
)} )}
{ctaLabel && ctaHref && ( {ctaLabel && ctaHref && (
<div className={`mt-8 animate-hero-fade-in-up ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`} style={{ animationDelay: badge ? '0.55s' : '0.4s', opacity: 0, animationFillMode: 'forwards' }}> <div className={`mt-8 animate-hero-fade-in-up ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`} style={{ animationDelay: badge ? '0.55s' : '0.4s', animationFillMode: 'both' }}>
<Button <Button
href={ctaHref} href={ctaHref}
variant="accent" variant="accent"

View File

@@ -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.webp'); defaultPoster = videoUrl.replace('.mp4', '-poster.jpg');
} }
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,14 +80,16 @@ 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" />
<img {/* ALWAYS render the Next.js optimized Image as the LCP element */}
<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}
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none" fill
decoding="sync" className="object-cover z-[1] pointer-events-none"
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 */}

View File

@@ -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"
loading="lazy" priority
/> />
</div> </div>

View File

@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
export const DynamicFramerMotion = dynamic(
() => import('./FramerMotionProvider').then((mod) => mod.FramerMotionProvider),
{ ssr: true }
);

View File

@@ -0,0 +1,11 @@
'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>
);
}

View File

@@ -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={{ x: '-150%' }} initial={{ left: '-150%' }}
animate={{ x: '250%' }} animate={{ left: '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 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left" className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
initial={{ scaleX: 0 }} initial={{ width: "0%" }}
animate={{ scaleX: 1 }} animate={{ width: "100%" }}
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>

View File

@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
<AnimatePresence> <AnimatePresence>
{isTransitioning && ( {isTransitioning && (
<m.div <m.div
key="page-transition-shutter" key="shutter"
initial={{ y: '-100%' }} initial={{ y: '-100%' }}
animate={{ y: 0 }} animate={{ y: '0%' }}
exit={{ y: '100%' }} exit={{ y: '100%' }}
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }} transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden" className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
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={{ x: '-150%' }} initial={{ left: '-150%' }}
animate={{ x: '250%' }} animate={{ left: '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>

View File

@@ -2,16 +2,6 @@
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;
@@ -24,12 +14,8 @@ 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(() => {
@@ -55,7 +41,6 @@ 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(() => {
@@ -83,10 +68,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTransitionMessage={setTransitionMessage} setTransitionMessage={setTransitionMessage}
/> />
</React.Suspense> </React.Suspense>
<LazyMotion features={loadFeatures}>
{hasMountedShutter && <DynamicPageTransitionShutter />}
{children} {children}
</LazyMotion>
</TransitionContext.Provider> </TransitionContext.Provider>
); );
} }

View File

@@ -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 (Nieder-/Mittel- und Hochspannung bis 110 kV)", description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
tag: "Energie", tag: "Energie",
size: "large", size: "large",
href: "/de/kabeltiefbau", href: "/de/kabeltiefbau",

View File

@@ -8,7 +8,7 @@ layout: "fullBleed"
<HeroSection <HeroSection
badge="Kernkompetenz" badge="Kernkompetenz"
title="Kabelleitungsnetzbau" title="Kabelleitungsnetzbau"
subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV" subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen 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"

View File

@@ -15,6 +15,7 @@ const nextConfig = {
pagesBufferLength: 2, pagesBufferLength: 2,
}, },
experimental: { experimental: {
inlineCss: true,
staleTimes: { staleTimes: {
dynamic: 0, dynamic: 0,
static: 30, static: 30,

View File

@@ -139,7 +139,7 @@
"prepare": "husky", "prepare": "husky",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"version": "2.4.5", "version": "2.4.24",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -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 (Nieder-/Mittel- und Hochspannung bis 110 kV)"'); expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen 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 Nieder-, Mittel- und Hochspannungsnetze 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('"Kabelmontagen bis 110 kV"'); expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
// EN Kabelnetzbau // EN Kabelnetzbau