fix(i18n): fix reference page translation, fix map zoom, connect contact form to server action, fix english 404 links, fix certificate pdf links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
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 6s

This commit is contained in:
2026-05-28 09:58:34 +02:00
parent 2a46015d0d
commit 070f97dd6f
20 changed files with 218 additions and 80 deletions

View File

@@ -129,6 +129,7 @@ export default async function Layout(props: {
'ReferencesSlider', 'ReferencesSlider',
'CompanyTimeline', 'CompanyTimeline',
'TeamGrid', 'TeamGrid',
'AISearch',
]; ];
const clientMessages: Record<string, any> = {}; const clientMessages: Record<string, any> = {};
for (const key of clientKeys) { for (const key of clientKeys) {

View File

@@ -57,6 +57,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
setRequestLocale(locale); setRequestLocale(locale);
const reference = await getReferenceBySlug(slug, locale); const reference = await getReferenceBySlug(slug, locale);
const t = await getTranslations('ReferenceDetail');
if (!reference) { if (!reference) {
notFound(); notFound();
@@ -119,12 +120,12 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
eventProperties={{ location: 'reference_back_btn' }} eventProperties={{ location: 'reference_back_btn' }}
> >
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
Zurück zur Übersicht {t('backToOverview')}
</TrackedLink> </TrackedLink>
</div> </div>
<div className="max-w-4xl"> <div className="max-w-4xl">
<Badge variant="accent" className="mb-4 md:mb-6"> <Badge variant="accent" className="mb-4 md:mb-6">
Projektreferenz {t('projectReference')}
</Badge> </Badge>
<Heading level={1} variant="white" className="mb-8 leading-tight"> <Heading level={1} variant="white" className="mb-8 leading-tight">
{reference.frontmatter.title} {reference.frontmatter.title}
@@ -134,21 +135,21 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm"> <div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<MapPin className="w-8 h-8 text-primary shrink-0" /> <MapPin className="w-8 h-8 text-primary shrink-0" />
<div> <div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Ort</p> <p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('location')}</p>
<p className="font-semibold text-lg">{reference.frontmatter.location}</p> <p className="font-semibold text-lg">{reference.frontmatter.location}</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm"> <div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Briefcase className="w-8 h-8 text-primary shrink-0" /> <Briefcase className="w-8 h-8 text-primary shrink-0" />
<div> <div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Auftraggeber</p> <p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('client')}</p>
<p className="font-semibold text-lg">{reference.frontmatter.client}</p> <p className="font-semibold text-lg">{reference.frontmatter.client}</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm"> <div className="flex items-start gap-4 p-6 bg-white/5 rounded-2xl border border-white/10 backdrop-blur-sm">
<Calendar className="w-8 h-8 text-primary shrink-0" /> <Calendar className="w-8 h-8 text-primary shrink-0" />
<div> <div>
<p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">Zeitraum</p> <p className="text-sm text-white/50 mb-1 uppercase tracking-wider font-bold">{t('period')}</p>
<p className="font-semibold text-lg">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p> <p className="font-semibold text-lg">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
</div> </div>
</div> </div>
@@ -160,7 +161,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
{/* Main Content Area */} {/* Main Content Area */}
<Container className="py-16 md:py-24"> <Container className="py-16 md:py-24">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-neutral-dark mb-8">Leistungsumfang & Projektbeschreibung</h2> <h2 className="text-3xl font-bold text-neutral-dark mb-8">{t('scopeTitle')}</h2>
<div className="w-full"> <div className="w-full">
<MDXRemote source={reference.content} components={mdxComponents} /> <MDXRemote source={reference.content} components={mdxComponents} />
@@ -173,7 +174,7 @@ export default async function ReferenceDetail(props: { params: Promise<{ locale:
eventProperties={{ location: 'reference_bottom_back_btn' }} eventProperties={{ location: 'reference_bottom_back_btn' }}
> >
<span className="relative z-10 flex items-center justify-center gap-2"> <span className="relative z-10 flex items-center justify-center gap-2">
Alle Referenzen ansehen {t('viewAll')}
</span> </span>
<ButtonOverlay variant="primary" /> <ButtonOverlay variant="primary" />
</TrackedLink> </TrackedLink>

View File

@@ -36,7 +36,7 @@ export default function LeafletMap({ locations }: LeafletMapProps) {
const firstLoc = locations[0]; const firstLoc = locations[0];
const map = L.map(mapRef.current, { const map = L.map(mapRef.current, {
center: [firstLoc.lat, firstLoc.lng], center: [firstLoc.lat, firstLoc.lng],
zoom: locations.length === 1 ? 14 : 6, zoom: locations.length === 1 ? 16 : 6,
scrollWheelZoom: false, scrollWheelZoom: false,
}); });

View File

@@ -29,42 +29,42 @@ const defaultCertificates: Certificate[] = [
{ {
title: 'ISO 14001:2015', title: 'ISO 14001:2015',
description: 'Umweltmanagementsystem', description: 'Umweltmanagementsystem',
pdfUrl: '/assets/certificates/231214_Zertifikat ISO 14001 Umweltmanagement.pdf', pdfUrl: '/assets/certificates/iso14001.pdf',
type: 'iso', type: 'iso',
date: '14.12.2023', date: '14.12.2023',
}, },
{ {
title: 'ISO 9001:2015', title: 'ISO 9001:2015',
description: 'Qualitätsmanagementsystem', description: 'Qualitätsmanagementsystem',
pdfUrl: '/assets/certificates/231214_Zertifikat ISO 9001 Qualitätsmanagement.pdf', pdfUrl: '/assets/certificates/iso9001.pdf',
type: 'iso', type: 'iso',
date: '14.12.2023', date: '14.12.2023',
}, },
{ {
title: 'DIN EN ISO 45001:2018', title: 'DIN EN ISO 45001:2018',
description: 'Arbeits- und Gesundheitsschutz', description: 'Arbeits- und Gesundheitsschutz',
pdfUrl: '/assets/certificates/Zertifizierung DIN EN ISO 45001 bis 05122028.pdf', pdfUrl: '/assets/certificates/iso45001.pdf',
type: 'iso', type: 'iso',
date: '05.12.2025', // Assuming valid till date: '05.12.2025', // Assuming valid till
}, },
{ {
title: 'Freistellungsbescheinigung', title: 'Freistellungsbescheinigung',
description: 'Nach § 48 b EStG', description: 'Nach § 48 b EStG',
pdfUrl: '/assets/certificates/240209_Freistellungsbescheinigung § 48 b.pdf', pdfUrl: '/assets/certificates/freistellung.pdf',
type: 'tax', type: 'tax',
date: '09.02.2024', date: '09.02.2024',
}, },
{ {
title: 'Nachweis § 13b UStG', title: 'Nachweis § 13b UStG',
description: 'Steuerschuldnerschaft des Leistungsempfängers', description: 'Steuerschuldnerschaft des Leistungsempfängers',
pdfUrl: '/assets/certificates/240209_Nachweis § 13 b.pdf', pdfUrl: '/assets/certificates/nachweis13b.pdf',
type: 'tax', type: 'tax',
date: '09.02.2024', date: '09.02.2024',
}, },
{ {
title: 'Bescheinigung in Steuersachen', title: 'Bescheinigung in Steuersachen',
description: 'Zertifikat des Finanzamtes', description: 'Zertifikat des Finanzamtes',
pdfUrl: '/assets/certificates/250213_Bescheinigung in Steuersachen.pdf', pdfUrl: '/assets/certificates/bescheinigung.pdf',
type: 'tax', type: 'tax',
date: '13.02.2025', date: '13.02.2025',
}, },

View File

@@ -30,10 +30,7 @@ export function InteractiveGermanyMap({
badge, badge,
title, title,
description, description,
stats = [ stats,
{ value: '100', suffix: '%', label: 'Überregionale Reichweite' },
{ value: '2', suffix: '+', label: 'Operative Standorte' },
],
locations = allLocations, locations = allLocations,
isHero = false isHero = false
}: InteractiveGermanyMapProps) { }: InteractiveGermanyMapProps) {
@@ -43,6 +40,11 @@ export function InteractiveGermanyMap({
const t = useTranslations('InteractiveGermanyMap'); const t = useTranslations('InteractiveGermanyMap');
const tStandard = useTranslations('StandardPage'); const tStandard = useTranslations('StandardPage');
const finalStats = stats || [
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
{ value: '2', suffix: '+', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte' },
];
const finalBadge = badge || tStandard('badge'); const finalBadge = badge || tStandard('badge');
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided // the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>); const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
@@ -88,7 +90,7 @@ export function InteractiveGermanyMap({
{/* Industrial Stats Grid */} {/* Industrial Stats Grid */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{stats.map((stat, i) => ( {finalStats.map((stat, i) => (
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group"> <div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> <div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10"> <div className="relative z-10">

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { sendContactFormAction } from '@/app/actions/contact';
export function ContactForm() { export function ContactForm() {
const [status, setStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const [status, setStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>('idle');
@@ -11,10 +12,18 @@ export function ContactForm() {
e.preventDefault(); e.preventDefault();
setStatus('loading'); setStatus('loading');
// Simulate server action using @mintel/mail or API route const formData = new FormData(e.currentTarget);
setTimeout(() => { try {
setStatus('success'); const result = await sendContactFormAction(formData);
}, 1500); if (result.success) {
setStatus('success');
} else {
setStatus('error');
}
} catch (error) {
console.error(error);
setStatus('error');
}
}; };
return ( return (
@@ -41,13 +50,31 @@ export function ContactForm() {
<p>Vielen Dank für Ihr Interesse an der E-TIB Gruppe. Ein Ansprechpartner wird sich zeitnah mit Ihnen in Verbindung setzen.</p> <p>Vielen Dank für Ihr Interesse an der E-TIB Gruppe. Ein Ansprechpartner wird sich zeitnah mit Ihnen in Verbindung setzen.</p>
</div> </div>
</motion.div> </motion.div>
) : ( ) : status === 'error' ? (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-red-500/10 border border-red-500 text-red-600 p-6 rounded-xl flex items-start gap-4 mb-6"
>
<div className="bg-red-500 text-white rounded-full p-1 mt-0.5">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</div>
<div>
<h4 className="font-bold text-lg mb-1">Fehler beim Senden</h4>
<p>Leider ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal oder kontaktieren Sie uns direkt per E-Mail oder Telefon.</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => setStatus('idle')}>Erneut versuchen</Button>
</div>
</motion.div>
) : null}
{status !== 'success' && status !== 'error' && (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="firstName" className="block text-sm font-semibold text-neutral-dark">Vorname</label> <label htmlFor="name" className="block text-sm font-semibold text-neutral-dark">Vorname</label>
<input <input
id="firstName" id="name"
name="name"
required required
disabled={status === 'loading'} disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
@@ -57,7 +84,8 @@ export function ContactForm() {
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="lastName" className="block text-sm font-semibold text-neutral-dark">Nachname</label> <label htmlFor="lastName" className="block text-sm font-semibold text-neutral-dark">Nachname</label>
<input <input
id="lastName" id="lastName"
name="lastName"
required required
disabled={status === 'loading'} disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
@@ -70,6 +98,7 @@ export function ContactForm() {
<label htmlFor="email" className="block text-sm font-semibold text-neutral-dark">E-Mail Adresse</label> <label htmlFor="email" className="block text-sm font-semibold text-neutral-dark">E-Mail Adresse</label>
<input <input
id="email" id="email"
name="email"
type="email" type="email"
required required
disabled={status === 'loading'} disabled={status === 'loading'}
@@ -82,6 +111,7 @@ export function ContactForm() {
<label htmlFor="company" className="block text-sm font-semibold text-neutral-dark">Unternehmen (Optional)</label> <label htmlFor="company" className="block text-sm font-semibold text-neutral-dark">Unternehmen (Optional)</label>
<input <input
id="company" id="company"
name="company"
disabled={status === 'loading'} disabled={status === 'loading'}
className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" className="w-full bg-neutral-50 px-4 py-3 rounded-lg border border-neutral-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
placeholder="Firma GmbH" placeholder="Firma GmbH"
@@ -92,6 +122,7 @@ export function ContactForm() {
<label htmlFor="message" className="block text-sm font-semibold text-neutral-dark">Ihre Nachricht</label> <label htmlFor="message" className="block text-sm font-semibold text-neutral-dark">Ihre Nachricht</label>
<textarea <textarea
id="message" id="message"
name="message"
required required
disabled={status === 'loading'} disabled={status === 'loading'}
rows={5} rows={5}
@@ -100,6 +131,8 @@ export function ContactForm() {
/> />
</div> </div>
<input type="text" name="company_website" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<Button <Button
type="submit" type="submit"
disabled={status === 'loading'} disabled={status === 'loading'}

View File

@@ -20,23 +20,23 @@ export default function Hero({ data }: { data?: any }) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const [heroPlaceholder, setHeroPlaceholder] = useState( const [heroPlaceholder, setHeroPlaceholder] = useState(
'Projekt beschreiben oder Kabel suchen...', t('searchPlaceholder')
); );
const typingRef = useRef<ReturnType<typeof setTimeout> | null>(null); const typingRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const HERO_PLACEHOLDERS = [ const HERO_PLACEHOLDERS = [
'Querschnittsberechnung für 110kV Trasse', // Hochspannung t('placeholder1'),
'Wie schwer ist NAYY 4x150?', t('placeholder2'),
'Ich plane einen Solarpark, was brauche ich?', // Projekt Solar t('placeholder3'),
'Unterschied zwischen N2XSY und NAY2XSY?', // Fach t('placeholder4'),
'Mittelspannungskabel für Windkraftanlage', // Windpark t('placeholder5'),
'Welches Aluminiumkabel für 20kV?', // Mittelspannung t('placeholder6'),
]; ];
// Typing animation for the hero search placeholder // Typing animation for the hero search placeholder
useEffect(() => { useEffect(() => {
if (searchQuery) { if (searchQuery) {
setHeroPlaceholder('Projekt beschreiben oder Kabel suchen...'); setHeroPlaceholder(t('searchPlaceholder'));
return; return;
} }
@@ -152,7 +152,7 @@ export default function Hero({ data }: { data?: any }) {
size="lg" size="lg"
className="rounded-xl px-6 py-4 shrink-0 flex items-center shadow-md font-bold cursor-pointer hover:bg-accent hover:brightness-110" className="rounded-xl px-6 py-4 shrink-0 flex items-center shadow-md font-bold cursor-pointer hover:bg-accent hover:brightness-110"
> >
Fragen {t('ask')}
<ChevronRight className="w-5 h-5 ml-2 -mr-1" /> <ChevronRight className="w-5 h-5 ml-2 -mr-1" />
</Button> </Button>
</form> </form>

View File

@@ -8,10 +8,8 @@ import { AnalyticsEvents } from '../analytics/analytics-events';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslations } from 'next-intl';
const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false }); const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false });
const LOADING_TEXTS = [
'Durchsuche das Kabelhandbuch... 📖',
'Frage den Senior-Ingenieur... 👴🔧', 'Frage den Senior-Ingenieur... 👴🔧',
'Frage ChatGPTs Cousin 2. Grades... 🤖', 'Frage ChatGPTs Cousin 2. Grades... 🤖',
]; ];
@@ -51,7 +49,13 @@ export function AISearchResults({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null); const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false); const [copiedAll, setCopiedAll] = useState(false);
const [loadingText, setLoadingText] = useState(LOADING_TEXTS[0]); const t = useTranslations('AISearch');
// We load texts dynamically from translations
const loadingTexts = t.raw('loadingTexts') as string[];
const prompts = t.raw('prompts') as string[];
const [loadingText, setLoadingText] = useState(loadingTexts[0]);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
@@ -141,11 +145,11 @@ export function AISearchResults({
setTimeout(() => { setTimeout(() => {
setIsLoading(true); setIsLoading(true);
// Start rotating loading texts // Start rotating loading texts
let textIdx = Math.floor(Math.random() * LOADING_TEXTS.length); let textIdx = Math.floor(Math.random() * loadingTexts.length);
setLoadingText(LOADING_TEXTS[textIdx]); setLoadingText(loadingTexts[textIdx]);
loadingIntervalRef.current = setInterval(() => { loadingIntervalRef.current = setInterval(() => {
textIdx = (textIdx + 1) % LOADING_TEXTS.length; textIdx = (textIdx + 1) % loadingTexts.length;
setLoadingText(LOADING_TEXTS[textIdx]); setLoadingText(loadingTexts[textIdx]);
}, 2500); }, 2500);
}, 400); }, 400);
@@ -191,8 +195,8 @@ export function AISearchResults({
console.error(err); console.error(err);
const msg = const msg =
err.name === 'AbortError' err.name === 'AbortError'
? 'Anfrage hat zu lange gedauert. Bitte versuche es erneut.' ? t('timeoutError')
: err.message || 'Ein Fehler ist aufgetreten.'; : err.message || t('genericError');
// Show error as a system message in the chat instead of a separate error banner // Show error as a system message in the chat instead of a separate error banner
setMessages((prev) => [ setMessages((prev) => [
@@ -248,7 +252,7 @@ export function AISearchResults({
const handleCopyChat = () => { const handleCopyChat = () => {
const fullChat = messages const fullChat = messages
.map((m) => `${m.role === 'user' ? 'Du' : 'Ohm'}:\n${m.content}`) .map((m) => `${m.role === 'user' ? t('you') : 'Ohm'}:\n${m.content}`)
.join('\n\n'); .join('\n\n');
handleCopy(fullChat); handleCopy(fullChat);
}; };
@@ -294,7 +298,7 @@ export function AISearchResults({
<div> <div>
<h2 className="text-white font-bold text-sm tracking-wide">Ohm</h2> <h2 className="text-white font-bold text-sm tracking-wide">Ohm</h2>
<p className="text-[10px] text-white/30 font-medium tracking-wider uppercase"> <p className="text-[10px] text-white/30 font-medium tracking-wider uppercase">
{isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'} {isLoading ? t('thinking') : error ? t('errorStatus') : t('online')}
</p> </p>
</div> </div>
</div> </div>
@@ -303,14 +307,14 @@ export function AISearchResults({
<button <button
onClick={handleCopyChat} onClick={handleCopyChat}
className="flex items-center gap-1.5 text-[10px] font-bold text-white/40 hover:text-white/80 transition-all duration-200 hover:bg-white/5 rounded-full px-3 py-1.5 cursor-pointer uppercase tracking-wider" className="flex items-center gap-1.5 text-[10px] font-bold text-white/40 hover:text-white/80 transition-all duration-200 hover:bg-white/5 rounded-full px-3 py-1.5 cursor-pointer uppercase tracking-wider"
title="gesamten Chat kopieren" title={t('copyChatTitle')}
> >
{copiedAll ? ( {copiedAll ? (
<Check className="w-3.5 h-3.5 text-accent" /> <Check className="w-3.5 h-3.5 text-accent" />
) : ( ) : (
<Copy className="w-3.5 h-3.5" /> <Copy className="w-3.5 h-3.5" />
)} )}
<span>{copiedAll ? 'Kopiert' : 'Chat kopieren'}</span> <span>{copiedAll ? t('copied') : t('copyChat')}</span>
</button> </button>
)} )}
<button <button
@@ -336,16 +340,15 @@ export function AISearchResults({
</div> </div>
<div> <div>
<p className="text-xl md:text-2xl font-bold text-white/80"> <p className="text-xl md:text-2xl font-bold text-white/80">
Wie kann ich helfen? {t('howCanIHelp')}
</p> </p>
<p className="text-sm text-white/30 mt-2 max-w-md"> <p className="text-sm text-white/30 mt-2 max-w-md">
Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine {t('helpDescription')}
Anforderungen.
</p> </p>
</div> </div>
{/* Quick prompts */} {/* Quick prompts */}
<div className="flex flex-wrap justify-center gap-2 mt-4"> <div className="flex flex-wrap justify-center gap-2 mt-4">
{['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel für Solarpark'].map( {prompts.map(
(prompt) => ( (prompt) => (
<button <button
key={prompt} key={prompt}
@@ -384,7 +387,7 @@ export function AISearchResults({
? 'top-2 right-2 bg-primary/10 hover:bg-primary/20 text-primary/60 hover:text-primary' ? 'top-2 right-2 bg-primary/10 hover:bg-primary/20 text-primary/60 hover:text-primary'
: 'top-2 right-2 bg-white/5 hover:bg-white/10 text-white/40 hover:text-white' : 'top-2 right-2 bg-white/5 hover:bg-white/10 text-white/40 hover:text-white'
}`} }`}
title="Nachricht kopieren" title={t('copyMessage')}
> >
{copiedIndex === index ? ( {copiedIndex === index ? (
<Check className="w-3.5 h-3.5" /> <Check className="w-3.5 h-3.5" />
@@ -431,7 +434,7 @@ export function AISearchResults({
{msg.role === 'assistant' && msg.products && msg.products.length > 0 && ( {msg.role === 'assistant' && msg.products && msg.products.length > 0 && (
<div className="mt-4 space-y-2 border-t border-white/[0.06] pt-4"> <div className="mt-4 space-y-2 border-t border-white/[0.06] pt-4">
<h4 className="text-[10px] font-bold tracking-widest uppercase text-white/30 mb-2"> <h4 className="text-[10px] font-bold tracking-widest uppercase text-white/30 mb-2">
Empfohlene Produkte {t('recommendedProducts')}
</h4> </h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{msg.products.map((product, idx) => ( {msg.products.map((product, idx) => (
@@ -509,7 +512,7 @@ export function AISearchResults({
<AIOrb isThinking={false} hasError={true} /> <AIOrb isThinking={false} hasError={true} />
</div> </div>
<div> <div>
<h3 className="text-sm font-bold text-red-300">Da ist was schiefgelaufen 😬</h3> <h3 className="text-sm font-bold text-red-300">{t('errorTitle')}</h3>
<p className="text-xs text-red-300/60 mt-1">{error}</p> <p className="text-xs text-red-300/60 mt-1">{error}</p>
<button <button
onClick={() => { onClick={() => {
@@ -519,7 +522,7 @@ export function AISearchResults({
className="flex items-center gap-1.5 text-[10px] font-bold text-red-300/50 hover:text-red-300 mt-2 transition-colors cursor-pointer" className="flex items-center gap-1.5 text-[10px] font-bold text-red-300/50 hover:text-red-300 mt-2 transition-colors cursor-pointer"
> >
<RotateCcw className="w-3 h-3" /> <RotateCcw className="w-3 h-3" />
Nochmal versuchen {t('tryAgain')}
</button> </button>
</div> </div>
</div> </div>
@@ -544,7 +547,7 @@ export function AISearchResults({
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
placeholder="Nachricht eingeben..." placeholder={t('placeholder')}
className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20" className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20"
disabled={isLoading} disabled={isLoading}
tabIndex={1} tabIndex={1}
@@ -567,20 +570,20 @@ export function AISearchResults({
? 'bg-accent text-primary shadow-lg shadow-accent/20 hover:shadow-accent/40 hover:scale-105 active:scale-95' ? 'bg-accent text-primary shadow-lg shadow-accent/20 hover:shadow-accent/40 hover:scale-105 active:scale-95'
: 'bg-white/5 text-white/20' : 'bg-white/5 text-white/20'
}`} }`}
aria-label="Nachricht senden" aria-label={t('send')}
> >
<ArrowUp className="w-4 h-4" strokeWidth={2.5} /> <ArrowUp className="w-4 h-4" strokeWidth={2.5} />
</button> </button>
</div> </div>
<div className="flex items-center justify-center gap-3 mt-2.5"> <div className="flex items-center justify-center gap-3 mt-2.5">
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15"> <span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
Enter zum Senden · Esc zum Schließen {t('footerShortcuts')}
</span> </span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15"> <span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
· ·
</span> </span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-accent/40 flex items-center gap-1"> <span className="text-[9px] uppercase tracking-[0.15em] font-medium text-accent/40 flex items-center gap-1">
🛡 DSGVO-konform · EU-Server {t('footerPrivacy')}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -21,42 +21,42 @@ layout: "fullBleed"
{ {
title: 'ISO 14001:2015', title: 'ISO 14001:2015',
description: 'Environmental Management System', description: 'Environmental Management System',
pdfUrl: '/assets/certificates/231214_Zertifikat ISO 14001 Umweltmanagement.pdf', pdfUrl: '/assets/certificates/iso14001.pdf',
type: 'iso', type: 'iso',
date: '14.12.2023', date: '14.12.2023',
}, },
{ {
title: 'ISO 9001:2015', title: 'ISO 9001:2015',
description: 'Quality Management System', description: 'Quality Management System',
pdfUrl: '/assets/certificates/231214_Zertifikat ISO 9001 Qualitätsmanagement.pdf', pdfUrl: '/assets/certificates/iso9001.pdf',
type: 'iso', type: 'iso',
date: '14.12.2023', date: '14.12.2023',
}, },
{ {
title: 'DIN EN ISO 45001:2018', title: 'DIN EN ISO 45001:2018',
description: 'Occupational Health and Safety', description: 'Occupational Health and Safety',
pdfUrl: '/assets/certificates/Zertifizierung DIN EN ISO 45001 bis 05122028.pdf', pdfUrl: '/assets/certificates/iso45001.pdf',
type: 'iso', type: 'iso',
date: '05.12.2025', date: '05.12.2025',
}, },
{ {
title: 'Exemption Certificate', title: 'Exemption Certificate',
description: 'According to § 48 b EStG', description: 'According to § 48 b EStG',
pdfUrl: '/assets/certificates/240209_Freistellungsbescheinigung § 48 b.pdf', pdfUrl: '/assets/certificates/freistellung.pdf',
type: 'tax', type: 'tax',
date: '09.02.2024', date: '09.02.2024',
}, },
{ {
title: 'Proof § 13b UStG', title: 'Proof § 13b UStG',
description: 'Tax liability of the service recipient', description: 'Tax liability of the service recipient',
pdfUrl: '/assets/certificates/240209_Nachweis § 13 b.pdf', pdfUrl: '/assets/certificates/nachweis13b.pdf',
type: 'tax', type: 'tax',
date: '09.02.2024', date: '09.02.2024',
}, },
{ {
title: 'Certificate in Tax Matters', title: 'Certificate in Tax Matters',
description: 'Tax Office Certificate', description: 'Tax Office Certificate',
pdfUrl: '/assets/certificates/250213_Bescheinigung in Steuersachen.pdf', pdfUrl: '/assets/certificates/bescheinigung.pdf',
type: 'tax', type: 'tax',
date: '13.02.2025', date: '13.02.2025',
} }

View File

@@ -146,7 +146,15 @@
"hero": { "hero": {
"title": "DIE EXPERTEN FÜR <green>KABELTIEFBAU</green>", "title": "DIE EXPERTEN FÜR <green>KABELTIEFBAU</green>",
"subtitle": "Wir helfen beim Ausbau der Energiekabelnetze für eine grüne Zukunft.", "subtitle": "Wir helfen beim Ausbau der Energiekabelnetze für eine grüne Zukunft.",
"cta": "Jetzt anfragen" "cta": "Jetzt anfragen",
"searchPlaceholder": "Projekt beschreiben oder Kabel suchen...",
"ask": "Fragen",
"placeholder1": "Querschnittsberechnung für 110kV Trasse",
"placeholder2": "Wie schwer ist NAYY 4x150?",
"placeholder3": "Ich plane einen Solarpark, was brauche ich?",
"placeholder4": "Unterschied zwischen N2XSY und NAY2XSY?",
"placeholder5": "Mittelspannungskabel für Windkraftanlage",
"placeholder6": "Welches Aluminiumkabel für 20kV?"
}, },
"video": { "video": {
"title": "Vom ersten Spatenstich bis zum Netzanschluss wir bauen die Infrastruktur von morgen." "title": "Vom ersten Spatenstich bis zum Netzanschluss wir bauen die Infrastruktur von morgen."
@@ -224,6 +232,10 @@
"title": "Gründung E-TIB GmbH", "title": "Gründung E-TIB GmbH",
"desc": "Ausführung elektrischer Infrastrukturprojekte, Kabeltiefbau und Horizontalspülbohrungen." "desc": "Ausführung elektrischer Infrastrukturprojekte, Kabeltiefbau und Horizontalspülbohrungen."
}, },
"2025": {
"title": "Gründung E-TIB Bohrtechnik GmbH",
"desc": "Spezialisierung auf präzise Horizontalspülbohrungen in allen Bodenklassen."
},
"2019_ing": { "2019_ing": {
"title": "Gründung E-TIB Ingenieurgesellschaft", "title": "Gründung E-TIB Ingenieurgesellschaft",
"desc": "Genehmigungs- und Ausführungsplanung, komplexe Querungen sowie Netzanschlussplanung." "desc": "Genehmigungs- und Ausführungsplanung, komplexe Querungen sowie Netzanschlussplanung."
@@ -231,10 +243,6 @@
"2019_verw": { "2019_verw": {
"title": "Gründung E-TIB Verwaltung GmbH", "title": "Gründung E-TIB Verwaltung GmbH",
"desc": "Zentrale Dienste, Erwerb, Vermietung, Verpachtung und Verwaltung von Immobilien und Maschinen." "desc": "Zentrale Dienste, Erwerb, Vermietung, Verpachtung und Verwaltung von Immobilien und Maschinen."
},
"2025": {
"title": "Gründung E-TIB Bohrtechnik GmbH",
"desc": "Spezialisierung auf präzise Horizontalspülbohrungen in allen Bodenklassen."
} }
} }
}, },
@@ -272,5 +280,46 @@
"branchETIB": "E-TIB GmbH", "branchETIB": "E-TIB GmbH",
"branchIng": "Ingenieurgesellschaft", "branchIng": "Ingenieurgesellschaft",
"branchBohr": "Bohrtechnik" "branchBohr": "Bohrtechnik"
},
"AISearch": {
"loadingTexts": [
"Durchsuche das Kabelhandbuch... 📖",
"Frage den Senior-Ingenieur... 👴🔧",
"Frage ChatGPTs Cousin 2. Grades... 🤖"
],
"thinking": "Denkt nach...",
"errorStatus": "Fehler aufgetreten",
"online": "Online",
"copyChat": "Chat kopieren",
"copyChatTitle": "gesamten Chat kopieren",
"copied": "Kopiert",
"close": "Schließen",
"howCanIHelp": "Wie kann ich helfen?",
"helpDescription": "Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine Anforderungen.",
"prompts": [
"Windpark 33kV Verkabelung",
"NYCWY 4x185",
"Erdkabel für Solarpark"
],
"you": "Du",
"copyMessage": "Nachricht kopieren",
"recommendedProducts": "Empfohlene Produkte",
"errorTitle": "Da ist was schiefgelaufen 😬",
"tryAgain": "Nochmal versuchen",
"placeholder": "Nachricht eingeben...",
"send": "Nachricht senden",
"footerShortcuts": "Enter zum Senden · Esc zum Schließen",
"footerPrivacy": "🛡️ DSGVO-konform · EU-Server",
"timeoutError": "Anfrage hat zu lange gedauert. Bitte versuche es erneut.",
"genericError": "Ein Fehler ist aufgetreten."
},
"ReferenceDetail": {
"backToOverview": "Zurück zur Übersicht",
"projectReference": "Projektreferenz",
"location": "Ort",
"client": "Auftraggeber",
"period": "Zeitraum",
"scopeTitle": "Leistungsumfang & Projektbeschreibung",
"viewAll": "Alle Referenzen ansehen"
} }
} }

View File

@@ -146,7 +146,15 @@
"hero": { "hero": {
"title": "THE EXPERTS FOR <green>UNDERGROUND CABLE ENGINEERING</green>", "title": "THE EXPERTS FOR <green>UNDERGROUND CABLE ENGINEERING</green>",
"subtitle": "We help expanding the energy cable networks for a green future.", "subtitle": "We help expanding the energy cable networks for a green future.",
"cta": "Request now" "cta": "Request now",
"searchPlaceholder": "Describe project or search for cables...",
"ask": "Ask",
"placeholder1": "Cross-section calculation for 110kV route",
"placeholder2": "How heavy is NAYY 4x150?",
"placeholder3": "I'm planning a solar park, what do I need?",
"placeholder4": "Difference between N2XSY and NAY2XSY?",
"placeholder5": "Medium voltage cable for wind turbine",
"placeholder6": "Which aluminum cable for 20kV?"
}, },
"video": { "video": {
"title": "From the first spade cut to the grid connection we build the infrastructure of tomorrow." "title": "From the first spade cut to the grid connection we build the infrastructure of tomorrow."
@@ -224,6 +232,10 @@
"title": "Foundation of E-TIB GmbH", "title": "Foundation of E-TIB GmbH",
"desc": "Execution of electrical infrastructure projects, cable civil engineering, and horizontal directional drilling." "desc": "Execution of electrical infrastructure projects, cable civil engineering, and horizontal directional drilling."
}, },
"2025": {
"title": "Foundation of E-TIB Bohrtechnik GmbH",
"desc": "Specialization in precise horizontal directional drilling in all soil classes."
},
"2019_ing": { "2019_ing": {
"title": "Foundation of E-TIB Ingenieurgesellschaft", "title": "Foundation of E-TIB Ingenieurgesellschaft",
"desc": "Permit and execution planning, complex crossings, and grid connection planning." "desc": "Permit and execution planning, complex crossings, and grid connection planning."
@@ -231,10 +243,6 @@
"2019_verw": { "2019_verw": {
"title": "Foundation of E-TIB Verwaltung GmbH", "title": "Foundation of E-TIB Verwaltung GmbH",
"desc": "Central services, acquisition, leasing, and management of real estate and machinery." "desc": "Central services, acquisition, leasing, and management of real estate and machinery."
},
"2025": {
"title": "Foundation of E-TIB Bohrtechnik GmbH",
"desc": "Specialization in precise horizontal directional drilling in all soil classes."
} }
} }
}, },
@@ -272,5 +280,46 @@
"branchETIB": "E-TIB GmbH", "branchETIB": "E-TIB GmbH",
"branchIng": "Engineering Company", "branchIng": "Engineering Company",
"branchBohr": "Drilling Technology" "branchBohr": "Drilling Technology"
},
"AISearch": {
"loadingTexts": [
"Searching the cable manual... 📖",
"Asking the senior engineer... 👴🔧",
"Asking ChatGPT's 2nd cousin... 🤖"
],
"thinking": "Thinking...",
"errorStatus": "Error occurred",
"online": "Online",
"copyChat": "Copy chat",
"copyChatTitle": "copy entire chat",
"copied": "Copied",
"close": "Close",
"howCanIHelp": "How can I help?",
"helpDescription": "Describe your project, ask for specific cables, or tell me your requirements.",
"prompts": [
"Wind park 33kV cabling",
"NYCWY 4x185",
"Underground cable for solar park"
],
"you": "You",
"copyMessage": "Copy message",
"recommendedProducts": "Recommended Products",
"errorTitle": "Something went wrong 😬",
"tryAgain": "Try again",
"placeholder": "Enter message...",
"send": "Send message",
"footerShortcuts": "Enter to send · Esc to close",
"footerPrivacy": "🛡️ GDPR compliant · EU servers",
"timeoutError": "Request took too long. Please try again.",
"genericError": "An error occurred."
},
"ReferenceDetail": {
"backToOverview": "Back to Overview",
"projectReference": "Project Reference",
"location": "Location",
"client": "Client",
"period": "Period",
"scopeTitle": "Scope & Project Description",
"viewAll": "View all References"
} }
} }