diff --git a/app/[locale]/team/opengraph-image.tsx b/app/[locale]/team/opengraph-image.tsx new file mode 100644 index 000000000..a08a23884 --- /dev/null +++ b/app/[locale]/team/opengraph-image.tsx @@ -0,0 +1,25 @@ +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( + , + { + ...OG_IMAGE_SIZE, + fonts, + }, + ); +} diff --git a/app/[locale]/team/page.tsx b/app/[locale]/team/page.tsx new file mode 100644 index 000000000..53d7d2eca --- /dev/null +++ b/app/[locale]/team/page.tsx @@ -0,0 +1,326 @@ +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 { + 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 ( +
+ + + + {/* Hero Section */} + +
+
+ E-TIB Team +
+
+ + + + {t('hero.badge')} + + + {t('hero.subtitle')} + +

+ {t('hero.title')} +

+
+
+
+ + {/* Michael Bodemer Section - Sticky Narrative Split Layout */} +
+
+ +
+
+ + {t('michael.role')} + + + {t('michael.name')} + +
+
+

+ {t('michael.quote')} +

+
+

+ {t('michael.description')} +

+ + {t('michael.linkedin')} + + +
+ + + {t('michael.name')} +
+ +
+
+ + {/* Legacy Section - Immersive Background */} + +
+
+ {t('legacy.subtitle')} +
+
+ +
+
+ + {t('legacy.title')} + +
+

+ {t('legacy.p1')} +

+

{t('legacy.p2')}

+
+
+
+
+
+ {t('legacy.expertise')} +
+
+ {t('legacy.expertiseDesc')} +
+
+
+
+ {t('legacy.network')} +
+
+ {t('legacy.networkDesc')} +
+
+
+
+
+
+
+ + {/* Klaus Mintel Section - Reversed Split Layout */} +
+
+ + {t('klaus.name')} +
+ + +
+
+ + {t('klaus.role')} + + + {t('klaus.name')} + +
+
+

+ {t('klaus.quote')} +

+
+

+ {t('klaus.description')} +

+ + {t('klaus.linkedin')} + + +
+ +
+
+ + {/* Manifesto Section - Modern Grid */} +
+ +
+
+
+ + {t('manifesto.title')} + +

+ {t('manifesto.tagline')} +

+ + {/* Mobile-only progress indicator */} +
+ {[0, 1, 2, 3, 4, 5].map((i) => ( +
+
+
+ ))} +
+
+
+
    + {[0, 1, 2, 3, 4, 5].map((idx) => ( +
  • +
    + + 0{idx + 1} + +
    +

    + {t(`manifesto.items.${idx}.title`)} +

    +

    + {t(`manifesto.items.${idx}.description`)} +

    +
  • + ))} +
+
+ +
+ + + + +
+ ); +} diff --git a/app/actions/brochure.ts b/app/actions/brochure.ts new file mode 100644 index 000000000..2d340abea --- /dev/null +++ b/app/actions/brochure.ts @@ -0,0 +1,102 @@ +'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 }; +} diff --git a/app/test/page.tsx b/app/test/page.tsx new file mode 100644 index 000000000..a9e56b17a --- /dev/null +++ b/app/test/page.tsx @@ -0,0 +1,7 @@ +export default function TestPage() { + return ( +
+

TEST PAGE WORKS

+
+ ); +} diff --git a/components/blocks/CallToAction.tsx b/components/blocks/CallToAction.tsx new file mode 100644 index 000000000..87eac0ae4 --- /dev/null +++ b/components/blocks/CallToAction.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Button } from '@/components/ui/Button'; + +export interface CallToActionProps { + title?: string; + description?: string; + ctaLabel?: string; + ctaHref?: string; + theme?: 'light' | 'dark'; +} + +export const CallToAction: React.FC = ({ + 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 ( +
+
+

+ {title} +

+

+ {description} +

+ +
+
+ ); +}; diff --git a/components/blocks/DeferredVideo.tsx b/components/blocks/DeferredVideo.tsx new file mode 100644 index 000000000..537ebc8b7 --- /dev/null +++ b/components/blocks/DeferredVideo.tsx @@ -0,0 +1,53 @@ +'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 ( +