feat(ui): finalize E-TIB modernization with footer redesign, video optimization, and TS fixes
Former-commit-id: 67ac02c8404cc66893fdf97308574701cca6000c
This commit is contained in:
40
components/blocks/CallToAction.tsx
Normal file
40
components/blocks/CallToAction.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
→
|
||||
<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">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
<ButtonOverlay variant="accent" />
|
||||
</TrackedLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
→
|
||||
<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">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
<ButtonOverlay variant={isDark ? 'accent' : 'saturated'} />
|
||||
</TrackedLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user