Compare commits

..

13 Commits

Author SHA1 Message Date
120ba0488c fix(content): update HDD drilling max distance from 250m to 400m in DE and EN kompetenzen
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-08-13 09:47:36 +02:00
ffb637e05c fix(team): disable carousel lightbox popup on mobile and enlarge grid profile photos
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m0s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-13 00:55:00 +02:00
b97de267d9 fix(team): significantly enlarge profile photos on mobile viewports for grid and carousel
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-08-13 00:01:11 +02:00
1c3918d9e3 fix(team): center and enlarge grid profile pictures on mobile viewports
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 2m43s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-08-12 23:27:15 +02:00
c89933a7d4 fix(team): apply portrait aspect ratio framing to both grid thumbnails and carousel modal
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 2m32s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-12 21:12:38 +02:00
d67085fef8 fix(team): optimize mobile lightbox portrait framing and card dimensions
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Successful in 23s
2026-08-12 21:11:52 +02:00
85bac72f95 feat(team): 3D carousel lightbox, mobile optimization and subtle E-TIB logo arcs
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m38s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 51s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-12 15:43:53 +02:00
b945ba31fb 2.4.48
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-08-12 14:17:17 +02:00
19c8cec1d3 2.4.47
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m26s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-08-12 13:37:55 +02:00
2a9f414e87 2.4.46
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m18s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-03 09:37:13 +02:00
652bd92321 test: fix absolute path in certificates test for CI pipeline 2026-08-03 09:37:12 +02:00
3ea8f21294 2.4.45
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Failing after 1m12s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-08-03 09:28:48 +02:00
3a216b3961 fix(ui): smooth icon hover transition on certificate download 2026-08-03 09:28:48 +02:00
23 changed files with 471 additions and 105 deletions

4
.gitignore vendored
View File

@@ -40,3 +40,7 @@ next-env.d.ts
# turborepo
.turbo
.turbo-test
# scratch
scratch/

View File

@@ -1,7 +1,7 @@
FROM node:20-alpine
# Install essential build tools if needed (e.g., for node-gyp)
RUN apk add --no-cache libc6-compat python3 make g++ curl
RUN apk add --no-cache libc6-compat python3 make g++ curl git
WORKDIR /app

View File

@@ -208,7 +208,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
</p>
</div>
<div className={`mt-auto inline-flex items-center text-sm font-bold uppercase tracking-wider transition-all ${
<div className={`mt-auto inline-flex items-center gap-2 text-sm font-bold uppercase tracking-wider transition-all duration-300 ${
cert.pdfUrl ? 'group-hover:gap-3' : ''
} ${
isIso ? 'text-primary-light' : 'text-primary'
@@ -216,7 +216,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
{cert.pdfUrl ? (
<>
<span>Download PDF</span>
<Download size={16} className="ml-2 group-hover:translate-y-0.5 transition-transform" />
<Download size={16} className="group-hover:translate-y-0.5 transition-transform" />
</>
) : (
<span className="text-neutral-400">Nachweis liegt vor</span>

View File

@@ -1,9 +1,12 @@
'use client';
import * as React from 'react';
import { m } from 'framer-motion';
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { m, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { LogoArcs } from '@/components/ui/LogoArcs';
export interface TeamMember {
id: string;
@@ -22,11 +25,102 @@ interface TeamGridProps {
members: TeamMember[];
}
function hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
export function getMemberLogoArcsProps(memberId: string) {
const hash = hashString(memberId);
const rotation = Math.round((hash * 137.5) % 360);
const size = 160 + ((hash * 13) % 180); // 160px to 340px
const top = -60 + ((hash * 7) % 80); // -60px to 20px
const right = (hash % 2 === 0) ? -50 + ((hash * 11) % 70) : undefined;
const left = (hash % 2 !== 0) ? -50 + ((hash * 11) % 70) : undefined;
const opacity = Number((0.015 + ((hash % 7) * 0.005)).toFixed(3)); // 0.015 to 0.045 (ultra subtle)
return { rotation, size, top, right, left, opacity };
}
export function TeamGrid({ members }: TeamGridProps) {
const t = useTranslations('TeamGrid');
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
const [mounted, setMounted] = useState(false);
const [[page, direction], setPage] = useState([0, 0]);
const selectedIndex = selectedMember ? members.findIndex(m => m.id === selectedMember.id) : -1;
const prevMember = selectedIndex !== -1 && members.length > 1 ? members[(selectedIndex - 1 + members.length) % members.length] : null;
const nextMember = selectedIndex !== -1 && members.length > 1 ? members[(selectedIndex + 1) % members.length] : null;
const paginate = (newDirection: number) => {
if (newDirection > 0 && nextMember) {
setPage([page + 1, newDirection]);
setSelectedMember(nextMember);
} else if (newDirection < 0 && prevMember) {
setPage([page - 1, newDirection]);
setSelectedMember(prevMember);
}
};
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setSelectedMember(null);
} else if (e.key === 'ArrowLeft' && prevMember) {
paginate(-1);
} else if (e.key === 'ArrowRight' && nextMember) {
paginate(1);
}
};
window.addEventListener('keydown', handleKeyDown);
if (selectedMember) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
window.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [selectedMember, prevMember, nextMember, page]);
const handlePreload = (url: string | null) => {
if (url && typeof window !== 'undefined') {
const img = new window.Image();
img.src = url;
}
};
if (!members || members.length === 0) return null;
const slideVariants = {
enter: (dir: number) => ({
x: dir > 0 ? 350 : dir < 0 ? -350 : 0,
opacity: 0,
scale: 0.82,
}),
center: {
x: 0,
opacity: 1,
scale: 1,
},
exit: (dir: number) => ({
x: dir < 0 ? 350 : dir > 0 ? -350 : 0,
opacity: 0,
scale: 0.82,
}),
};
return (
<section className="py-16 md:py-24 lg:py-32 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl relative z-10">
@@ -39,75 +133,353 @@ export function TeamGrid({ members }: TeamGridProps) {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 xl:gap-10">
{members.map((member, i) => (
<m.div
key={member.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
{/* Card Banner */}
<div className="h-32 bg-neutral-50 relative border-b border-neutral-100 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-neutral-100 to-neutral-200/50" />
<div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'20\\' height=\\'20\\' viewBox=\\'0 0 20 20\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Crect width=\\'1\\' height=\\'1\\' fill=\\'%23000000\\'/%3E%3C/svg%3E')" }} />
</div>
{members.map((member, i) => {
const imageUrl = member.image ? (typeof member.image === 'string' ? member.image : member.image.url) : null;
const arcsProps = getMemberLogoArcsProps(member.id);
<div className="px-8 pb-8 relative flex-grow flex flex-col">
{/* Overlapping Profile Picture */}
<div className="w-24 h-24 rounded-2xl overflow-hidden border-4 border-white shadow-xl bg-white relative -mt-12 mb-6 group-hover:-translate-y-2 transition-transform duration-500">
{member.image && (typeof member.image === 'string' ? member.image : member.image.url) ? (
<Image
src={typeof member.image === 'string' ? member.image : member.image.url}
alt={member.name}
fill
sizes="96px"
className="object-cover"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-neutral-300 bg-neutral-50">
<svg className="w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
return (
<m.div
key={member.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
onMouseEnter={() => handlePreload(imageUrl)}
onTouchStart={() => handlePreload(imageUrl)}
className="group flex flex-col bg-white rounded-[2rem] border border-neutral-100 shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_20px_40px_rgba(238,114,3,0.1)] transition-all duration-500 hover:-translate-y-2 overflow-hidden"
>
{/* Card Banner with Signature E-TIB LogoArcs Design Element */}
<div className="h-44 sm:h-52 md:h-36 bg-neutral-50 relative border-b border-neutral-100 overflow-hidden select-none">
<div className="absolute inset-0 bg-gradient-to-br from-neutral-100/90 via-neutral-50 to-orange-50/20" />
{/* Signature E-TIB Logo Arcs Design Element */}
<div
className="absolute pointer-events-none transition-transform duration-700 group-hover:scale-110 text-primary"
style={{
width: `${arcsProps.size}px`,
height: `${arcsProps.size}px`,
top: `${arcsProps.top}px`,
right: arcsProps.right !== undefined ? `${arcsProps.right}px` : undefined,
left: arcsProps.left !== undefined ? `${arcsProps.left}px` : undefined,
transform: `rotate(${arcsProps.rotation}deg)`,
opacity: arcsProps.opacity,
}}
>
<LogoArcs className="w-full h-full" />
</div>
<div className="absolute inset-0 opacity-[0.025] pointer-events-none" style={{ backgroundImage: "url('data:image/svg+xml,%3Csvg width=\\'20\\' height=\\'20\\' viewBox=\\'0 0 20 20\\' xmlns=\\'http://www.w3.org/2000/svg\\'%3E%3Crect width=\\'1\\' height=\\'1\\' fill=\\'%23000000\\'/%3E%3C/svg%3E')" }} />
</div>
<div className="px-6 sm:px-8 pb-8 relative flex-grow flex flex-col items-center md:items-start text-center md:text-left">
{/* Overlapping Profile Picture (Extra Large Centered on Mobile, Lightbox modal active only on Desktop) */}
<m.div
layoutId={`team-avatar-${member.id}`}
onClick={() => {
if (typeof window !== 'undefined' && window.innerWidth >= 768) {
setPage([0, 0]);
setSelectedMember(member);
}
}}
onMouseEnter={() => handlePreload(imageUrl)}
onTouchStart={() => handlePreload(imageUrl)}
className="w-52 h-64 sm:w-60 sm:h-72 md:w-36 md:h-44 mx-auto md:mx-0 rounded-2xl overflow-hidden border-4 border-white shadow-xl bg-white relative -mt-28 md:-mt-24 mb-6 transition-transform duration-500 cursor-default md:cursor-pointer group/avatar"
>
{imageUrl ? (
<>
<Image
src={imageUrl}
alt={member.name}
fill
sizes="(max-width: 768px) 300px, 144px"
className="object-cover object-[center_15%] transition-transform duration-500 group-hover/avatar:scale-105"
/>
{/* Zoom Icon Overlay on Hover (Desktop only) */}
<div className="hidden md:flex absolute inset-0 bg-black/40 opacity-0 group-hover/avatar:opacity-100 transition-opacity duration-300 items-center justify-center text-white cursor-pointer">
<svg className="w-8 h-8 drop-shadow-md" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</div>
</>
) : (
<div className="absolute inset-0 flex items-center justify-center text-neutral-300 bg-neutral-50 group-hover/avatar:bg-neutral-100 transition-colors">
<svg className="w-16 h-16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
<div className="hidden md:flex absolute inset-0 bg-black/30 opacity-0 group-hover/avatar:opacity-100 transition-opacity duration-300 items-center justify-center text-white cursor-pointer">
<svg className="w-8 h-8 drop-shadow-md" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</div>
</div>
)}
</m.div>
{/* Details */}
<h4 className="font-heading font-extrabold text-2xl text-neutral-dark mb-1 group-hover:text-primary transition-colors duration-300 leading-tight">{member.name}</h4>
<p className="text-primary font-bold text-xs uppercase tracking-widest mb-6">{member.position}</p>
{member.branch && (
<div className="mb-6">
<span className="inline-block px-3 py-1 bg-neutral-100 text-neutral-600 rounded-md text-xs uppercase tracking-widest font-semibold border border-neutral-200/60">
{member.branch === 'e-tib' ? t('branchETIB') : member.branch === 'ing' ? t('branchIng') : member.branch === 'bohrtechnik' ? t('branchBohr') : member.branch}
</span>
</div>
)}
</div>
{/* Details */}
<h4 className="font-heading font-extrabold text-2xl text-neutral-dark mb-1 group-hover:text-primary transition-colors duration-300 leading-tight">{member.name}</h4>
<p className="text-primary font-bold text-xs uppercase tracking-widest mb-6">{member.position}</p>
{member.branch && (
<div className="mb-6">
<span className="inline-block px-3 py-1 bg-neutral-100 text-neutral-600 rounded-md text-xs uppercase tracking-widest font-semibold border border-neutral-200/60">
{member.branch === 'e-tib' ? t('branchETIB') : member.branch === 'ing' ? t('branchIng') : member.branch === 'bohrtechnik' ? t('branchBohr') : member.branch}
</span>
{/* Contacts (Sticky at bottom) */}
<div className="mt-auto pt-6 border-t border-neutral-100 flex flex-col gap-4 w-full text-left">
{member.email && (
<a href={`mailto:${member.email}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium cursor-pointer">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</div>
<span className="truncate">{member.email}</span>
</a>
)}
{member.phone && (
<a href={`tel:${member.phone}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium cursor-pointer">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
<span>{member.phone}</span>
</a>
)}
</div>
)}
{/* Contacts (Sticky at bottom) */}
<div className="mt-auto pt-6 border-t border-neutral-100 flex flex-col gap-4">
{member.email && (
<a href={`mailto:${member.email}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</div>
<span className="truncate">{member.email}</span>
</a>
)}
{member.phone && (
<a href={`tel:${member.phone}`} className="group/link flex items-center gap-4 text-text-secondary hover:text-primary transition-colors font-medium">
<div className="w-10 h-10 rounded-xl bg-neutral-50 flex items-center justify-center text-neutral-400 group-hover/link:bg-primary/10 group-hover/link:text-primary transition-colors border border-neutral-100">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
<span>{member.phone}</span>
</a>
)}
</div>
</div>
</m.div>
))}
</m.div>
);
})}
</div>
</div>
{/* 3D Carousel Lightbox Modal with Neighbor Blurred Preview Cards & React Portal */}
{mounted && createPortal(
<AnimatePresence>
{selectedMember && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedMember(null)}
className="fixed inset-0 z-[9999] bg-black/85 backdrop-blur-xl flex flex-col items-center justify-center p-4 sm:p-6 md:p-12 overflow-y-auto"
>
{/* Close Button */}
<button
onClick={() => setSelectedMember(null)}
className="absolute top-4 right-4 sm:top-6 sm:right-6 z-50 w-10 h-10 sm:w-12 sm:h-12 rounded-full bg-white/10 hover:bg-white/30 text-white flex items-center justify-center backdrop-blur-md transition-colors cursor-pointer"
aria-label="Close"
>
<svg className="w-5 h-5 sm:w-6 sm:h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/* Desktop Left Arrow Navigation Button */}
{prevMember && (
<button
onClick={(e) => {
e.stopPropagation();
paginate(-1);
}}
className="hidden md:flex absolute left-4 md:left-8 z-40 w-12 h-12 rounded-full bg-white/10 hover:bg-white/30 text-white items-center justify-center backdrop-blur-md transition-all duration-300 hover:scale-110 cursor-pointer"
aria-label="Previous Team Member"
>
<svg className="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
)}
{/* Desktop Right Arrow Navigation Button */}
{nextMember && (
<button
onClick={(e) => {
e.stopPropagation();
paginate(1);
}}
className="hidden md:flex absolute right-4 md:right-8 z-40 w-12 h-12 rounded-full bg-white/10 hover:bg-white/30 text-white items-center justify-center backdrop-blur-md transition-all duration-300 hover:scale-110 cursor-pointer"
aria-label="Next Team Member"
>
<svg className="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
)}
{/* 3D Carousel Stage */}
<div className="relative w-full max-w-6xl flex items-center justify-center">
{/* Previous Neighbor Card (Blurred Background Left - Desktop only) */}
{prevMember && (
<m.div
key={`prev-${prevMember.id}`}
layout
onClick={(e) => {
e.stopPropagation();
paginate(-1);
}}
className="hidden lg:flex flex-col absolute -left-12 xl:-left-20 w-64 lg:w-72 rounded-3xl overflow-hidden bg-white/10 border border-white/20 backdrop-blur-md opacity-35 hover:opacity-70 scale-90 transition-all duration-500 cursor-pointer z-10 shadow-2xl select-none"
>
<div className="relative w-full aspect-[3/4] bg-neutral-900 overflow-hidden">
{prevMember.image && (
<Image
src={typeof prevMember.image === 'string' ? prevMember.image : prevMember.image.url}
alt={prevMember.name}
fill
sizes="300px"
className="object-cover blur-[2px]"
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
<div className="absolute bottom-4 left-4 right-4 text-white">
<p className="font-heading font-extrabold text-lg truncate">{prevMember.name}</p>
<p className="text-neutral-300 text-xs truncate">{prevMember.position}</p>
</div>
</div>
</m.div>
)}
{/* Active Main Card with Directional Slide Animation into Center */}
<AnimatePresence custom={direction} mode="popLayout">
<m.div
key={selectedMember.id}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.6}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.y) > 100 || Math.abs(info.velocity.y) > 500) {
setSelectedMember(null);
}
}}
transition={{
x: { type: 'spring', stiffness: 300, damping: 30 },
opacity: { duration: 0.25 },
scale: { duration: 0.25 },
}}
onClick={(e) => e.stopPropagation()}
className="bg-white rounded-[2rem] sm:rounded-3xl overflow-hidden max-w-[380px] sm:max-w-md md:max-w-lg w-full shadow-2xl border border-white/20 relative flex flex-col z-20 my-auto touch-none cursor-grab active:cursor-grabbing shrink-0"
>
{/* Mobile Drag Indicator Bar */}
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-30 w-12 h-1.5 rounded-full bg-white/50 backdrop-blur-sm pointer-events-none" />
{/* Portrait Container - Immersive Extra Large Portrait Aspect Framing for Mobile */}
<m.div
layoutId={`team-avatar-${selectedMember.id}`}
className="relative w-full h-[60vh] min-h-[440px] max-h-[620px] md:h-[460px] bg-neutral-900 overflow-hidden shrink-0"
>
{selectedMember.image ? (
<Image
src={typeof selectedMember.image === 'string' ? selectedMember.image : selectedMember.image.url}
alt={selectedMember.name}
fill
sizes="(max-width: 768px) 100vw, 600px"
className="object-cover object-[center_15%] pointer-events-none select-none"
priority
/>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center text-neutral-400 bg-neutral-800">
<svg className="w-24 h-24 mb-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-transparent to-transparent pointer-events-none" />
<div className="absolute bottom-5 left-5 right-5 text-white pointer-events-none">
<h3 className="font-heading font-extrabold text-2xl sm:text-3xl drop-shadow-md">{selectedMember.name}</h3>
<p className="text-primary-light font-semibold text-xs sm:text-sm uppercase tracking-widest drop-shadow-sm">{selectedMember.position}</p>
</div>
</m.div>
{/* Contact Info Footer */}
<div className="p-5 sm:p-6 bg-white flex flex-col gap-3">
{selectedMember.email && (
<a href={`mailto:${selectedMember.email}`} className="flex items-center gap-3 text-neutral-700 hover:text-primary font-medium transition-colors cursor-pointer text-sm sm:text-base">
<div className="w-9 h-9 rounded-lg bg-neutral-100 flex items-center justify-center text-neutral-500 shrink-0">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</div>
<span className="truncate">{selectedMember.email}</span>
</a>
)}
{selectedMember.phone && (
<a href={`tel:${selectedMember.phone}`} className="flex items-center gap-3 text-neutral-700 hover:text-primary font-medium transition-colors cursor-pointer text-sm sm:text-base">
<div className="w-9 h-9 rounded-lg bg-neutral-100 flex items-center justify-center text-neutral-500 shrink-0">
<svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</div>
<span>{selectedMember.phone}</span>
</a>
)}
</div>
</m.div>
</AnimatePresence>
{/* Next Neighbor Card (Blurred Background Right - Desktop only) */}
{nextMember && (
<m.div
key={`next-${nextMember.id}`}
layout
onClick={(e) => {
e.stopPropagation();
paginate(1);
}}
className="hidden lg:flex flex-col absolute -right-12 xl:-right-20 w-64 lg:w-72 rounded-3xl overflow-hidden bg-white/10 border border-white/20 backdrop-blur-md opacity-35 hover:opacity-70 scale-90 transition-all duration-500 cursor-pointer z-10 shadow-2xl select-none"
>
<div className="relative w-full aspect-[3/4] bg-neutral-900 overflow-hidden">
{nextMember.image && (
<Image
src={typeof nextMember.image === 'string' ? nextMember.image : nextMember.image.url}
alt={nextMember.name}
fill
sizes="300px"
className="object-cover blur-[2px]"
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
<div className="absolute bottom-4 left-4 right-4 text-white">
<p className="font-heading font-extrabold text-lg truncate">{nextMember.name}</p>
<p className="text-neutral-300 text-xs truncate">{nextMember.position}</p>
</div>
</div>
</m.div>
)}
</div>
{/* Mobile Dedicated Navigation Bar (Bottom) */}
<div
onClick={(e) => e.stopPropagation()}
className="flex md:hidden items-center justify-between w-full max-w-md mt-4 px-4 py-2.5 bg-white/10 backdrop-blur-xl rounded-full border border-white/20 text-white z-40 shrink-0"
>
<button
onClick={() => paginate(-1)}
className="w-10 h-10 rounded-full bg-white/20 active:bg-white/40 flex items-center justify-center text-white cursor-pointer"
aria-label="Previous"
>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6"></polyline></svg>
</button>
<span className="text-xs font-bold uppercase tracking-widest text-white/90">
{selectedIndex + 1} / {members.length}
</span>
<button
onClick={() => paginate(1)}
className="w-10 h-10 rounded-full bg-white/20 active:bg-white/40 flex items-center justify-center text-white cursor-pointer"
aria-label="Next"
>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6"></polyline></svg>
</button>
</div>
</m.div>
)}
</AnimatePresence>,
document.body
)}
</section>
);
}

View File

@@ -104,8 +104,8 @@ layout: "fullBleed"
icon: "hdd",
lists: [
[
"Horizontalspülbohrverfahren (bis 250m)",
"Horizontalspülbohrverfahren (bis 400er Rohr)",
"Horizontalspülbohrverfahren (bis 400m)",
"Horizontalspülbohrverfahren (bis 600er Rohr)",
"Erdrakete (bis 15m DÖ-Länge)",
"Erdrakete (bis 160er Rohr)"
]

View File

@@ -9,7 +9,7 @@ layout: "fullBleed"
badge="Unternehmen"
title="Unser Team"
subtitle="Die Experten hinter der E-TIB Gruppe"
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
backgroundImage={{ url: '/assets/photos/E-TIB_Gruppenfoto.jpg', focalY: 25 }}
alignment="center"
/>
@@ -68,15 +68,15 @@ layout: "fullBleed"
name: "Kerstin Joseph",
position: "Buchhaltung / Controlling",
email: "k.joseph@e-tib.com",
phone: "+49 3561 6857733",
image: "/assets/photos/team/kerstin.jpg"
phone: "+49 3561 6857733"
},
{
id: "katrin-haigold",
name: "Katrin Haigold",
position: "Buchhaltung / Personalwesen",
email: "k.haigold@e-tib.com",
phone: "+49 3561 6851692"
phone: "+49 3561 6851692",
image: "/assets/photos/team/katrin-heigold.jpg"
},
{
id: "maik-weichert",

View File

@@ -104,8 +104,8 @@ layout: "fullBleed"
icon: "hdd",
lists: [
[
"Horizontal directional drilling (up to 250m)",
"Horizontal directional drilling (up to 400mm pipe)",
"Horizontal directional drilling (up to 400m)",
"Horizontal directional drilling (up to 600mm pipe)",
"Earth rocket (up to 15m crossing length)",
"Earth rocket (up to 160mm pipe)"
]

View File

@@ -9,7 +9,7 @@ layout: "fullBleed"
badge="Company"
title="Our Team"
subtitle="The Experts behind the E-TIB Group"
backgroundImage={{ url: '/assets/photos/DJI_0048.JPG' }}
backgroundImage={{ url: '/assets/photos/E-TIB_Gruppenfoto.jpg', focalY: 25 }}
alignment="center"
/>
@@ -68,15 +68,15 @@ layout: "fullBleed"
name: "Kerstin Joseph",
position: "Accounting / Controlling",
email: "k.joseph@e-tib.com",
phone: "+49 3561 6857733",
image: "/assets/photos/team/kerstin.jpg"
phone: "+49 3561 6857733"
},
{
id: "katrin-haigold",
name: "Katrin Haigold",
position: "Accounting / Human Resources",
email: "k.haigold@e-tib.com",
phone: "+49 3561 6851692"
phone: "+49 3561 6851692",
image: "/assets/photos/team/katrin-heigold.jpg"
},
{
id: "maik-weichert",

View File

@@ -8,7 +8,7 @@ services:
- infra
- default
labels:
- "caddy=http://${TRAEFIK_HOST:-etib.localhost}"
- "caddy=http://${TRAEFIK_HOST:-etib.localhost}, http://e-tib.localhost"
- "caddy.reverse_proxy=http://etib-app:3001"
extra_hosts:
- "host.docker.internal:host-gateway"
@@ -34,11 +34,9 @@ services:
# Force Garbage Collection before Docker kills the container (OOM)
NODE_OPTIONS: "--max-old-space-size=6144"
NEXT_TELEMETRY_DISABLED: "1"
UV_THREADPOOL_SIZE: "1"
RAYON_NUM_THREADS: "1"
NEXT_PRIVATE_WORKER_THREADS: "false"
NPM_TOKEN: ${NPM_TOKEN:-}
CI: "true"
HUSKY: "0"
WATCHPACK_POLLING: "true"
ports:
- "3001:3001"
@@ -48,7 +46,6 @@ services:
- etib_next_cache:/app/.next
- etib_turbo_cache:/app/.turbo
- etib_pnpm_store:/pnpm
- /app/.git
- /app/reference
- /app/data
@@ -57,19 +54,7 @@ services:
limits:
memory: 8G
command: >
sh -c "pnpm install --no-frozen-lockfile &&
while true; do
(
echo '[warmup] Waiting for Next.js to be reachable...'
until curl -sf http://localhost:3001 > /dev/null; do sleep 2; done
echo '[warmup] Server is up! Pre-compiling routes...'
curl -sf http://localhost:3001/de > /dev/null 2>&1 && echo '[warmup] /de ready'
echo '[warmup] All routes pre-compiled ✓'
) &
pnpm next dev --webpack --hostname 0.0.0.0 --port 3001;
echo '[etib-app] next dev exited, restarting in 2s...';
sleep 2;
done"
sh -c "pnpm install --no-frozen-lockfile && pnpm next dev --webpack --hostname 0.0.0.0 --port 3001"
labels:
- "traefik.enable=true"
- "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3001"
@@ -77,7 +62,7 @@ services:
networks:
default:
name: ${PROJECT_NAME:-e-tib}-internal
name: ${COMPOSE_PROJECT_NAME:-etib}-dev-internal
infra:
external: true

View File

@@ -1,7 +1,8 @@
{
"/assets/photos/DJI_0048.JPG": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADwAQCdASoKAAcABUB8JYwCdH8AGDNIbaAA92kbyv6vgOVMTIAAAA==",
"/assets/videos/web/hero-bahnkreuzung-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA=",
"/assets/videos/web/hero-bohrung-poster.jpg": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACwAQCdASoKAAYABUB8JZACdAEOO2gAAN5Rr3co8j7CDYNKNCAAAA==",
"/assets/videos/web/hero-kabelpflug-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA=",
"/assets/videos/web/hero-messe-poster.jpg": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoKAAYABUB8JaQAA3AA/vDUoAA="
"/assets/photos/DJI_0048.JPG": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADwAQCdASoKAAcABUB8JYwCdH8AGDXtwwAA92kbyv6vgOVMTIAAAA==",
"/assets/photos/E-TIB_Gruppenfoto.jpg": "data:image/webp;base64,UklGRiwAAABXRUJQVlA4ICAAAACQAQCdASoKAAUABUB8JZQCdAEOgwAA4XqSiWtFZ0AAAA==",
"/assets/videos/web/hero-bahnkreuzung-poster.jpg": "data:image/webp;base64,UklGRi4AAABXRUJQVlA4ICIAAACQAQCdASoKAAYABUB8JQAAXKXv4AAA/UZNt5FbP3v64AAA",
"/assets/videos/web/hero-bohrung-poster.jpg": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACwAQCdASoKAAYABUB8JZACdAEOI+oAAN5Rr3co8j7CDYNKNCAAAA==",
"/assets/videos/web/hero-kabelpflug-poster.jpg": "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAADQAQCdASoKAAYABUB8JagCw7EOwCTyAAD9QIvsqI74a4qbWwAAAA==",
"/assets/videos/web/hero-messe-poster.jpg": "data:image/webp;base64,UklGRi4AAABXRUJQVlA4ICIAAABQAQCdASoKAAYABUB8JQBOgC6gAP7hJjEBwUoIEPzDKgAA"
}

View File

@@ -1,5 +1,6 @@
{
"name": "e-tib-nextjs",
"version": "2.4.55",
"type": "module",
"private": true,
"packageManager": "pnpm@10.18.3",
@@ -100,7 +101,7 @@
"xlsx-cli": "^1.1.3"
},
"scripts": {
"dev": "bash -c 'bash scripts/registry-auth.sh && [ -f .env ] || (cp .env.example .env && sed -i.bak \"s/TRAEFIK_HOST=e-tib.com/TRAEFIK_HOST=etib.localhost/\" .env && rm -f .env.bak && echo \"✅ Created .env from .env.example\"); trap \"COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml up etib-app etib-proxy --remove-orphans'",
"dev": "bash -c 'bash scripts/registry-auth.sh && [ -f .env ] || (cp .env.example .env && sed -i.bak \"s/TRAEFIK_HOST=e-tib.com/TRAEFIK_HOST=etib.localhost/\" .env && rm -f .env.bak && echo \"✅ Created .env from .env.example\"); trap \"COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml up --build etib-app etib-proxy --remove-orphans'",
"dev:local": "bash -c 'bash scripts/registry-auth.sh && next dev --webpack --port 3100 --hostname 0.0.0.0'",
"dev:infra": "COMPOSE_PROJECT_NAME=etib docker compose -f docker-compose.dev.yml up -d etib-proxy",
"build": "next build",
@@ -140,7 +141,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.4.44",
"version": "2.4.48",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -5,6 +5,7 @@ import sharp from 'sharp';
const PUBLIC_DIR = path.join(process.cwd(), 'public');
const IMAGES_TO_PROCESS = [
'/assets/photos/DJI_0048.JPG',
'/assets/photos/E-TIB_Gruppenfoto.jpg',
'/assets/videos/web/hero-bahnkreuzung-poster.jpg',
'/assets/videos/web/hero-bohrung-poster.jpg',
'/assets/videos/web/hero-kabelpflug-poster.jpg',

View File

@@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest';
import fs from 'fs';
import { defaultCertificates } from '../components/blocks/CertificatesBlock';
import path from 'path';
describe('Certificates & Insurance Integration (DE + EN)', () => {
it('includes the new RheinLand Betriebshaftpflichtversicherung in defaultCertificates (DE)', () => {
const haftpflichtCert = defaultCertificates.find(
@@ -13,14 +15,14 @@ describe('Certificates & Insurance Integration (DE + EN)', () => {
});
it('verifies that content/en/zertifikate.mdx includes Commercial Liability Insurance (EN)', () => {
const enMdx = fs.readFileSync('/Volumes/Alpha SSD/Coding/e-tib.com/content/en/zertifikate.mdx', 'utf-8');
const enMdx = fs.readFileSync(path.join(__dirname, '../content/en/zertifikate.mdx'), 'utf-8');
expect(enMdx).toContain("title: 'Commercial Liability Insurance'");
expect(enMdx).toContain("pdfUrl: '/assets/certificates/betriebshaftpflicht.pdf'");
expect(enMdx).toContain("date: '25.03.2026'");
});
it('verifies that the PDF asset exists in public/assets/certificates/betriebshaftpflicht.pdf', () => {
const filePath = '/Volumes/Alpha SSD/Coding/e-tib.com/public/assets/certificates/betriebshaftpflicht.pdf';
const filePath = path.join(__dirname, '../public/assets/certificates/betriebshaftpflicht.pdf');
expect(fs.existsSync(filePath)).toBe(true);
});
});