feat(ui): finalize E-TIB modernization with footer redesign, video optimization, and TS fixes

Former-commit-id: 67ac02c8404cc66893fdf97308574701cca6000c
This commit is contained in:
2026-05-07 10:43:20 +02:00
parent f2fdea96ec
commit 5439df72ea
3712 changed files with 932332 additions and 1867 deletions

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Button } from '@/components/ui/Button';
export interface CallToActionProps {
title?: string;
description?: string;
ctaLabel?: string;
ctaHref?: string;
theme?: 'light' | 'dark';
}
export const CallToAction: React.FC<CallToActionProps> = ({
title = 'Bereit für Ihr Projekt?',
description = 'Wir suchen stets nach neuen Herausforderungen und starken Partnern. Kontaktieren Sie uns für eine unverbindliche Beratung zu Ihrem Vorhaben.',
ctaLabel = 'Jetzt Kontakt aufnehmen',
ctaHref = '/de/kontakt',
theme = 'light'
}) => {
const isDark = theme === 'dark';
return (
<div className={`py-24 text-center ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-50 border-t border-neutral-100'}`}>
<div className="container max-w-3xl mx-auto px-4">
<h2 className={`font-heading text-4xl font-extrabold mb-6 ${isDark ? 'text-white' : 'text-neutral-dark'}`}>
{title}
</h2>
<p className={`text-xl mb-10 leading-relaxed ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>
{description}
</p>
<Button
href={ctaHref}
size="xl"
variant={isDark ? 'white' : 'primary'}
>
{ctaLabel}
</Button>
</div>
</div>
);
};

View File

@@ -3,34 +3,46 @@
import * as React from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
const milestones = [
const defaultMilestones = [
{
year: '2015',
date: '16.12.2015',
title: 'Gründung E-TIB GmbH',
desc: 'Aufbau der Kernkompetenzen: Kabelbau, Kabelpflugarbeiten, Horizontalspülbohrungen, Elektromontagen bis 110 kV, Glasfaser-Kabelmontagen.',
date: '2015',
title: 'Gründung der E-TIB GmbH',
desc: 'Gründung im brandenburgischen Guben mit Fokus auf Tiefbauarbeiten und Kabelmontagen.',
},
{
year: '2019',
date: '04.02.2019',
title: 'Gründung E-TIB Ingenieurgesellschaft mbH',
desc: 'Erweiterung in Planung, Projektierung und Dokumentation. Abdeckung komplexer Querungen.',
date: 'Wachstum',
title: 'Erweiterung der Kompetenzen',
desc: 'Konsequenter Ausbau der Leistungen um Horizontalspülbohrungen und Kabelpflugarbeiten.',
},
{
year: '2019',
date: '14.11.2019',
title: 'Gründung E-TIB Verwaltung GmbH',
desc: 'Bündelung von Erwerb, Vermietung, Verpachtung und Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.',
date: 'Unternehmensgruppe',
title: 'Zusammenschluss',
desc: 'Bündelung der Fachbereiche mit der NEMO GmbH und der E-TIB Ingenieurgesellschaft mbH.',
},
{
year: '2025',
date: '21.10.2025',
title: 'Gründung E-TIB Bohrtechnik GmbH',
desc: 'Spezialisierung auf präzise Horizontalspülbohrungen in allen Bodenklassen zur Sicherung der technologischen Marktdominanz.',
date: 'Heute',
title: 'Die E-TIB Gruppe',
desc: 'Ein starker Verbund mit über 50 Mitarbeitern für bundesweite Infrastrukturprojekte.',
},
];
export function CompanyTimeline() {
interface Milestone {
date: string;
title: string;
desc: string;
}
interface CompanyTimelineProps {
badge?: string;
title?: string;
milestones?: Milestone[];
}
export function CompanyTimeline({
badge = 'Unsere Geschichte',
title = 'Meilensteine der Entwicklung',
milestones = defaultMilestones
}: CompanyTimelineProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
@@ -43,8 +55,8 @@ export function CompanyTimeline() {
<section className="py-24 bg-white relative">
<div className="container max-w-4xl">
<div className="text-center mb-16">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">Unsere Geschichte</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark">Meilensteine der Entwicklung</h3>
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-4xl font-extrabold text-neutral-dark">{title}</h3>
</div>
<div ref={containerRef} className="relative">
@@ -56,9 +68,11 @@ export function CompanyTimeline() {
/>
</div>
<div className="space-y-16">
<div className="space-y-12 md:space-y-0">
{milestones.map((milestone, i) => {
const isEven = i % 2 === 0;
// isEven (0, 2): Left side on desktop
// !isEven (1, 3): Right side on desktop
return (
<motion.div
key={i}
@@ -66,19 +80,19 @@ export function CompanyTimeline() {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
className={`relative flex flex-col md:flex-row items-center ${isEven ? 'md:flex-row-reverse' : ''}`}
className="relative flex flex-col w-full"
>
{/* Timeline Dot */}
<div className="absolute left-4 md:left-1/2 w-8 h-8 rounded-full bg-white border-4 border-primary z-10 -translate-x-1/2 shadow-lg" />
<div className="absolute left-4 md:left-1/2 top-8 md:top-1/2 w-6 h-6 rounded-full bg-white border-4 border-primary z-10 -translate-x-1/2 md:-translate-y-1/2 shadow-lg" />
{/* Content Container */}
<div className={`w-full pl-16 md:pl-0 md:w-1/2 ${isEven ? 'md:pr-16 text-left md:text-right' : 'md:pl-16 text-left'}`}>
<div className="bg-neutral-50 p-6 rounded-2xl shadow-sm border border-neutral-100 hover:shadow-md transition-shadow">
<span className="inline-block py-1 px-3 rounded-full bg-primary/10 text-primary font-bold text-sm mb-4">
{/* Content Box */}
<div className={`w-full md:w-[calc(50%-2.5rem)] pl-16 md:pl-0 ${isEven ? 'md:mr-auto md:text-right' : 'md:ml-auto md:text-left'}`}>
<div className="bg-neutral-50 p-6 md:p-8 rounded-2xl shadow-sm border border-neutral-100 hover:shadow-md transition-shadow relative">
<span className="inline-block py-1.5 px-4 rounded-full bg-primary/10 text-primary font-bold text-sm mb-4">
{milestone.date}
</span>
<h4 className="font-heading font-bold text-2xl mb-2 text-neutral-dark">{milestone.title}</h4>
<p className="text-text-secondary">{milestone.desc}</p>
<h4 className="font-heading font-bold text-2xl md:text-3xl mb-3 text-neutral-dark leading-tight">{milestone.title}</h4>
<p className="text-text-secondary leading-relaxed">{milestone.desc}</p>
</div>
</div>
</motion.div>

View File

@@ -6,6 +6,20 @@ import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
interface CompetenceBentoGridProps {
badge?: string;
title?: string;
ctaLabel?: string;
ctaHref?: string;
items?: {
title?: string;
description?: string;
tag?: string;
image?: {
url?: string;
alt?: string;
} | any;
size?: 'large' | 'medium' | 'small' | 'accent';
}[];
data?: {
badge?: string;
title?: string;
@@ -24,18 +38,19 @@ interface CompetenceBentoGridProps {
};
}
export function CompetenceBentoGrid({ data }: CompetenceBentoGridProps) {
export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
const { data } = props;
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const badge = data?.badge || 'Leistungsspektrum';
const title = data?.title || 'Umfassende Lösungen für komplexe Netzwerke';
const ctaLabel = data?.ctaLabel || 'Alle Kompetenzen ansehen';
const ctaHref = data?.ctaHref || '/kompetenzen';
const items = data?.items || [];
const badge = props.badge || data?.badge || 'Leistungsspektrum';
const title = props.title || data?.title || 'Umfassende Lösungen für komplexe Netzwerke';
const ctaLabel = props.ctaLabel || data?.ctaLabel || 'Alle Kompetenzen ansehen';
const ctaHref = props.ctaHref || data?.ctaHref || '/kompetenzen';
const items = props.items || data?.items || [];
// Static fallback for SSR
if (!isMounted) {
@@ -77,7 +92,7 @@ export function CompetenceBentoGrid({ data }: CompetenceBentoGridProps) {
const isAccent = item.size === 'accent';
const imgSrc = item.image?.url;
let gridClasses = "rounded-none overflow-hidden relative group ";
let gridClasses = "rounded-2xl overflow-hidden relative group ";
if (isLarge) gridClasses += "md:col-span-2 md:row-span-2";
else if (isMedium) gridClasses += "md:col-span-2 lg:col-span-2";
else if (isAccent) gridClasses += "md:col-span-1 bg-primary text-white p-6 md:p-8 flex flex-col justify-center border-none shadow-md";

View File

@@ -2,6 +2,7 @@ import React from 'react';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
export interface HeroSectionProps {
title: string;
@@ -55,15 +56,16 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
)}
{ctaLabel && ctaHref && (
<div className="mt-8">
<a
<Button
href={ctaHref}
className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group"
variant="accent"
size="lg"
>
{ctaLabel}
<span className="ml-3 transition-transform group-hover:translate-x-2">
<span className="ml-3 transition-transform group-hover/btn:translate-x-2">
&rarr;
</span>
</a>
</Button>
</div>
)}
</div>

View File

@@ -1,58 +1,50 @@
'use client';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import Image from 'next/image';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
interface HeroVideoProps {
data?: {
title?: string;
subtitle?: string;
videoUrl?: string;
posterImage?: {
url?: string;
alt?: string;
} | any;
ctaLabel?: string;
ctaHref?: string;
secondaryCtaLabel?: string;
secondaryCtaHref?: string;
};
data?: any;
title?: string;
subtitle?: string;
description?: string;
videoUrl?: string;
posterImage?: any;
ctaLabel?: string;
ctaHref?: string;
linkText?: string;
linkHref?: string;
secondaryCtaLabel?: string;
secondaryCtaHref?: string;
badge?: string;
}
export function HeroVideo({ data }: HeroVideoProps) {
const title = data?.title || 'DIE EXPERTEN FÜR KABELTIEFBAU';
const subtitle = data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const videoUrl = data?.videoUrl || '/assets/dummy-hero.mp4';
export function HeroVideo(props: HeroVideoProps) {
const { data } = props;
const title = props.title || data?.title || 'DIE EXPERTEN FÜR KABELTIEFBAU';
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const videoUrl = props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4';
const posterSrc = data?.posterImage?.url || "/assets/photos/DJI_0048.JPG";
const posterAlt = data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
const ctaLabel = data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = data?.ctaHref || '#unternehmen';
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
const secondaryCtaLabel = data?.secondaryCtaLabel || 'Projekt anfragen';
const secondaryCtaHref = data?.secondaryCtaHref || '/kontakt';
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || 'Projekt anfragen';
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || '/kontakt';
return (
<div className="relative w-full h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* High-quality background image (Immediate visible content) */}
<div className="absolute inset-0 z-0 overflow-hidden">
<Image
src={posterSrc}
alt={posterAlt}
fill
priority
className="object-cover opacity-40 grayscale contrast-125"
sizes="100vw"
/>
</div>
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{/* Video element */}
{videoUrl && (
<video
className="absolute inset-0 w-full h-full object-cover opacity-50 z-1 pointer-events-none"
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
src={videoUrl}
poster={posterSrc}
autoPlay
@@ -63,8 +55,12 @@ export function HeroVideo({ data }: HeroVideoProps) {
/>
)}
<div className="absolute inset-0 bg-primary-dark/30 z-[2] mix-blend-multiply" />
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-black/60 z-[3]" />
{/* Cinematic Color Grading Overlay */}
<div className="absolute inset-0 bg-[#0a192f]/30 z-[2] mix-blend-multiply" />
{/* Dramatic Vignette & Fade to content */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-transparent via-black/20 to-black/80 z-[2]" />
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/60 to-transparent z-[3]" />
<div className="container relative z-[4] text-center px-4">
<motion.div
@@ -82,19 +78,22 @@ export function HeroVideo({ data }: HeroVideoProps) {
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
<Link
<Button
href={ctaHref}
className="group relative inline-flex items-center justify-center px-8 py-4 text-lg font-bold text-white bg-primary rounded-none shadow-lg transition-transform hover:-translate-y-1"
variant="primary"
size="lg"
>
<span className="relative z-10">{ctaLabel}</span>
</Link>
{ctaLabel}
</Button>
<Link
<Button
href={secondaryCtaHref}
className="px-8 py-4 text-lg font-bold text-white border-2 border-white/60 hover:bg-white hover:text-primary rounded-none transition-colors"
variant="outline"
size="lg"
className="border-white/60 text-white hover:text-primary hover:bg-white"
>
{secondaryCtaLabel}
</Link>
</Button>
</div>
</motion.div>
</div>

View File

@@ -1,39 +1,49 @@
import React from 'react';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import Link from 'next/link';
export interface JobListingBlockProps {
title?: string;
showJobs?: boolean;
showFairs?: boolean;
fairsTitle?: string;
fairs?: Array<{ name: string; date: string; type: string }>;
emptyStateMessage?: string;
emptyStateLinkText?: string;
emptyStateLinkHref?: string;
}
export const JobListingBlock = async (props: JobListingBlockProps) => {
const { title, showFairs } = props;
const payload = await getPayload({ config: configPromise });
const {
title,
showJobs = true,
showFairs,
fairsTitle = 'Nächste Messetermine 2026',
fairs = [
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe' },
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress' },
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse' }
],
emptyStateMessage = 'Aktuell sind alle Positionen besetzt. Senden Sie uns gerne eine Initiativbewerbung!',
emptyStateLinkText = 'Jetzt Kontakt aufnehmen',
emptyStateLinkHref = '/kontakt'
} = props;
const { docs: jobs } = await payload.find({
collection: 'jobs',
limit: 100,
});
// Since Payload is removed, we use an empty array or static data
const jobs: any[] = [];
return (
<div className="py-12 md:py-24">
<div className="container max-w-4xl mx-auto">
{showFairs && (
<div className="bg-neutral-dark rounded-3xl p-8 md:p-12 mb-16 text-white overflow-hidden relative group">
<div className="bg-neutral-dark rounded-2xl p-8 md:p-12 mb-16 text-white overflow-hidden relative group">
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/20 blur-3xl -translate-y-1/2 translate-x-1/2" />
<h3 className="font-heading font-bold text-3xl mb-8 relative z-10 flex items-center gap-3">
<span className="w-8 h-1 bg-primary" />
Nächste Messetermine 2026
{fairsTitle}
</h3>
<ul className="space-y-6 relative z-10">
{[
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe' },
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress' },
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse' }
].map((messe) => (
<li key={messe.name} className="flex flex-col md:flex-row md:items-center justify-between border-b border-white/10 pb-6 group/item hover:bg-white/5 transition-colors p-4 -m-4 rounded-xl">
{fairs.map((messe) => (
<li key={messe.name} className="flex flex-col md:flex-row md:items-center justify-between border-b border-white/10 pb-6 group/item hover:bg-white/5 transition-colors p-4 -m-4 rounded-2xl">
<div>
<span className="text-primary-light font-bold text-sm uppercase tracking-widest block mb-1">{messe.type}</span>
<span className="font-heading font-bold text-2xl">{messe.name}</span>
@@ -47,41 +57,43 @@ export const JobListingBlock = async (props: JobListingBlockProps) => {
</div>
)}
<div className="mb-12">
<h2 className="font-heading font-extrabold text-4xl text-neutral-dark mb-8">
{title || 'Aktuelle Stellenangebote'}
</h2>
{jobs.length > 0 ? (
<div className="grid gap-6">
{jobs.map((job: any) => (
<Link
key={job.id}
href={`/karriere/${job.slug}`}
className="group bg-white border border-neutral-100 p-8 rounded-2xl shadow-sm hover:shadow-xl hover:border-primary/20 transition-all flex flex-col md:flex-row md:items-center justify-between gap-6"
>
<div>
<div className="flex items-center gap-3 mb-2">
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-bold rounded-full uppercase">{job.type}</span>
<span className="text-text-secondary text-sm font-medium">{job.department}</span>
{showJobs && (
<div className="mb-12">
<h2 className="font-heading font-extrabold text-4xl text-neutral-dark mb-8">
{title || 'Aktuelle Stellenangebote'}
</h2>
{jobs.length > 0 ? (
<div className="grid gap-6">
{jobs.map((job: any) => (
<Link
key={job.id}
href={`/karriere/${job.slug}`}
className="group bg-white border border-neutral-100 p-8 rounded-2xl shadow-sm hover:shadow-xl hover:border-primary/20 transition-all flex flex-col md:flex-row md:items-center justify-between gap-6"
>
<div>
<div className="flex items-center gap-3 mb-2">
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-bold rounded-full uppercase">{job.type}</span>
<span className="text-text-secondary text-sm font-medium">{job.department}</span>
</div>
<h4 className="font-heading font-bold text-2xl text-neutral-dark group-hover:text-primary transition-colors">{job.title}</h4>
<p className="text-text-secondary mt-1">{job.location}</p>
</div>
<h4 className="font-heading font-bold text-2xl text-neutral-dark group-hover:text-primary transition-colors">{job.title}</h4>
<p className="text-text-secondary mt-1">{job.location}</p>
</div>
<div className="flex items-center text-primary font-bold group-hover:translate-x-2 transition-transform">
Details ansehen
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</Link>
))}
</div>
) : (
<div className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center">
<p className="text-text-secondary text-lg">Aktuell sind alle Positionen besetzt. Senden Sie uns gerne eine Initiativbewerbung!</p>
<Link href="/kontakt" className="inline-block mt-6 text-primary font-bold hover:underline">Jetzt Kontakt aufnehmen</Link>
</div>
)}
</div>
<div className="flex items-center text-primary font-bold group-hover:translate-x-2 transition-transform">
Details ansehen
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</Link>
))}
</div>
) : (
<div className="bg-neutral-50 border border-neutral-100 rounded-2xl p-12 text-center">
<p className="text-text-secondary text-lg">{emptyStateMessage}</p>
<Link href={emptyStateLinkHref} className="inline-block mt-6 text-primary font-bold hover:underline">{emptyStateLinkText}</Link>
</div>
)}
</div>
)}
</div>
</div>
);

View File

@@ -17,7 +17,12 @@ export interface Reference {
}
interface ReferencesSliderProps {
references: Reference[];
references?: Reference[];
badge?: string;
title?: string;
description?: string;
ctaLabel?: string;
ctaHref?: string;
data?: {
badge?: string;
title?: string;
@@ -27,14 +32,16 @@ interface ReferencesSliderProps {
}
}
export function ReferencesSlider({ references, data }: ReferencesSliderProps) {
export function ReferencesSlider(props: ReferencesSliderProps) {
const { data } = props;
const references = props.references || [];
const containerRef = React.useRef<HTMLDivElement>(null);
const badge = data?.badge || 'Ausgewählte Projekte';
const title = data?.title || 'Referenzen & Erfolge';
const description = data?.description || 'Ein Auszug unserer erfolgreich abgeschlossenen Projekte im Bereich Kabeltiefbau, Bohrtechnik und Netzinfrastruktur.';
const ctaLabel = data?.ctaLabel || 'Alle Referenzen ansehen';
const ctaHref = data?.ctaHref || '/referenzen';
const badge = props.badge || data?.badge || 'Ausgewählte Projekte';
const title = props.title || data?.title || 'Referenzen & Erfolge';
const description = props.description || data?.description || 'Ein Auszug unserer erfolgreich abgeschlossenen Projekte im Bereich Kabeltiefbau, Bohrtechnik und Netzinfrastruktur.';
const ctaLabel = props.ctaLabel || data?.ctaLabel || 'Alle Referenzen ansehen';
const ctaHref = props.ctaHref || data?.ctaHref || '/referenzen';
if (!references || references.length === 0) return null;
@@ -63,7 +70,7 @@ export function ReferencesSlider({ references, data }: ReferencesSliderProps) {
<div className="relative z-10">
<div
ref={containerRef}
className="flex gap-6 overflow-x-auto pb-12 pt-4 snap-x snap-mandatory hide-scrollbar px-[calc((100vw-min(100%,1280px))/2)]"
className="flex gap-6 overflow-x-auto pb-12 pt-4 snap-x snap-mandatory px-[calc((100vw-min(100%,1280px))/2)] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
>
{references.map((ref, i) => {
const imgSrc = (ref.image && typeof ref.image === 'object' && ref.image.url)
@@ -79,7 +86,7 @@ export function ReferencesSlider({ references, data }: ReferencesSliderProps) {
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group cursor-pointer"
>
<div className="relative aspect-[16/10] bg-neutral-800 rounded-3xl overflow-hidden mb-5 border border-white/5 shadow-2xl">
<div className="relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl">
<Image
src={imgSrc}
alt={ref.title}

View File

@@ -6,6 +6,18 @@ import Image from 'next/image';
import Link from 'next/link';
interface SubCompanyTilesProps {
badge?: string;
title?: string;
companies?: {
title?: string;
description?: string;
icon?: string;
url?: string;
backgroundImage?: {
url?: string;
alt?: string;
} | any;
}[];
data?: {
badge?: string;
title?: string;
@@ -13,6 +25,7 @@ interface SubCompanyTilesProps {
title?: string;
description?: string;
icon?: string;
url?: string;
backgroundImage?: {
url?: string;
alt?: string;
@@ -63,15 +76,22 @@ const itemVariants: Variants = {
visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] } },
};
export function SubCompanyTiles({ data }: SubCompanyTilesProps) {
const badge = data?.badge || 'Die Gruppe';
const title = data?.title || 'Geballte Kompetenz unter einem Dach';
const companiesData = data?.companies?.map(c => ({
export function SubCompanyTiles(props: SubCompanyTilesProps) {
const { data } = props;
const badge = props.badge || data?.badge || 'Die Gruppe';
const title = props.title || data?.title || 'Geballte Kompetenz unter einem Dach';
const rawCompanies = props.companies || data?.companies;
const companiesData = rawCompanies?.map(c => ({
title: c.title,
desc: c.description,
url: c.url,
icon: c.icon || 'M13 10V3L4 14h7v7l9-11h-7z',
bgImage: c.backgroundImage?.url || '/assets/photos/DJI_0243.JPG',
bgImage: c.backgroundImage?.url || c.backgroundImage || '/assets/photos/DJI_0243.JPG',
})) || defaultCompanies;
const gridColsClass = companiesData.length === 3
? "grid-cols-1 lg:grid-cols-3"
: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4";
return (
<section id="unternehmen" className="py-24 bg-neutral overflow-hidden">
@@ -82,47 +102,63 @@ export function SubCompanyTiles({ data }: SubCompanyTilesProps) {
</div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
className={`grid gap-8 ${gridColsClass}`}
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
>
{companiesData.map((company, index) => (
<motion.div
key={index}
variants={itemVariants}
className="group relative rounded-none overflow-hidden aspect-[4/5] flex flex-col justify-end p-8 shadow-xl border-b-4 border-primary transition-colors duration-500 bg-white"
>
<div className="absolute inset-0 bg-white z-0">
<Image
src={company.bgImage}
alt={company.title || ''}
fill
className="object-cover opacity-80 group-hover:scale-105 group-hover:opacity-100 transition-all duration-700 ease-in-out"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
/>
</div>
{/* Premium Bright Gradient */}
<div className="absolute inset-0 bg-gradient-to-t from-white via-white/80 to-white/10 opacity-90 group-hover:opacity-100 transition-opacity duration-500 z-0" />
<div className="relative z-10 flex flex-col h-full justify-between">
<div className="w-12 h-12 rounded-none bg-primary text-white flex items-center justify-center group-hover:bg-primary-dark transition-all duration-300 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d={company.icon} />
</svg>
</div>
<div className="mt-auto transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 ease-out">
<h4 className="font-heading font-bold text-2xl text-text-primary mb-2 leading-tight group-hover:text-primary transition-colors">{company.title}</h4>
<p className="text-text-secondary leading-relaxed text-sm h-0 opacity-0 group-hover:h-auto group-hover:opacity-100 group-hover:mt-3 transition-all duration-500 ease-out overflow-hidden line-clamp-3">
{company.desc}
</p>
</div>
</div>
</motion.div>
))}
{companiesData.map((company, index) => {
const CardWrapper = company.url ? 'a' : 'div';
const wrapperProps = company.url ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
return (
<motion.div
key={index}
variants={itemVariants}
className="h-full"
>
<CardWrapper
{...wrapperProps}
className="group relative rounded-lg overflow-hidden aspect-[3/4] flex flex-col justify-end p-8 shadow-xl border-t-4 border-primary transition-all duration-500 hover:shadow-2xl hover:-translate-y-2 bg-neutral-dark cursor-pointer block w-full h-full"
>
<div className="absolute inset-0 bg-[#0a192f] z-0">
<Image
src={company.bgImage}
alt={company.title || ''}
fill
className="object-cover opacity-50 group-hover:opacity-70 group-hover:scale-105 transition-all duration-700 ease-in-out grayscale-[30%] group-hover:grayscale-0"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
{/* Premium Dark Gradient, lighter this time */}
<div className="absolute inset-0 bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/40 to-transparent z-0" />
<div className="relative z-10 flex flex-col h-full justify-between">
<div className="w-14 h-14 rounded-none bg-primary/90 text-white flex items-center justify-center group-hover:bg-primary transition-all duration-300 shadow-lg backdrop-blur-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d={company.icon} />
</svg>
</div>
<div className="mt-auto transform translate-y-6 group-hover:translate-y-0 transition-transform duration-500 ease-out">
<h4 className="font-heading font-bold text-3xl text-white mb-3 leading-tight group-hover:text-primary transition-colors">{company.title}</h4>
<p className="text-white/80 leading-relaxed text-base h-0 opacity-0 group-hover:h-auto group-hover:opacity-100 group-hover:mt-4 transition-all duration-500 ease-out overflow-hidden line-clamp-4">
{company.desc}
</p>
{company.url && (
<div className="h-0 opacity-0 group-hover:h-auto group-hover:opacity-100 group-hover:mt-4 transition-all duration-500 ease-out flex items-center text-primary font-bold uppercase text-xs tracking-widest">
Website Besuchen
<svg className="w-4 h-4 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</div>
)}
</div>
</div>
</CardWrapper>
</motion.div>
);
})}
</motion.div>
</div>
</section>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
export interface SupportCTAProps {
title: string;
@@ -19,15 +20,18 @@ export const SupportCTA: React.FC<SupportCTAProps> = (props) => {
<p className="text-lg text-white/70 mb-8">{description}</p>
<TrackedLink
href={buttonHref}
className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link"
className={getButtonClasses('accent', 'lg')}
eventProperties={{
location: 'block_support_cta',
}}
>
{buttonLabel}
<span className="ml-2 transition-transform group-hover/link:translate-x-1">
&rarr;
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
{buttonLabel}
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
&rarr;
</span>
</span>
<ButtonOverlay variant="accent" />
</TrackedLink>
</div>
</div>

View File

@@ -1,43 +1,35 @@
import React from 'react';
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import * as React from 'react';
import { TeamGrid as TeamGridComponent } from './TeamGrid';
export interface TeamMember {
id: string;
name: string;
position: string;
email?: string | null;
phone?: string | null;
image?: { url: string; alt?: string } | string | null;
branch: string;
}
export interface TeamGridBlockProps {
title?: string;
subtitle?: string;
filterBranch?: string;
department?: string;
showContact?: boolean;
}
export const TeamGridBlock = async (props: TeamGridBlockProps) => {
const { title, subtitle, filterBranch } = props;
const payload = await getPayload({ config: configPromise });
const query: any = {
collection: 'team',
limit: 100,
depth: 1,
};
if (filterBranch && filterBranch !== 'all') {
query.where = {
branch: {
equals: filterBranch,
},
};
}
const { docs: teamMembers } = await payload.find(query);
// Map Payload docs to TeamGrid expected format
export const TeamGridBlock = async ({ title, department, showContact = true }: TeamGridBlockProps) => {
// Static fallback since Payload CMS is removed
const teamMembers: any[] = [];
// Map docs to TeamGrid expected format
const members = teamMembers.map((m: any) => ({
id: m.id,
name: m.name,
position: m.position,
position: m.role || m.position || '',
branch: m.department || m.branch || 'e-tib',
email: m.email,
phone: m.phone,
image: m.image,
branch: m.branch,
}));
return (

View File

@@ -3,6 +3,7 @@ import Image from 'next/image';
import Reveal from '@/components/Reveal';
import { Badge, Heading } from '@/components/ui';
import TrackedLink from '@/components/analytics/TrackedLink';
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
export interface TeamProfileProps {
name: string;
@@ -71,17 +72,20 @@ export const TeamProfile: React.FC<TeamProfileProps> = (props) => {
{linkedinUrl && (
<TrackedLink
href={linkedinUrl}
className={`inline-flex items-center px-8 py-4 font-bold rounded-full transition-all duration-300 group ${isDark ? 'bg-accent text-primary-dark hover:bg-white' : 'bg-saturated text-white hover:bg-primary'}`}
className={getButtonClasses(isDark ? 'accent' : 'saturated', 'lg')}
eventProperties={{
type: 'social_linkedin',
person: name,
location: 'team_page',
}}
>
{linkedinLabel || 'LinkedIn'}
<span className="ml-3 transition-transform group-hover:translate-x-2">
&rarr;
<span className={`relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 ${isDark ? 'group-hover/btn:text-primary-dark' : 'group-hover/btn:text-white'}`}>
{linkedinLabel || 'LinkedIn'}
<span className="ml-3 transition-transform group-hover/btn:translate-x-2">
&rarr;
</span>
</span>
<ButtonOverlay variant={isDark ? 'accent' : 'saturated'} />
</TrackedLink>
)}
</div>