Compare commits

..

18 Commits

Author SHA1 Message Date
51a8e63d63 fix(css): enforce explicit layout limits on footer and blog page
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m5s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-10 21:25:19 +02:00
0f95f09711 fix(css): enforce explicit layout limits via tailwind utilities across all container blocks
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Successful in 2m48s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 56s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-10 20:31:00 +02:00
0ed28915cc fix(css): add max-width to .container breakpoints in globals.css
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 53s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-10 19:06:46 +02:00
b8c21ca4b3 fix(test): remove isolate false to fix cross-file DOM leakage in CI
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m39s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-10 18:23:06 +02:00
4d4185b1f3 2.4.28
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 QA (push) Failing after 57s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-10 16:39:09 +02:00
59b8e24bcf fix: resolve tailwind production container alignment & vitest swc plugins 2026-07-10 16:39:02 +02:00
adf2851504 chore(ci): restore qa.yml target to test.e-tib.com
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-09 11:46:10 +02:00
e398fbceed chore(ci): point qa.yml to branch domain for verification
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 49s
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-07-09 11:42:32 +02:00
850d9e392e fix(ci): bypass Gatekeeper for /assets and disable TLS rejection in loopback
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 53s
Build & Deploy / 🧪 QA (push) Successful in 1m18s
Build & Deploy / 🏗️ Build (push) Successful in 2m25s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-09 11:35:45 +02:00
8f86575471 2.4.27
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-04 10:40:02 +02:00
97467cbda9 fix(content): sort trade fairs ascending by date 2026-07-04 10:39:57 +02:00
076aa5c13d Update Intersolar date to 2027
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m29s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 54s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-03 21:25:29 +02:00
a010a63281 fix(ui): center heading in contact form success and error states
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m15s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-03 16:29:06 +02:00
9880a3f8dd Merge hotfix/2.2.95-fixes into main, replacing all content
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m16s
Build & Deploy / 🏗️ Build (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 50s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 15:41:47 +02:00
531e1a49f8 fix: remove inline opacity: 0 from HeroSection and use animation-fill-mode both to prevent invisible text
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 47s
Build & Deploy / 🧪 QA (push) Successful in 1m14s
Build & Deploy / 🏗️ Build (push) Successful in 2m15s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-07-03 14:45:37 +02:00
324cfb0ca3 fix: ci hang in maintenance, restore hero animations, bump version 2.4.23
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 49s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 13:18:33 +02:00
69d2c42ef7 chore: bump version to 2.4.22
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Failing after 22m7s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-07-03 12:22:32 +02:00
6fb46278e0 fix: restore hero animations in tailwind v4 on 2.2.95
Some checks failed
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🔍 Prepare (push) Has been cancelled
2026-07-03 12:21:43 +02:00
198 changed files with 3981 additions and 98349 deletions

9
.agents/AGENTS.md Normal file
View File

@@ -0,0 +1,9 @@
<RULE[zero_trust_no_proof]>
# 🚫 ZERO TRUST & BEWEISPFLICHT
Dein Wort hat absolut keinen Wert, wenn du es nicht mit einem harten Test beweisen kannst.
- **Keine Behauptungen:** Wenn du sagst, dass etwas funktioniert, ist das wertlos. Code, der nicht getestet ist, gilt als defekt.
- **Beweispflicht:** Jede Code-Änderung, jeder Fix und jede Logik-Anpassung MUSS durch einen begleitenden, erfolgreichen automatisierten Test bewiesen werden.
- **TDD (Test-Driven Development) ist Gesetz:** Ohne einen fehlschlagenden Test darfst du keinen Code schreiben. Ohne einen grünen Test darfst du keinen Code als fertig deklarieren.
</RULE[zero_trust_no_proof]>

View File

@@ -47,14 +47,7 @@ jobs:
run: |
echo "=== System Disk Usage ==="
df -h || true
echo "=== Tool Cache Usage ==="
du -sh /opt/hostedtoolcache/* || true
du -sh /opt/hostedtoolcache/.[!.]* || true
echo "Purging old tool caches skipped to prevent runner corruption."
echo "=== Host Disk Usage ==="
docker run --rm -v /:/host-root alpine df -h || true
echo "=== Host Inode Usage ==="
docker run --rm -v /:/host-root alpine df -i || true
echo "=== Running Host Containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
echo "=== Host Docker Info ==="
@@ -340,6 +333,9 @@ jobs:
{
echo "# Generated by CI - $TARGET"
echo "IMAGE_TAG=$IMAGE_TAG"
if [[ "$TARGET" != "production" ]]; then
echo "NODE_TLS_REJECT_UNAUTHORIZED=0"
fi
echo "NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL"
echo "GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN"
echo "SENTRY_DSN=$SENTRY_DSN"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -118,7 +118,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
{/* Title overlay on image */}
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
<div className="container mx-auto px-4">
<div className="container mx-auto max-w-7xl px-4">
<div className="max-w-4xl bg-neutral-dark/40 backdrop-blur-md p-6 rounded-2xl border border-white/10">
{post.frontmatter.category && (
<div className="overflow-hidden mb-6">
@@ -192,7 +192,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
)}
{/* Main Content Area with Sticky Narrative Layout */}
<div className="container mx-auto px-4 py-16 md:py-24">
<div className="container mx-auto max-w-7xl px-4 py-16 md:py-24">
<div className="sticky-narrative-container">
{/* Left Column: Content */}
<div className="sticky-narrative-content">

View File

@@ -20,8 +20,9 @@ import { TransitionProvider } from '@/components/providers/TransitionProvider';
import { DynamicInitialLoader as InitialLoader } from '@/components/providers/DynamicInitialLoader';
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
// Removed CorporateBackground to improve LCP by eliminating Framer Motion main thread block
const corporateBackground = null;
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
import { DynamicPageTransitionShutter as PageTransitionShutter } from '@/components/providers/DynamicPageTransitionShutter';
import { DynamicFramerMotion as FramerMotionProvider } from '@/components/providers/DynamicFramerMotion';
const inter = Inter({
subsets: ['latin'],
@@ -190,24 +191,28 @@ export default async function Layout(props: {
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white" suppressHydrationWarning>
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
<TransitionProvider>
{/* PageTransitionShutter loaded dynamically in TransitionProvider */}
<SkipLink />
<Header navLinks={navLinks} />
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
<main
id="main-content"
className="flex-grow overflow-visible"
tabIndex={-1}
>
{children}
</main>
<FramerMotionProvider>
<CorporateBackground />
<InitialLoader shouldShowLoader={!hasSeenLoader} />
<PageTransitionShutter />
<SkipLink />
<Header navLinks={navLinks} />
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
<main
id="main-content"
className="flex-grow overflow-visible"
tabIndex={-1}
>
{children}
</main>
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
<Footer companyInfo={companyInfo} />
<JsonLd />
<AnalyticsShell />
<FeedbackClientWrapper />
<FeedbackClientWrapper />
</FramerMotionProvider>
</TransitionProvider>
</NextIntlClientProvider>
</body>

View File

@@ -132,12 +132,7 @@ export default async function Home(props: { params: Promise<{ locale: string }>
}
setRequestLocale(locale);
preload('/assets/videos/web/hero-kabelpflug-poster-v2.webp', {
as: 'image',
fetchPriority: 'high',
imageSrcSet: '/assets/videos/web/hero-kabelpflug-poster-mobile-v2.webp 800w, /assets/videos/web/hero-kabelpflug-poster-v2.webp 1920w',
imageSizes: '100vw'
});
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
const mdx = await getMdxContent(locale, 'home');
const allReferences = await getAllReferences(locale);

View File

@@ -1,25 +0,0 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
export const runtime = 'nodejs';
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Our Team" />,
{
...OG_IMAGE_SIZE,
fonts,
},
);
}

View File

@@ -1,326 +0,0 @@
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge } from '@/components/ui';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
import TrackedButton from '@/components/analytics/TrackedButton';
interface TeamPageProps {
params: Promise<{
locale: string;
}>;
}
export async function generateMetadata({ params }: TeamPageProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
return {
title,
description,
alternates: {
canonical: `${SITE_URL}/${locale}/team`,
languages: {
de: `${SITE_URL}/de/team`,
en: `${SITE_URL}/en/team`,
'x-default': `${SITE_URL}/en/team`,
},
},
openGraph: {
title: `${title} | E-TIB`,
description,
url: `${SITE_URL}/${locale}/team`,
},
twitter: {
card: 'summary_large_image',
title: `${title} | E-TIB`,
description,
},
};
}
export default async function TeamPage({ params }: TeamPageProps) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'Team' });
return (
<div className="flex flex-col min-h-screen bg-neutral-light">
<JsonLd
id="breadcrumb-team"
data={getBreadcrumbSchema([{ name: t('hero.subtitle'), item: `/team` }])}
/>
<JsonLd
id="person-michael"
data={{
'@context': 'https://schema.org',
'@type': 'Person',
name: t('michael.name'),
jobTitle: t('michael.role'),
worksFor: {
'@type': 'Organization',
name: 'E-TIB',
},
sameAs: ['https://www.linkedin.com/in/michael-bodemer-33b493122/'],
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`,
}}
/>
<JsonLd
id="person-klaus"
data={{
'@context': 'https://schema.org',
'@type': 'Person',
name: t('klaus.name'),
jobTitle: t('klaus.role'),
worksFor: {
'@type': 'Organization',
name: 'E-TIB',
},
sameAs: ['https://www.linkedin.com/in/klaus-mintel-b80a8b193/'],
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`,
}}
/>
{/* Hero Section */}
<Reveal>
<section className="relative flex items-center justify-center overflow-hidden bg-primary-dark pt-32 pb-24 md:pt-[14%] md:pb-[12%]">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC07655-Large.webp"
alt="E-TIB Team"
fill
className="object-cover scale-105 animate-slow-zoom opacity-30 md:opacity-40"
sizes="100vw"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
</div>
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{t('hero.badge')}
</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>
</Container>
</section>
</Reveal>
{/* Michael Bodemer Section - Sticky Narrative Split Layout */}
<article className="relative bg-white overflow-hidden">
<div className="flex flex-col lg:flex-row">
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-primary-dark text-white relative order-2 lg:order-1">
<div className="absolute top-0 right-0 w-32 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
<div className="relative z-10">
<Badge variant="accent" className="mb-4 md:mb-8">
{t('michael.role')}
</Badge>
<Heading level={2} className="text-white mb-6 md:mb-10 text-2xl md:text-4xl">
<span className="text-white">{t('michael.name')}</span>
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-accent rounded-full" />
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-white/90">
{t('michael.quote')}
</p>
</div>
<p className="text-base md:text-xl leading-relaxed text-white/70 mb-6 md:mb-12 max-w-xl">
{t('michael.description')}
</p>
<TrackedButton
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
variant="accent"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
eventProperties={{
type: 'social_linkedin',
person: 'Michael Bodemer',
location: 'team_page',
}}
>
{t('michael.linkedin')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</TrackedButton>
</div>
</Reveal>
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1 lg:order-2">
<Image
src="/uploads/2024/12/DSC07768-Large.webp"
alt={t('michael.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent" />
</Reveal>
</div>
</article>
{/* Legacy Section - Immersive Background */}
<Reveal>
<section className="relative py-16 md:py-48 bg-primary-dark text-white overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy.webp"
alt={t('legacy.subtitle')}
fill
className="object-cover opacity-20 md:opacity-30 scale-110 animate-slow-zoom"
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary-dark/60 mix-blend-multiply" />
</div>
<Container className="relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
<div className="lg:col-span-6">
<Heading
level={2}
subtitle={t('legacy.subtitle')}
className="text-white mb-6 md:mb-10"
>
<span className="text-white">{t('legacy.title')}</span>
</Heading>
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
{t('legacy.p1')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">{t('legacy.p2')}</p>
</div>
</div>
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.expertise')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.expertiseDesc')}
</div>
</div>
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.network')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.networkDesc')}
</div>
</div>
</div>
</div>
</Container>
</section>
</Reveal>
{/* Klaus Mintel Section - Reversed Split Layout */}
<article className="relative bg-white overflow-hidden">
<div className="flex flex-col lg:flex-row">
<Reveal className="w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden order-1">
<Image
src="/uploads/2024/12/DSC07963-Large.webp"
alt={t('klaus.name')}
fill
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
quality={100}
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent" />
</Reveal>
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
<div className="relative z-10">
<Badge variant="saturated" className="mb-4 md:mb-8">
{t('klaus.role')}
</Badge>
<Heading level={2} className="text-saturated mb-6 md:mb-10 text-2xl md:text-4xl">
{t('klaus.name')}
</Heading>
<div className="relative mb-6 md:mb-12">
<div className="absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 bg-saturated rounded-full" />
<p className="text-base md:text-xl font-bold italic leading-relaxed pl-5 md:pl-8 text-text-secondary">
{t('klaus.quote')}
</p>
</div>
<p className="text-base md:text-xl leading-relaxed text-text-secondary mb-6 md:mb-12 max-w-xl">
{t('klaus.description')}
</p>
<TrackedButton
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
variant="saturated"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
eventProperties={{
type: 'social_linkedin',
person: 'Klaus Mintel',
location: 'team_page',
}}
>
{t('klaus.linkedin')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</TrackedButton>
</div>
</Reveal>
</div>
</article>
{/* Manifesto Section - Modern Grid */}
<Section className="bg-white text-primary py-16 md:py-28">
<Container>
<div className="sticky-narrative-container">
<div className="sticky-narrative-sidebar mb-8 lg:mb-0">
<div className="lg:sticky lg:top-32">
<Heading level={2} subtitle={t('manifesto.subtitle')} className="mb-4 md:mb-8">
{t('manifesto.title')}
</Heading>
<p className="text-base md:text-xl text-text-secondary leading-relaxed">
{t('manifesto.tagline')}
</p>
{/* Mobile-only progress indicator */}
<div className="flex lg:hidden mt-8 gap-2">
{[0, 1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden"
>
<div className="h-full bg-accent w-0 group-active:w-full transition-all duration-500" />
</div>
))}
</div>
</div>
</div>
<ul className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10 list-none p-0 m-0">
{[0, 1, 2, 3, 4, 5].map((idx) => (
<li
key={idx}
className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none"
>
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">
0{idx + 1}
</span>
</div>
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">
{t(`manifesto.items.${idx}.title`)}
</h3>
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">
{t(`manifesto.items.${idx}.description`)}
</p>
</li>
))}
</ul>
</div>
</Container>
</Section>
<Reveal>
<Gallery />
</Reveal>
</div>
);
}

19
app/[locale]/template.tsx Normal file
View File

@@ -0,0 +1,19 @@
'use client';
import { m } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<m.div
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
transition={{
duration: 0.7,
ease: [0.16, 1, 0.3, 1],
}}
className="w-full h-full"
>
{children}
</m.div>
);
}

View File

@@ -1,102 +0,0 @@
'use server';
import { getServerAppServices } from '@/lib/services/create-services.server';
export async function requestBrochureAction(formData: FormData) {
const services = getServerAppServices();
const logger = services.logger.child({ action: 'requestBrochureAction' });
const { headers } = await import('next/headers');
const requestHeaders = await headers();
if ('setServerContext' in services.analytics) {
(services.analytics as any).setServerContext({
userAgent: requestHeaders.get('user-agent') || undefined,
language: requestHeaders.get('accept-language')?.split(',')[0] || undefined,
referrer: requestHeaders.get('referer') || undefined,
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined,
});
}
services.analytics.track('brochure-request-attempt');
const email = formData.get('email') as string;
const locale = (formData.get('locale') as string) || 'en';
// Anti-spam Honeypot Check
const honeypot = formData.get('company_website') as string;
if (honeypot) {
logger.warn('Spam detected via honeypot in brochure request', { email });
// Silently succeed to fool the bot without doing actual work
return { success: true };
}
if (!email) {
logger.warn('Missing email in brochure request');
return { success: false, error: 'Missing email address' };
}
// Basic email validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return { success: false, error: 'Invalid email address' };
}
// 1. Save to CMS - Removed along with Payload CMS
// 2. Notify via Gotify
try {
await services.notifications.notify({
title: '📑 Brochure Download Request',
message: `New brochure download request from ${email} (${locale})`,
priority: 3,
});
} catch (error) {
logger.error('Failed to send notification', { error });
}
// 3. Send Brochure via Email
const brochureUrl = `https://e-tib.com/brochure/etib-product-catalog-${locale}.pdf`;
try {
const { sendEmail } = await import('@/lib/mail/mailer');
const { render } = await import('@mintel/mail');
const React = await import('react');
const { BrochureDeliveryEmail } = await import('@/components/emails/BrochureDeliveryEmail');
const html = await render(
React.createElement(BrochureDeliveryEmail, {
_email: email,
brochureUrl,
locale: locale as 'en' | 'de',
}),
);
const emailResult = await sendEmail({
to: email,
subject: locale === 'de' ? 'Ihr E-TIB Kabelkatalog' : 'Your E-TIB Cable Catalog',
html,
});
if (emailResult.success) {
logger.info('Brochure email sent successfully', { email });
} else {
logger.error('Failed to send brochure email', { error: emailResult.error, email });
services.errors.captureException(new Error(`Brochure email failed: ${emailResult.error}`), {
action: 'requestBrochureAction_email',
email,
});
return { success: false, error: 'Failed to send email. Please try again later.' };
}
} catch (error) {
logger.error('Exception while sending brochure email', { error });
return { success: false, error: 'Failed to send email. Please try again later.' };
}
// 4. Track success
services.analytics.track('brochure-request-success', {
locale,
delivery_method: 'email',
});
return { success: true };
}

145
app/globals.css Normal file
View File

@@ -0,0 +1,145 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@config "../tailwind.config.cjs";
/* Tailwind v4 Container Polyfill */
@utility container {
width: 100%;
margin-inline: auto !important;
padding-inline: 1rem;
@media (min-width: 640px) {
max-width: 640px;
padding-inline: 1.5rem;
}
@media (min-width: 768px) {
max-width: 768px;
padding-inline: 2rem;
}
@media (min-width: 1024px) {
max-width: 1024px;
padding-inline: 2.5rem;
}
@media (min-width: 1280px) {
max-width: 1280px;
padding-inline: 3rem;
}
@media (min-width: 1536px) {
max-width: 1536px;
padding-inline: 4rem;
}
}
/* E-TIB Custom Utilities */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.hide-scrollbar {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none !important; /* Chrome, Safari and Opera */
width: 0 !important;
height: 0 !important;
}
@keyframes bg-pulse-15 {
0%, 90%, 100% { opacity: 0.04; }
95% { opacity: 0.10; filter: brightness(1.2); }
}
@keyframes bg-pulse-20 {
0%, 90%, 100% { opacity: 0.07; }
95% { opacity: 0.14; filter: brightness(1.2); }
}
.animate-bg-pulse-15 { animation: bg-pulse-15 12s infinite ease-in-out; }
.animate-bg-pulse-20 { animation: bg-pulse-20 12s infinite ease-in-out; }
}
/* Base Styles */
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-neutral font-sans text-neutral-dark antialiased;
}
/* Interactive elements pointer */
a,
button,
[role="button"],
input[type="button"],
input[type="submit"],
input[type="reset"],
select,
summary,
label[for] {
cursor: pointer;
}
/* Ensure disabled elements don't show pointer */
button:disabled,
[role="button"]:disabled,
input:disabled,
select:disabled,
button[disabled] {
cursor: not-allowed;
}
/* Custom Modern Scrollbar */
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(14, 122, 92, 0.4);
border-radius: 10px;
border: 4px solid transparent;
background-clip: padding-box;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(14, 122, 92, 0.8);
}
/* Firefox */
html {
scrollbar-color: rgba(14, 122, 92, 0.5) transparent;
scrollbar-width: thin;
}
}
/* trigger rebuild */
@keyframes hero-fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@utility animate-hero-fade-in-up {
animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes hero-zoom-out {
from {
transform: scale(1.05);
}
to {
transform: scale(1);
}
}
@utility animate-hero-zoom-out {
animation: hero-zoom-out 1.5s ease-out forwards;
}

View File

@@ -1,7 +0,0 @@
export default function TestPage() {
return (
<div>
<h1>TEST PAGE WORKS</h1>
</div>
);
}

View File

@@ -81,7 +81,7 @@ export default function ContactForm() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<Heading level={3} className="mb-4">
<Heading level={3} align="center" className="mb-4">
{t('form.successTitle') || 'Message Sent!'}
</Heading>
<p className="text-text-secondary text-lg mb-8">
@@ -115,7 +115,7 @@ export default function ContactForm() {
<line x1="9" y1="9" x2="15" y2="15" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<Heading level={3} className="mb-4 text-destructive font-black">
<Heading level={3} align="center" className="mb-4 text-destructive font-black">
{t('form.errorTitle') || 'Submission Failed!'}
</Heading>
<p className="text-destructive/80 text-lg mb-8 leading-relaxed font-medium">

View File

@@ -215,9 +215,9 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
<AnimatePresence mode="wait" initial={false}>
<m.div
key={currentIndex}
initial={{ opacity: 0, scale: 1.1 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.9, filter: 'blur(10px)' }}
transition={{ duration: 0.7, ease: [0.25, 0.46, 0.45, 0.94] }}
className="relative w-full h-full"
>

View File

@@ -1,40 +0,0 @@
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

@@ -85,7 +85,7 @@ export function CompanyTimeline({
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -translate-y-1/2 translate-x-1/3" />
<div className="absolute bottom-0 left-0 w-[800px] h-[800px] bg-neutral-200/40 rounded-full blur-[120px] pointer-events-none translate-y-1/3 -translate-x-1/3" />
<div className="container max-w-5xl relative z-10">
<div className="container mx-auto max-w-5xl relative z-10">
<div className="text-center mb-12 md:mb-24">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-neutral-200 text-neutral-dark text-xs font-bold uppercase tracking-wider mb-6 shadow-sm">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />

View File

@@ -1,53 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface DeferredVideoProps {
videoUrl: string;
pathname: string;
}
export function DeferredVideo({ videoUrl, pathname }: DeferredVideoProps) {
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
let interactionTriggered = false;
const handleInteraction = () => {
if (interactionTriggered) return;
interactionTriggered = true;
setVideoSrcLoaded(true);
// Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
// Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true })
);
return () => {
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
}, []);
if (!videoUrl) return null;
return (
<video
key={`${pathname}-${videoUrl}`}
className={`absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90 transition-opacity duration-1000 ${videoSrcLoaded ? 'opacity-100' : 'opacity-0'}`}
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="none"
/>
);
}

View File

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

View File

@@ -1,6 +1,9 @@
'use client';
import { Button } from '@/components/ui/Button';
import { DeferredVideo } from './DeferredVideo';
import { headers } from 'next/headers';
import Image from 'next/image';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface HeroVideoProps {
data?: any;
@@ -27,66 +30,84 @@ export function HeroVideo(props: HeroVideoProps) {
const subtitle = props.subtitle || props.description || data?.subtitle || 'Wir verbinden Infrastruktur mit Präzision. Von Horizontalbohrungen bis zu komplexen Leitungsnetzen.';
const explicitNoVideo = props.videoUrl === 'none' || props.videoUrl === '';
const videoUrl = explicitNoVideo ? null : (props.videoUrl || data?.videoUrl || '/assets/dummy-hero.mp4');
// In server components, we don't have usePathname, so we can extract it from headers if needed,
// or just use a default fallback for the keys.
const pathname = '/';
const pathname = usePathname();
let defaultPoster = "/assets/photos/DJI_0048.JPG";
if (videoUrl && videoUrl.endsWith('.mp4')) {
defaultPoster = videoUrl.replace('.mp4', '-poster-v2.webp');
defaultPoster = videoUrl.replace('.mp4', '-poster.jpg');
}
let posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
// Force -v2 to bypass CDN cache of the heavy 39KB image
posterSrc = posterSrc.replace('-poster.webp', '-poster-v2.webp');
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
const posterAlt = props.backgroundImage?.alt || data?.posterImage?.alt || "E-TIB Gruppe Baustelle";
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
const currentLocale = 'de'; // The layout or page should technically pass the locale, but for now fallback
const currentLocale = pathname?.startsWith('/en') ? 'en' : 'de';
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || 'Projekt anfragen';
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/de/contact`;
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
useEffect(() => {
let interactionTriggered = false;
const handleInteraction = () => {
if (interactionTriggered) return;
interactionTriggered = true;
setVideoSrcLoaded(true);
// Cleanup listeners
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
// Listen for any user interaction
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.addEventListener(event, handleInteraction, { passive: true, once: true })
);
return () => {
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach((event) =>
window.removeEventListener(event, handleInteraction)
);
};
}, []);
return (
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
{/* Background color while video loads */}
<div className="absolute inset-0 z-0 bg-neutral-dark" />
{/* Preload the LCP image in the document head */}
{(() => {
const isV2 = posterSrc.includes('-poster-v2.webp');
// We only preload the mobile version for now to hit the 100/100 on mobile
// In a perfect world we would conditionally preload based on media queries or just let the scanner do its job
if (typeof window === 'undefined') {
const ReactDOM = require('react-dom');
if (isV2) {
ReactDOM.preload(posterSrc.replace('-poster-v2.webp', '-poster-mobile-v2.webp'), { as: 'image', fetchPriority: 'high' });
} else {
ReactDOM.preload(posterSrc, { as: 'image', fetchPriority: 'high' });
}
}
return null;
})()}
<img
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
<Image
key={`img-${pathname}-${posterSrc}`}
src={posterSrc}
srcSet={posterSrc.includes('-poster-v2.webp') ? `${posterSrc.replace('-poster-v2.webp', '-poster-mobile-v2.webp')} 800w, ${posterSrc} 1920w` : undefined}
sizes={posterSrc.includes('-poster-v2.webp') ? "100vw" : undefined}
alt={posterAlt}
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
loading="eager"
fill
className="object-cover z-[1] pointer-events-none"
sizes="100vw"
quality={25}
priority
fetchPriority="high"
/>
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
<div className="absolute inset-0 bg-black/20 z-[1] pointer-events-none mix-blend-multiply" />
{/* Render video on top if available, but defer src to avoid blocking LCP */}
<DeferredVideo videoUrl={videoUrl || ''} pathname={pathname} />
{videoUrl && (
<video
key={`${pathname}-${videoUrl}`}
className={`absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90 transition-opacity duration-1000 ${videoSrcLoaded ? 'opacity-100' : 'opacity-0'}`}
src={videoSrcLoaded ? videoUrl : undefined}
autoPlay
muted
loop
playsInline
preload="none"
/>
)}
{/* Cinematic Color Grading Overlay */}
<div className="absolute inset-0 bg-[#0a192f]/30 z-[2] mix-blend-multiply" />
@@ -98,7 +119,7 @@ export function HeroVideo(props: HeroVideoProps) {
{/* Top Fade for Header Navigation Readability */}
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
<div className="container relative z-[4] text-center px-4">
<div className="container mx-auto max-w-7xl relative z-[4] text-center px-4">
<div
key={`hero-content-${pathname}`}
className="relative"

View File

@@ -53,7 +53,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
showFairs,
fairsTitle = t('fairsTitle'),
fairs = [
{ name: 'Intersolar München', date: 'Termin folgt', type: 'Energie-Messe', location: 'Messe München' },
{ name: 'Intersolar München', date: '8. - 10. Juni 2027', type: 'Energie-Messe', location: 'Messe München' },
{ name: 'Windenergietage Linstow', date: 'Termin folgt', type: 'Fachkongress', location: 'Linstow' },
{ name: 'Kabelwerkstatt Wiesbaden', date: 'Termin folgt', type: 'Fachmesse', location: 'Wiesbaden' }
],
@@ -69,7 +69,7 @@ export const JobListingBlock = (props: JobListingBlockProps) => {
return (
<div className="py-12 md:py-24">
<div className="container mx-auto">
<div className="container max-w-7xl mx-auto">
{showFairs && (
<div className="mb-12 md:mb-24 relative">
<m.h3

View File

@@ -98,7 +98,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
<section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
<div className="container relative z-10 mb-12 flex flex-col items-center justify-center text-center gap-6">
<div className="container mx-auto max-w-7xl relative z-10 mb-12 flex flex-col items-center justify-center text-center gap-6">
<div>
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
@@ -179,7 +179,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
</div>
<div className="container relative z-10 mt-4 flex justify-center">
<div className="container mx-auto max-w-7xl relative z-10 mt-4 flex justify-center">
<Link
href={ctaHref}
className="px-8 py-4 rounded-xl bg-white/5 border border-white/20 hover:bg-white hover:text-neutral-dark hover:shadow-[0_0_20px_rgba(255,255,255,0.3)] transition-all font-bold flex items-center gap-3 backdrop-blur-sm"

View File

@@ -69,7 +69,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
return (
<section id="unternehmen" className="pt-12 pb-16 md:py-24 lg:py-32 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
<div className="container relative z-10 px-4">
<div className="container mx-auto max-w-7xl relative z-10 px-4">
{(badge || title) && (
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">

View File

@@ -29,7 +29,7 @@ export function TeamGrid({ members }: TeamGridProps) {
return (
<section className="py-16 md:py-24 lg:py-32 bg-white relative overflow-hidden">
<div className="container relative z-10">
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl relative z-10">
<div className="mb-12">
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{t('badge')}</h2>
<h3 className="font-heading text-3xl md:text-4xl font-extrabold text-neutral-dark mb-4">{t('title')}</h3>

View File

@@ -23,7 +23,7 @@ export function EUFundingBadge() {
alt="European Union Background"
fill
className="object-cover object-top"
loading="lazy"
priority
/>
</div>

View File

@@ -1,145 +0,0 @@
import {
Body,
Container,
Head,
Heading,
Hr,
Html,
Preview,
Section,
Text,
Button,
} from '@react-email/components';
import * as React from 'react';
interface BrochureDeliveryEmailProps {
_email: string;
brochureUrl: string;
locale: 'en' | 'de';
}
export const BrochureDeliveryEmail = ({
_email,
brochureUrl,
locale = 'en',
}: BrochureDeliveryEmailProps) => {
const t =
locale === 'de'
? {
subject: 'Ihr E-TIB Kabelkatalog',
greeting: 'Vielen Dank für Ihr Interesse an E-TIB.',
body: 'Anbei erhalten Sie den Link zu unserem aktuellen Produktkatalog. Dieser enthält alle wichtigen technischen Spezifikationen und detaillierten Produktdaten.',
button: 'Katalog herunterladen',
footer: 'Diese E-Mail wurde von e-tib.com gesendet.',
}
: {
subject: 'Your E-TIB Cable Catalog',
greeting: 'Thank you for your interest in E-TIB.',
body: 'Below you will find the link to our current product catalog. It contains all key technical specifications and detailed product data.',
button: 'Download Catalog',
footer: 'This email was sent from e-tib.com.',
};
return (
<Html>
<Head />
<Preview>{t.subject}</Preview>
<Body style={main}>
<Container style={container}>
<Section style={headerSection}>
<Heading style={h1}>{t.subject}</Heading>
</Section>
<Section style={section}>
<Text style={text}>
<strong>{t.greeting}</strong>
</Text>
<Text style={text}>{t.body}</Text>
<Section style={buttonContainer}>
<Button style={button} href={brochureUrl}>
{t.button}
</Button>
</Section>
<Hr style={hr} />
</Section>
<Text style={footer}>{t.footer}</Text>
</Container>
</Body>
</Html>
);
};
export default BrochureDeliveryEmail;
const main = {
backgroundColor: '#f6f9fc',
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '0 0 48px',
marginBottom: '64px',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #e6ebf1',
};
const headerSection = {
backgroundColor: '#111111',
padding: '32px 48px',
borderBottom: '4px solid #117c61',
};
const h1 = {
color: '#ffffff',
fontSize: '24px',
fontWeight: 'bold',
margin: '0',
};
const section = {
padding: '32px 48px 0',
};
const text = {
color: '#333',
fontSize: '16px',
lineHeight: '24px',
textAlign: 'left' as const,
};
const buttonContainer = {
textAlign: 'center' as const,
marginTop: '32px',
marginBottom: '32px',
};
const button = {
backgroundColor: '#117c61',
borderRadius: '4px',
color: '#ffffff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'inline-block',
padding: '16px 32px',
};
const hr = {
borderColor: '#e6ebf1',
margin: '20px 0',
};
const footer = {
color: '#8898aa',
fontSize: '12px',
lineHeight: '16px',
textAlign: 'center' as const,
marginTop: '20px',
};

View File

@@ -29,7 +29,7 @@ export function Footer({ companyInfo }: FooterProps) {
<EUFundingBadge />
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-4 md:mb-12">
<div className="container mx-auto max-w-7xl relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-4 md:gap-6 lg:gap-8 mb-4 md:mb-12">
{/* Brand Column Bento */}
<div className="col-span-1 md:col-span-4 lg:col-span-5 flex flex-col justify-between bg-white/[0.02] border border-white/5 rounded-3xl p-5 md:p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
@@ -217,7 +217,7 @@ export function Footer({ companyInfo }: FooterProps) {
</div>
</div>
<div className="container relative z-10">
<div className="container mx-auto max-w-7xl relative z-10">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 md:gap-6 py-4 px-5 md:py-6 md:px-8 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-md text-center md:text-left">
<div className="flex flex-col md:flex-row items-center gap-4 md:gap-6 h-full">
<p className="text-neutral-400 text-xs font-medium m-0">&copy; {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>

View File

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

View File

@@ -0,0 +1,11 @@
'use client';
import { LazyMotion } from 'framer-motion';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
export function FramerMotionProvider({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={loadFeatures} strict>
{children}
</LazyMotion>
);
}

View File

@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
{/* Sweeping intense white light */}
<m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ x: '-150%' }}
animate={{ x: '250%' }}
initial={{ left: '-150%' }}
animate={{ left: '250%' }}
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
/>
</div>
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
>
<m.div
className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
initial={{ width: "0%" }}
animate={{ width: "100%" }}
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
/>
</m.div>

View File

@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
<AnimatePresence>
{isTransitioning && (
<m.div
key="page-transition-shutter"
key="shutter"
initial={{ y: '-100%' }}
animate={{ y: 0 }}
animate={{ y: '0%' }}
exit={{ y: '100%' }}
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
style={{
height: '100vh',
top: '0',
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
{/* Sweeping intense white light */}
<m.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
initial={{ x: '-150%' }}
animate={{ x: '250%' }}
initial={{ left: '-150%' }}
animate={{ left: '250%' }}
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
/>
</div>

View File

@@ -2,16 +2,6 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { LazyMotion } from 'framer-motion';
import dynamic from 'next/dynamic';
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
const DynamicPageTransitionShutter = dynamic(
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
{ ssr: false }
);
interface TransitionContextType {
isTransitioning: boolean;
@@ -24,12 +14,8 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
export function TransitionProvider({ children }: { children: ReactNode }) {
const [isTransitioning, setIsTransitioning] = useState(false);
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
const [hasMountedShutter, setHasMountedShutter] = useState(false);
const router = useRouter();
// We no longer eagerly load the shutter in the background.
// It will be loaded precisely when the user initiates a transition.
// We use a ref to track transition state without triggering the route change effect when it turns true
const isTransitioningRef = React.useRef(isTransitioning);
useEffect(() => {
@@ -55,7 +41,6 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTransitionMessage(message);
setIsTransitioning(true);
setHasMountedShutter(true);
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
setTimeout(() => {
@@ -83,10 +68,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
setTransitionMessage={setTransitionMessage}
/>
</React.Suspense>
<LazyMotion features={loadFeatures}>
{hasMountedShutter && <DynamicPageTransitionShutter />}
{children}
</LazyMotion>
{children}
</TransitionContext.Provider>
);
}

View File

@@ -1,14 +0,0 @@
import os
from PIL import Image
def optimize_webp_aggressive(path, width):
if not os.path.exists(path):
return
img = Image.open(path)
wpercent = (width / float(img.size[0]))
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((width, hsize), Image.Resampling.LANCZOS)
img.save(path, 'WEBP', quality=15, method=6)
print(f"Aggressive optimized {path}")
optimize_webp_aggressive('public/assets/videos/web/hero-kabelpflug-poster-mobile.webp', 600)

View File

@@ -1,30 +0,0 @@
---
title: "Moderne Verfahren im Kabeltiefbau"
date: "2024-03-15T10:00:00.000Z"
excerpt: "Erfahren Sie mehr über die neuesten Technologien und Verfahren im modernen Kabelleitungstiefbau, von Horizontalspülbohrungen bis hin zu grabenlosen Verlegetechniken."
featuredImage: "/assets/images/blog/tiefbau.jpg"
category: "Technik"
public: true
---
# Moderne Verfahren im Kabeltiefbau
Der Kabelleitungstiefbau hat sich in den letzten Jahren rasant entwickelt. Neue Technologien ermöglichen eine effizientere und umweltschonendere Verlegung von Versorgungsleitungen.
## Grabenlose Verlegetechniken
Eines der wichtigsten Verfahren ist die **Horizontalspülbohrung (HDD)**. Dieses Verfahren ermöglicht es, Leitungen unter Hindernissen wie Straßen, Flüssen oder Gebäuden zu verlegen, ohne die Oberfläche aufbrechen zu müssen.
### Vorteile der HDD-Bohrung:
- Minimale Beeinträchtigung des Verkehrs
- Schutz der Umwelt und vorhandener Vegetation
- Kürzere Bauzeiten
- Geringere Kosten bei komplexen Unterquerungen
## Klassischer Tiefbau in neuer Qualität
Auch der klassische Grabenbau profitiert von modernen Maschinen und präziser Planung. Durch den Einsatz von Saugbaggern und GPS-gestützter Vermessung können wir Schäden an Bestandsleitungen nahezu ausschließen.
## Fazit
Die Kombination aus bewährter Handwerkskunst und modernster Technik ist der Schlüssel zu erfolgreichen Infrastrukturprojekten. Wir bei E-TIB setzen konsequent auf Innovation, um unseren Kunden die bestmöglichen Ergebnisse zu liefern.

View File

@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
items={[
{
title: "Kabelleitungsnetzbau",
description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
tag: "Energie",
size: "large",
href: "/de/kabeltiefbau",

View File

@@ -8,7 +8,7 @@ layout: "fullBleed"
<HeroSection
badge="Kernkompetenz"
title="Kabelleitungsnetzbau"
subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
alignment="center"
ctaLabel="Projekt anfragen"

View File

@@ -20,9 +20,9 @@ layout: "fullBleed"
showFairs={true}
showJobs={false}
fairs={[
{ name: 'Intersolar München', date: '23. - 25. Juni 2026', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' },
{ name: 'Windenergietage Linstow', date: '10. - 12. November 2026', type: 'Fachkongress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
{ name: 'Werkstatt Kabel 2026', date: '24. - 25. November 2026', type: 'Fachtagung', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' }
{ name: 'Werkstatt Kabel 2026', date: '24. - 25. November 2026', type: 'Fachtagung', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' },
{ name: 'Intersolar München', date: '8. - 10. Juni 2027', type: 'Messe', location: 'München', url: 'https://www.intersolar.de/' }
]}
/>
</div>

View File

@@ -1,30 +0,0 @@
---
title: "Modern Methods in Cable Civil Engineering"
date: "2024-03-15T10:00:00.000Z"
excerpt: "Learn more about the latest technologies and methods in modern cable civil engineering, from horizontal directional drilling to trenchless installation techniques."
featuredImage: "/assets/images/blog/tiefbau.jpg"
category: "Technology"
public: true
---
# Modern Methods in Cable Civil Engineering
Cable civil engineering has evolved rapidly in recent years. New technologies enable more efficient and environmentally friendly installation of utility lines.
## Trenchless Installation Techniques
One of the most important methods is **Horizontal Directional Drilling (HDD)**. This method allows pipes to be laid under obstacles such as roads, rivers, or buildings without having to break open the surface.
### Benefits of HDD Drilling:
- Minimal impact on traffic
- Protection of the environment and existing vegetation
- Shorter construction times
- Lower costs for complex crossings
## Classic Civil Engineering in New Quality
Classic trench construction also benefits from modern machinery and precise planning. By using suction excavators and GPS-supported surveying, we can virtually eliminate damage to existing utility lines.
## Conclusion
The combination of proven craftsmanship and state-of-the-art technology is the key to successful infrastructure projects. At E-TIB, we consistently focus on innovation to deliver the best possible results for our customers.

View File

@@ -20,9 +20,9 @@ layout: "fullBleed"
showFairs={true}
showJobs={false}
fairs={[
{ name: 'Intersolar Munich', date: 'June 2325, 2026', type: 'Fair', location: 'Munich', url: 'https://www.intersolar.de/' },
{ name: 'Wind Energy Days Linstow', date: 'November 1012, 2026', type: 'Specialist Congress', location: 'Linstow', url: 'https://www.windenergietage.de/' },
{ name: 'Werkstatt Kabel 2026', date: 'November 2425, 2026', type: 'Symposium', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' }
{ name: 'Werkstatt Kabel 2026', date: 'November 2425, 2026', type: 'Symposium', location: 'Wiesbaden', url: 'https://werkstatt-kabel.essociation.de/' },
{ name: 'Intersolar Munich', date: 'June 810, 2027', type: 'Fair', location: 'Munich', url: 'https://www.intersolar.de/' }
]}
/>
</div>

View File

@@ -27,7 +27,7 @@ services:
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-etib-ratelimit,etib-forward,etib-compress}"
# Public Router paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-e-tib.com}`) || Host(`staging.${TRAEFIK_HOST:-e-tib.com}`) || Host(`testing.${TRAEFIK_HOST:-e-tib.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-e-tib.com}`) || Host(`staging.${TRAEFIK_HOST:-e-tib.com}`) || Host(`testing.${TRAEFIK_HOST:-e-tib.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|assets|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,388 +0,0 @@
/**
* Excel Products Module
*
* Provides typed access to technical product data from Excel source files.
* Reuses the parsing logic from the PDF datasheet generator.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as XLSX from 'xlsx';
// Configuration
const EXCEL_SOURCE_FILES = [
path.join(process.cwd(), 'data/excel/high-voltage.xlsx'),
path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/solar-cables.xlsx'),
];
// Types
export type ExcelRow = Record<string, string | number | boolean | Date>;
export interface ExcelMatch {
rows: ExcelRow[];
units: Record<string, string>;
}
export interface TechnicalData {
configurations: string[];
attributes: Array<{
name: string;
options: string[];
}>;
}
export interface ProductLookupParams {
name?: string;
slug?: string;
sku?: string;
translationKey?: string;
}
// Cache singleton
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
/**
* Normalize Excel key to match product identifiers
* Examples:
* - "NA2XS(FL)2Y" -> "NA2XSFL2Y"
* - "na2xsfl2y-3" -> "NA2XSFL2Y"
*/
function normalizeExcelKey(value: string): string {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
/**
* Normalize value (strip HTML, trim whitespace)
*/
function normalizeValue(value: string): string {
if (!value) return '';
return String(value)
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Check if value looks numeric
*/
function looksNumeric(value: string): boolean {
const v = normalizeValue(value).replace(/,/g, '.');
return /^-?\d+(?:\.\d+)?$/.test(v);
}
/**
* Load Excel rows from a file using xlsx library
*/
function loadExcelRows(filePath: string): ExcelRow[] {
if (!fs.existsSync(filePath)) {
console.warn(`[excel-products] File not found: ${filePath}`);
return [];
}
try {
const workbook = XLSX.readFile(filePath, {
cellDates: false,
cellNF: false,
cellText: false
});
// Get the first sheet
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// Convert to JSON
const rows = XLSX.utils.sheet_to_json(worksheet, {
defval: '',
raw: false
}) as ExcelRow[];
return rows;
} catch (error) {
console.error(`[excel-products] Error reading ${filePath}:`, error);
return [];
}
}
/**
* Build the Excel index from all source files
*/
export function getExcelIndex(): Map<string, ExcelMatch> {
if (EXCEL_INDEX) return EXCEL_INDEX;
const idx = new Map<string, ExcelMatch>();
for (const file of EXCEL_SOURCE_FILES) {
const rows = loadExcelRows(file);
if (rows.length === 0) continue;
// Find units row (if present)
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
const units: Record<string, string> = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === 'Part Number') continue;
const unit = normalizeValue(String(v ?? ''));
if (unit) units[k] = unit;
}
}
// Index rows by Part Number
for (const r of rows) {
const pn = r?.['Part Number'];
if (!pn || pn === 'Units') continue;
const key = normalizeExcelKey(String(pn));
if (!key) continue;
const cur = idx.get(key);
if (!cur) {
idx.set(key, { rows: [r], units });
} else {
cur.rows.push(r);
// Keep the most comprehensive units
if (Object.keys(cur.units).length < Object.keys(units).length) {
cur.units = units;
}
}
}
}
EXCEL_INDEX = idx;
return idx;
}
/**
* Find Excel match for a product using various identifiers
*/
function findExcelForProduct(params: ProductLookupParams): ExcelMatch | null {
const idx = getExcelIndex();
const candidates = [
params.name,
params.slug ? params.slug.replace(/-\d+$/g, '') : '',
params.sku,
params.translationKey,
].filter(Boolean) as string[];
for (const c of candidates) {
const key = normalizeExcelKey(c);
const match = idx.get(key);
if (match && match.rows.length) return match;
}
return null;
}
/**
* Guess column key based on patterns
*/
function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
const keys = Object.keys(row || {});
for (const re of patterns) {
const k = keys.find(x => {
const key = String(x);
// Specific exclusions to prevent wrong matches
if (re.test('conductor') && /ross section conductor/i.test(key)) return false;
if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false;
if (re.test('conductor') && !/^conductor$/i.test(key)) return false;
if (re.test('insulation') && !/^insulation$/i.test(key)) return false;
if (re.test('sheath') && !/^sheath$/i.test(key)) return false;
if (re.test('norm') && !/^norm$/i.test(key)) return false;
return re.test(key);
});
if (k) return k;
}
return null;
}
/**
* Get unique non-empty values from an array
*/
function getUniqueNonEmpty(options: string[]): string[] {
const uniq: string[] = [];
const seen = new Set<string>();
for (const v of options.map(normalizeValue).filter(Boolean)) {
const k = v.toLowerCase();
if (seen.has(k)) continue;
seen.add(k);
uniq.push(v);
}
return uniq;
}
/**
* Get technical data for a product from Excel files
*/
export function getExcelTechnicalDataForProduct(params: ProductLookupParams): TechnicalData | null {
const match = findExcelForProduct(params);
if (!match || match.rows.length === 0) return null;
const rows = match.rows;
const sample = rows[0];
// Find cross-section column
const csKey = guessColumnKey(sample, [
/number of cores and cross-section/i,
/cross.?section/i,
/ross section conductor/i,
]);
if (!csKey) return null;
// Extract configurations
const voltageKey = guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
const configurations = rows
.map(r => {
const cs = normalizeValue(String(r?.[csKey] ?? ''));
const v = voltageKey ? normalizeValue(String(r?.[voltageKey] ?? '')) : '';
if (!cs) return '';
if (!v) return cs;
const vHasUnit = /\bkv\b/i.test(v);
const vText = vHasUnit ? v : `${v} kV`;
return `${cs} - ${vText}`;
})
.filter(Boolean);
if (configurations.length === 0) return null;
// Extract technical attributes
const attributes: Array<{ name: string; options: string[] }> = [];
// Key technical columns
const outerKey = guessColumnKey(sample, [/outer diameter\b/i, /outer diameter.*approx/i, /outer diameter of cable/i, /außen/i]);
const weightKey = guessColumnKey(sample, [/weight\b/i, /gewicht/i, /cable weight/i]);
const dcResKey = guessColumnKey(sample, [/dc resistance/i, /resistance conductor/i, /leiterwiderstand/i]);
const ratedVoltKey = voltageKey;
const testVoltKey = guessColumnKey(sample, [/test voltage/i, /prüfspannung/i]);
const tempRangeKey = guessColumnKey(sample, [/operating temperature range/i, /temperature range/i, /temperaturbereich/i]);
const conductorKey = guessColumnKey(sample, [/^conductor$/i]);
const insulationKey = guessColumnKey(sample, [/^insulation$/i]);
const sheathKey = guessColumnKey(sample, [/^sheath$/i]);
const normKey = guessColumnKey(sample, [/^norm$/i, /^standard$/i]);
const cprKey = guessColumnKey(sample, [/cpr class/i]);
const packagingKey = guessColumnKey(sample, [/^packaging$/i]);
const shapeKey = guessColumnKey(sample, [/shape of conductor/i]);
const flameKey = guessColumnKey(sample, [/flame retardant/i]);
const diamCondKey = guessColumnKey(sample, [/diameter conductor/i]);
const diamInsKey = guessColumnKey(sample, [/diameter over insulation/i]);
const diamScreenKey = guessColumnKey(sample, [/diameter over screen/i]);
const metalScreenKey = guessColumnKey(sample, [/metallic screen/i]);
const capacitanceKey = guessColumnKey(sample, [/capacitance/i]);
const reactanceKey = guessColumnKey(sample, [/reactance/i]);
const electricalStressKey = guessColumnKey(sample, [/electrical stress/i]);
const pullingForceKey = guessColumnKey(sample, [/max\. pulling force/i, /pulling force/i]);
const heatingTrefoilKey = guessColumnKey(sample, [/heating time constant.*trefoil/i]);
const heatingFlatKey = guessColumnKey(sample, [/heating time constant.*flat/i]);
const currentAirTrefoilKey = guessColumnKey(sample, [/current ratings in air.*trefoil/i]);
const currentAirFlatKey = guessColumnKey(sample, [/current ratings in air.*flat/i]);
const currentGroundTrefoilKey = guessColumnKey(sample, [/current ratings in ground.*trefoil/i]);
const currentGroundFlatKey = guessColumnKey(sample, [/current ratings in ground.*flat/i]);
const scCurrentCondKey = guessColumnKey(sample, [/conductor shortcircuit current/i]);
const scCurrentScreenKey = guessColumnKey(sample, [/screen shortcircuit current/i]);
const minLayKey = guessColumnKey(sample, [/minimal temperature for laying/i]);
const minStoreKey = guessColumnKey(sample, [/minimal storage temperature/i]);
const maxOpKey = guessColumnKey(sample, [/maximal operating conductor temperature/i, /max\. operating/i]);
const maxScKey = guessColumnKey(sample, [/maximal short-circuit temperature/i, /short\s*circuit\s*temperature/i]);
const insThkKey = guessColumnKey(sample, [/nominal insulation thickness/i, /insulation thickness/i]);
const sheathThkKey = guessColumnKey(sample, [/nominal sheath thickness/i, /minimum sheath thickness/i]);
const maxResKey = guessColumnKey(sample, [/maximum resistance of conductor/i]);
const bendKey = guessColumnKey(sample, [/bending radius/i, /min\. bending radius/i]);
// Helper to add attribute
const addAttr = (name: string, key: string | null, unit?: string) => {
if (!key) return;
const options = rows
.map(r => normalizeValue(String(r?.[key] ?? '')))
.map(v => (unit && v && looksNumeric(v) ? `${v} ${unit}` : v))
.filter(Boolean);
if (options.length === 0) return;
const uniqueOptions = getUniqueNonEmpty(options);
attributes.push({ name, options: uniqueOptions });
};
// Add attributes
addAttr('Outer diameter', outerKey, 'mm');
addAttr('Weight', weightKey, 'kg/km');
addAttr('DC resistance at 20 °C', dcResKey, 'Ω/km');
addAttr('Rated voltage', ratedVoltKey, '');
addAttr('Test voltage', testVoltKey, '');
addAttr('Operating temperature range', tempRangeKey, '');
addAttr('Minimal temperature for laying', minLayKey, '');
addAttr('Minimal storage temperature', minStoreKey, '');
addAttr('Maximal operating conductor temperature', maxOpKey, '');
addAttr('Maximal short-circuit temperature', maxScKey, '');
addAttr('Nominal insulation thickness', insThkKey, 'mm');
addAttr('Nominal sheath thickness', sheathThkKey, 'mm');
addAttr('Maximum resistance of conductor', maxResKey, 'Ω/km');
addAttr('Conductor', conductorKey, '');
addAttr('Insulation', insulationKey, '');
addAttr('Sheath', sheathKey, '');
addAttr('Standard', normKey, '');
addAttr('Conductor diameter', diamCondKey, 'mm');
addAttr('Insulation diameter', diamInsKey, 'mm');
addAttr('Screen diameter', diamScreenKey, 'mm');
addAttr('Metallic screen', metalScreenKey, '');
addAttr('Max. pulling force', pullingForceKey, '');
addAttr('Electrical stress conductor', electricalStressKey, '');
addAttr('Electrical stress insulation', electricalStressKey, '');
addAttr('Reactance', reactanceKey, '');
addAttr('Heating time constant trefoil', heatingTrefoilKey, 's');
addAttr('Heating time constant flat', heatingFlatKey, 's');
addAttr('Flame retardant', flameKey, '');
addAttr('CPR class', cprKey, '');
addAttr('Packaging', packagingKey, '');
addAttr('Bending radius', bendKey, 'mm');
addAttr('Shape of conductor', shapeKey, '');
addAttr('Capacitance', capacitanceKey, 'μF/km');
addAttr('Current ratings in air, trefoil', currentAirTrefoilKey, 'A');
addAttr('Current ratings in air, flat', currentAirFlatKey, 'A');
addAttr('Current ratings in ground, trefoil', currentGroundTrefoilKey, 'A');
addAttr('Current ratings in ground, flat', currentGroundFlatKey, 'A');
addAttr('Conductor shortcircuit current', scCurrentCondKey, 'kA');
addAttr('Screen shortcircuit current', scCurrentScreenKey, 'kA');
return {
configurations,
attributes,
};
}
/**
* Get raw Excel rows for a product (for detailed inspection)
*/
export function getExcelRowsForProduct(params: ProductLookupParams): ExcelRow[] {
const match = findExcelForProduct(params);
return match?.rows || [];
}
/**
* Clear the Excel index cache (useful for development)
*/
export function clearExcelCache(): void {
EXCEL_INDEX = null;
}
/**
* Preload Excel data on module initialization
* This ensures the cache is built during build time
*/
export function preloadExcelData(): void {
getExcelIndex();
}
// Preload when imported
if (require.main === module) {
preloadExcelData();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,329 +0,0 @@
import * as React from 'react';
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
// Register fonts (using system fonts for now, can be customized)
Font.register({
family: 'Helvetica',
fonts: [
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
],
});
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
const C = {
navy: '#001a4d',
navyDeep: '#000d26',
green: '#4da612',
greenLight: '#e8f5d8',
white: '#FFFFFF',
offWhite: '#f8f9fa',
gray100: '#f3f4f6',
gray200: '#e5e7eb',
gray300: '#d1d5db',
gray400: '#9ca3af',
gray600: '#4b5563',
gray900: '#111827',
};
const MARGIN = 56;
const styles = StyleSheet.create({
page: {
color: C.gray900,
lineHeight: 1.5,
backgroundColor: C.white,
paddingTop: 0,
paddingBottom: 80,
fontFamily: 'Helvetica',
},
// Hero-style header
hero: {
backgroundColor: C.white,
paddingTop: 24,
paddingBottom: 0,
paddingHorizontal: MARGIN,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
logoText: {
fontSize: 22,
fontWeight: 700,
color: C.navyDeep,
letterSpacing: 2,
textTransform: 'uppercase',
},
docTitle: {
fontSize: 8,
fontWeight: 700,
color: C.green,
letterSpacing: 2,
textTransform: 'uppercase',
},
// Content Area
content: {
paddingHorizontal: MARGIN,
},
pageTitle: {
fontSize: 24,
fontWeight: 700,
color: C.navyDeep,
marginBottom: 8,
marginTop: 10,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
accentBar: {
width: 30,
height: 2,
backgroundColor: C.green,
marginBottom: 20,
borderRadius: 1,
},
// Lexical Elements
paragraph: {
fontSize: 10,
color: C.gray600,
lineHeight: 1.7,
marginBottom: 12,
},
heading1: {
fontSize: 16,
fontWeight: 700,
color: C.navyDeep,
marginTop: 20,
marginBottom: 10,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
heading2: {
fontSize: 12,
fontWeight: 700,
color: C.navyDeep,
marginTop: 16,
marginBottom: 8,
},
heading3: {
fontSize: 10,
fontWeight: 700,
color: C.navyDeep,
marginTop: 12,
marginBottom: 6,
},
list: {
marginBottom: 12,
marginLeft: 8,
},
listItem: {
flexDirection: 'row',
marginBottom: 4,
},
listItemBullet: {
width: 12,
fontSize: 10,
color: C.green,
fontWeight: 700,
},
listItemContent: {
flex: 1,
fontSize: 10,
color: C.gray600,
lineHeight: 1.7,
},
link: {
color: C.green,
textDecoration: 'none',
},
textBold: {
fontWeight: 700,
fontFamily: 'Helvetica-Bold',
color: C.navyDeep,
},
textItalic: {
fontStyle: 'italic',
},
// Footer — matches brochure style
footer: {
position: 'absolute',
bottom: 28,
left: MARGIN,
right: MARGIN,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 12,
borderTopWidth: 2,
borderTopColor: C.green,
},
footerText: {
fontSize: 7,
color: C.gray400,
fontWeight: 400,
textTransform: 'uppercase',
letterSpacing: 0.8,
},
footerBrand: {
fontSize: 9,
fontWeight: 700,
color: C.navyDeep,
textTransform: 'uppercase',
letterSpacing: 1.5,
},
});
// ─── Lexical to React-PDF Renderer ────────────────────────────────
const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
if (!node) return null;
switch (node.type) {
case 'text': {
const format = node.format || 0;
const isBold = (format & 1) !== 0;
const isItalic = (format & 2) !== 0;
let elementStyle: any = {};
if (isBold) elementStyle = { ...elementStyle, ...styles.textBold };
if (isItalic) elementStyle = { ...elementStyle, ...styles.textItalic };
return (
<Text key={idx} style={elementStyle}>
{node.text}
</Text>
);
}
case 'paragraph': {
return (
<Text key={idx} style={styles.paragraph}>
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
</Text>
);
}
case 'heading': {
let hStyle = styles.heading3;
if (node.tag === 'h1') hStyle = styles.heading1;
if (node.tag === 'h2') hStyle = styles.heading2;
return (
<Text key={idx} style={hStyle}>
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
</Text>
);
}
case 'list': {
return (
<View key={idx} style={styles.list}>
{node.children?.map((child: any, i: number) => {
if (child.type === 'listitem') {
return (
<View key={i} style={styles.listItem}>
<Text style={styles.listItemBullet}>
{node.listType === 'number' ? `${i + 1}.` : '•'}
</Text>
<Text style={styles.listItemContent}>
{child.children?.map((c: any, ci: number) => renderLexicalNode(c, ci))}
</Text>
</View>
);
}
return renderLexicalNode(child, i);
})}
</View>
);
}
case 'link': {
const href = node.fields?.url || node.url || '#';
return (
<Link key={idx} src={href} style={styles.link}>
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
</Link>
);
}
case 'linebreak': {
return <Text key={idx}>{'\n'}</Text>;
}
// Ignore payload blocks recursively to avoid crashing
case 'block':
return null;
default:
if (node.children) {
return (
<Text key={idx}>
{node.children.map((child: any, i: number) => renderLexicalNode(child, i))}
</Text>
);
}
return null;
}
};
interface PDFPageProps {
page: any;
locale?: string;
}
export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Hero Header */}
<View style={styles.hero} fixed>
<View style={styles.header}>
<View>
<Text style={styles.logoText}>KLZ</Text>
</View>
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
</View>
</View>
<View style={styles.content}>
<Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
<View>
{page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),
)}
</View>
</View>
{/* Minimal footer */}
<View style={styles.footer} fixed>
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>{dateStr}</Text>
</View>
</Page>
</Document>
);
};

View File

@@ -66,10 +66,6 @@
"role": "Geschäftsführer",
"description": "Danny Joseph leitet die E-TIB GmbH und koordiniert die strategische Ausrichtung der gesamten Unternehmensgruppe vom Hauptsitz in Guben aus."
},
"gallery": {
"title": "Einblicke in unsere Arbeit",
"subtitle": "Eindrücke aus dem Arbeitsalltag und Projekten"
},
"groups": {
"title": "Unsere Unternehmensgruppe",
"subtitle": "Spezialisierte Einheiten für komplexe Projekte",

View File

@@ -63,12 +63,8 @@
},
"danny": {
"name": "Danny Joseph",
"role": "Managing Director",
"description": "Danny Joseph leads E-TIB GmbH and coordinates the strategic direction of the entire corporate group from the headquarters in Guben."
},
"gallery": {
"title": "Insights into our work",
"subtitle": "Impressions from daily work and projects"
"role": "CEO",
"description": "Danny Joseph leads E-TIB GmbH and coordinates the strategic direction of the entire group from its headquarters in Guben."
},
"groups": {
"title": "Our Corporate Group",

View File

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

View File

@@ -1,6 +1,5 @@
{
"name": "e-tib-nextjs",
"version": "2.4.19",
"type": "module",
"private": true,
"packageManager": "pnpm@10.18.3",
@@ -12,7 +11,6 @@
"@mintel/next-utils": "1.9.5",
"@next/mdx": "^16.1.6",
"@react-email/components": "1.0.8",
"@react-pdf/renderer": "^4.5.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@sentry/nextjs": "^10.39.0",
@@ -74,6 +72,7 @@
"@types/sharp": "^0.31.1",
"@types/three": "^0.183.1",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react-swc": "^4.3.1",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.23",
"cheerio": "^1.2.0",
@@ -141,6 +140,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.4.28",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

300
pnpm-lock.yaml generated
View File

@@ -34,9 +34,6 @@ importers:
'@react-email/components':
specifier: 1.0.8
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-pdf/renderer':
specifier: ^4.5.1
version: 4.5.1(react@19.2.4)
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
@@ -215,6 +212,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^5.1.4
version: 5.2.0(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitejs/plugin-react-swc':
specifier: ^4.3.1
version: 4.3.1(@swc/helpers@0.5.20)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/ui':
specifier: ^4.0.16
version: 4.1.2(vitest@4.1.2)
@@ -2213,49 +2213,6 @@ packages:
react: ^18.0.0
react-dom: ^18.0.0
'@react-pdf/fns@3.1.3':
resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==}
'@react-pdf/font@4.0.8':
resolution: {integrity: sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==}
'@react-pdf/image@3.1.0':
resolution: {integrity: sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==}
'@react-pdf/layout@4.6.1':
resolution: {integrity: sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==}
'@react-pdf/pdfkit@5.1.1':
resolution: {integrity: sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==}
'@react-pdf/primitives@4.3.0':
resolution: {integrity: sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==}
'@react-pdf/reconciler@2.0.0':
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-pdf/render@4.5.1':
resolution: {integrity: sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==}
'@react-pdf/renderer@4.5.1':
resolution: {integrity: sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-pdf/stylesheet@6.2.1':
resolution: {integrity: sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==}
'@react-pdf/svg@1.1.0':
resolution: {integrity: sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==}
'@react-pdf/textkit@6.3.0':
resolution: {integrity: sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==}
'@react-pdf/types@2.11.1':
resolution: {integrity: sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==}
'@react-three/drei@10.7.7':
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
peerDependencies:
@@ -2398,6 +2355,9 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.3':
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@rollup/plugin-commonjs@28.0.1':
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
@@ -3332,6 +3292,12 @@ packages:
peerDependencies:
react: '>= 16.8.0'
'@vitejs/plugin-react-swc@4.3.1':
resolution: {integrity: sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4 || ^5 || ^6 || ^7 || ^8
'@vitejs/plugin-react@5.2.0':
resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3430,9 +3396,6 @@ packages:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
abs-svg-path@0.1.1:
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -3768,9 +3731,6 @@ packages:
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
browserify-zlib@0.2.0:
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
browserslist@4.28.2:
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -3965,14 +3925,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-name@2.1.0:
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
engines: {node: '>=12.20'}
color-string@2.1.4:
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
engines: {node: '>=18'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -4424,9 +4376,6 @@ packages:
electron-to-chromium@1.5.330:
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -5150,12 +5099,6 @@ packages:
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
engines: {node: '>= 6.0.0'}
hsl-to-hex@1.0.0:
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
hsl-to-rgb-for-reals@1.1.1:
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -5237,9 +5180,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
hyphen@1.14.1:
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
i18next-fs-backend@2.6.3:
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
@@ -5531,9 +5471,6 @@ packages:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@@ -5575,9 +5512,6 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jay-peg@1.1.1:
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
@@ -6011,9 +5945,6 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
media-engine@1.0.3:
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -6354,9 +6285,6 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
normalize-svg-path@1.1.0:
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -6503,9 +6431,6 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -6524,9 +6449,6 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-svg-path@0.1.2:
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
@@ -6650,9 +6572,6 @@ packages:
png-js@1.0.0:
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
png-js@2.0.0:
resolution: {integrity: sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==}
po-parser@2.1.1:
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
@@ -6797,9 +6716,6 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -7093,9 +7009,6 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.25.0-rc-603e6108-20241029:
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -7369,9 +7282,6 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@@ -7457,9 +7367,6 @@ packages:
peerDependencies:
react: '>=17.0'
svg-arc-to-cubic-bezier@3.2.0:
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
svg-to-pdfkit@0.1.8:
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
@@ -7843,10 +7750,6 @@ packages:
victory-vendor@37.3.6:
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
vite-compatible-readable-stream@3.6.1:
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
engines: {node: '>= 6'}
vite@8.0.3:
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -8185,9 +8088,6 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
yoga-layout@3.2.1:
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'}
@@ -10110,110 +10010,6 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@react-pdf/fns@3.1.3': {}
'@react-pdf/font@4.0.8':
dependencies:
'@react-pdf/pdfkit': 5.1.1
'@react-pdf/types': 2.11.1
fontkit: 2.0.4
is-url: 1.2.4
'@react-pdf/image@3.1.0':
dependencies:
'@react-pdf/svg': 1.1.0
jay-peg: 1.1.1
png-js: 2.0.0
'@react-pdf/layout@4.6.1':
dependencies:
'@react-pdf/fns': 3.1.3
'@react-pdf/image': 3.1.0
'@react-pdf/primitives': 4.3.0
'@react-pdf/stylesheet': 6.2.1
'@react-pdf/textkit': 6.3.0
'@react-pdf/types': 2.11.1
emoji-regex-xs: 1.0.0
queue: 6.0.2
yoga-layout: 3.2.1
'@react-pdf/pdfkit@5.1.1':
dependencies:
'@babel/runtime': 7.29.2
'@noble/ciphers': 1.3.0
'@noble/hashes': 1.8.0
browserify-zlib: 0.2.0
fontkit: 2.0.4
jay-peg: 1.1.1
js-md5: 0.8.3
linebreak: 1.1.0
png-js: 2.0.0
vite-compatible-readable-stream: 3.6.1
'@react-pdf/primitives@4.3.0': {}
'@react-pdf/reconciler@2.0.0(react@19.2.4)':
dependencies:
object-assign: 4.1.1
react: 19.2.4
scheduler: 0.25.0-rc-603e6108-20241029
'@react-pdf/render@4.5.1':
dependencies:
'@babel/runtime': 7.29.2
'@react-pdf/fns': 3.1.3
'@react-pdf/primitives': 4.3.0
'@react-pdf/textkit': 6.3.0
'@react-pdf/types': 2.11.1
abs-svg-path: 0.1.1
color-string: 2.1.4
normalize-svg-path: 1.1.0
parse-svg-path: 0.1.2
svg-arc-to-cubic-bezier: 3.2.0
'@react-pdf/renderer@4.5.1(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@react-pdf/fns': 3.1.3
'@react-pdf/font': 4.0.8
'@react-pdf/layout': 4.6.1
'@react-pdf/pdfkit': 5.1.1
'@react-pdf/primitives': 4.3.0
'@react-pdf/reconciler': 2.0.0(react@19.2.4)
'@react-pdf/render': 4.5.1
'@react-pdf/types': 2.11.1
events: 3.3.0
object-assign: 4.1.1
prop-types: 15.8.1
queue: 6.0.2
react: 19.2.4
'@react-pdf/stylesheet@6.2.1':
dependencies:
'@react-pdf/fns': 3.1.3
'@react-pdf/types': 2.11.1
color-string: 2.1.4
hsl-to-hex: 1.0.0
media-engine: 1.0.3
postcss-value-parser: 4.2.0
'@react-pdf/svg@1.1.0':
dependencies:
'@react-pdf/primitives': 4.3.0
'@react-pdf/textkit@6.3.0':
dependencies:
'@react-pdf/fns': 3.1.3
bidi-js: 1.0.3
hyphen: 1.14.1
unicode-properties: 1.4.1
'@react-pdf/types@2.11.1':
dependencies:
'@react-pdf/font': 4.0.8
'@react-pdf/primitives': 4.3.0
'@react-pdf/stylesheet': 6.2.1
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
dependencies:
'@babel/runtime': 7.29.2
@@ -10333,6 +10129,8 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.3': {}
'@rolldown/pluginutils@1.0.1': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.60.1)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
@@ -11244,6 +11042,14 @@ snapshots:
'@use-gesture/core': 10.3.1
react: 19.2.4
'@vitejs/plugin-react-swc@4.3.1(@swc/helpers@0.5.20)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
'@swc/core': 1.15.21(@swc/helpers@0.5.20)
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- '@swc/helpers'
'@vitejs/plugin-react@5.2.0(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@babel/core': 7.29.0
@@ -11392,8 +11198,6 @@ snapshots:
abbrev@2.0.0: {}
abs-svg-path@0.1.1: {}
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -11714,10 +11518,6 @@ snapshots:
dependencies:
base64-js: 1.5.1
browserify-zlib@0.2.0:
dependencies:
pako: 1.0.11
browserslist@4.28.2:
dependencies:
baseline-browser-mapping: 2.10.13
@@ -11932,12 +11732,6 @@ snapshots:
color-name@1.1.4: {}
color-name@2.1.0: {}
color-string@2.1.4:
dependencies:
color-name: 2.1.0
colorette@2.0.20: {}
combined-stream@1.0.8:
@@ -12415,8 +12209,6 @@ snapshots:
electron-to-chromium@1.5.330: {}
emoji-regex-xs@1.0.0: {}
emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {}
@@ -13469,12 +13261,6 @@ snapshots:
hoopy@0.1.4: {}
hsl-to-hex@1.0.0:
dependencies:
hsl-to-rgb-for-reals: 1.1.1
hsl-to-rgb-for-reals@1.1.1: {}
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
dependencies:
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
@@ -13573,8 +13359,6 @@ snapshots:
husky@9.1.7: {}
hyphen@1.14.1: {}
i18next-fs-backend@2.6.3: {}
i18next@25.10.10(typescript@5.9.3):
@@ -13863,8 +13647,6 @@ snapshots:
is-unicode-supported@2.1.0: {}
is-url@1.2.4: {}
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@@ -13915,10 +13697,6 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jay-peg@1.1.1:
dependencies:
restructure: 3.0.2
jest-worker@27.5.1:
dependencies:
'@types/node': 22.19.15
@@ -14467,8 +14245,6 @@ snapshots:
mdn-data@2.27.1: {}
media-engine@1.0.3: {}
media-typer@0.3.0: {}
meow@13.2.0: {}
@@ -14929,10 +14705,6 @@ snapshots:
normalize-path@3.0.0: {}
normalize-svg-path@1.1.0:
dependencies:
svg-arc-to-cubic-bezier: 3.2.0
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1
@@ -15143,8 +14915,6 @@ snapshots:
pako@0.2.9: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -15172,8 +14942,6 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-svg-path@0.1.2: {}
parse5-htmlparser2-tree-adapter@7.1.0:
dependencies:
domhandler: 5.0.3
@@ -15316,10 +15084,6 @@ snapshots:
png-js@1.0.0: {}
png-js@2.0.0:
dependencies:
fflate: 0.8.2
po-parser@2.1.1: {}
possible-typed-array-names@1.1.0: {}
@@ -15477,10 +15241,6 @@ snapshots:
queue-microtask@1.2.3: {}
queue@6.0.2:
dependencies:
inherits: 2.0.4
quick-format-unescaped@4.0.4: {}
range-parser@1.2.1: {}
@@ -15899,8 +15659,6 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.25.0-rc-603e6108-20241029: {}
scheduler@0.27.0: {}
schema-dts@1.1.5: {}
@@ -16289,10 +16047,6 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
@@ -16363,8 +16117,6 @@ snapshots:
dependencies:
react: 19.2.4
svg-arc-to-cubic-bezier@3.2.0: {}
svg-to-pdfkit@0.1.8:
dependencies:
pdfkit: 0.18.0
@@ -16833,12 +16585,6 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-compatible-readable-stream@3.6.1:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
lightningcss: 1.32.0
@@ -17170,8 +16916,6 @@ snapshots:
yoctocolors@2.1.2: {}
yoga-layout@3.2.1: {}
zod-validation-error@4.0.2(zod@3.25.76):
dependencies:
zod: 3.25.76

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Some files were not shown because too many files have changed in this diff Show More