Initial commit: Portfolio Website

Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 15:07:20 +01:00
co-authored by Claude Opus 4.5
commit e1bbe5455d
315 changed files with 94124 additions and 0 deletions
+257
View File
@@ -0,0 +1,257 @@
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 } 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}`;
// Enhanced keywords for English
const enhancedKeywords = locale === 'en'
? [...service.technologies, 'Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK'].join(', ')
: service.technologies.join(', ');
return {
title,
description: service.description[locale as Locale] || service.description.de,
keywords: enhancedKeywords,
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')),
},
};
}
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);
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}
/>
{/* 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-zinc-400 hover:text-white 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-zinc-800/50 rounded-xl">
<IconComponent className="h-8 w-8 text-white" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
{service.name[locale as Locale]}
</h1>
<p className="text-xl text-zinc-400 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-zinc-800/50 border border-zinc-700 text-sm text-zinc-300 rounded-full"
>
{tech}
</span>
))}
</div>
</div>
</section>
{/* Features Section */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white 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-zinc-800/30 rounded-lg"
>
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-300">{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-white 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-zinc-800/30 border border-zinc-800 rounded-xl"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-zinc-700 rounded-full flex items-center justify-center text-sm font-semibold text-white">
{index + 1}
</div>
<span className="text-zinc-200">{useCase}</span>
</div>
</div>
))}
</div>
</div>
</section>
{/* Related Services */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white 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-zinc-800/30 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-all duration-300"
>
<div className="p-2 bg-zinc-800/50 rounded-lg w-fit mb-3">
<RelatedIcon className="h-5 w-5 text-zinc-400 group-hover:text-white transition-colors" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">
{relatedService.name[locale as Locale]}
</h3>
<p className="text-sm text-zinc-400">
{relatedService.shortDescription[locale as Locale]}
</p>
</Link>
);
})}
</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-white 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-zinc-400 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-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 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>
);
}
+479
View File
@@ -0,0 +1,479 @@
import { setRequestLocale } from 'next-intl/server';
import { getTranslations } from 'next-intl/server';
import type { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import { Brain, Mic, Workflow, Cloud, Code, ArrowRight, MapPin, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
import { services, techServices, industryServices } from '@/data/services';
import { cities } from '@/data/cities';
import { BreadcrumbJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale } from '@/i18n/config';
type Props = {
params: Promise<{ locale: string }>;
};
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Brain,
Mic,
Workflow,
Cloud,
Code,
Bot,
Building,
ShoppingCart,
Factory,
};
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const titles: Record<string, string> = {
de: 'Leistungen - KI-Entwicklung, Voice AI & Automatisierung | Damjan Savić',
en: 'AI Development Services | Voice AI, Automation & SaaS | Remote Developer',
sr: 'Usluge - AI Razvoj, Voice AI & Automatizacija | Damjan Savić',
};
const descriptions: Record<string, string> = {
de: 'Professionelle KI-Entwicklung, Voice AI, Prozessautomatisierung und SaaS-Entwicklung. Maßgeschneiderte Lösungen für Unternehmen in Köln, Düsseldorf, Frankfurt und ganz Deutschland.',
en: 'Expert AI development services for businesses in USA, UK & Europe. Specialized in AI agents, Voice AI, n8n automation, and custom SaaS solutions. Remote-first, German engineering quality. Get a free consultation.',
sr: 'Profesionalni AI razvoj, Voice AI, n8n automatizacija i SaaS razvoj. Prilagođena rešenja za kompanije iz Srbije (Beograd, Novi Sad, Niš) i srpsku dijasporu u Nemačkoj, Austriji i Švajcarskoj.',
};
const keywords: Record<string, string[]> = {
de: [
'KI-Entwicklung',
'Voice AI',
'Prozessautomatisierung',
'SaaS Entwicklung',
'n8n',
'GPT-4 Integration',
'KI-Agenten',
'Automatisierung Köln',
'AI Developer Deutschland',
],
en: [
'AI Development Services',
'Voice AI Developer',
'Process Automation',
'SaaS Development',
'n8n Automation',
'GPT-4 Integration',
'AI Agents',
'ChatGPT Integration',
'Remote AI Developer',
'AI Developer USA',
'AI Developer UK',
'AI Developer London',
'AI Developer New York',
'Freelance AI Developer',
'AI Consultant',
'LLM Integration',
'Custom AI Solutions',
],
sr: [
'AI Razvoj Srbija',
'Voice AI Srbija',
'Automatizacija procesa',
'SaaS Razvoj Srbija',
'n8n Automatizacija',
'GPT-4 Integracija',
'AI Agenti Beograd',
'Fullstack Programer Srbija',
'AI Developer Novi Sad',
'Python Programer Srbija',
'Remote Developer Dijaspora',
'Softverski Razvoj Beograd',
],
};
const localePath = locale === 'de' ? '/leistungen' : `/${locale}/leistungen`;
return {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de,
keywords: keywords[locale] || keywords.de,
alternates: {
canonical: `${BASE_URL}${localePath}`,
languages: {
'x-default': `${BASE_URL}/leistungen`,
de: `${BASE_URL}/leistungen`,
en: `${BASE_URL}/en/leistungen`,
sr: `${BASE_URL}/sr/leistungen`,
},
},
openGraph: {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.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')),
},
};
}
export default async function ServicesPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const breadcrumbItems = [
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
{ name: locale === 'de' ? 'Leistungen' : locale === 'sr' ? 'Usluge' : 'Services', url: `/${locale}/leistungen` },
];
const faqItems = locale === 'de' ? [
{
question: 'Welche KI-Technologien setzen Sie ein?',
answer: 'Ich arbeite mit führenden LLM-Anbietern wie OpenAI (GPT-4, GPT-4o), Anthropic (Claude) und Open-Source-Modellen wie Llama. Für Voice AI nutze ich Vapi, ElevenLabs und Whisper.',
},
{
question: 'Wie lange dauert die Entwicklung eines MVP?',
answer: 'Ein typisches MVP kann in 4-8 Wochen entwickelt werden, abhängig vom Umfang und den Anforderungen des Projekts. Komplexere SaaS-Produkte benötigen 8-12 Wochen.',
},
{
question: 'Arbeiten Sie auch mit Unternehmen außerhalb von Köln?',
answer: 'Ja, ich arbeite remote mit Unternehmen in ganz Deutschland (Düsseldorf, Frankfurt, München, Berlin, Hamburg, Stuttgart) und international. Die meisten meiner Projekte werden vollständig remote abgewickelt.',
},
{
question: 'Was kostet eine KI-Automatisierung?',
answer: 'Die Kosten variieren je nach Komplexität. Einfache n8n-Workflows starten ab ca. 1.500€, während komplexe KI-Agenten-Systeme zwischen 5.000€ und 25.000€ kosten können. Ich erstelle gerne ein individuelles Angebot.',
},
{
question: 'Sind Ihre Lösungen DSGVO-konform?',
answer: 'Ja, Datenschutz hat höchste Priorität. Ich setze auf Self-Hosted-Lösungen (n8n, Supabase) und sichere API-Anbindungen. Bei Bedarf können alle Daten in deutschen Rechenzentren verarbeitet werden.',
},
{
question: 'Bieten Sie auch Support und Wartung an?',
answer: 'Ja, ich biete flexible Support-Pakete für Wartung, Updates und Weiterentwicklung. Nach dem Launch bin ich weiterhin für Sie da, um Ihre Lösung optimal zu betreuen.',
},
] : locale === 'en' ? [
{
question: 'Do you work with clients in the USA and UK?',
answer: 'Yes! I work remotely with clients worldwide, including the USA (New York, Los Angeles, San Francisco, Miami, Chicago) and UK (London). Most of my international projects are handled completely remotely with regular video calls to accommodate different time zones.',
},
{
question: 'What AI technologies do you use?',
answer: 'I work with leading LLM providers like OpenAI (GPT-4, GPT-4o), Anthropic (Claude) and open-source models like Llama. For Voice AI, I use Vapi, ElevenLabs and Whisper. I also specialize in n8n for workflow automation.',
},
{
question: 'How long does MVP development take?',
answer: 'A typical MVP can be developed in 4-8 weeks, depending on the scope and requirements of the project. More complex SaaS products require 8-12 weeks. I follow agile methodologies with weekly demos.',
},
{
question: 'What are your rates for international clients?',
answer: 'I offer competitive rates for international clients. Simple n8n automations start at around $1,600, while complex AI agent systems range from $5,500 to $27,000. I provide detailed proposals with fixed-price options for most projects.',
},
{
question: 'How do you handle time zone differences?',
answer: 'I am flexible with communication and can accommodate US Eastern, US Pacific, and UK time zones. I typically overlap 4-6 hours with US clients and have full overlap with UK working hours. Async communication via Slack/Email works great for most projects.',
},
{
question: 'Are your solutions GDPR and data privacy compliant?',
answer: 'Yes, data protection is a top priority. I am well-versed in GDPR, CCPA, and international data privacy regulations. I use self-hosted solutions (n8n, Supabase) and can ensure data stays in specific regions (US, EU) as needed.',
},
{
question: 'What makes you different from US/UK-based developers?',
answer: 'I combine German engineering precision with competitive rates and strong English communication. You get enterprise-quality solutions at freelancer prices, plus experience working with international Fortune 500 companies and startups alike.',
},
{
question: 'Do you offer ongoing support and maintenance?',
answer: 'Yes, I offer flexible support packages including 24/7 monitoring for critical systems, regular maintenance windows, and priority support SLAs. I can work within your existing ticketing and communication systems.',
},
] : [
{
question: 'Koje AI tehnologije koristite?',
answer: 'Radim sa vodećim LLM provajderima kao što su OpenAI (GPT-4, GPT-4o), Anthropic (Claude) i open-source modeli kao Llama. Za Voice AI koristim Vapi, ElevenLabs i Whisper.',
},
{
question: 'Koliko traje razvoj MVP-a?',
answer: 'Tipičan MVP može se razviti za 4-8 nedelja, u zavisnosti od obima i zahteva projekta. Kompleksniji SaaS proizvodi zahtevaju 8-12 nedelja.',
},
{
question: 'Da li radite i sa kompanijama van Kelna?',
answer: 'Da, radim remote sa kompanijama širom Nemačke (Diseldorf, Frankfurt, Minhen, Berlin, Hamburg, Štutgart) i međunarodno. Većina mojih projekata se obrađuje potpuno remote.',
},
{
question: 'Koliko košta AI automatizacija?',
answer: 'Troškovi variraju u zavisnosti od kompleksnosti. Jednostavni n8n radni tokovi počinju od oko 1.500€, dok kompleksni sistemi AI agenata mogu koštati između 5.000€ i 25.000€. Rado ću vam dati individualizovanu ponudu.',
},
{
question: 'Da li su vaša rešenja usklađena sa GDPR-om?',
answer: 'Da, zaštita podataka ima najviši prioritet. Oslanjam se na self-hosted rešenja (n8n, Supabase) i sigurne API konekcije. Po potrebi, svi podaci mogu se obrađivati u nemačkim data centrima.',
},
{
question: 'Da li nudite podršku i održavanje?',
answer: 'Da, nudim fleksibilne pakete podrške za održavanje, ažuriranja i dalji razvoj. Nakon lansiranja, tu sam za vas da optimalno podržim vaše rešenje.',
},
];
const pageTitle = locale === 'de' ? 'Meine Leistungen' : locale === 'sr' ? 'Moje Usluge' : 'My Services';
const pageSubtitle = locale === 'de'
? 'Von KI-Entwicklung über Voice AI bis zur Prozessautomatisierung - maßgeschneiderte Lösungen für Ihr Unternehmen.'
: locale === 'sr'
? 'Od AI razvoja preko Voice AI do automatizacije procesa - prilagođena rešenja za vašu kompaniju.'
: 'From AI development to Voice AI to process automation - customized solutions for your business.';
const citiesTitle = locale === 'de' ? 'Standorte' : locale === 'sr' ? 'Lokacije' : 'Locations';
const citiesSubtitle = locale === 'de'
? 'Ich arbeite mit Unternehmen in folgenden Städten:'
: locale === 'sr'
? 'Radim sa kompanijama u sledećim gradovima:'
: 'I work with companies in the following cities:';
// Section titles for service categories
const coreServicesTitle = locale === 'de' ? 'Kern-Leistungen' : locale === 'sr' ? 'Osnovne Usluge' : 'Core Services';
const techServicesTitle = locale === 'de' ? 'Technologie-Spezialisierungen' : locale === 'sr' ? 'Tehnološke Specijalizacije' : 'Technology Specializations';
const industryServicesTitle = locale === 'de' ? 'Branchen-Lösungen' : locale === 'sr' ? 'Industrijska Rešenja' : 'Industry Solutions';
return (
<div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<FAQJsonLd items={faqItems} locale={locale as Locale} />
{/* Hero Section */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="text-center lg:text-left">
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
{pageTitle}
</h1>
<p className="text-xl text-zinc-400 max-w-2xl">
{pageSubtitle}
</p>
</div>
<div className="relative aspect-video rounded-2xl overflow-hidden">
<Image
src="/images/gallery/walking-phone.avif"
alt="Damjan Savić - Professional Services"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover"
/>
</div>
</div>
</div>
</section>
{/* Core Services Grid */}
<section className="pb-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{coreServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((service) => {
const IconComponent = iconMap[service.icon] || Brain;
return (
<Link
key={service.slug}
href={`/${locale}/leistungen/${service.slug}`}
className="group bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 hover:border-zinc-700 transition-all duration-300"
>
<div className="p-3 bg-zinc-800/50 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-400 group-hover:text-white transition-colors" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">
{service.name[locale as Locale]}
</h3>
<p className="text-zinc-400 mb-4">
{service.shortDescription[locale as Locale]}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => (
<span
key={tech}
className="px-2 py-1 bg-zinc-800/50 text-xs text-zinc-400 rounded"
>
{tech}
</span>
))}
</div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors">
<span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span>
<ArrowRight className="h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform" />
</div>
</Link>
);
})}
</div>
</div>
</section>
{/* Technology Services Grid */}
<section className="pb-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{techServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{techServices.map((service) => {
const IconComponent = iconMap[service.icon] || Brain;
return (
<Link
key={service.slug}
href={`/${locale}/leistungen/${service.slug}`}
className="group bg-gradient-to-br from-zinc-800/40 to-zinc-800/20 border border-zinc-700/50 rounded-xl p-6 hover:border-zinc-600 transition-all duration-300"
>
<div className="p-3 bg-zinc-700/50 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-300 group-hover:text-white transition-colors" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">
{service.name[locale as Locale]}
</h3>
<p className="text-zinc-400 mb-4">
{service.shortDescription[locale as Locale]}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => (
<span
key={tech}
className="px-2 py-1 bg-zinc-700/50 text-xs text-zinc-300 rounded"
>
{tech}
</span>
))}
</div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors">
<span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span>
<ArrowRight className="h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform" />
</div>
</Link>
);
})}
</div>
</div>
</section>
{/* Industry Services Grid */}
<section className="pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{industryServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{industryServices.map((service) => {
const IconComponent = iconMap[service.icon] || Brain;
return (
<Link
key={service.slug}
href={`/${locale}/leistungen/${service.slug}`}
className="group bg-gradient-to-br from-zinc-800/30 to-zinc-900/30 border border-zinc-800 rounded-xl p-6 hover:border-zinc-600 transition-all duration-300"
>
<div className="p-3 bg-zinc-800/70 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-400 group-hover:text-white transition-colors" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">
{service.name[locale as Locale]}
</h3>
<p className="text-zinc-400 mb-4">
{service.shortDescription[locale as Locale]}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => (
<span
key={tech}
className="px-2 py-1 bg-zinc-800/50 text-xs text-zinc-400 rounded"
>
{tech}
</span>
))}
</div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors">
<span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span>
<ArrowRight className="h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform" />
</div>
</Link>
);
})}
</div>
</div>
</section>
{/* Cities Section */}
<section className="py-24 bg-zinc-800/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-white mb-4">{citiesTitle}</h2>
<p className="text-zinc-400">{citiesSubtitle}</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{cities.map((city) => (
<Link
key={city.slug}
href={`/${locale}/leistungen/standort/${city.slug}`}
className="group flex items-center gap-2 p-4 bg-zinc-800/30 border border-zinc-800 rounded-lg hover:border-zinc-700 transition-all duration-300"
>
<MapPin className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors" />
<span className="text-zinc-300 group-hover:text-white transition-colors">
{city.name[locale as Locale]}
</span>
</Link>
))}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-24">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-white text-center mb-12">
{locale === 'de' ? 'Häufige Fragen' : locale === 'sr' ? 'Česta pitanja' : 'Frequently Asked Questions'}
</h2>
<div className="space-y-6">
{faqItems.map((item, index) => (
<div
key={index}
className="bg-zinc-800/30 border border-zinc-800 rounded-lg p-6"
>
<h3 className="text-lg font-semibold text-white mb-2">
{item.question}
</h3>
<p className="text-zinc-400">{item.answer}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24 bg-zinc-800/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="relative aspect-video rounded-2xl overflow-hidden order-2 lg:order-1">
<Image
src="/images/gallery/handshake.avif"
alt="Business Partnership"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover"
/>
</div>
<div className="text-center lg:text-left order-1 lg:order-2">
<h2 className="text-3xl font-bold text-white mb-4">
{locale === 'de' ? 'Bereit für Ihr Projekt?' : locale === 'sr' ? 'Spremni za vaš projekat?' : 'Ready for your project?'}
</h2>
<p className="text-zinc-400 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-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors"
>
{locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'}
<ArrowRight className="h-5 w-5 ml-2" />
</Link>
</div>
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,384 @@
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';
import Link from 'next/link';
import { ArrowLeft, ArrowRight, MapPin, Brain, Mic, Workflow, Cloud, Code, CheckCircle } from 'lucide-react';
import { notFound } from 'next/navigation';
import { cities, getCityBySlug, getAllCitySlugs } from '@/data/cities';
import { services } from '@/data/services';
import { BreadcrumbJsonLd, ServiceJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale, locales } from '@/i18n/config';
type Props = {
params: Promise<{ locale: string; city: string }>;
};
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Brain,
Mic,
Workflow,
Cloud,
Code,
};
export async function generateStaticParams() {
const slugs = getAllCitySlugs();
return locales.flatMap((locale) =>
slugs.map((city) => ({
locale,
city,
}))
);
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, city: citySlug } = await params;
const city = getCityBySlug(citySlug);
if (!city) {
return { title: 'City Not Found' };
}
const titles: Record<string, string> = {
de: `KI Entwickler ${city.name.de} - AI & Automatisierung | Damjan Savić`,
en: `AI Developer ${city.name.en} - AI & Automation | Damjan Savić`,
sr: `AI Developer ${city.name.sr} - AI & Automatizacija | Damjan Savić`,
};
const descriptions: Record<string, string> = {
de: `Professionelle KI-Entwicklung und Automatisierung in ${city.name.de}. Voice AI, Prozessautomatisierung, SaaS-Entwicklung. Ihr AI & Automation Specialist für ${city.name.de} und ${city.region}.`,
en: `Professional AI development and automation in ${city.name.en}. Voice AI, process automation, SaaS development. Your AI & Automation Specialist for ${city.name.en} and ${city.region}.`,
sr: `Profesionalni AI razvoj i automatizacija u ${city.name.sr}. Voice AI, automatizacija procesa, SaaS razvoj. Vaš AI & Automation Specialist za ${city.name.sr} i ${city.region}.`,
};
return {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de,
keywords: city.keywords[locale as Locale]?.join(', ') || city.keywords.de.join(', '),
alternates: {
canonical: `/${locale}/leistungen/standort/${citySlug}`,
languages: {
de: `/de/leistungen/standort/${citySlug}`,
en: `/en/leistungen/standort/${citySlug}`,
sr: `/sr/leistungen/standort/${citySlug}`,
},
},
openGraph: {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de,
type: 'website',
},
};
}
export default async function CityPage({ params }: Props) {
const { locale, city: citySlug } = await params;
setRequestLocale(locale);
const city = getCityBySlug(citySlug);
if (!city) {
notFound();
}
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: city.name[locale as Locale], url: `/${locale}/leistungen/standort/${citySlug}` },
];
const faqItems = locale === 'de' ? [
{
question: `Bieten Sie Ihre Dienstleistungen auch vor Ort in ${city.name.de} an?`,
answer: `Ja, ich arbeite mit Unternehmen in ${city.name.de} und Umgebung. Die meisten Projekte wickle ich remote ab, aber persönliche Meetings in ${city.name.de} sind bei Bedarf möglich.`,
},
{
question: `Welche Branchen bedienen Sie in ${city.name.de}?`,
answer: `Ich arbeite branchenübergreifend mit Startups, mittelständischen Unternehmen und Konzernen in ${city.name.de}. Besonders häufig sind Projekte in den Bereichen E-Commerce, Fintech, Healthcare und B2B-Services.`,
},
{
question: `Wie schnell können Sie mit einem Projekt in ${city.name.de} starten?`,
answer: 'Je nach aktuellem Projektstand kann ich meist innerhalb von 1-2 Wochen mit neuen Projekten beginnen. Für dringende Anfragen stehe ich auch kurzfristiger zur Verfügung.',
},
] : locale === 'en' ? [
{
question: `Do you also offer your services on-site in ${city.name.en}?`,
answer: `Yes, I work with companies in ${city.name.en} and the surrounding area. Most projects are handled remotely, but in-person meetings in ${city.name.en} are possible if needed.`,
},
{
question: `Which industries do you serve in ${city.name.en}?`,
answer: `I work across industries with startups, medium-sized companies and corporations in ${city.name.en}. Projects are particularly common in e-commerce, fintech, healthcare and B2B services.`,
},
{
question: `How quickly can you start a project in ${city.name.en}?`,
answer: 'Depending on my current project status, I can usually start new projects within 1-2 weeks. I am also available at shorter notice for urgent requests.',
},
] : [
{
question: `Da li nudite usluge i na licu mesta u ${city.name.sr}?`,
answer: `Da, radim sa kompanijama u ${city.name.sr} i okolini. Većina projekata se obrađuje remote, ali lični sastanci u ${city.name.sr} su mogući po potrebi.`,
},
{
question: `Koje industrije opslužujete u ${city.name.sr}?`,
answer: `Radim u svim industrijama sa startapima, srednjim preduzećima i korporacijama u ${city.name.sr}. Projekti su posebno česti u e-commerce, fintech, zdravstvu i B2B uslugama.`,
},
{
question: `Koliko brzo možete započeti projekat u ${city.name.sr}?`,
answer: 'U zavisnosti od trenutnog statusa projekta, obično mogu započeti nove projekte u roku od 1-2 nedelje. Dostupan sam i u kraćem roku za hitne zahteve.',
},
];
const pageTitle = locale === 'de'
? `KI Entwickler in ${city.name.de}`
: locale === 'sr'
? `AI Developer u ${city.name.sr}`
: `AI Developer in ${city.name.en}`;
// Get other cities for internal linking
const otherCities = cities.filter((c) => c.slug !== citySlug).slice(0, 4);
return (
<div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<ServiceJsonLd
name={pageTitle}
description={city.description[locale as Locale]}
url={`/${locale}/leistungen/standort/${citySlug}`}
locale={locale as Locale}
areaServed={[city.name.de, ...city.nearbyAreas]}
/>
<FAQJsonLd items={faqItems} 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-zinc-400 hover:text-white 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-2 text-zinc-400 mb-4">
<MapPin className="h-5 w-5" />
<span>{city.region}, Deutschland</span>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
{pageTitle}
</h1>
<p className="text-xl text-zinc-400 mb-8">
{city.description[locale as Locale]}
</p>
{/* Keywords as tags */}
<div className="flex flex-wrap gap-2">
{city.keywords[locale as Locale]?.slice(0, 5).map((keyword) => (
<span
key={keyword}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-700 text-sm text-zinc-300 rounded-full"
>
{keyword}
</span>
))}
</div>
</div>
</section>
{/* Services Section */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">
{locale === 'de'
? `Meine Leistungen in ${city.name.de}`
: locale === 'sr'
? `Moje usluge u ${city.name.sr}`
: `My services in ${city.name.en}`}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{services.map((service) => {
const IconComponent = iconMap[service.icon] || Brain;
return (
<Link
key={service.slug}
href={`/${locale}/leistungen/${service.slug}`}
className="group flex items-start gap-4 p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-all duration-300"
>
<div className="p-2 bg-zinc-800/50 rounded-lg">
<IconComponent className="h-5 w-5 text-zinc-400 group-hover:text-white transition-colors" />
</div>
<div>
<h3 className="text-lg font-semibold text-white mb-1">
{service.name[locale as Locale]}
</h3>
<p className="text-sm text-zinc-400">
{service.shortDescription[locale as Locale]}
</p>
</div>
</Link>
);
})}
</div>
</div>
</section>
{/* Why Choose Me 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-white mb-8">
{locale === 'de'
? `Warum mit mir arbeiten in ${city.name.de}?`
: locale === 'sr'
? `Zašto raditi sa mnom u ${city.name.sr}?`
: `Why work with me in ${city.name.en}?`}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{(locale === 'de'
? [
'Über 7 Jahre Erfahrung in der Softwareentwicklung',
'Spezialisiert auf KI und Automatisierung',
'Remote-Arbeit mit persönlichen Meetings möglich',
'Klare Kommunikation auf Deutsch und Englisch',
'Flexible Zusammenarbeit und transparente Preise',
'Nachweisbare Erfolge in ähnlichen Projekten',
]
: locale === 'en'
? [
'Over 7 years of software development experience',
'Specialized in AI and automation',
'Remote work with in-person meetings possible',
'Clear communication in German and English',
'Flexible collaboration and transparent pricing',
'Proven success in similar projects',
]
: [
'Preko 7 godina iskustva u razvoju softvera',
'Specijalizovan za AI i automatizaciju',
'Remote rad sa mogućnošću ličnih sastanaka',
'Jasna komunikacija na nemačkom i engleskom',
'Fleksibilna saradnja i transparentne cene',
'Dokazani uspesi u sličnim projektima',
]
).map((point, index) => (
<div
key={index}
className="flex items-start gap-3 p-4 bg-zinc-800/30 rounded-lg"
>
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-300">{point}</span>
</div>
))}
</div>
</div>
</section>
{/* Nearby Areas */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-4">
{locale === 'de'
? 'Auch verfügbar in der Region'
: locale === 'sr'
? 'Takođe dostupno u regionu'
: 'Also available in the region'}
</h2>
<p className="text-zinc-400 mb-6">
{locale === 'de'
? `Neben ${city.name.de} arbeite ich auch mit Unternehmen in:`
: locale === 'sr'
? `Pored ${city.name.sr}, radim i sa kompanijama u:`
: `In addition to ${city.name.en}, I also work with companies in:`}
</p>
<div className="flex flex-wrap gap-2">
{city.nearbyAreas.map((area) => (
<span
key={area}
className="px-4 py-2 bg-zinc-800/50 border border-zinc-700 text-zinc-300 rounded-lg"
>
{area}
</span>
))}
</div>
</div>
</section>
{/* Other Cities */}
<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-white mb-8">
{locale === 'de' ? 'Weitere Standorte' : locale === 'sr' ? 'Ostale lokacije' : 'Other Locations'}
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{otherCities.map((otherCity) => (
<Link
key={otherCity.slug}
href={`/${locale}/leistungen/standort/${otherCity.slug}`}
className="group flex items-center gap-2 p-4 bg-zinc-800/30 border border-zinc-800 rounded-lg hover:border-zinc-700 transition-all duration-300"
>
<MapPin className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors" />
<span className="text-zinc-300 group-hover:text-white transition-colors">
{otherCity.name[locale as Locale]}
</span>
</Link>
))}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white text-center mb-8">
{locale === 'de'
? `Häufige Fragen für ${city.name.de}`
: locale === 'sr'
? `Česta pitanja za ${city.name.sr}`
: `FAQ for ${city.name.en}`}
</h2>
<div className="space-y-4">
{faqItems.map((item, index) => (
<div
key={index}
className="bg-zinc-800/30 border border-zinc-800 rounded-lg p-6"
>
<h3 className="text-lg font-semibold text-white mb-2">
{item.question}
</h3>
<p className="text-zinc-400">{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-white mb-4">
{locale === 'de'
? `Projekt in ${city.name.de} starten?`
: locale === 'sr'
? `Započnite projekat u ${city.name.sr}?`
: `Start a project in ${city.name.en}?`}
</h2>
<p className="text-zinc-400 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-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors"
>
{locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'}
<ArrowRight className="h-5 w-5 ml-2" />
</Link>
</div>
</section>
</div>
);
}