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

@@ -1,28 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
const BrochureModal = dynamic(() => import('./BrochureModal'), { ssr: false });
export default function AutoBrochureModal() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
// Check if user has already seen or interacted with the modal
const hasSeenModal = localStorage.getItem('klz_brochure_modal_seen');
if (!hasSeenModal) {
// Auto-open after 5 seconds to not interrupt immediate page load
const timer = setTimeout(() => {
setIsOpen(true);
// Mark as seen so it doesn't bother them again on next page load
localStorage.setItem('klz_brochure_modal_seen', 'true');
}, 5000);
return () => clearTimeout(timer);
}
}, []);
return <BrochureModal isOpen={isOpen} onClose={() => setIsOpen(false)} />;
}

View File

@@ -1,88 +0,0 @@
'use client';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { cn } from '@/components/ui/utils';
import dynamic from 'next/dynamic';
const BrochureModal = dynamic(() => import('./BrochureModal'), { ssr: false });
interface Props {
className?: string;
compact?: boolean;
}
/**
* BrochureCTA — Shows a button that opens a modal asking for an email address.
* The full-catalog PDF is ONLY revealed after email submission.
* No direct download link is exposed anywhere.
*/
export default function BrochureCTA({ className, compact = false }: Props) {
const t = useTranslations('Brochure');
const [open, setOpen] = useState(false);
return (
<>
<div className={cn(className)}>
<button
type="button"
onClick={() => setOpen(true)}
className={cn(
'group relative flex w-full items-center gap-4 overflow-hidden rounded-[28px] bg-[#000d26] border border-white/[0.08] text-left cursor-pointer',
'transition-all duration-300 hover:border-[#82ed20]/30 hover:shadow-[0_8px_30px_rgba(0,0,0,0.3)]',
compact ? 'p-4 md:p-5' : 'p-6 md:p-8',
)}
>
{/* Green top accent */}
<span className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#82ed20]/50 to-transparent" />
{/* Icon */}
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[#82ed20]/10 border border-[#82ed20]/20 group-hover:bg-[#82ed20] transition-colors duration-300">
<svg
className="h-5 w-5 text-[#82ed20] group-hover:text-[#000d26] transition-colors duration-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
</span>
{/* Labels */}
<span className="flex-1 min-w-0">
<span className="block text-[9px] font-black uppercase tracking-[0.2em] text-[#82ed20] mb-0.5">
PDF Katalog
</span>
<span
className={cn(
'block font-black text-white uppercase tracking-tight group-hover:text-[#82ed20] transition-colors duration-200',
compact ? 'text-base' : 'text-lg md:text-xl',
)}
>
{t('ctaTitle')}
</span>
</span>
{/* Arrow */}
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/5 text-white/20 group-hover:bg-[#82ed20] group-hover:text-[#000d26] transition-all duration-300">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M9 5l7 7-7 7"
/>
</svg>
</span>
</button>
</div>
<BrochureModal isOpen={open} onClose={() => setOpen(false)} />
</>
);
}

View File

@@ -1,256 +0,0 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslations, useLocale } from 'next-intl';
import { cn } from '@/components/ui/utils';
import { requestBrochureAction } from '@/app/actions/brochure';
import { useAnalytics } from './analytics/useAnalytics';
import { AnalyticsEvents } from './analytics/analytics-events';
interface BrochureModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function BrochureModal({ isOpen, onClose }: BrochureModalProps) {
const t = useTranslations('Brochure');
const locale = useLocale();
const { trackEvent } = useAnalytics();
const formRef = useRef<HTMLFormElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const [state, setState] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const [errorMsg, setErrorMsg] = useState('');
// Close on escape + lock scroll + focus trap
useEffect(() => {
if (!isOpen) return;
// Auto-focus input when opened
const firstInput = document.getElementById('brochure-email');
if (firstInput) {
setTimeout(() => firstInput.focus(), 50);
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Tab' && modalRef.current) {
const focusable = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
) as NodeListOf<HTMLElement>;
if (focusable.length > 0) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
last.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
// Strict overflow lock on mobile as well
document.documentElement.style.setProperty('overflow', 'hidden', 'important');
document.body.style.setProperty('overflow', 'hidden', 'important');
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formRef.current) return;
setState('submitting');
setErrorMsg('');
try {
const formData = new FormData(formRef.current);
formData.set('locale', locale);
const result = await requestBrochureAction(formData);
if (result.success) {
setState('success');
trackEvent(AnalyticsEvents.DOWNLOAD, {
file_name: `klz-product-catalog-${locale}.pdf`,
file_type: 'brochure',
location: 'brochure_modal',
});
} else {
setState('error');
setErrorMsg(result.error || 'Something went wrong');
}
} catch {
setState('error');
setErrorMsg('Network error');
}
};
const handleClose = () => {
setState('idle');
setErrorMsg('');
onClose();
};
if (!isOpen) return null;
const modal = (
<div
className="fixed inset-0 z-[9999] flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={handleClose}
aria-hidden="true"
/>
{/* Modal Panel */}
<div
ref={modalRef}
className="relative z-10 w-full max-w-md rounded-[28px] bg-[#000d26] border border-white/10 shadow-[0_40px_80px_rgba(0,0,0,0.6)] overflow-hidden"
>
{/* Accent bar at top */}
<div className="h-1 w-full bg-gradient-to-r from-[#82ed20] via-[#5cb516] to-[#82ed20]" />
{/* Close Button */}
<button
type="button"
onClick={handleClose}
className="absolute top-4 right-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/5 text-white/40 hover:bg-white/10 hover:text-white transition-colors cursor-pointer"
aria-label={t('close')}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<div className="p-8 pt-7">
{/* Icon + Header */}
<div className="mb-7">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#82ed20]/10 border border-[#82ed20]/20 mb-4">
<svg
className="h-6 w-6 text-[#82ed20]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
</div>
<h2 className="text-2xl font-black text-white uppercase tracking-tight leading-none mb-2">
{t('title')}
</h2>
<p className="text-sm text-white/50 leading-relaxed">{t('subtitle')}</p>
</div>
{state === 'success' ? (
<div>
<div className="flex items-center gap-3 mb-6 p-4 rounded-2xl bg-[#82ed20]/10 border border-[#82ed20]/20">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#82ed20]/20">
<svg
className="h-5 w-5 text-[#82ed20]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div>
<p className="text-sm font-bold text-[#82ed20]">
{locale === 'de' ? 'Erfolgreich gesendet' : 'Successfully sent'}
</p>
<p className="text-xs text-white/50 mt-0.5">
{locale === 'de'
? 'Bitte prüfen Sie Ihren Posteingang.'
: 'Please check your inbox.'}
</p>
</div>
</div>
<button
type="button"
onClick={handleClose}
className="flex items-center justify-center gap-3 w-full py-4 px-6 rounded-2xl bg-white/10 hover:bg-white/20 text-white font-black text-sm uppercase tracking-widest transition-colors"
>
{t('close')}
</button>
</div>
) : (
<form ref={formRef} onSubmit={handleSubmit}>
<div className="mb-5">
<label
htmlFor="brochure-email"
className="block text-[10px] font-black uppercase tracking-[0.2em] text-white/40 mb-2"
>
{t('emailLabel')}
</label>
<input
id="brochure-email"
name="email"
type="email"
required
autoComplete="email"
placeholder={t('emailPlaceholder')}
className="w-full rounded-xl bg-white/5 border border-white/10 px-4 py-3.5 text-white placeholder:text-white/20 text-sm font-medium focus:outline-none focus:border-[#82ed20]/40 transition-colors"
disabled={state === 'submitting'}
/>
</div>
{state === 'error' && errorMsg && (
<p className="text-red-400 text-xs mb-4 font-medium">{errorMsg}</p>
)}
<button
type="submit"
disabled={state === 'submitting'}
className={cn(
'w-full py-4 px-6 rounded-2xl font-black text-sm uppercase tracking-widest transition-colors',
state === 'submitting'
? 'bg-white/10 text-white/40 cursor-wait'
: 'bg-[#82ed20] hover:bg-[#6dd318] text-[#000d26]',
)}
>
{state === 'submitting' ? t('submitting') : t('submit')}
</button>
<p className="mt-4 text-[10px] text-white/25 text-center leading-relaxed">
{t('privacyNote')}
</p>
</form>
)}
</div>
</div>
</div>
);
return createPortal(modal, document.body);
}

View File

@@ -1,330 +0,0 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { useTranslations, useLocale } from 'next-intl';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
import { Container } from './ui';
import { useAnalytics } from './analytics/useAnalytics';
import { AnalyticsEvents } from './analytics/analytics-events';
import FooterBrochureForm from './FooterBrochureForm';
export default function Footer() {
const t = useTranslations('Footer');
const navT = useTranslations('Navigation');
const { trackEvent } = useAnalytics();
const locale = useLocale();
const currentYear = new Date().getFullYear();
return (
<footer className="bg-primary text-white py-14 md:py-24 relative overflow-hidden content-visibility-auto">
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
<Container>
<h2 className="sr-only">Footer Navigation</h2>
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-12 gap-10 md:gap-16 mb-12 md:mb-20">
{/* Brand Column full width on mobile */}
<div className="col-span-2 md:col-span-2 lg:col-span-4 space-y-6 md:space-y-8">
<Link
href={`/${locale}`}
className="inline-block group"
onClick={() =>
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
target: 'home_logo',
location: 'footer',
})
}
>
<Image
src="/logo-white.svg"
alt="KLZ Vertriebs GmbH"
width={150}
height={40}
style={{ width: 'auto' }}
className="h-10 w-auto transition-transform duration-500 group-hover:scale-110"
/>
</Link>
<p className="text-white/60 text-base md:text-lg leading-relaxed max-w-sm">
{t('tagline')}
</p>
<div className="flex gap-4">
<a
href="https://www.linkedin.com/company/klz-vertriebs-gmbh/"
target="_blank"
rel="noopener noreferrer"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
type: 'social',
target: 'linkedin',
location: 'footer',
})
}
className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center text-white hover:bg-accent hover:text-primary-dark transition-all duration-300 border border-white/10"
>
<span className="sr-only">LinkedIn</span>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
</div>
</div>
{/* Legal Column */}
<div className="col-span-1 lg:col-span-2">
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
{t('legal')}
</h3>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li>
<Link
href={`/${locale}/${t('legalNoticeSlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: t('legalNotice'),
href: t('legalNoticeSlug'),
location: 'footer_legal',
})
}
>
{t('legalNotice')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${t('privacyPolicySlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: t('privacyPolicy'),
href: t('privacyPolicySlug'),
location: 'footer_legal',
})
}
>
{t('privacyPolicy')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${t('termsSlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: t('terms'),
href: t('termsSlug'),
location: 'footer_legal',
})
}
>
{t('terms')}
</Link>
</li>
</ul>
</div>
{/* Company Column */}
<div className="col-span-1 lg:col-span-2">
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
{t('company')}
</h3>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li>
<Link
href={`/${locale}/team`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: navT('team'),
href: '/team',
location: 'footer_company',
})
}
>
{navT('team')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${locale === 'de' ? 'produkte' : 'products'}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: navT('products'),
href: locale === 'de' ? '/produkte' : '/products',
location: 'footer_company',
})
}
>
{navT('products')}
</Link>
</li>
<li>
<Link
href={`/${locale}/blog`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: navT('blog'),
href: '/blog',
location: 'footer_company',
})
}
>
{navT('blog')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: navT('contact'),
href: locale === 'de' ? '/kontakt' : '/contact',
location: 'footer_company',
})
}
>
{navT('contact')}
</Link>
</li>
</ul>
</div>
{/* Recent Posts Column full width on mobile */}
<div className="col-span-2 md:col-span-2 lg:col-span-4">
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
{t('recentPosts')}
</h3>
<ul className="space-y-6 list-none m-0 p-0">
{[
{
title:
locale === 'de'
? 'Windparkbau im Fokus: drei typische Kabelherausforderungen'
: 'Focus on wind farm construction: three typical cable challenges',
slug:
locale === 'de'
? 'windparkbau-im-fokus-drei-typische-kabelherausforderungen'
: 'focus-on-wind-farm-construction-three-typical-cable-challenges',
},
{
title:
locale === 'de'
? 'Warum das N2XS(F)2Y das ideale Kabel für Ihr Energieprojekt ist'
: 'Why the N2XS(F)2Y is the ideal cable for your energy project',
slug:
locale === 'de'
? 'n2xsf2y-mittelspannungskabel-energieprojekt'
: 'why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project',
},
].map((post, i) => (
<li key={i}>
<Link
href={`/${locale}/blog/${post.slug}`}
className="group block text-white/80"
onClick={() =>
trackEvent(AnalyticsEvents.BLOG_POST_VIEW, {
title: post.title,
slug: post.slug,
location: 'footer_recent',
})
}
>
<p className="text-white/80 font-bold group-hover:text-accent transition-colors leading-snug mb-2 text-base md:text-base">
{post.title}
</p>
<span className="text-xs text-white/70 uppercase tracking-widest">
{t('readArticle')} &rarr;
</span>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="mb-12 md:mb-16">
<FooterBrochureForm />
</div>
<div className="pt-8 md:pt-12 border-t border-white/10 flex flex-row justify-between items-center gap-4 text-white/70 text-xs md:text-sm font-medium">
<p>{t('copyright', { year: currentYear })}</p>
<div className="flex gap-8">
<Link
href="/en"
className="hover:text-white transition-colors"
onClick={() =>
trackEvent(AnalyticsEvents.TOGGLE_SWITCH, {
type: 'language',
from: locale,
to: 'en',
location: 'footer',
})
}
>
English
</Link>
<Link
href="/de"
className="hover:text-white transition-colors"
onClick={() =>
trackEvent(AnalyticsEvents.TOGGLE_SWITCH, {
type: 'language',
from: locale,
to: 'de',
location: 'footer',
})
}
>
Deutsch
</Link>
</div>
</div>
{/* Brand & Quality Sub-Footer */}
<div className="pt-8 mt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-white/40 text-[10px] sm:text-xs">
<div>
<a
href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
onClick={() =>
trackEvent(AnalyticsEvents.LINK_CLICK, {
target: 'mintel_agency',
location: 'sub_footer',
})
}
className="hover:text-white/80 transition-colors flex items-center gap-1.5"
>
Website entwickelt von Marc Mintel
</a>
</div>
<div className="flex flex-wrap justify-center md:justify-end gap-x-6 gap-y-3">
<div className="flex items-center gap-1.5" title="SSL Secured">
<ShieldCheck className="w-3.5 h-3.5" />
<span>SSL Secured</span>
</div>
<div className="flex items-center gap-1.5" title="Green Hosting">
<Leaf className="w-3.5 h-3.5" />
<span>Green Hosting</span>
</div>
<div className="flex items-center gap-1.5" title="DSGVO Compliant">
<Lock className="w-3.5 h-3.5" />
<span>DSGVO Compliant</span>
</div>
<div className="flex items-center gap-1.5" title="WCAG">
<Accessibility className="w-3.5 h-3.5" />
<span>WCAG</span>
</div>
<div className="flex items-center gap-1.5" title="PageSpeed 90+">
<Zap className="w-3.5 h-3.5" />
<span>PageSpeed 90+</span>
</div>
</div>
</div>
</Container>
</footer>
);
}

View File

@@ -1,134 +0,0 @@
'use client';
import { useState, useRef } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import { requestBrochureAction } from '@/app/actions/brochure';
import { cn } from '@/components/ui/utils';
import { useAnalytics } from './analytics/useAnalytics';
import { AnalyticsEvents } from './analytics/analytics-events';
interface Props {
className?: string;
}
export default function FooterBrochureForm({ className }: Props) {
const t = useTranslations('Brochure');
const locale = useLocale();
const { trackEvent } = useAnalytics();
const formRef = useRef<HTMLFormElement>(null);
const [phase, setPhase] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [err, setErr] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!formRef.current) return;
setPhase('loading');
const fd = new FormData(formRef.current);
fd.set('locale', locale);
try {
const res = await requestBrochureAction(fd);
if (res.success) {
setPhase('success');
trackEvent(AnalyticsEvents.DOWNLOAD, {
file_name: `klz-product-catalog-${locale}.pdf`,
file_type: 'brochure',
location: 'footer_inline',
});
} else {
setErr(res.error || 'Error');
setPhase('error');
}
} catch {
setErr('Network error');
setPhase('error');
}
}
if (phase === 'success') {
return (
<div
className={cn(
'flex flex-col sm:flex-row items-center gap-4 bg-white/5 border border-[#82ed20]/20 rounded-2xl p-6',
className,
)}
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#82ed20]/20 text-[#82ed20]">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<h4 className="text-white font-bold mb-1">
{locale === 'de' ? 'Erfolgreich angefordert!' : 'Successfully requested!'}
</h4>
<p className="text-white/60 text-sm">
{locale === 'de'
? 'Wir haben Ihnen den Katalog soeben per E-Mail zugesendet.'
: 'We have just sent the catalog to your email.'}
</p>
</div>
</div>
);
}
return (
<div
className={cn(
'bg-white/5 border border-white/10 rounded-3xl p-6 md:p-8 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 md:gap-12',
className,
)}
>
<div className="flex-1 max-w-xl">
<h4 className="text-lg font-black text-white uppercase tracking-tight mb-2">
{t('ctaTitle')}
</h4>
<p className="text-sm text-white/60 leading-relaxed mb-0">{t('subtitle')}</p>
</div>
<form
ref={formRef}
onSubmit={handleSubmit}
className="w-full md:w-auto flex flex-col sm:flex-row gap-3"
>
{/* Anti-spam Honeypot */}
<input
type="text"
name="company_website"
tabIndex={-1}
autoComplete="off"
style={{ display: 'none' }}
aria-hidden="true"
/>
<div className="relative w-full sm:w-64">
<input
name="email"
type="email"
required
placeholder={t('emailPlaceholder')}
disabled={phase === 'loading'}
className="w-full bg-primary-dark border border-white/20 rounded-xl px-4 py-3 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-[#82ed20]/50 transition-colors"
/>
</div>
<button
type="submit"
disabled={phase === 'loading'}
className={cn(
'flex items-center justify-center shrink-0 px-6 py-3 rounded-xl font-bold text-sm uppercase tracking-widest transition-colors',
phase === 'loading'
? 'bg-white/10 text-white/40 cursor-wait'
: 'bg-[#82ed20] text-[#000d26] hover:bg-[#6dd318] cursor-pointer',
)}
>
{phase === 'loading' ? t('submitting') : t('submit')}
</button>
</form>
{phase === 'error' && err && (
<div className="absolute mt-16 text-red-400 text-xs font-medium">{err}</div>
)}
</div>
);
}

View File

@@ -1,492 +0,0 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { usePathname } from 'next/navigation';
import { Button } from './ui';
import { useEffect, useState, useRef } from 'react';
import { cn } from './ui';
import { useAnalytics } from './analytics/useAnalytics';
import { AnalyticsEvents } from './analytics/analytics-events';
import { Search } from 'lucide-react';
import { AISearchResults } from './search/AISearchResults';
export default function Header() {
const t = useTranslations('Navigation');
const pathname = usePathname();
const { trackEvent } = useAnalytics();
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const mobileMenuRef = useRef<HTMLDivElement>(null);
// Extract locale from pathname
const currentLocale = pathname.split('/')[1] || 'en';
// Check if homepage
const isHomePage = pathname === `/${currentLocale}` || pathname === '/';
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Prevent scroll when mobile menu is open and handle focus trap
useEffect(() => {
if (isMobileMenuOpen) {
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
// Focus trap logic
const focusableElements = mobileMenuRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusableElements && focusableElements.length > 0) {
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};
const handleEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsMobileMenuOpen(false);
}
};
document.addEventListener('keydown', handleTabKey);
document.addEventListener('keydown', handleEscapeKey);
// Focus the first element when menu opens
setTimeout(() => firstElement.focus(), 100);
return () => {
document.removeEventListener('keydown', handleTabKey);
document.removeEventListener('keydown', handleEscapeKey);
};
}
} else {
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
}
}, [isMobileMenuOpen]);
// Function to get path for a different locale with segment translation
const getPathForLocale = (newLocale: string) => {
const segments = pathname.split('/');
const originLocale = segments[1] || 'en';
// Translation map for localized URL segments
const segmentMap: Record<string, Record<string, string>> = {
de: {
produkte: 'products',
kontakt: 'contact',
impressum: 'legal-notice',
datenschutz: 'privacy-policy',
agbs: 'terms',
niederspannungskabel: 'low-voltage-cables',
mittelspannungskabel: 'medium-voltage-cables',
hochspannungskabel: 'high-voltage-cables',
solarkabel: 'solar-cables',
},
en: {
products: 'produkte',
contact: 'kontakt',
'legal-notice': 'impressum',
'privacy-policy': 'datenschutz',
terms: 'agbs',
'low-voltage-cables': 'niederspannungskabel',
'medium-voltage-cables': 'mittelspannungskabel',
'high-voltage-cables': 'hochspannungskabel',
'solar-cables': 'solarkabel',
},
};
// Replace the locale segment
segments[1] = newLocale;
// Translate other segments if they exist in our map
const translatedSegments = segments.map((segment, index) => {
if (index <= 1) return segment; // Skip empty and locale segments
const mapping = segmentMap[originLocale as keyof typeof segmentMap];
return mapping && mapping[segment] ? mapping[segment] : segment;
});
return translatedSegments.join('/');
};
const menuItems = [
{ label: t('home'), href: '/' },
{ label: t('team'), href: '/team' },
{ label: t('products'), href: currentLocale === 'de' ? '/produkte' : '/products' },
{ label: t('blog'), href: '/blog' },
];
const headerClass = cn(
'fixed top-0 left-0 right-0 z-50 transition-all duration-500 safe-area-p transform-gpu fill-mode-both',
{
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
isHomePage && !isScrolled && !isMobileMenuOpen,
'bg-primary/90 backdrop-blur-md py-3 md:py-4 shadow-2xl':
!isHomePage || isScrolled || isMobileMenuOpen,
},
);
const textColorClass = 'text-white';
const logoSrc = '/logo-white.svg';
return (
<>
<header className={headerClass} style={{ animationDuration: '800ms' }}>
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
<div className="flex-shrink-0 group touch-target fill-mode-both">
<Link
href={`/${currentLocale}`}
onClick={() =>
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
target: 'home_logo',
location: 'header',
})
}
>
<Image
src={logoSrc}
alt={t('home')}
width={120}
height={120}
style={{ width: 'auto' }}
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
priority
fetchPriority="high"
loading="eager"
decoding="sync"
/>
</Link>
</div>
<div className="flex items-center gap-4 md:gap-12">
<nav className="hidden lg:flex items-center space-x-10">
{menuItems.map((item, idx) => (
<div
key={item.href}
className="animate-in fade-in slide-in-from-bottom-4 fill-mode-both"
style={{ animationDuration: '500ms', animationDelay: `${150 + idx * 80}ms` }}
>
{(() => {
const fullHref = `/${currentLocale}${item.href === '/' ? '' : item.href}`;
const isActive =
item.href === '/'
? pathname === `/${currentLocale}` || pathname === '/'
: pathname.startsWith(fullHref);
return (
<Link
href={fullHref}
aria-current={isActive ? 'page' : undefined}
onClick={() => {
setIsMobileMenuOpen(false);
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: item.label,
href: item.href,
location: 'header_nav',
});
}}
className={cn(
textColorClass,
'hover:text-accent font-bold transition-all duration-500 text-base md:text-lg tracking-tight relative group inline-block hover:-translate-y-0.5',
isActive && 'text-accent',
)}
>
{item.label}
<span
className={cn(
'absolute -bottom-2 left-0 h-1 bg-accent transition-all duration-500 rounded-full shadow-[0_0_12px_rgba(130,237,32,0.6)]',
isActive ? 'w-full' : 'w-0 group-hover:w-full',
)}
/>
</Link>
);
})()}
</div>
))}
</nav>
<div
className={cn(
'hidden lg:flex items-center space-x-8 animate-in fade-in slide-in-from-right-8 fill-mode-both',
textColorClass,
)}
style={{ animationDuration: '600ms', animationDelay: '300ms' }}
>
<div
className="flex items-center space-x-4 text-xs md:text-sm font-extrabold tracking-widest uppercase animate-in fade-in slide-in-from-left-4 fill-mode-both"
style={{ animationDuration: '500ms', animationDelay: '600ms' }}
>
<div>
<Link
href={getPathForLocale('en')}
onClick={() =>
trackEvent(AnalyticsEvents.TOGGLE_SWITCH, {
type: 'language',
from: currentLocale,
to: 'en',
location: 'header',
})
}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
>
EN
</Link>
</div>
<div className="w-px h-4 bg-current opacity-30" />
<div>
<Link
href={getPathForLocale('de')}
onClick={() =>
trackEvent(AnalyticsEvents.TOGGLE_SWITCH, {
type: 'language',
from: currentLocale,
to: 'de',
location: 'header',
})
}
className={`hover:text-accent transition-colors flex items-center gap-2 touch-target ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
>
DE
</Link>
</div>
</div>
<div
className="animate-in fade-in zoom-in-95 fill-mode-both"
style={{ animationDuration: '600ms', animationDelay: '700ms' }}
>
<button
onClick={() => setIsSearchOpen(true)}
className="hover:text-accent transition-colors p-2"
aria-label="Search"
>
<Search className="w-5 h-5 md:w-6 md:h-6" />
</button>
</div>
<div
className="animate-in fade-in zoom-in-95 fill-mode-both"
style={{ animationDuration: '600ms', animationDelay: '800ms' }}
>
<Button
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
variant="white"
size="md"
className="px-8 shadow-xl hover:scale-105 transition-transform"
onClick={() =>
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
label: t('contact'),
location: 'header_cta',
})
}
>
{t('contact')}
</Button>
</div>
</div>
{/* Mobile Menu Button */}
<button
className={cn(
'lg:hidden touch-target p-2 rounded-xl bg-white/10 border border-white/20 z-[70] relative transition-all duration-300',
textColorClass,
isMobileMenuOpen ? 'rotate-90 scale-110' : 'rotate-0 scale-100',
)}
aria-label={t('toggleMenu')}
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => {
const newState = !isMobileMenuOpen;
setIsMobileMenuOpen(newState);
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
type: 'mobile_menu',
action: newState ? 'open' : 'close',
});
}}
>
<svg
className="w-7 h-7 transition-transform duration-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{isMobileMenuOpen ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
</div>
</div>
</header>
{/* Mobile Menu Overlay */}
<div
className={cn(
'fixed inset-0 bg-primary/95 backdrop-blur-3xl z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
isMobileMenuOpen
? 'opacity-100 translate-y-0'
: 'opacity-0 -translate-y-full pointer-events-none',
)}
id="mobile-menu"
role="dialog"
aria-modal="true"
aria-label={t('menu')}
ref={mobileMenuRef}
inert={isMobileMenuOpen ? undefined : true}
>
{/* Close Button inside overlay */}
<div className="flex justify-end p-6 pt-8">
<button
className="touch-target p-2 rounded-xl bg-white/10 border border-white/20 text-white hover:bg-white/20 transition-all duration-300"
aria-label={t('toggleMenu')}
onClick={() => {
setIsMobileMenuOpen(false);
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
type: 'mobile_menu',
action: 'close',
});
}}
>
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
{menuItems.map((item, idx) => (
<div
key={item.href}
className={cn(
'transition-all duration-500 transform',
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
)}
style={{ transitionDelay: `${isMobileMenuOpen ? 200 + idx * 80 : 0}ms` }}
>
<Link
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
aria-current={
(
item.href === '/'
? pathname === `/${currentLocale}` || pathname === '/'
: pathname.startsWith(`/${currentLocale}${item.href}`)
)
? 'page'
: undefined
}
onClick={() => {
setIsMobileMenuOpen(false);
trackEvent(AnalyticsEvents.LINK_CLICK, {
label: item.label,
href: item.href,
location: 'mobile_menu',
});
}}
className={cn(
'text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4',
(item.href === '/'
? pathname === `/${currentLocale}` || pathname === '/'
: pathname.startsWith(`/${currentLocale}${item.href}`)) && 'text-accent',
)}
>
{item.label}
</Link>
</div>
))}
<div
className={cn(
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
)}
style={{ transitionDelay: isMobileMenuOpen ? '600ms' : '0ms' }}
>
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
<div>
<Link
href={getPathForLocale('en')}
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
>
EN
</Link>
</div>
<div className="w-px h-6 bg-white/30" />
<div>
<Link
href={getPathForLocale('de')}
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
>
DE
</Link>
</div>
</div>
<div className="w-full max-w-xs">
<Button
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
variant="accent"
size="lg"
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
>
{t('contact')}
</Button>
</div>
</div>
{/* Bottom Branding */}
<div
className={cn(
'p-12 flex justify-center transition-all duration-700',
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
)}
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
>
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
</div>
</nav>
</div>
<AISearchResults isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
</>
);
}

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>

View File

@@ -1,6 +1,8 @@
import Link from 'next/link';
import Image from 'next/image';
import { useLocale } from 'next-intl';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
interface CompanyInfo {
contactEmail: string;
@@ -16,49 +18,116 @@ export function Footer({ companyInfo }: FooterProps) {
const locale = useLocale();
return (
<footer className="bg-neutral-dark text-neutral-light py-16">
<div className="container grid grid-cols-1 md:grid-cols-4 gap-12">
<div className="col-span-1 md:col-span-2">
<Link href={`/${locale}`} className="relative block h-14 w-56 mb-6">
<footer className="bg-neutral-dark text-neutral-light py-16 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="container grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-12 lg:gap-8 mb-12">
{/* Brand Column */}
<div className="col-span-1 md:col-span-2 lg:col-span-4 space-y-6">
<Link href={`/${locale}`} className="relative block h-14 w-48 mb-6 group">
<Image
src="/assets/logo-white.png"
alt="E-TIB Gruppe"
fill
className="object-contain object-left"
className="object-contain object-left transition-transform duration-500 group-hover:scale-105"
/>
</Link>
<p className="text-text-light max-w-sm mb-6">
Die Experten für Kabeltiefbau, Horizontalspülbohrungen, komplexe Querungen und umfassende Infrastrukturprojekte.
<p className="text-white/60 text-base leading-relaxed max-w-sm">
{locale === 'de'
? 'Die Experten für Kabeltiefbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
</p>
{companyInfo && (
<div className="text-text-light text-sm space-y-2 mt-6 border-t border-white/10 pt-6 max-w-xs">
<p className="whitespace-pre-line">{companyInfo.address}</p>
<p>Email: <a href={`mailto:${companyInfo.contactEmail}`} className="hover:text-white">{companyInfo.contactEmail}</a></p>
<p>Tel: <a href={`tel:${companyInfo.contactPhone.replace(/\\s+/g, '')}`} className="hover:text-white">{companyInfo.contactPhone}</a></p>
<div className="text-white/70 text-sm space-y-3 mt-8 border-t border-white/10 pt-6 max-w-xs">
<p className="whitespace-pre-line font-medium">{companyInfo.address}</p>
<p>Email: <a href={`mailto:${companyInfo.contactEmail}`} className="hover:text-primary transition-colors">{companyInfo.contactEmail}</a></p>
<p>Tel: <a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} className="hover:text-primary transition-colors">{companyInfo.contactPhone}</a></p>
</div>
)}
</div>
<div>
<h4 className="font-heading font-semibold text-lg mb-4">Unternehmen</h4>
<ul className="space-y-2">
<li><Link href={`/${locale}/kompetenzen`} className="text-text-light hover:text-primary-light transition-colors">Kompetenzen</Link></li>
<li><Link href={`/${locale}/ueber-uns`} className="text-text-light hover:text-primary-light transition-colors">Über uns</Link></li>
<li><Link href={`/${locale}/karriere`} className="text-text-light hover:text-primary-light transition-colors">Karriere / Jobs</Link></li>
<li><Link href={`/${locale}/kontakt`} className="text-text-light hover:text-primary-light transition-colors">Kontakt</Link></li>
{/* Company Column */}
<div className="col-span-1 lg:col-span-3 lg:col-start-6">
<h4 className="font-heading font-bold uppercase tracking-widest text-xs mb-6 text-white/40">
{locale === 'de' ? 'Unternehmen' : 'Company'}
</h4>
<ul className="space-y-4">
<li>
<Link href={`/${locale}/kompetenzen`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Kompetenzen' : 'Competencies'}
</Link>
</li>
<li>
<Link href={`/${locale}/ueber-uns`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Über uns' : 'About us'}
</Link>
</li>
<li>
<Link href={`/${locale}/karriere`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Karriere' : 'Career'}
</Link>
</li>
<li>
<Link href={`/${locale}/messen`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Messen & Events' : 'Fairs & Events'}
</Link>
</li>
<li>
<Link href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Kontakt' : 'Contact'}
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-heading font-semibold text-lg mb-4">Rechtliches</h4>
<ul className="space-y-2">
<li><Link href={`/${locale}/impressum`} className="text-text-light hover:text-primary-light transition-colors">Impressum</Link></li>
<li><Link href={`/${locale}/datenschutz`} className="text-text-light hover:text-primary-light transition-colors">Datenschutz</Link></li>
<li><Link href={`/${locale}/${locale === 'de' ? 'agb' : 'terms'}`} className="text-text-light hover:text-primary-light transition-colors">{locale === 'de' ? 'AGB' : 'Terms'}</Link></li>
<li><button className="text-text-light hover:text-primary-light transition-colors">Cookie-Einstellungen</button></li>
{/* Legal Column */}
<div className="col-span-1 lg:col-span-3">
<h4 className="font-heading font-bold uppercase tracking-widest text-xs mb-6 text-white/40">
{locale === 'de' ? 'Rechtliches' : 'Legal'}
</h4>
<ul className="space-y-4">
<li>
<Link href={`/${locale}/impressum`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Impressum' : 'Imprint'}
</Link>
</li>
<li>
<Link href={`/${locale}/datenschutz`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'Datenschutz' : 'Privacy Policy'}
</Link>
</li>
<li>
<Link href={`/${locale}/${locale === 'de' ? 'agb' : 'terms'}`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
{locale === 'de' ? 'AGB' : 'Terms'}
</Link>
</li>
</ul>
</div>
</div>
<div className="container mt-12 pt-8 border-t border-neutral-dark/40 border-t-white/10 text-center text-text-light text-sm">
<p>&copy; {new Date().getFullYear()} E-TIB GmbH. Alle Rechte vorbehalten.</p>
<div className="container">
<div className="pt-8 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-6 text-white/50 text-sm">
<p>&copy; {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
<div className="flex items-center gap-4">
<LanguageSwitcher isSolidMode={false} />
</div>
</div>
{/* Quality Badges */}
<div className="pt-8 mt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-white/30 text-xs">
<div>
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-white/60 transition-colors">
Website developed by Mintel
</a>
</div>
<div className="flex flex-wrap justify-center gap-x-6 gap-y-3">
<div className="flex items-center gap-1.5"><ShieldCheck className="w-3.5 h-3.5" /><span>SSL Secured</span></div>
<div className="flex items-center gap-1.5"><Leaf className="w-3.5 h-3.5" /><span>Green Hosting</span></div>
<div className="flex items-center gap-1.5"><Lock className="w-3.5 h-3.5" /><span>DSGVO Compliant</span></div>
<div className="flex items-center gap-1.5"><Accessibility className="w-3.5 h-3.5" /><span>WCAG</span></div>
<div className="flex items-center gap-1.5"><Zap className="w-3.5 h-3.5" /><span>PageSpeed 90+</span></div>
</div>
</div>
</div>
</footer>
);

View File

@@ -5,6 +5,7 @@ import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { LanguageSwitcher } from './LanguageSwitcher';
export interface NavLink {
label: string;
@@ -18,6 +19,7 @@ interface HeaderProps {
export function Header({ navLinks }: HeaderProps) {
const [isScrolled, setIsScrolled] = React.useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const [forceSolid, setForceSolid] = React.useState(false);
const pathname = usePathname() || '/';
// Determine current locale from pathname
@@ -35,27 +37,39 @@ export function Header({ navLinks }: HeaderProps) {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Check for forced solid header (used by error pages, legal pages without hero, etc.)
const checkSolid = () => {
setForceSolid(document.body.getAttribute('data-force-solid-header') === 'true');
};
checkSolid();
// Set up observer for client-side navigation changes
const observer = new MutationObserver(checkSolid);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-force-solid-header'] });
// Re-check when pathname changes
checkSolid();
return () => {
window.removeEventListener('scroll', handleScroll);
observer.disconnect();
};
}, [pathname]);
// Close mobile menu on route change
React.useEffect(() => {
setIsMobileMenuOpen(false);
}, [pathname]);
const LanguageSwitcher = ({ mobile = false }) => (
<div className={`flex items-center gap-2 ${mobile ? 'text-xl' : 'text-sm font-bold'} ${mobile ? 'text-neutral-dark' : isScrolled ? 'text-text-primary' : 'text-neutral-light'}`}>
<Link href={getSwitchedUrl('de')} className={`transition-colors hover:text-primary ${currentLocale === 'de' ? 'text-primary' : ''}`}>DE</Link>
<span className="opacity-50">|</span>
<Link href={getSwitchedUrl('en')} className={`transition-colors hover:text-primary ${currentLocale === 'en' ? 'text-primary' : ''}`}>EN</Link>
</div>
);
const isSolidMode = isScrolled || forceSolid;
return (
<>
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ease-in-out ${
isScrolled
isSolidMode
? 'bg-neutral-light/90 backdrop-blur-md shadow-sm py-4'
: 'bg-transparent py-6'
}`}
@@ -63,7 +77,7 @@ export function Header({ navLinks }: HeaderProps) {
<div className="container flex items-center justify-between">
<Link href={`/${currentLocale}`} className="relative flex items-center h-12 w-48 z-50">
<Image
src={(isScrolled || isMobileMenuOpen) ? "/assets/logo.png" : "/assets/logo-white.png"}
src={(isSolidMode || isMobileMenuOpen) ? "/assets/logo.png" : "/assets/logo-white.png"}
alt="E-TIB Gruppe"
fill
className="object-contain object-left"
@@ -83,7 +97,7 @@ export function Header({ navLinks }: HeaderProps) {
key={link.url}
href={mappedUrl}
className={`font-semibold text-sm uppercase tracking-widest transition-all duration-300 ${
isScrolled
isSolidMode
? 'text-text-primary hover:text-primary'
: 'text-neutral-light hover:text-primary-light'
}`}
@@ -92,8 +106,8 @@ export function Header({ navLinks }: HeaderProps) {
</Link>
);
})}
<div className="h-4 w-px bg-white/20 mx-2" />
<LanguageSwitcher />
<div className={`h-4 w-px mx-2 transition-colors duration-300 ${isSolidMode ? 'bg-neutral-300' : 'bg-white/20'}`} />
<LanguageSwitcher isSolidMode={isSolidMode} />
<Link
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
className="rounded-xl bg-primary hover:bg-primary-dark text-neutral-light px-8 py-3 font-bold text-sm uppercase tracking-wider transition-all shadow-xl hover:shadow-primary/20 hover:-translate-y-0.5 active:translate-y-0"
@@ -108,7 +122,7 @@ export function Header({ navLinks }: HeaderProps) {
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
>
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke={(isScrolled || isMobileMenuOpen) ? "currentColor" : "white"} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke={(isSolidMode || isMobileMenuOpen) ? "currentColor" : "white"} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
{isMobileMenuOpen ? (
<>
<line x1="18" y1="6" x2="6" y2="18"></line>

View File

@@ -0,0 +1,84 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface LanguageSwitcherProps {
mobile?: boolean;
isSolidMode?: boolean;
}
export function LanguageSwitcher({ mobile = false, isSolidMode = false }: LanguageSwitcherProps) {
const pathname = usePathname() || '/';
const currentLocale = pathname.startsWith('/en') ? 'en' : 'de';
const getSwitchedUrl = (newLocale: string) => {
const pathWithoutLocale = pathname.replace(/^\/(en|de)/, '');
return `/${newLocale}${pathWithoutLocale === '' ? '' : pathWithoutLocale}`;
};
const locales = [
{ code: 'de', label: 'DE', fullLabel: 'Deutsch', flag: '🇩🇪' },
{ code: 'en', label: 'EN', fullLabel: 'English', flag: '🇬🇧' },
] as const;
if (mobile) {
return (
<div className="flex flex-col gap-3">
{locales.map((loc) => (
<Link
key={loc.code}
href={getSwitchedUrl(loc.code)}
className={`flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 ${
currentLocale === loc.code
? 'bg-primary/10 text-primary border border-primary/20'
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
}`}
>
<span className="text-2xl" aria-hidden="true">{loc.flag}</span>
<span>{loc.fullLabel}</span>
{currentLocale === loc.code && (
<svg className="w-5 h-5 ml-auto text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</Link>
))}
</div>
);
}
// Desktop styles dependent on isSolidMode
const containerClass = isSolidMode
? 'bg-neutral-100 border-neutral-200'
: 'bg-white/10 backdrop-blur-sm border-white/10';
return (
<div className={`flex items-center rounded-full p-0.5 border ${containerClass} transition-colors duration-300`}>
{locales.map((loc) => {
const isActive = currentLocale === loc.code;
let linkClass = '';
if (isActive) {
linkClass = 'bg-white text-primary shadow-sm';
} else {
linkClass = isSolidMode
? 'text-text-secondary hover:text-primary'
: 'text-white/70 hover:text-white';
}
return (
<Link
key={loc.code}
href={getSwitchedUrl(loc.code)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 ${linkClass}`}
aria-label={`Switch to ${loc.fullLabel}`}
>
<span className="text-sm" aria-hidden="true">{loc.flag}</span>
<span>{loc.label}</span>
</Link>
);
})}
</div>
);
}

View File

@@ -18,6 +18,56 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
children?: React.ReactNode;
}
export const buttonBaseStyles =
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
export const buttonVariantsMap = {
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
secondary: 'bg-secondary text-white shadow-md hover:shadow-secondary/30 hover:shadow-2xl',
accent: 'bg-accent text-primary-dark shadow-md hover:shadow-accent/30 hover:shadow-2xl',
saturated: 'bg-saturated text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
outline:
'border-2 border-primary bg-transparent text-primary hover:text-white hover:shadow-primary/20 hover:shadow-xl',
ghost: 'text-primary hover:shadow-lg',
white:
'bg-white text-primary shadow-md hover:shadow-primary/30 hover:shadow-2xl hover:text-white',
destructive:
'bg-destructive text-destructive-foreground shadow-md hover:shadow-destructive/30 hover:shadow-2xl',
};
export const buttonSizesMap = {
sm: 'h-9 px-4 text-sm md:text-base',
md: 'h-11 px-6 text-base md:text-lg',
lg: 'h-14 px-5 md:px-8 text-base md:text-lg',
xl: 'h-16 px-6 md:px-10 text-lg md:text-xl',
};
export const buttonOverlayColorsMap = {
primary: 'bg-primary-dark',
secondary: 'bg-secondary-light',
accent: 'bg-accent-dark',
saturated: 'bg-primary',
outline: 'bg-primary',
ghost: 'bg-primary-light/10',
white: 'bg-primary-light',
destructive: 'bg-destructive/90',
};
export function getButtonClasses(variant: keyof typeof buttonVariantsMap = 'primary', size: keyof typeof buttonSizesMap = 'md', className?: string) {
return cn(buttonBaseStyles, buttonVariantsMap[variant], buttonSizesMap[size], className);
}
export function ButtonOverlay({ variant = 'primary' }: { variant?: keyof typeof buttonVariantsMap }) {
return (
<span
className={cn(
'absolute inset-0 translate-y-[101%] group-hover/btn:translate-y-0 transition-transform duration-500 ease-out',
buttonOverlayColorsMap[variant]
)}
/>
);
}
export function Button({
className,
variant = 'primary',
@@ -25,42 +75,7 @@ export function Button({
href,
...props
}: ButtonProps) {
const baseStyles =
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
const variants = {
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
secondary: 'bg-secondary text-white shadow-md hover:shadow-secondary/30 hover:shadow-2xl',
accent: 'bg-accent text-primary-dark shadow-md hover:shadow-accent/30 hover:shadow-2xl',
saturated: 'bg-saturated text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
outline:
'border-2 border-primary bg-transparent text-primary hover:text-white hover:shadow-primary/20 hover:shadow-xl',
ghost: 'text-primary hover:shadow-lg',
white:
'bg-white text-primary shadow-md hover:shadow-primary/30 hover:shadow-2xl hover:text-white',
destructive:
'bg-destructive text-destructive-foreground shadow-md hover:shadow-destructive/30 hover:shadow-2xl',
};
const sizes = {
sm: 'h-9 px-4 text-sm md:text-base',
md: 'h-11 px-6 text-base md:text-lg',
lg: 'h-14 px-5 md:px-8 text-base md:text-lg',
xl: 'h-16 px-6 md:px-10 text-lg md:text-xl',
};
const styles = cn(baseStyles, variants[variant], sizes[size], className);
const overlayColors = {
primary: 'bg-primary-dark',
secondary: 'bg-secondary-light',
accent: 'bg-accent-dark',
saturated: 'bg-primary',
outline: 'bg-primary',
ghost: 'bg-primary-light/10',
white: 'bg-primary-light',
destructive: 'bg-destructive/90',
};
const styles = getButtonClasses(variant, size, className);
const content = (
<>
@@ -72,12 +87,7 @@ export function Button({
>
{props.children}
</span>
<span
className={cn(
'absolute inset-0 translate-y-[101%] group-hover/btn:translate-y-0 transition-transform duration-500 ease-out',
overlayColors[variant],
)}
/>
<ButtonOverlay variant={variant} />
</>
);