Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 4s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
259 lines
12 KiB
TypeScript
259 lines
12 KiB
TypeScript
import { notFound, redirect } from 'next/navigation';
|
|
import { Container, Badge, Heading } from '@/components/ui';
|
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
|
import { Metadata } from 'next';
|
|
import { getPageBySlug } from '@/lib/pages';
|
|
import { mapSlugToFileSlug, mapFileSlugToTranslated } from '@/lib/slugs';
|
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
import { SITE_URL } from '@/lib/schema';
|
|
import TrackedLink from '@/components/analytics/TrackedLink';
|
|
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
|
import { getAllReferences } from '@/lib/references';
|
|
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
|
// Import components used in MDX
|
|
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
|
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
|
|
import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid';
|
|
import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider';
|
|
import { CompanyTimeline } from '@/components/blocks/CompanyTimeline';
|
|
import { JobListingBlock } from '@/components/blocks/JobListingBlock';
|
|
import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid';
|
|
import { BenefitGrid } from '@/components/blocks/BenefitGrid';
|
|
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
|
import { FaqBlock } from '@/components/blocks/FaqBlock';
|
|
import { CertificatesBlock } from '@/components/blocks/CertificatesBlock';
|
|
import { HeroSection } from '@/components/blocks/HeroSection';
|
|
import { TeamGrid } from '@/components/blocks/TeamGrid';
|
|
import { DeepDrillAnimation } from '@/components/blocks/DeepDrillAnimation';
|
|
import { DataGridPulse } from '@/components/blocks/DataGridPulse';
|
|
import { GrowthChart } from '@/components/blocks/GrowthChart';
|
|
import JsonLd from '@/components/JsonLd';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { AnimatedCounter } from '@/components/ui';
|
|
|
|
|
|
const mdxComponents = {
|
|
HomeHero,
|
|
HomeSubCompanyTiles,
|
|
HomeCompetenceBentoGrid,
|
|
HomeReferencesSlider,
|
|
CompanyTimeline,
|
|
JobListingBlock,
|
|
ServiceDetailGrid,
|
|
BenefitGrid,
|
|
InteractiveGermanyMap,
|
|
FaqBlock,
|
|
CertificatesBlock,
|
|
HeroSection,
|
|
TeamGrid,
|
|
JsonLd,
|
|
Button,
|
|
Badge,
|
|
AnimatedCounter,
|
|
GrowthChart,
|
|
DeepDrillAnimation,
|
|
DataGridPulse,
|
|
// Standard HTML element mapping for consistent E-TIB typography
|
|
h1: (props: any) => <Heading level={1} size="2" className="hidden" {...props} />, // Hidden because Hero handles H1
|
|
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4" {...props} />,
|
|
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
|
|
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
|
|
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
|
|
ul: (props: any) => <ul className="list-none mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
|
|
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
|
|
li: (props: any) => (
|
|
<li className="relative pl-6 before:content-[''] before:absolute before:left-0 before:top-[0.6em] before:w-2 before:h-2 before:bg-primary/50 before:rounded-sm" {...props} />
|
|
),
|
|
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
|
|
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
|
|
blockquote: (props: any) => (
|
|
<blockquote className="border-l-4 border-primary pl-6 py-3 my-10 italic bg-neutral-50 rounded-r-2xl text-neutral-700 font-medium max-w-3xl shadow-sm" {...props} />
|
|
),
|
|
hr: (props: any) => <hr className="my-16 border-t-2 border-neutral-100 max-w-3xl" {...props} />,
|
|
img: (props: any) => <img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" {...props} />,
|
|
};
|
|
|
|
interface PageProps {
|
|
params: Promise<{
|
|
locale: string;
|
|
slug: string;
|
|
}>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
const { locale, slug } = await params;
|
|
|
|
// Guard against invalid locales
|
|
if (locale !== 'de' && locale !== 'en') return {};
|
|
|
|
const pageData = await getPageBySlug(slug, locale);
|
|
|
|
if (!pageData) return {};
|
|
|
|
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
|
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
|
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
|
|
|
// Determine correct localized slug based on current locale
|
|
const currentLocaleSlug = locale === 'de' ? deSlug : enSlug;
|
|
|
|
return {
|
|
title: pageData.frontmatter.title,
|
|
description: pageData.frontmatter.excerpt || '',
|
|
alternates: {
|
|
canonical: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
|
languages: {
|
|
de: `${SITE_URL}/de/${deSlug}`,
|
|
en: `${SITE_URL}/en/${enSlug}`,
|
|
'x-default': `${SITE_URL}/en/${enSlug}`,
|
|
},
|
|
},
|
|
openGraph: {
|
|
title: `${pageData.frontmatter.title} | E-TIB`,
|
|
description: pageData.frontmatter.excerpt || '',
|
|
url: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: `${pageData.frontmatter.title} | E-TIB`,
|
|
description: pageData.frontmatter.excerpt || '',
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function Page(props: { params: Promise<{ locale: string; slug: string }> }) {
|
|
const { locale, slug } = await props.params;
|
|
|
|
// Guard against invalid locales (e.g. favicon.ico) hitting the DB
|
|
if (locale !== 'de' && locale !== 'en') {
|
|
notFound();
|
|
}
|
|
|
|
setRequestLocale(locale);
|
|
const pageData = await getPageBySlug(slug, locale);
|
|
const t = await getTranslations('StandardPage');
|
|
|
|
if (!pageData) {
|
|
notFound();
|
|
}
|
|
|
|
// Redirect logic removed to simplify for now, as slugs match file names
|
|
/*
|
|
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
|
const correctSlug = await mapFileSlugToTranslated(fileSlug, locale);
|
|
if (correctSlug && correctSlug !== slug) {
|
|
redirect(`/${locale}/${correctSlug}`);
|
|
}
|
|
*/
|
|
|
|
// Determine layout type
|
|
const isFullBleed = pageData.frontmatter.layout === 'fullBleed';
|
|
|
|
// Fetch references to enrich the map if it's used
|
|
const references = await getAllReferences(locale);
|
|
const allLocations = [...defaultLocations, ...minorLocations];
|
|
const enrichedLocations = allLocations.map(loc => {
|
|
if (loc.type === 'project') {
|
|
const ref = references.find(r => r.slug === loc.id);
|
|
if (ref) {
|
|
return {
|
|
...loc,
|
|
featuredImage: ref.frontmatter.featuredImage,
|
|
details: [
|
|
ref.frontmatter.client || t('client'),
|
|
ref.frontmatter.dateString || new Date(ref.frontmatter.date).getFullYear().toString(),
|
|
],
|
|
};
|
|
}
|
|
}
|
|
return loc;
|
|
});
|
|
|
|
const pageMdxComponents = {
|
|
...mdxComponents,
|
|
InteractiveGermanyMap: (props: any) => <InteractiveGermanyMap locations={enrichedLocations} {...props} />,
|
|
HomeReferencesSlider: (props: any) => <HomeReferencesSlider {...props} references={references.map(r => ({
|
|
id: r.slug,
|
|
slug: r.slug,
|
|
title: r.frontmatter.title,
|
|
category: Array.isArray(r.frontmatter.category) ? r.frontmatter.category[0] : (r.frontmatter.category || 'Projekt'),
|
|
image: r.frontmatter.featuredImage
|
|
}))} />,
|
|
};
|
|
|
|
return (
|
|
<div className={`flex flex-col min-h-screen ${isFullBleed ? '' : 'bg-white'}`}>
|
|
{/* Generic Hero Section (only for standard pages) */}
|
|
{!isFullBleed && (
|
|
<section key={`hero-section-${slug}`} className="bg-primary-dark text-white pt-28 pb-10 md:pt-56 md:pb-16 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
|
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
|
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
|
|
</div>
|
|
<Container className="relative z-10">
|
|
<div className="max-w-4xl">
|
|
<Badge variant="accent" className="mb-4 md:mb-6 shadow-sm">
|
|
{t('badge')}
|
|
</Badge>
|
|
<Heading level={1} variant="white" className="mb-0 drop-shadow-sm">
|
|
{pageData.frontmatter.title}
|
|
</Heading>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
)}
|
|
|
|
{/* Main Content Area */}
|
|
{isFullBleed ? (
|
|
<div className="w-full">
|
|
<MDXRemote source={pageData.content} components={pageMdxComponents} />
|
|
</div>
|
|
) : (
|
|
<Container className="py-16 md:py-24">
|
|
{/* Excerpt/Lead paragraph if available */}
|
|
{pageData.frontmatter.excerpt && (
|
|
<div className="mb-16 max-w-4xl mx-auto">
|
|
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
|
|
{pageData.frontmatter.excerpt}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main content */}
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="prose prose-lg md:prose-xl prose-neutral max-w-none prose-headings:font-heading prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:border-b prose-h2:border-neutral-200 prose-h2:pb-4 prose-h3:text-2xl prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:font-medium prose-a:underline prose-a:underline-offset-4 prose-a:decoration-primary/30 hover:prose-a:decoration-primary prose-strong:text-text-primary prose-strong:font-semibold prose-li:text-text-secondary prose-ul:list-disc prose-ol:list-decimal">
|
|
<MDXRemote source={pageData.content} components={pageMdxComponents} />
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
)}
|
|
|
|
{/* Support Section */}
|
|
<Container>
|
|
<div className="mt-24 mb-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group animate-slight-fade-in-from-bottom">
|
|
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
|
|
<div className="relative z-10 max-w-2xl">
|
|
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
|
|
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
|
|
<TrackedLink
|
|
href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`}
|
|
className={getButtonClasses('accent', 'lg')}
|
|
eventProperties={{
|
|
location: 'generic_page_support_cta',
|
|
page_slug: slug,
|
|
}}
|
|
>
|
|
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
|
|
{t('contactUs')}
|
|
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
|
|
→
|
|
</span>
|
|
</span>
|
|
<ButtonOverlay variant="accent" />
|
|
</TrackedLink>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|