feat: integrate standalone standorte pages with real photos and map routing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 41s
Build & Deploy / 🔔 Notify (push) Successful in 2s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Successful in 1m6s
Build & Deploy / 🏗️ Build (push) Successful in 1m35s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 41s
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
202
app/[locale]/standorte/[slug]/page.tsx
Normal file
202
app/[locale]/standorte/[slug]/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Container } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { standorteData, getStandortById } from '@/lib/standorte-data';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { MapPin, Phone, Mail, Navigation, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const locales = ['de', 'en'];
|
||||
const params: { locale: string; slug: string }[] = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
for (const standort of standorteData) {
|
||||
params.push({ locale, slug: standort.id });
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { locale, slug } = await params;
|
||||
if (locale !== 'de' && locale !== 'en') return {};
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) return {};
|
||||
|
||||
return {
|
||||
title: `${standort.name} | E-TIB Gruppe`,
|
||||
description: standort.description[locale],
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${locale}/standorte/${slug}`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/standorte/${slug}`,
|
||||
en: `${SITE_URL}/en/standorte/${slug}`,
|
||||
'x-default': `${SITE_URL}/en/standorte/${slug}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StandortDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
|
||||
const { locale, slug } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24 pt-32">
|
||||
<Container>
|
||||
{/* Header Section */}
|
||||
<div className="mb-12 md:mb-16">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-6">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{standort.type === 'hq' ? (safeLocale === 'de' ? 'Hauptsitz' : 'Headquarters') : (safeLocale === 'de' ? 'Niederlassung' : 'Branch')}</span>
|
||||
</div>
|
||||
<h1 className="font-heading text-4xl lg:text-5xl xl:text-6xl font-extrabold mb-6 leading-[1.1] text-neutral-dark text-balance">
|
||||
{standort.name}
|
||||
</h1>
|
||||
<p className="text-xl text-neutral-500 max-w-3xl leading-relaxed">
|
||||
{standort.description[safeLocale]}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
|
||||
{/* Main Content (Images & Description) */}
|
||||
<div className="lg:col-span-8 space-y-8">
|
||||
{/* Featured Image */}
|
||||
<div className="relative aspect-video w-full rounded-3xl overflow-hidden bg-neutral-900 shadow-xl border border-neutral-200">
|
||||
<Image
|
||||
src={standort.image}
|
||||
alt={standort.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Key Features */}
|
||||
<div className="bg-white rounded-3xl p-8 md:p-10 shadow-sm border border-neutral-100">
|
||||
<h2 className="text-2xl font-bold font-heading mb-6 text-neutral-dark">
|
||||
{safeLocale === 'de' ? 'Unsere Leistungen vor Ort' : 'Our Local Services'}
|
||||
</h2>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{standort.keyFeatures[safeLocale].map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
||||
<span className="text-neutral-700 font-medium">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Gallery (if any) */}
|
||||
{standort.gallery && standort.gallery.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{standort.gallery.map((img, idx) => (
|
||||
<div key={idx} className="relative aspect-[4/3] rounded-2xl overflow-hidden bg-neutral-100">
|
||||
<Image src={img} alt={`Gallery ${idx + 1}`} fill className="object-cover" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar (Contact & Address) */}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="sticky top-32 space-y-6">
|
||||
|
||||
{/* Contact Card */}
|
||||
<div className="bg-[#050B14] text-white rounded-3xl p-8 shadow-2xl relative overflow-hidden group">
|
||||
<AnimatedGlossyBorder opacity={0.5} className="z-10" />
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-primary/5 blur-3xl -translate-y-1/2 translate-x-1/3" />
|
||||
|
||||
<div className="relative z-20">
|
||||
<h3 className="text-xl font-bold font-heading mb-6 flex items-center gap-2">
|
||||
<Navigation className="w-5 h-5 text-primary" />
|
||||
{safeLocale === 'de' ? 'Kontakt & Anfahrt' : 'Contact & Directions'}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<MapPin className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||
</div>
|
||||
<div className="text-white font-medium">
|
||||
{standort.address.street}<br />
|
||||
{standort.address.city}
|
||||
</div>
|
||||
<TrackedLink
|
||||
href={`https://maps.google.com/?q=${encodeURIComponent(`${standort.address.street}, ${standort.address.city}`)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 mt-2 text-primary hover:text-white transition-colors text-sm font-bold"
|
||||
eventProperties={{ location: 'standort_maps_link', slug: standort.id }}
|
||||
>
|
||||
{safeLocale === 'de' ? 'In Google Maps öffnen' : 'Open in Google Maps'}
|
||||
<ArrowUpRight className="w-3.5 h-3.5" />
|
||||
</TrackedLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<Phone className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
{safeLocale === 'de' ? 'Telefon' : 'Phone'}
|
||||
</div>
|
||||
<a href={`tel:${standort.contact.phone.replace(/[^0-9+]/g, '')}`} className="text-white font-medium hover:text-primary transition-colors">
|
||||
{standort.contact.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<Mail className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
Email
|
||||
</div>
|
||||
<a href={`mailto:${standort.contact.email}`} className="text-white font-medium hover:text-primary transition-colors">
|
||||
{standort.contact.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user