337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
import { setRequestLocale } from 'next-intl/server';
|
|
import type { Metadata } from 'next';
|
|
import Link from 'next/link';
|
|
import { ArrowLeft, ArrowRight, CheckCircle, Brain, Mic, Workflow, Cloud, Code, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
|
|
import { notFound } from 'next/navigation';
|
|
import { allServices, getServiceBySlug, getAllServiceSlugs } from '@/data/services';
|
|
import { BreadcrumbJsonLd, ServiceJsonLd, FAQJsonLd } from '@/components/seo';
|
|
import { type Locale, locales } from '@/i18n/config';
|
|
|
|
type Props = {
|
|
params: Promise<{ locale: string; slug: string }>;
|
|
};
|
|
|
|
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
Brain,
|
|
Mic,
|
|
Workflow,
|
|
Cloud,
|
|
Code,
|
|
Bot,
|
|
Building,
|
|
ShoppingCart,
|
|
Factory,
|
|
};
|
|
|
|
export async function generateStaticParams() {
|
|
const slugs = getAllServiceSlugs();
|
|
return locales.flatMap((locale) =>
|
|
slugs.map((slug) => ({
|
|
locale,
|
|
slug,
|
|
}))
|
|
);
|
|
}
|
|
|
|
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { locale, slug } = await params;
|
|
const service = getServiceBySlug(slug);
|
|
|
|
if (!service) {
|
|
return { title: 'Service Not Found' };
|
|
}
|
|
|
|
const title = locale === 'de'
|
|
? `${service.name.de} - Köln & Deutschland | Damjan Savić`
|
|
: locale === 'sr'
|
|
? `${service.name.sr} - Keln & Nemačka | Damjan Savić`
|
|
: `${service.name.en} | Remote AI Developer for USA, UK & Europe`;
|
|
|
|
const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`;
|
|
|
|
// Use seoKeywords if available, otherwise fall back to technologies
|
|
const seoKw = service.seoKeywords?.[locale as Locale] || [];
|
|
const allKeywords = [...service.technologies, ...seoKw];
|
|
if (locale === 'en') {
|
|
allKeywords.push('Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK');
|
|
}
|
|
|
|
return {
|
|
title,
|
|
description: service.description[locale as Locale] || service.description.de,
|
|
keywords: allKeywords.join(', '),
|
|
alternates: {
|
|
canonical: `${BASE_URL}${localePath}`,
|
|
languages: {
|
|
'x-default': `${BASE_URL}/de/leistungen/${slug}`,
|
|
de: `${BASE_URL}/de/leistungen/${slug}`,
|
|
en: `${BASE_URL}/en/leistungen/${slug}`,
|
|
sr: `${BASE_URL}/sr/leistungen/${slug}`,
|
|
},
|
|
},
|
|
openGraph: {
|
|
title,
|
|
description: service.shortDescription[locale as Locale] || service.shortDescription.de,
|
|
url: `${BASE_URL}${localePath}`,
|
|
type: 'website',
|
|
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
|
|
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
|
|
images: [
|
|
{
|
|
url: `${BASE_URL}/images/og-image.avif`,
|
|
width: 1200,
|
|
height: 630,
|
|
alt: `${service.name[locale as Locale]} - Damjan Savić`,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function ServicePage({ params }: Props) {
|
|
const { locale, slug } = await params;
|
|
setRequestLocale(locale);
|
|
|
|
const service = getServiceBySlug(slug);
|
|
|
|
if (!service) {
|
|
notFound();
|
|
}
|
|
|
|
const IconComponent = iconMap[service.icon] || Brain;
|
|
|
|
const breadcrumbItems = [
|
|
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
|
|
{ name: locale === 'de' ? 'Leistungen' : locale === 'sr' ? 'Usluge' : 'Services', url: `/${locale}/leistungen` },
|
|
{ name: service.name[locale as Locale], url: `/${locale}/leistungen/${slug}` },
|
|
];
|
|
|
|
// Get related services (excluding current)
|
|
const relatedServices = allServices.filter((s) => s.slug !== slug).slice(0, 3);
|
|
|
|
// FAQ items for this service
|
|
const serviceFaqItems = locale === 'de' ? [
|
|
{
|
|
question: `Was kostet ${service.name.de}?`,
|
|
answer: `Die Kosten für ${service.name.de} variieren je nach Projektumfang und Komplexität. Ich erstelle gerne ein individuelles Angebot nach einem kostenlosen Erstgespräch.`,
|
|
},
|
|
{
|
|
question: `Welche Technologien nutzen Sie für ${service.name.de}?`,
|
|
answer: `Für ${service.name.de} setze ich auf bewährte Technologien: ${service.technologies.join(', ')}. Die genaue Auswahl hängt von Ihren Anforderungen ab.`,
|
|
},
|
|
{
|
|
question: `Wie lange dauert ein typisches ${service.name.de}-Projekt?`,
|
|
answer: `Die Dauer eines ${service.name.de}-Projekts hängt vom Umfang ab. Einfache Projekte können in 2-4 Wochen abgeschlossen werden, komplexere Lösungen in 6-12 Wochen.`,
|
|
},
|
|
] : locale === 'en' ? [
|
|
{
|
|
question: `How much does ${service.name.en} cost?`,
|
|
answer: `The cost of ${service.name.en} varies depending on project scope and complexity. I'm happy to provide a custom quote after a free initial consultation.`,
|
|
},
|
|
{
|
|
question: `What technologies do you use for ${service.name.en}?`,
|
|
answer: `For ${service.name.en}, I use proven technologies: ${service.technologies.join(', ')}. The exact selection depends on your requirements.`,
|
|
},
|
|
{
|
|
question: `How long does a typical ${service.name.en} project take?`,
|
|
answer: `The duration of a ${service.name.en} project depends on scope. Simple projects can be completed in 2-4 weeks, more complex solutions in 6-12 weeks.`,
|
|
},
|
|
] : [
|
|
{
|
|
question: `Koliko košta ${service.name.sr}?`,
|
|
answer: `Troškovi za ${service.name.sr} variraju u zavisnosti od obima i kompleksnosti projekta. Rado ću vam dati individualizovanu ponudu nakon besplatnog inicijalnog razgovora.`,
|
|
},
|
|
{
|
|
question: `Koje tehnologije koristite za ${service.name.sr}?`,
|
|
answer: `Za ${service.name.sr} koristim proverene tehnologije: ${service.technologies.join(', ')}. Tačan izbor zavisi od vaših zahteva.`,
|
|
},
|
|
{
|
|
question: `Koliko traje tipičan ${service.name.sr} projekat?`,
|
|
answer: `Trajanje ${service.name.sr} projekta zavisi od obima. Jednostavni projekti mogu se završiti za 2-4 nedelje, kompleksnija rešenja za 6-12 nedelja.`,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
|
<ServiceJsonLd
|
|
name={service.name[locale as Locale]}
|
|
description={service.description[locale as Locale]}
|
|
url={`/${locale}/leistungen/${slug}`}
|
|
locale={locale as Locale}
|
|
/>
|
|
<FAQJsonLd items={serviceFaqItems} locale={locale as Locale} />
|
|
|
|
{/* Hero Section */}
|
|
<section className="py-24">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
{/* Back Link */}
|
|
<Link
|
|
href={`/${locale}/leistungen`}
|
|
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
{locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'}
|
|
</Link>
|
|
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="p-4 bg-card/50 rounded-xl">
|
|
<IconComponent className="h-8 w-8 text-foreground" />
|
|
</div>
|
|
</div>
|
|
|
|
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6">
|
|
{service.name[locale as Locale]}
|
|
</h1>
|
|
|
|
<p className="text-xl text-muted-foreground mb-8">
|
|
{service.description[locale as Locale]}
|
|
</p>
|
|
|
|
{/* Technologies */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{service.technologies.map((tech) => (
|
|
<span
|
|
key={tech}
|
|
className="px-3 py-1 bg-card/50 border border-border text-sm text-foreground/80 rounded-full"
|
|
>
|
|
{tech}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Features Section */}
|
|
<section className="py-16 bg-card/20">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h2 className="text-2xl font-bold text-foreground mb-8">
|
|
{locale === 'de' ? 'Was ich anbiete' : locale === 'sr' ? 'Šta nudim' : 'What I offer'}
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{service.features[locale as Locale].map((feature, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-start gap-3 p-4 bg-card/30 rounded-lg"
|
|
>
|
|
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
|
<span className="text-foreground/80">{feature}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Use Cases Section */}
|
|
<section className="py-16">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h2 className="text-2xl font-bold text-foreground mb-8">
|
|
{locale === 'de' ? 'Anwendungsbeispiele' : locale === 'sr' ? 'Primeri primene' : 'Use Cases'}
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{service.useCases[locale as Locale].map((useCase, index) => (
|
|
<div
|
|
key={index}
|
|
className="p-6 bg-card/30 border border-border rounded-xl"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-accent rounded-full flex items-center justify-center text-sm font-semibold text-foreground">
|
|
{index + 1}
|
|
</div>
|
|
<span className="text-foreground/90">{useCase}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Related Services */}
|
|
<section className="py-16 bg-card/20">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h2 className="text-2xl font-bold text-foreground mb-8">
|
|
{locale === 'de' ? 'Weitere Leistungen' : locale === 'sr' ? 'Ostale usluge' : 'Other Services'}
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{relatedServices.map((relatedService) => {
|
|
const RelatedIcon = iconMap[relatedService.icon] || Brain;
|
|
return (
|
|
<Link
|
|
key={relatedService.slug}
|
|
href={`/${locale}/leistungen/${relatedService.slug}`}
|
|
className="group p-6 bg-card/30 border border-border rounded-xl hover:border-accent transition-all duration-300"
|
|
>
|
|
<div className="p-2 bg-card/50 rounded-lg w-fit mb-3">
|
|
<RelatedIcon className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
{relatedService.name[locale as Locale]}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{relatedService.shortDescription[locale as Locale]}
|
|
</p>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* FAQ Section */}
|
|
<section className="py-16">
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h2 className="text-2xl font-bold text-foreground text-center mb-8">
|
|
{locale === 'de'
|
|
? `Häufige Fragen zu ${service.name.de}`
|
|
: locale === 'sr'
|
|
? `Česta pitanja o ${service.name.sr}`
|
|
: `FAQ about ${service.name.en}`}
|
|
</h2>
|
|
<div className="space-y-4">
|
|
{serviceFaqItems.map((item, index) => (
|
|
<div
|
|
key={index}
|
|
className="bg-card/30 border border-border rounded-lg p-6"
|
|
>
|
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
{item.question}
|
|
</h3>
|
|
<p className="text-muted-foreground">{item.answer}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA Section */}
|
|
<section className="py-24">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
<h2 className="text-3xl font-bold text-foreground mb-4">
|
|
{locale === 'de'
|
|
? `${service.name.de} für Ihr Unternehmen?`
|
|
: locale === 'sr'
|
|
? `${service.name.sr} za vašu kompaniju?`
|
|
: `${service.name.en} for your business?`}
|
|
</h2>
|
|
<p className="text-muted-foreground mb-8">
|
|
{locale === 'de'
|
|
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
|
|
: locale === 'sr'
|
|
? 'Razgovarajmo o tome kako vam mogu pomoći.'
|
|
: "Let's discuss how I can help you."}
|
|
</p>
|
|
<Link
|
|
href={`/${locale}/contact`}
|
|
className="inline-flex items-center px-8 py-4 bg-primary text-background font-semibold rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
{locale === 'de' ? 'Projekt besprechen' : locale === 'sr' ? 'Razgovarajte o projektu' : 'Discuss project'}
|
|
<ArrowRight className="h-5 w-5 ml-2" />
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|