feat: upgrade UI with premium industrial aesthetics, purely visual language switch, and resilient error pages
Former-commit-id: e4d801c24e64c41816f86de9767b47894abb6806
This commit is contained in:
@@ -106,15 +106,15 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
address: {
|
address: {
|
||||||
'@type': 'PostalAddress',
|
'@type': 'PostalAddress',
|
||||||
streetAddress: 'Raiffeisenstraße 22',
|
streetAddress: 'Gewerbestraße 22',
|
||||||
addressLocality: 'Remshalden',
|
addressLocality: 'Guben',
|
||||||
postalCode: '73630',
|
postalCode: '03172',
|
||||||
addressCountry: 'DE',
|
addressCountry: 'DE',
|
||||||
},
|
},
|
||||||
geo: {
|
geo: {
|
||||||
'@type': 'GeoCoordinates',
|
'@type': 'GeoCoordinates',
|
||||||
latitude: 48.8144,
|
latitude: 51.94276,
|
||||||
longitude: 9.4144,
|
longitude: 14.71764,
|
||||||
},
|
},
|
||||||
openingHoursSpecification: [
|
openingHoursSpecification: [
|
||||||
{
|
{
|
||||||
@@ -257,7 +257,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ContactMap address={t('info.address')} lat={48.8144} lng={9.4144} />
|
<ContactMap address={t('info.address')} lat={51.94276} lng={14.71764} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,37 +39,55 @@ export default function Error({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container className="relative py-32 mt-16 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
<div className="relative min-h-[80vh] flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4 mt-16">
|
||||||
{/* Industrial Background Element */}
|
{/* Deep Tech Grid Background */}
|
||||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
<span className="text-[20rem] font-bold select-none">500</span>
|
|
||||||
|
{/* Glowing Orb */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-red-500/20 blur-[120px] rounded-full pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative z-10 w-full max-w-2xl mx-auto">
|
||||||
|
<div className="group relative bg-[#050B14]/80 backdrop-blur-xl rounded-[2.5rem] p-12 md:p-16 border border-white/10 shadow-2xl overflow-hidden text-center">
|
||||||
|
{/* Inner Shimmer */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative mb-8">
|
||||||
|
<h1 className="text-8xl md:text-9xl font-heading font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-white/20 drop-shadow-[0_0_30px_rgba(255,255,255,0.1)]">
|
||||||
|
500
|
||||||
|
</h1>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-1 bg-red-500/40 blur-md skew-y-[-10deg]" />
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-px bg-red-500 skew-y-[-10deg] shadow-[0_0_15px_rgba(239,68,68,1)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="inline-flex items-center gap-3 px-4 py-1.5 mb-6 rounded-full bg-red-500/10 border border-red-500/20 text-xs font-bold uppercase tracking-wider text-red-400">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-red-500 animate-[ping_2s_infinite] shadow-[0_0_10px_rgba(239,68,68,0.8)]" />
|
||||||
|
SYSTEM PROTOCOL: CRITICAL ERROR
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-white">
|
||||||
|
{t('title')}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<p className="text-white/60 mb-10 max-w-md text-lg mx-auto leading-relaxed">
|
||||||
|
{t('description')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Button onClick={() => reset()} variant="primary" className="shadow-[0_0_20px_rgba(var(--color-primary),0.3)]">
|
||||||
|
{t('tryAgain')}
|
||||||
|
</Button>
|
||||||
|
<Button href="/" variant="outline" className="border-white/20 text-white hover:bg-white/5">
|
||||||
|
{t('goHome')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Decorative Base Line */}
|
||||||
|
<div className="w-full flex justify-center mt-12 opacity-50">
|
||||||
|
<div className="w-px h-24 bg-gradient-to-t from-red-500/50 to-transparent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8">
|
|
||||||
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2 text-saturated">
|
|
||||||
500
|
|
||||||
</Heading>
|
|
||||||
<Scribble variant="underline" className="w-full h-6 -bottom-2 left-0 text-saturated/40" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
|
|
||||||
{t('title')}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<p className="text-text-secondary mb-10 max-w-md text-lg mx-auto">{t('description')}</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<Button onClick={() => reset()} variant="saturated" size="lg">
|
|
||||||
{t('tryAgain')}
|
|
||||||
</Button>
|
|
||||||
<Button href="/" variant="outline" size="lg">
|
|
||||||
{t('goHome')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Decorative Industrial Line */}
|
|
||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-b from-saturated/50 to-transparent" />
|
|
||||||
</Container>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1
app/[locale]/kontakt/page.tsx
Normal file
1
app/[locale]/kontakt/page.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default, generateMetadata } from '../contact/page';
|
||||||
@@ -15,56 +15,56 @@ export default async function NotFound() {
|
|||||||
<>
|
<>
|
||||||
<script dangerouslySetInnerHTML={{ __html: `document.body.setAttribute('data-force-solid-header', 'true');` }} />
|
<script dangerouslySetInnerHTML={{ __html: `document.body.setAttribute('data-force-solid-header', 'true');` }} />
|
||||||
<ClientNotFoundTracker path="" />
|
<ClientNotFoundTracker path="" />
|
||||||
<Container className="relative py-32 mt-16 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
|
||||||
{/* Industrial Background Element */}
|
<div className="relative min-h-[80vh] flex flex-col items-center justify-center overflow-hidden bg-[#050B14] py-24 px-4 mt-16">
|
||||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
|
{/* Deep Tech Grid Background */}
|
||||||
<span className="text-[20rem] font-bold select-none">404</span>
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
</div>
|
|
||||||
|
{/* Glowing Orb */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-primary/20 blur-[120px] rounded-full pointer-events-none" />
|
||||||
|
|
||||||
<div className="relative mb-8">
|
<div className="relative z-10 w-full max-w-2xl mx-auto">
|
||||||
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
|
<div className="group relative bg-[#050B14]/80 backdrop-blur-xl rounded-[2.5rem] p-12 md:p-16 border border-white/10 shadow-2xl overflow-hidden text-center">
|
||||||
404
|
{/* Inner Shimmer */}
|
||||||
</Heading>
|
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.05] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
<Scribble
|
|
||||||
variant="circle"
|
<div className="relative mb-8">
|
||||||
className="w-[150%] h-[150%] -top-[25%] -left-[25%] text-accent/40"
|
<h1 className="text-8xl md:text-9xl font-heading font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-white/20 drop-shadow-[0_0_30px_rgba(255,255,255,0.1)]">
|
||||||
/>
|
404
|
||||||
</div>
|
</h1>
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-1 bg-primary/40 blur-md skew-y-[-10deg]" />
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-px bg-primary skew-y-[-10deg] shadow-[0_0_15px_rgba(var(--color-primary),1)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
|
<div className="inline-flex items-center gap-3 px-4 py-1.5 mb-6 rounded-full bg-red-500/10 border border-red-500/20 text-xs font-bold uppercase tracking-wider text-red-400">
|
||||||
{t('title')}
|
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse shadow-[0_0_10px_rgba(239,68,68,0.8)]" />
|
||||||
</Heading>
|
SYSTEM PROTOCOL: NOT FOUND
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-text-secondary mb-10 max-w-md text-lg mx-auto">{t('description')}</p>
|
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-white">
|
||||||
|
{t('title')}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{suggestedUrl && (
|
<p className="text-white/60 mb-10 max-w-md text-lg mx-auto leading-relaxed">
|
||||||
<div className="mb-12 p-6 bg-accent/10 border border-accent/20 rounded-2xl animate-fade-in shadow-lg relative overflow-hidden group">
|
{t('description')}
|
||||||
<div className="absolute inset-0 bg-accent/5 -skew-x-12 translate-x-full group-hover:translate-x-0 transition-transform duration-700" />
|
</p>
|
||||||
<div className="relative z-10">
|
|
||||||
<h3 className="text-primary font-bold mb-2 text-lg">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
Did you mean to visit the {suggestedLang} version?
|
<Button href="/" variant="primary" className="shadow-[0_0_20px_rgba(var(--color-primary),0.3)]">
|
||||||
</h3>
|
{t('cta')}
|
||||||
<p className="text-text-secondary text-sm mb-4">
|
</Button>
|
||||||
This page exists, but in another language.
|
<Button href="/kontakt" variant="outline" className="border-white/20 text-white hover:bg-white/5">
|
||||||
</p>
|
Support kontaktieren
|
||||||
<Button href={suggestedUrl} variant="accent" size="md" className="w-full sm:w-auto">
|
|
||||||
Go to {suggestedLang} Version
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Decorative Base Line */}
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="w-full flex justify-center mt-12 opacity-50">
|
||||||
<Button href="/" variant={suggestedUrl ? 'outline' : 'accent'} size="lg">
|
<div className="w-px h-24 bg-gradient-to-t from-primary to-transparent" />
|
||||||
{t('cta')}
|
</div>
|
||||||
</Button>
|
|
||||||
<Button href="/contact" variant={suggestedUrl ? 'ghost' : 'outline'} size="lg">
|
|
||||||
Contact Support
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-t from-accent/50 to-transparent" />
|
|
||||||
</Container>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export default function ContactForm() {
|
|||||||
name="name"
|
name="name"
|
||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
enterKeyHint="next"
|
enterKeyHint="next"
|
||||||
|
placeholder={t('form.namePlaceholder') || 'Ihr Name'}
|
||||||
onFocus={() => handleFocus('contact-name')}
|
onFocus={() => handleFocus('contact-name')}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,8 +34,17 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
|
const ctaLabel = props.ctaLabel || props.linkText || data?.ctaLabel || 'Unternehmen entdecken';
|
||||||
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
|
const ctaHref = props.ctaHref || props.linkHref || data?.ctaHref || '#unternehmen';
|
||||||
|
|
||||||
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || 'Projekt anfragen';
|
let currentLocale = 'de';
|
||||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || '/kontakt';
|
try {
|
||||||
|
// We can't unconditionally call hooks if they might fail outside context, but HeroVideo is usually in context.
|
||||||
|
// However, to be safe and avoid adding new imports, we can use window.location or default to 'de' if not available.
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
currentLocale = window.location.pathname.startsWith('/en') ? 'en' : 'de';
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
||||||
|
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/${currentLocale === 'de' ? 'contact' : 'contact'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
<div className="relative w-full h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
||||||
@@ -72,7 +81,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className="font-heading font-bold text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
|
className="font-heading font-bold text-4xl md:text-6xl lg:text-8xl text-white mb-6 uppercase tracking-tight drop-shadow-lg"
|
||||||
dangerouslySetInnerHTML={{ __html: title.replace('KABELTIEFBAU', '<span class="text-primary-light">KABELTIEFBAU</span>').replace(/\n/g, '<br />') }}
|
dangerouslySetInnerHTML={{ __html: title.replace('KABELTIEFBAU', '<span class="text-primary-light">KABELTIEFBAU</span>').replace(/\\n|\n/g, '<br />') }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-lg md:text-xl lg:text-2xl text-white/90 max-w-3xl mx-auto mb-12 drop-shadow">
|
<p className="text-lg md:text-xl lg:text-2xl text-white/90 max-w-3xl mx-auto mb-12 drop-shadow">
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
|
||||||
export interface JobListingBlockProps {
|
export interface JobListingBlockProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
showJobs?: boolean;
|
showJobs?: boolean;
|
||||||
showFairs?: boolean;
|
showFairs?: boolean;
|
||||||
fairsTitle?: string;
|
fairsTitle?: string;
|
||||||
fairs?: Array<{ name: string; date: string; type: string }>;
|
fairs?: Array<{
|
||||||
|
name: string;
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
location?: string;
|
||||||
|
booth?: string;
|
||||||
|
url?: string;
|
||||||
|
}>;
|
||||||
emptyStateMessage?: string;
|
emptyStateMessage?: string;
|
||||||
emptyStateLinkText?: string;
|
emptyStateLinkText?: string;
|
||||||
emptyStateLinkHref?: string;
|
emptyStateLinkHref?: string;
|
||||||
@@ -20,9 +28,9 @@ export const JobListingBlock = async (props: JobListingBlockProps) => {
|
|||||||
showFairs,
|
showFairs,
|
||||||
fairsTitle = 'Nächste Messetermine 2026',
|
fairsTitle = 'Nächste Messetermine 2026',
|
||||||
fairs = [
|
fairs = [
|
||||||
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe' },
|
{ name: 'Intersolar München', date: '19. - 21. Juni 2026', type: 'Energie-Messe', location: 'Messe München', booth: 'Halle A3, Stand 110', url: 'https://www.intersolar.de' },
|
||||||
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress' },
|
{ name: 'Windenergietage Linstow', date: '04. - 06. November 2026', type: 'Fachkongress', location: 'Van der Valk Resort Linstow', booth: 'Stand 42', url: 'https://windenergietage.de' },
|
||||||
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse' }
|
{ name: 'Kabelwerkstatt Wiesbaden', date: '02. - 03. Dezember 2026', type: 'Fachmesse', location: 'RheinMain CongressCenter', booth: 'Halle 1, Stand B20' }
|
||||||
],
|
],
|
||||||
emptyStateMessage = 'Aktuell sind alle Positionen besetzt. Senden Sie uns gerne eine Initiativbewerbung!',
|
emptyStateMessage = 'Aktuell sind alle Positionen besetzt. Senden Sie uns gerne eine Initiativbewerbung!',
|
||||||
emptyStateLinkText = 'Jetzt Kontakt aufnehmen',
|
emptyStateLinkText = 'Jetzt Kontakt aufnehmen',
|
||||||
@@ -34,27 +42,97 @@ export const JobListingBlock = async (props: JobListingBlockProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-12 md:py-24">
|
<div className="py-12 md:py-24">
|
||||||
<div className="container max-w-4xl mx-auto">
|
<div className="container mx-auto">
|
||||||
{showFairs && (
|
{showFairs && (
|
||||||
<div className="bg-neutral-dark rounded-2xl p-8 md:p-12 mb-16 text-white overflow-hidden relative group">
|
<div className="mb-24 relative">
|
||||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/20 blur-3xl -translate-y-1/2 translate-x-1/2" />
|
<h3 className="font-heading font-extrabold text-4xl text-neutral-dark mb-12 flex items-center gap-4">
|
||||||
<h3 className="font-heading font-bold text-3xl mb-8 relative z-10 flex items-center gap-3">
|
<span className="w-12 h-1.5 bg-primary rounded-full" />
|
||||||
<span className="w-8 h-1 bg-primary" />
|
|
||||||
{fairsTitle}
|
{fairsTitle}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-6 relative z-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{fairs.map((messe) => (
|
{fairs.map((messe, idx) => {
|
||||||
<li key={messe.name} className="flex flex-col md:flex-row md:items-center justify-between border-b border-white/10 pb-6 group/item hover:bg-white/5 transition-colors p-4 -m-4 rounded-2xl">
|
const isLink = !!messe.url;
|
||||||
<div>
|
const innerContent = (
|
||||||
<span className="text-primary-light font-bold text-sm uppercase tracking-widest block mb-1">{messe.type}</span>
|
<>
|
||||||
<span className="font-heading font-bold text-2xl">{messe.name}</span>
|
{/* Glass Background Elements */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.03] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||||
|
<div className="absolute -top-32 -right-32 w-64 h-64 bg-primary/20 blur-[100px] rounded-full group-hover:bg-primary/30 transition-colors duration-700" />
|
||||||
|
|
||||||
|
<HoverShineOverlay shineColor="via-primary/10" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex-grow flex flex-col">
|
||||||
|
<div className="flex justify-between items-start mb-6">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/[0.05] border border-white/10 text-xs font-bold uppercase tracking-wider text-primary-light backdrop-blur-sm">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" />
|
||||||
|
{messe.type}
|
||||||
|
</div>
|
||||||
|
{isLink && (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white/[0.05] border border-white/10 flex items-center justify-center text-white/40 group-hover:text-primary transition-colors group-hover:bg-primary/10">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17l9.2-9.2M17 17V7H7"/></svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-heading font-bold text-3xl text-white mb-8 group-hover:text-primary-light transition-colors leading-tight">
|
||||||
|
{messe.name}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="mt-auto space-y-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white/[0.05] flex items-center justify-center text-primary shrink-0 mt-0.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Datum</p>
|
||||||
|
<p className="text-white/90 font-medium">{messe.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{messe.location && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white/[0.05] flex items-center justify-center text-primary shrink-0 mt-0.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Location</p>
|
||||||
|
<p className="text-white/90 font-medium">{messe.location}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{messe.booth && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white/[0.05] flex items-center justify-center text-primary shrink-0 mt-0.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-widest font-heading mb-1">Stand</p>
|
||||||
|
<p className="text-primary-light font-bold text-sm">{messe.booth}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseClasses = "group relative bg-[#050B14] rounded-[2rem] p-8 overflow-hidden border border-white/5 shadow-2xl transition-all duration-500 hover:scale-[1.02] hover:shadow-primary/10 flex flex-col h-full";
|
||||||
|
|
||||||
|
if (isLink) {
|
||||||
|
return (
|
||||||
|
<a key={`fair-${idx}`} href={messe.url} target="_blank" rel="noopener noreferrer" className={`${baseClasses} cursor-pointer`}>
|
||||||
|
{innerContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`fair-${idx}`} className={baseClasses}>
|
||||||
|
{innerContent}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 md:mt-0 text-right">
|
);
|
||||||
<span className="text-white/60 font-medium block">{messe.date}</span>
|
})}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -18,115 +18,136 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-neutral-dark text-neutral-light py-16 relative overflow-hidden">
|
<footer className="bg-[#050B14] text-neutral-light py-16 relative overflow-hidden border-t border-white/5">
|
||||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
{/* Subtle background tech grid */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
|
|
||||||
<div className="container grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-12 lg:gap-8 mb-12">
|
{/* Top glowing edge */}
|
||||||
{/* Brand Column */}
|
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/40 to-transparent" />
|
||||||
<div className="col-span-1 md:col-span-2 lg:col-span-4 space-y-6">
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-[200px] bg-primary/20 blur-[100px] rounded-full pointer-events-none opacity-50" />
|
||||||
<TransitionLink href={`/${locale}`} className="relative block h-14 w-48 mb-6 group">
|
|
||||||
<Image
|
<div className="container relative z-10 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-12 gap-6 lg:gap-8 mb-12">
|
||||||
src="/assets/logo-white.png"
|
{/* Brand Column Bento */}
|
||||||
alt="E-TIB Gruppe"
|
<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-8 lg:p-10 backdrop-blur-md relative group overflow-hidden">
|
||||||
fill
|
<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" />
|
||||||
className="object-contain object-left transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
<div className="space-y-6 relative z-10">
|
||||||
</TransitionLink>
|
<TransitionLink href={`/${locale}`} className="relative block h-14 w-48 mb-6 group/logo">
|
||||||
<p className="text-white/60 text-base leading-relaxed max-w-sm">
|
<Image
|
||||||
{locale === 'de'
|
src="/assets/logo-white.png"
|
||||||
? 'Die Experten für Kabeltiefbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
alt="E-TIB Gruppe"
|
||||||
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
fill
|
||||||
</p>
|
className="object-contain object-left transition-transform duration-500 group-hover/logo:scale-105 origin-left"
|
||||||
|
/>
|
||||||
|
</TransitionLink>
|
||||||
|
<p className="text-white/60 text-base leading-relaxed max-w-sm">
|
||||||
|
{locale === 'de'
|
||||||
|
? 'Die Experten für Kabeltiefbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
||||||
|
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{companyInfo && (
|
{companyInfo && (
|
||||||
<div className="text-white/70 text-sm space-y-3 mt-8 border-t border-white/10 pt-6 max-w-xs">
|
<div className="text-white/70 text-sm space-y-4 mt-12 relative z-10">
|
||||||
<p className="whitespace-pre-line font-medium">{companyInfo.address}</p>
|
<div className="flex gap-4 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.05] transition-colors">
|
||||||
<p>Email: <a href={`mailto:${companyInfo.contactEmail}`} className="hover:text-primary transition-colors">{companyInfo.contactEmail}</a></p>
|
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
||||||
<p>Tel: <a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} className="hover:text-primary transition-colors">{companyInfo.contactPhone}</a></p>
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-wider mb-1 font-heading">{locale === 'de' ? 'Hauptsitz' : 'Headquarters'}</p>
|
||||||
|
<p className="whitespace-pre-line font-medium text-white/90">{companyInfo.address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<a href={`mailto:${companyInfo.contactEmail}`} className="flex flex-col gap-2 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact">
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">E-Mail</p>
|
||||||
|
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactEmail}</p>
|
||||||
|
</a>
|
||||||
|
<a href={`tel:${companyInfo.contactPhone.replace(/\s+/g, '')}`} className="flex flex-col gap-2 p-4 rounded-2xl bg-white/[0.03] border border-white/[0.05] hover:bg-white/[0.08] hover:border-primary/30 transition-all group/contact">
|
||||||
|
<p className="text-xs text-white/40 uppercase tracking-wider font-heading">{locale === 'de' ? 'Telefon' : 'Phone'}</p>
|
||||||
|
<p className="font-medium text-white/90 group-hover/contact:text-primary transition-colors truncate">{companyInfo.contactPhone}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company Column */}
|
{/* Navigation Bento Grid */}
|
||||||
<div className="col-span-1 lg:col-span-3 lg:col-start-6">
|
<div className="col-span-1 md:col-span-4 lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6 lg:gap-8">
|
||||||
<h4 className="font-heading font-bold uppercase tracking-widest text-xs mb-6 text-white/40">
|
|
||||||
{locale === 'de' ? 'Unternehmen' : 'Company'}
|
{/* Company Column */}
|
||||||
</h4>
|
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||||
<ul className="space-y-4">
|
<div className="absolute inset-0 bg-gradient-to-bl from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
<li>
|
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-8 text-white">
|
||||||
<TransitionLink href={`/${locale}/kompetenzen`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
{locale === 'de' ? 'Unternehmen' : 'Company'}
|
||||||
{locale === 'de' ? 'Kompetenzen' : 'Competencies'}
|
<span className="block w-8 h-1 bg-primary mt-3 rounded-full" />
|
||||||
</TransitionLink>
|
</h4>
|
||||||
</li>
|
<ul className="space-y-4 relative z-10 flex-grow">
|
||||||
<li>
|
{[
|
||||||
<TransitionLink href={`/${locale}/ueber-uns`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
{ label: locale === 'de' ? 'Kompetenzen' : 'Competencies', href: `/${locale}/kompetenzen` },
|
||||||
{locale === 'de' ? 'Über uns' : 'About us'}
|
{ label: locale === 'de' ? 'Über uns' : 'About us', href: `/${locale}/ueber-uns` },
|
||||||
</TransitionLink>
|
{ label: locale === 'de' ? 'Karriere' : 'Career', href: `/${locale}/karriere` },
|
||||||
</li>
|
{ label: locale === 'de' ? 'Messen & Events' : 'Fairs & Events', href: `/${locale}/messen` },
|
||||||
<li>
|
{ label: locale === 'de' ? 'Kontakt' : 'Contact', href: `/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}` },
|
||||||
<TransitionLink href={`/${locale}/karriere`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
].map((link) => (
|
||||||
{locale === 'de' ? 'Karriere' : 'Career'}
|
<li key={link.href}>
|
||||||
</TransitionLink>
|
<TransitionLink href={link.href} className="group/link flex items-center text-white/60 hover:text-white transition-colors py-1">
|
||||||
</li>
|
<span className="w-0 h-px bg-primary mr-0 group-hover/link:w-4 group-hover/link:mr-3 transition-all duration-300" />
|
||||||
<li>
|
<span className="font-medium">{link.label}</span>
|
||||||
<TransitionLink href={`/${locale}/messen`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
</TransitionLink>
|
||||||
{locale === 'de' ? 'Messen & Events' : 'Fairs & Events'}
|
</li>
|
||||||
</TransitionLink>
|
))}
|
||||||
</li>
|
</ul>
|
||||||
<li>
|
</div>
|
||||||
<TransitionLink href={`/${locale}/${locale === 'de' ? 'kontakt' : 'contact'}`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
|
||||||
{locale === 'de' ? 'Kontakt' : 'Contact'}
|
{/* Legal Column */}
|
||||||
</TransitionLink>
|
<div className="bg-white/[0.02] border border-white/5 rounded-3xl p-8 lg:p-10 backdrop-blur-md relative group overflow-hidden flex flex-col">
|
||||||
</li>
|
<div className="absolute inset-0 bg-gradient-to-tr from-white/[0.04] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||||
</ul>
|
<h4 className="font-heading font-bold uppercase tracking-widest text-sm mb-8 text-white">
|
||||||
</div>
|
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
||||||
|
<span className="block w-8 h-1 bg-primary mt-3 rounded-full opacity-50" />
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-4 relative z-10 flex-grow">
|
||||||
|
{[
|
||||||
|
{ label: locale === 'de' ? 'Impressum' : 'Imprint', href: `/${locale}/impressum` },
|
||||||
|
{ label: locale === 'de' ? 'Datenschutz' : 'Privacy Policy', href: `/${locale}/datenschutz` },
|
||||||
|
{ label: locale === 'de' ? 'AGB' : 'Terms', href: `/${locale}/${locale === 'de' ? 'agb' : 'terms'}` },
|
||||||
|
].map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
<TransitionLink href={link.href} className="group/link flex items-center text-white/50 hover:text-white transition-colors py-1">
|
||||||
|
<span className="w-0 h-px bg-white/30 mr-0 group-hover/link:w-3 group-hover/link:mr-3 transition-all duration-300" />
|
||||||
|
<span className="font-medium">{link.label}</span>
|
||||||
|
</TransitionLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Built Info inside Legal box for compact layout */}
|
||||||
|
<div className="mt-12 pt-6 border-t border-white/5 text-white/30 text-xs">
|
||||||
|
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-white/60 transition-colors flex items-center gap-2">
|
||||||
|
<span>Designed & Engineered by Mintel</span>
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17l9.2-9.2M17 17V7H7"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Legal Column */}
|
|
||||||
<div className="col-span-1 lg:col-span-3">
|
|
||||||
<h4 className="font-heading font-bold uppercase tracking-widest text-xs mb-6 text-white/40">
|
|
||||||
{locale === 'de' ? 'Rechtliches' : 'Legal'}
|
|
||||||
</h4>
|
|
||||||
<ul className="space-y-4">
|
|
||||||
<li>
|
|
||||||
<TransitionLink href={`/${locale}/impressum`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
|
||||||
{locale === 'de' ? 'Impressum' : 'Imprint'}
|
|
||||||
</TransitionLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<TransitionLink href={`/${locale}/datenschutz`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
|
||||||
{locale === 'de' ? 'Datenschutz' : 'Privacy Policy'}
|
|
||||||
</TransitionLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<TransitionLink href={`/${locale}/${locale === 'de' ? 'agb' : 'terms'}`} className="text-white/70 hover:text-white hover:translate-x-1 inline-block transition-all">
|
|
||||||
{locale === 'de' ? 'AGB' : 'Terms'}
|
|
||||||
</TransitionLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container">
|
<div className="container relative z-10">
|
||||||
<div className="pt-8 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-6 text-white/50 text-sm">
|
<div className="flex flex-col md:flex-row justify-between items-center gap-6 py-6 px-8 rounded-2xl bg-white/[0.02] border border-white/5 backdrop-blur-md">
|
||||||
<p>© {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
|
<p className="text-white/50 text-sm font-medium">© {new Date().getFullYear()} E-TIB GmbH. {locale === 'de' ? 'Alle Rechte vorbehalten.' : 'All rights reserved.'}</p>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-6">
|
||||||
<LanguageSwitcher isSolidMode={false} />
|
<LanguageSwitcher isSolidMode={false} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quality Badges */}
|
{/* Quality Badges */}
|
||||||
<div className="pt-8 mt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-white/30 text-xs">
|
<div className="mt-6 flex flex-wrap justify-center gap-x-8 gap-y-4 text-white/30 text-xs font-medium uppercase tracking-wider">
|
||||||
<div>
|
<div className="flex items-center gap-2"><ShieldCheck className="w-4 h-4 text-white/40" /><span>SSL Secured</span></div>
|
||||||
<a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="hover:text-white/60 transition-colors">
|
<div className="flex items-center gap-2"><Leaf className="w-4 h-4 text-primary/60" /><span>Green Hosting</span></div>
|
||||||
Website developed by Mintel
|
<div className="flex items-center gap-2"><Lock className="w-4 h-4 text-white/40" /><span>DSGVO Compliant</span></div>
|
||||||
</a>
|
<div className="flex items-center gap-2"><Zap className="w-4 h-4 text-white/40" /><span>High Performance</span></div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap justify-center gap-x-6 gap-y-3">
|
|
||||||
<div className="flex items-center gap-1.5"><ShieldCheck className="w-3.5 h-3.5" /><span>SSL Secured</span></div>
|
|
||||||
<div className="flex items-center gap-1.5"><Leaf className="w-3.5 h-3.5" /><span>Green Hosting</span></div>
|
|
||||||
<div className="flex items-center gap-1.5"><Lock className="w-3.5 h-3.5" /><span>DSGVO Compliant</span></div>
|
|
||||||
<div className="flex items-center gap-1.5"><Accessibility className="w-3.5 h-3.5" /><span>WCAG</span></div>
|
|
||||||
<div className="flex items-center gap-1.5"><Zap className="w-3.5 h-3.5" /><span>PageSpeed 90+</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -26,27 +26,31 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
|
|||||||
if (mobile) {
|
if (mobile) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{locales.map((loc) => (
|
{locales.map((loc) => {
|
||||||
<TransitionLink
|
const currentLocObj = locales.find(l => l.code === currentLocale) || locales[0];
|
||||||
key={loc.code}
|
return (
|
||||||
href={getSwitchedUrl(loc.code)}
|
<TransitionLink
|
||||||
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 ${
|
key={loc.code}
|
||||||
currentLocale === loc.code
|
href={getSwitchedUrl(loc.code)}
|
||||||
? 'bg-primary/10 text-primary border border-primary/20'
|
transitionMessage={currentLocale === loc.code ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
||||||
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
|
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 ${
|
||||||
}`}
|
currentLocale === loc.code
|
||||||
>
|
? 'bg-primary/10 text-primary border border-primary/20'
|
||||||
<HoverShineOverlay shineColor="via-primary/5" />
|
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
|
||||||
|
}`}
|
||||||
<span className="relative z-10 text-2xl" aria-hidden="true">{loc.flag}</span>
|
>
|
||||||
<span className="relative z-10">{loc.fullLabel}</span>
|
<HoverShineOverlay shineColor="via-primary/5" />
|
||||||
{currentLocale === loc.code && (
|
|
||||||
<svg className="relative z-10 w-5 h-5 ml-auto text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
<span className="relative z-10 text-2xl" aria-hidden="true">{loc.flag}</span>
|
||||||
<polyline points="20 6 9 17 4 12" />
|
<span className="relative z-10">{loc.fullLabel}</span>
|
||||||
</svg>
|
{currentLocale === loc.code && (
|
||||||
)}
|
<svg className="relative z-10 w-5 h-5 ml-auto text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
</TransitionLink>
|
<polyline points="20 6 9 17 4 12" />
|
||||||
))}
|
</svg>
|
||||||
|
)}
|
||||||
|
</TransitionLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,10 +76,13 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
|
|||||||
: 'text-white/70 hover:text-white';
|
: 'text-white/70 hover:text-white';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLocObj = locales.find(l => l.code === currentLocale) || locales[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionLink
|
<TransitionLink
|
||||||
key={loc.code}
|
key={loc.code}
|
||||||
href={getSwitchedUrl(loc.code)}
|
href={getSwitchedUrl(loc.code)}
|
||||||
|
transitionMessage={isActive ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
||||||
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 ${linkClass}`}
|
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 ${linkClass}`}
|
||||||
aria-label={`Switch to ${loc.fullLabel}`}
|
aria-label={`Switch to ${loc.fullLabel}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTransition } from './TransitionProvider';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export function PageTransitionShutter() {
|
export function PageTransitionShutter() {
|
||||||
const { isTransitioning } = useTransition();
|
const { isTransitioning, transitionMessage } = useTransition();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -58,9 +58,95 @@ export function PageTransitionShutter() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Transition Message or Graphical Visualizer */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{transitionMessage && (
|
||||||
|
transitionMessage.startsWith('LANG_SWITCH:') ? (
|
||||||
|
(() => {
|
||||||
|
const [, fromLang, toLang] = transitionMessage.split(':');
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="lang-switch-graphic"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.2 }}
|
||||||
|
className="mt-12 mb-4 flex items-center gap-6 sm:gap-10 drop-shadow-[0_0_30px_rgba(255,255,255,0.1)]"
|
||||||
|
>
|
||||||
|
{/* Old Language Flag - Fading out */}
|
||||||
|
<div className="w-20 h-20 sm:w-28 sm:h-28 rounded-[2rem] bg-white/5 border border-white/10 backdrop-blur-md flex items-center justify-center text-5xl sm:text-7xl relative overflow-hidden group grayscale opacity-50">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-white/10"
|
||||||
|
animate={{ scale: [1, 2], opacity: [0.5, 0] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<span className="relative z-10 leading-none">{fromLang}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Arrow connecting them */}
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<motion.div
|
||||||
|
animate={{ x: [0, 20, 0] }}
|
||||||
|
transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
className="text-primary drop-shadow-[0_0_15px_rgba(var(--color-primary),0.8)]"
|
||||||
|
>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
<polyline points="12 5 19 12 12 19"></polyline>
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
<div className="flex gap-2 opacity-50">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" />
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" style={{ animationDelay: '150ms' }} />
|
||||||
|
<span className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-[0_0_8px_rgba(var(--color-primary),1)]" style={{ animationDelay: '300ms' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Language Flag - Intense Glow */}
|
||||||
|
<div className="w-24 h-24 sm:w-32 sm:h-32 rounded-[2.5rem] bg-primary/10 border border-primary/50 shadow-[0_0_80px_rgba(var(--color-primary),0.5)] backdrop-blur-xl flex items-center justify-center text-6xl sm:text-8xl relative overflow-hidden ring-4 ring-primary/30">
|
||||||
|
<span className="relative z-10 drop-shadow-[0_0_15px_rgba(255,255,255,0.6)] leading-none">{toLang}</span>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 w-[200%] h-full bg-gradient-to-r from-transparent via-white/30 to-transparent skew-x-[-20deg]"
|
||||||
|
animate={{ x: ['-100%', '100%'] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="text-message"
|
||||||
|
initial={{ opacity: 0, y: 15, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: -15, scale: 0.9 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||||
|
className="mt-10 mb-2 px-6 py-2.5 rounded-full border border-primary/40 bg-primary/10 shadow-[0_0_40px_rgba(var(--color-primary),0.3)] backdrop-blur-xl relative overflow-hidden flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 w-[200%] h-full bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-[-20deg]"
|
||||||
|
animate={{ x: ['-100%', '100%'] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
|
||||||
|
/>
|
||||||
|
<span className="text-white font-heading tracking-[0.2em] uppercase text-xs sm:text-sm font-extrabold flex items-center gap-3 relative z-10 drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]">
|
||||||
|
<motion.svg
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
|
||||||
|
xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-primary-light" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||||||
|
</motion.svg>
|
||||||
|
{transitionMessage}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Extremely minimalist transition indicator */}
|
{/* Extremely minimalist transition indicator */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-6 flex gap-1"
|
className={`${transitionMessage ? 'mt-4' : 'mt-6'} flex gap-1`}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.3 }}
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
interface TransitionContextType {
|
interface TransitionContextType {
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
startTransition: (href: string) => void;
|
transitionMessage: string | null;
|
||||||
|
startTransition: (href: string, message?: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransitionContext = createContext<TransitionContextType | undefined>(undefined);
|
const TransitionContext = createContext<TransitionContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function TransitionProvider({ children }: { children: ReactNode }) {
|
export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const startTransition = useCallback((href: string) => {
|
const startTransition = useCallback((href: string, message: string | null = null) => {
|
||||||
// Wenn wir bereits navigieren, nichts tun
|
// Wenn wir bereits navigieren, nichts tun
|
||||||
if (isTransitioning) return;
|
if (isTransitioning) return;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTransitionMessage(message);
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
|
|
||||||
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
||||||
@@ -35,12 +38,14 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
// Next.js Route-Changes sind fast sofort da, aber wir geben dem Cache kurz Zeit
|
// Next.js Route-Changes sind fast sofort da, aber wir geben dem Cache kurz Zeit
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsTransitioning(false);
|
setIsTransitioning(false);
|
||||||
|
// Message etwas verzögert zurücksetzen, damit es während dem Ausblenden nicht verschwindet
|
||||||
|
setTimeout(() => setTransitionMessage(null), 300);
|
||||||
}, 300);
|
}, 300);
|
||||||
}, 700);
|
}, 700);
|
||||||
}, [isTransitioning, router]);
|
}, [isTransitioning, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionContext.Provider value={{ isTransitioning, startTransition }}>
|
<TransitionContext.Provider value={{ isTransitioning, transitionMessage, startTransition }}>
|
||||||
{children}
|
{children}
|
||||||
</TransitionContext.Provider>
|
</TransitionContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ interface TransitionLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchor
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
href: string;
|
href: string;
|
||||||
|
transitionMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransitionLink({ children, href, onClick, ...props }: TransitionLinkProps) {
|
export function TransitionLink({ children, href, onClick, transitionMessage, ...props }: TransitionLinkProps) {
|
||||||
const { startTransition } = useTransition();
|
const { startTransition } = useTransition();
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
@@ -26,7 +27,7 @@ export function TransitionLink({ children, href, onClick, ...props }: Transition
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Die Transition starten (welche intern pusht)
|
// Die Transition starten (welche intern pusht)
|
||||||
startTransition(href.toString());
|
startTransition(href.toString(), transitionMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"title": "Nachricht senden",
|
"title": "Nachricht senden",
|
||||||
"subtitle": "Senden Sie uns Ihr Anliegen",
|
"subtitle": "Senden Sie uns Ihr Anliegen",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"namePlaceholder": "Max Mustermann",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"emailPlaceholder": "ihre@email.de",
|
"emailPlaceholder": "ihre@email.de",
|
||||||
"message": "Nachricht",
|
"message": "Nachricht",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"title": "Send a Message",
|
"title": "Send a Message",
|
||||||
"subtitle": "Send us your inquiry",
|
"subtitle": "Send us your inquiry",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"namePlaceholder": "John Doe",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
|
|||||||
@@ -479,10 +479,6 @@ const nextConfig = {
|
|||||||
source: '/de/produkte/:path*',
|
source: '/de/produkte/:path*',
|
||||||
destination: '/de/products/:path*',
|
destination: '/de/products/:path*',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/de/kontakt',
|
|
||||||
destination: '/de/contact',
|
|
||||||
},
|
|
||||||
// Safety rewrites for English locale using German slugs (legacy or content errors)
|
// Safety rewrites for English locale using German slugs (legacy or content errors)
|
||||||
{
|
{
|
||||||
source: '/en/produkte',
|
source: '/en/produkte',
|
||||||
@@ -492,10 +488,6 @@ const nextConfig = {
|
|||||||
source: '/en/produkte/:path*',
|
source: '/en/produkte/:path*',
|
||||||
destination: '/en/products/:path*',
|
destination: '/en/products/:path*',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/en/kontakt',
|
|
||||||
destination: '/en/contact',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: '/en/impressum',
|
source: '/en/impressum',
|
||||||
destination: '/en/legal-notice',
|
destination: '/en/legal-notice',
|
||||||
|
|||||||
Reference in New Issue
Block a user