feat: Add professional portfolio images and Hero redesign
- Add 15 optimized WebP images for portfolio (21-53KB each) - Redesign Hero section with full-width background image - Add SSR support for Hero component - Update About section with new portrait image - Update Contact page with professional headshot - Add images to Services/Leistungen page - Add image optimization script (sharp) - Update translations for gallery section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+108
-58
@@ -1,19 +1,83 @@
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Link from 'next/link';
|
||||
import { Hero, Skills, Workflow, Journey } from '@/components/about';
|
||||
import { ProfilePageJsonLd, BreadcrumbJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
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 t = await getTranslations({ locale, namespace: 'pages.about.seo' });
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
de: [
|
||||
'Über Damjan Savić',
|
||||
'KI Entwickler Köln',
|
||||
'AI Developer Deutschland',
|
||||
'Fullstack Entwickler',
|
||||
'Lebenslauf',
|
||||
'Skills',
|
||||
'Erfahrung',
|
||||
'Software Entwickler',
|
||||
],
|
||||
en: [
|
||||
'About Damjan Savić',
|
||||
'Remote AI Developer',
|
||||
'AI Developer Europe',
|
||||
'Fullstack Developer',
|
||||
'AI Consultant',
|
||||
'Resume',
|
||||
'Skills',
|
||||
'Experience',
|
||||
'Software Developer',
|
||||
'Hire AI Developer',
|
||||
'Freelance AI Developer USA',
|
||||
'Freelance AI Developer UK',
|
||||
'Remote Developer',
|
||||
'n8n Expert',
|
||||
'Voice AI Developer',
|
||||
],
|
||||
sr: [
|
||||
'O Damjanu Saviću',
|
||||
'AI Developer Keln',
|
||||
'AI Developer Nemačka',
|
||||
'Fullstack Developer',
|
||||
'Biografija',
|
||||
'Veštine',
|
||||
'Iskustvo',
|
||||
'Software Developer',
|
||||
],
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/about' : `/${locale}/about`;
|
||||
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
keywords: keywords[locale] || keywords.de,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/about`,
|
||||
de: `${BASE_URL}/about`,
|
||||
en: `${BASE_URL}/en/about`,
|
||||
sr: `${BASE_URL}/sr/about`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
url: `${BASE_URL}${localePath}`,
|
||||
type: 'profile',
|
||||
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')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,67 +87,53 @@ export default async function AboutPage({ params }: Props) {
|
||||
|
||||
const t = await getTranslations('pages.about');
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
|
||||
{ name: locale === 'de' ? 'Über mich' : locale === 'sr' ? 'O meni' : 'About', url: `/${locale}/about` },
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8">
|
||||
<Link href={`/${locale}`} className="text-zinc-400 hover:text-white transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span className="text-zinc-600 mx-2">/</span>
|
||||
<span className="text-white">About</span>
|
||||
</nav>
|
||||
<div className="relative min-h-screen">
|
||||
<ProfilePageJsonLd locale={locale as Locale} />
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="mb-16">
|
||||
<p className="text-zinc-400 text-sm tracking-wider mb-2">
|
||||
{t('hero.aboutMe')}
|
||||
</p>
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-6">
|
||||
{t('hero.title')}
|
||||
</h1>
|
||||
<p className="text-zinc-300 text-lg leading-relaxed">
|
||||
{t('hero.description')}
|
||||
</p>
|
||||
</section>
|
||||
{/* Hero Section */}
|
||||
<section className="min-h-screen relative">
|
||||
<Hero />
|
||||
</section>
|
||||
|
||||
{/* Skills Section */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold mb-8">Core Expertise</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-zinc-800/50 rounded-lg p-6 border border-zinc-700/50">
|
||||
<h3 className="text-lg font-semibold mb-2">AI & Automation</h3>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
GPT-4, Claude API, Voice AI, n8n, Process Automation
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-zinc-800/50 rounded-lg p-6 border border-zinc-700/50">
|
||||
<h3 className="text-lg font-semibold mb-2">Fullstack Development</h3>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
Python, TypeScript, React, Next.js, Node.js
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-zinc-800/50 rounded-lg p-6 border border-zinc-700/50">
|
||||
<h3 className="text-lg font-semibold mb-2">System Integration</h3>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
ERP Systems, APIs, Database Design, DevOps
|
||||
</p>
|
||||
</div>
|
||||
{/* Skills Section */}
|
||||
<section id="skills" className="py-24 relative">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-white mb-4">
|
||||
{t('skills.title')}
|
||||
</h2>
|
||||
<p className="text-zinc-400 max-w-2xl mx-auto">
|
||||
{t('skills.description')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<Skills />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact CTA */}
|
||||
<section className="text-center">
|
||||
<h2 className="text-2xl font-bold mb-4">Interested in working together?</h2>
|
||||
<Link
|
||||
href={`/${locale}/contact`}
|
||||
className="inline-block px-8 py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{/* Workflow Section */}
|
||||
<section className="py-24 relative">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-white mb-4">
|
||||
{t('workflow.title')}
|
||||
</h2>
|
||||
<p className="text-zinc-400 max-w-2xl mx-auto">
|
||||
{t('workflow.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Workflow />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Journey Section */}
|
||||
<Journey />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+517
-131
@@ -1,25 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, Loader2, Calendar, Tag, ExternalLink } from 'lucide-react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { Metadata } from 'next';
|
||||
import { getAllBlogPosts, BlogPost } from '@/lib/blog';
|
||||
|
||||
interface BlogPost {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
excerpt: string;
|
||||
coverImage: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
const POSTS_PER_PAGE = 12;
|
||||
|
||||
// Blog posts data - in a real app, this would come from a CMS or MDX files
|
||||
const blogPostsData: Record<string, BlogPost[]> = {
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
};
|
||||
|
||||
// Legacy blog posts with translations (these have manual translations)
|
||||
const legacyBlogPosts: Record<string, BlogPost[]> = {
|
||||
de: [
|
||||
{
|
||||
slug: 'n8n-automatisierung-tutorial',
|
||||
title: 'n8n Tutorial: Workflows automatisieren ohne Code - Schritt für Schritt',
|
||||
date: '2026-01-18',
|
||||
excerpt: 'Lernen Sie, wie Sie mit n8n leistungsstarke Automatisierungen erstellen. Von der Installation bis zum ersten produktiven Workflow - inklusive Best Practices.',
|
||||
category: 'Automatisierung',
|
||||
tags: ['n8n', 'Tutorial', 'Automatisierung', 'Workflow', 'No-Code', 'Self-Hosted'],
|
||||
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'geo-generative-engine-optimization',
|
||||
title: 'GEO: Generative Engine Optimization - SEO für die KI-Ära',
|
||||
date: '2026-01-17',
|
||||
excerpt: 'Wie Sie Ihre Inhalte für ChatGPT, Perplexity und andere KI-Suchmaschinen optimieren. Der neue Standard nach klassischem SEO.',
|
||||
category: 'Marketing',
|
||||
tags: ['GEO', 'SEO', 'KI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
|
||||
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'gpt-api-integration-praxisguide',
|
||||
title: 'GPT API Integration: Vom API-Key zur produktionsreifen Anwendung',
|
||||
date: '2026-01-16',
|
||||
excerpt: 'Praktische Anleitung zur Integration von OpenAI GPT-4 in Ihre Anwendungen. Token-Optimierung, Fehlerbehandlung und Best Practices.',
|
||||
category: 'KI-Entwicklung',
|
||||
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
|
||||
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'ki-agenten-unternehmen-guide',
|
||||
title: 'KI-Agenten für Unternehmen: Der ultimative Praxisguide 2026',
|
||||
date: '2026-01-15',
|
||||
excerpt: 'Wie Unternehmen KI-Agenten einsetzen können, um Prozesse zu automatisieren, Kosten zu senken und Wettbewerbsvorteile zu gewinnen.',
|
||||
category: 'KI-Entwicklung',
|
||||
tags: ['KI-Agenten', 'Automatisierung', 'GPT-4', 'Claude', 'LLM', 'Unternehmen'],
|
||||
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'n8n-make-zapier-vergleich',
|
||||
title: 'n8n vs Make vs Zapier: Welches Automatisierungstool passt zu Ihnen?',
|
||||
date: '2026-01-10',
|
||||
excerpt: 'Ein detaillierter Vergleich der führenden Workflow-Automatisierungstools mit Vor- und Nachteilen für verschiedene Anwendungsfälle.',
|
||||
category: 'Automatisierung',
|
||||
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatisierung', 'No-Code'],
|
||||
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'voice-ai-kundenservice-trends',
|
||||
title: 'Voice AI im Kundenservice: Trends und Best Practices 2026',
|
||||
date: '2026-01-05',
|
||||
excerpt: 'Wie Voice AI den Kundenservice revolutioniert und welche Technologien Sie für Ihr Unternehmen evaluieren sollten.',
|
||||
category: 'Voice AI',
|
||||
tags: ['Voice AI', 'Sprachassistent', 'Kundenservice', 'NLP', 'Chatbot', 'Vapi'],
|
||||
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'saas-mvp-entwicklung-4-wochen',
|
||||
title: 'SaaS MVP in 4 Wochen: Von der Idee zum Launch',
|
||||
date: '2025-12-20',
|
||||
excerpt: 'Schritt-für-Schritt Anleitung zur schnellen Entwicklung eines SaaS MVP mit Next.js, Prisma und modernen Cloud-Diensten.',
|
||||
category: 'SaaS-Entwicklung',
|
||||
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Produktentwicklung', 'React'],
|
||||
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'erp-integration-breuninger',
|
||||
title: 'ApparelMagic und TradeByte: Analyse komplexer Integrationen',
|
||||
@@ -31,6 +90,69 @@ const blogPostsData: Record<string, BlogPost[]> = {
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
slug: 'n8n-automatisierung-tutorial',
|
||||
title: 'n8n Tutorial: Automate Workflows Without Code - Step by Step',
|
||||
date: '2026-01-18',
|
||||
excerpt: 'Learn how to create powerful automations with n8n. From installation to your first production workflow - including best practices.',
|
||||
category: 'Automation',
|
||||
tags: ['n8n', 'Tutorial', 'Automation', 'Workflow', 'No-Code', 'Self-Hosted'],
|
||||
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'geo-generative-engine-optimization',
|
||||
title: 'GEO: Generative Engine Optimization - SEO for the AI Era',
|
||||
date: '2026-01-17',
|
||||
excerpt: 'How to optimize your content for ChatGPT, Perplexity and other AI search engines. The new standard after traditional SEO.',
|
||||
category: 'Marketing',
|
||||
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
|
||||
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'gpt-api-integration-praxisguide',
|
||||
title: 'GPT API Integration: From API Key to Production-Ready Application',
|
||||
date: '2026-01-16',
|
||||
excerpt: 'Practical guide to integrating OpenAI GPT-4 into your applications. Token optimization, error handling and best practices.',
|
||||
category: 'AI Development',
|
||||
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
|
||||
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'ki-agenten-unternehmen-guide',
|
||||
title: 'AI Agents for Business: The Ultimate Practical Guide 2026',
|
||||
date: '2026-01-15',
|
||||
excerpt: 'How companies can use AI agents to automate processes, reduce costs and gain competitive advantages.',
|
||||
category: 'AI Development',
|
||||
tags: ['AI Agents', 'Automation', 'GPT-4', 'Claude', 'LLM', 'Enterprise'],
|
||||
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'n8n-make-zapier-vergleich',
|
||||
title: 'n8n vs Make vs Zapier: Which Automation Tool is Right for You?',
|
||||
date: '2026-01-10',
|
||||
excerpt: 'A detailed comparison of leading workflow automation tools with pros and cons for different use cases.',
|
||||
category: 'Automation',
|
||||
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automation', 'No-Code'],
|
||||
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'voice-ai-kundenservice-trends',
|
||||
title: 'Voice AI in Customer Service: Trends and Best Practices 2026',
|
||||
date: '2026-01-05',
|
||||
excerpt: 'How Voice AI is revolutionizing customer service and which technologies you should evaluate for your business.',
|
||||
category: 'Voice AI',
|
||||
tags: ['Voice AI', 'Voice Assistant', 'Customer Service', 'NLP', 'Chatbot', 'Vapi'],
|
||||
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'saas-mvp-entwicklung-4-wochen',
|
||||
title: 'SaaS MVP in 4 Weeks: From Idea to Launch',
|
||||
date: '2025-12-20',
|
||||
excerpt: 'Step-by-step guide to quickly developing a SaaS MVP with Next.js, Prisma and modern cloud services.',
|
||||
category: 'SaaS Development',
|
||||
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Product Development', 'React'],
|
||||
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'erp-integration-breuninger',
|
||||
title: 'ApparelMagic and TradeByte: Complex Integration Analysis',
|
||||
@@ -42,6 +164,69 @@ const blogPostsData: Record<string, BlogPost[]> = {
|
||||
},
|
||||
],
|
||||
sr: [
|
||||
{
|
||||
slug: 'n8n-automatisierung-tutorial',
|
||||
title: 'n8n Tutorial: Automatizujte Radne Tokove Bez Koda - Korak po Korak',
|
||||
date: '2026-01-18',
|
||||
excerpt: 'Naučite kako da kreirate moćne automatizacije sa n8n. Od instalacije do prvog produktivnog radnog toka - uključujući najbolje prakse.',
|
||||
category: 'Automatizacija',
|
||||
tags: ['n8n', 'Tutorial', 'Automatizacija', 'Workflow', 'No-Code', 'Self-Hosted'],
|
||||
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'geo-generative-engine-optimization',
|
||||
title: 'GEO: Generative Engine Optimization - SEO za AI Eru',
|
||||
date: '2026-01-17',
|
||||
excerpt: 'Kako da optimizujete sadržaj za ChatGPT, Perplexity i druge AI pretraživače. Novi standard posle klasičnog SEO-a.',
|
||||
category: 'Marketing',
|
||||
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
|
||||
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'gpt-api-integration-praxisguide',
|
||||
title: 'GPT API Integracija: Od API Ključa do Produkcione Aplikacije',
|
||||
date: '2026-01-16',
|
||||
excerpt: 'Praktični vodič za integraciju OpenAI GPT-4 u vaše aplikacije. Optimizacija tokena, rukovanje greškama i najbolje prakse.',
|
||||
category: 'AI Razvoj',
|
||||
tags: ['GPT-4', 'OpenAI', 'API', 'Integracija', 'Python', 'LLM', 'Prompt Engineering'],
|
||||
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'ki-agenten-unternehmen-guide',
|
||||
title: 'AI Agenti za Preduzeća: Ultimativni Praktični Vodič 2026',
|
||||
date: '2026-01-15',
|
||||
excerpt: 'Kako kompanije mogu koristiti AI agente za automatizaciju procesa, smanjenje troškova i sticanje konkurentskih prednosti.',
|
||||
category: 'AI Razvoj',
|
||||
tags: ['AI Agenti', 'Automatizacija', 'GPT-4', 'Claude', 'LLM', 'Preduzeća'],
|
||||
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'n8n-make-zapier-vergleich',
|
||||
title: 'n8n vs Make vs Zapier: Koji Alat za Automatizaciju je Pravi za Vas?',
|
||||
date: '2026-01-10',
|
||||
excerpt: 'Detaljna uporedna analiza vodećih alata za automatizaciju radnih tokova sa prednostima i manama za različite slučajeve upotrebe.',
|
||||
category: 'Automatizacija',
|
||||
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatizacija', 'No-Code'],
|
||||
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'voice-ai-kundenservice-trends',
|
||||
title: 'Voice AI u Korisničkoj Službi: Trendovi i Najbolje Prakse 2026',
|
||||
date: '2026-01-05',
|
||||
excerpt: 'Kako Voice AI revolucioniše korisničku službu i koje tehnologije treba da evaluirate za vaš posao.',
|
||||
category: 'Voice AI',
|
||||
tags: ['Voice AI', 'Glasovni Asistent', 'Korisnička Služba', 'NLP', 'Chatbot', 'Vapi'],
|
||||
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'saas-mvp-entwicklung-4-wochen',
|
||||
title: 'SaaS MVP za 4 Nedelje: Od Ideje do Lansiranja',
|
||||
date: '2025-12-20',
|
||||
excerpt: 'Korak-po-korak vodič za brz razvoj SaaS MVP-a sa Next.js, Prisma i modernim cloud servisima.',
|
||||
category: 'SaaS Razvoj',
|
||||
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Razvoj Proizvoda', 'React'],
|
||||
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
|
||||
},
|
||||
{
|
||||
slug: 'erp-integration-breuninger',
|
||||
title: 'ApparelMagic i TradeByte: Analiza kompleksnih integracija',
|
||||
@@ -54,29 +239,131 @@ const blogPostsData: Record<string, BlogPost[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
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 t = await getTranslations({ locale, namespace: 'blog.meta' });
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
de: [
|
||||
'KI Blog',
|
||||
'Automatisierung Blog',
|
||||
'Tech Blog',
|
||||
'KI-Agenten',
|
||||
'n8n Tutorials',
|
||||
'Voice AI',
|
||||
'SaaS Entwicklung',
|
||||
'Python Tutorials',
|
||||
],
|
||||
en: [
|
||||
'AI Blog',
|
||||
'Automation Blog',
|
||||
'Tech Blog',
|
||||
'AI Agents Guide',
|
||||
'n8n Tutorials',
|
||||
'Voice AI Guide',
|
||||
'SaaS Development',
|
||||
'Python Tutorials',
|
||||
'GPT-4 Integration Guide',
|
||||
'AI Developer Blog',
|
||||
'Process Automation Tips',
|
||||
],
|
||||
sr: [
|
||||
'AI Blog',
|
||||
'Automatizacija Blog',
|
||||
'Tech Blog',
|
||||
'AI Agenti',
|
||||
'n8n Tutoriali',
|
||||
'Voice AI',
|
||||
'SaaS Razvoj',
|
||||
'Python Tutoriali',
|
||||
],
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/blog' : `/${locale}/blog`;
|
||||
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
keywords: keywords[locale] || keywords.de,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/blog`,
|
||||
de: `${BASE_URL}/blog`,
|
||||
en: `${BASE_URL}/en/blog`,
|
||||
sr: `${BASE_URL}/sr/blog`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
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')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getFormattedDate(date: string, locale: string) {
|
||||
const languageMap: Record<string, string> = {
|
||||
de: 'de-DE',
|
||||
en: 'en-US',
|
||||
sr: 'sr-RS',
|
||||
};
|
||||
|
||||
return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function getImagePath(coverImage: string) {
|
||||
if (coverImage.startsWith('http')) {
|
||||
return coverImage;
|
||||
}
|
||||
return coverImage.replace('/blog/', '/images/posts/');
|
||||
}
|
||||
|
||||
// Placeholder image for posts without cover images
|
||||
const PLACEHOLDER_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(`
|
||||
<svg width="1792" height="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#181C14"/>
|
||||
<rect x="40%" y="35%" width="20%" height="30%" rx="8" fill="#697565" opacity="0.3"/>
|
||||
<circle cx="50%" cy="45%" r="8%" fill="#697565" opacity="0.2"/>
|
||||
<rect x="30%" y="65%" width="40%" height="4%" rx="2" fill="#465B50" opacity="0.3"/>
|
||||
<rect x="35%" y="72%" width="30%" height="3%" rx="2" fill="#465B50" opacity="0.2"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
// Known existing images (from public/images/posts/)
|
||||
const EXISTING_IMAGES = new Set([
|
||||
'/images/posts/erp-integration-breuninger/cover.jpg',
|
||||
'/images/posts/fullstack-development-timetracking/cover.jpg',
|
||||
'/images/posts/rfid-automation/cover.jpg',
|
||||
'/images/posts/automated-ad-creatives/cover.jpg',
|
||||
]);
|
||||
|
||||
// Blog image component with fallback
|
||||
function BlogImage({ src, alt, fallback }: { src: string; alt: string; fallback: string }) {
|
||||
const imageSrc = EXISTING_IMAGES.has(src) ? src : fallback;
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
unoptimized={imageSrc.startsWith('data:')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
|
||||
|
||||
const getFormattedDate = (date: string) => {
|
||||
const languageMap: Record<string, string> = {
|
||||
de: 'de-DE',
|
||||
en: 'en-US',
|
||||
sr: 'sr-RS',
|
||||
};
|
||||
|
||||
return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const getImagePath = (coverImage: string) => {
|
||||
if (coverImage.startsWith('http')) {
|
||||
return coverImage;
|
||||
}
|
||||
return coverImage.replace('/blog/', '/images/posts/');
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/${locale}/blog/${post.slug}`}
|
||||
@@ -87,20 +374,13 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
|
||||
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950
|
||||
transform hover:-translate-y-1"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative overflow-hidden rounded-xl"
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-xl">
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900">
|
||||
<Image
|
||||
<BlogImage
|
||||
src={getImagePath(post.coverImage)}
|
||||
alt={post.title}
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
fallback={PLACEHOLDER_IMAGE}
|
||||
/>
|
||||
<div className="absolute inset-x-0 -bottom-20 top-0 bg-gradient-to-t from-zinc-900 via-zinc-900/70 to-transparent opacity-95" />
|
||||
</div>
|
||||
@@ -110,7 +390,7 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
|
||||
<div className="flex items-center gap-4 mb-4 text-zinc-500">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{getFormattedDate(post.date)}</span>
|
||||
<span>{getFormattedDate(post.date, locale)}</span>
|
||||
</div>
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
@@ -159,110 +439,216 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
|
||||
<ExternalLink className="h-4 w-4 text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BlogPage() {
|
||||
const t = useTranslations('blog');
|
||||
const locale = useLocale();
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Pagination component
|
||||
function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
locale,
|
||||
t,
|
||||
}: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
locale: string;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const getPageNumbers = () => {
|
||||
const pages: (number | 'ellipsis')[] = [];
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading
|
||||
const loadPosts = async () => {
|
||||
setLoading(true);
|
||||
// Get posts for current locale
|
||||
const currentPosts = blogPostsData[locale] || blogPostsData.de;
|
||||
setPosts(currentPosts);
|
||||
setLoading(false);
|
||||
};
|
||||
loadPosts();
|
||||
}, [locale]);
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages if 7 or fewer
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex flex-col items-center space-y-4"
|
||||
if (currentPage > 3) {
|
||||
pages.push('ellipsis');
|
||||
}
|
||||
|
||||
// Show pages around current
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 2) {
|
||||
pages.push('ellipsis');
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-center gap-2 mt-12 sm:mt-16" aria-label="Pagination">
|
||||
{/* Previous button */}
|
||||
{currentPage > 1 ? (
|
||||
<Link
|
||||
href={`/${locale}/blog${currentPage > 2 ? `?page=${currentPage - 1}` : ''}`}
|
||||
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white
|
||||
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50
|
||||
hover:ring-zinc-600 transition-all duration-200"
|
||||
aria-label={t('ui.pagination.previous')}
|
||||
>
|
||||
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
|
||||
<p className="text-zinc-400 text-sm">{t('ui.loading.posts')}</p>
|
||||
</motion.div>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t('ui.pagination.previous')}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span
|
||||
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600
|
||||
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t('ui.pagination.previous')}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Page numbers */}
|
||||
<div className="flex items-center gap-1">
|
||||
{getPageNumbers().map((page, index) => {
|
||||
if (page === 'ellipsis') {
|
||||
return (
|
||||
<span key={`ellipsis-${index}`} className="px-2 text-zinc-600">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const isCurrentPage = page === currentPage;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={page}
|
||||
href={`/${locale}/blog${page > 1 ? `?page=${page}` : ''}`}
|
||||
className={`min-w-[40px] h-10 flex items-center justify-center text-sm rounded-lg
|
||||
ring-1 transition-all duration-200
|
||||
${isCurrentPage
|
||||
? 'bg-[#697565] text-white ring-[#697565] font-medium'
|
||||
: 'text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 ring-zinc-700/50 hover:ring-zinc-600'
|
||||
}`}
|
||||
aria-current={isCurrentPage ? 'page' : undefined}
|
||||
>
|
||||
{page}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Next button */}
|
||||
{currentPage < totalPages ? (
|
||||
<Link
|
||||
href={`/${locale}/blog?page=${currentPage + 1}`}
|
||||
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white
|
||||
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50
|
||||
hover:ring-zinc-600 transition-all duration-200"
|
||||
aria-label={t('ui.pagination.next')}
|
||||
>
|
||||
<span className="hidden sm:inline">{t('ui.pagination.next')}</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<span
|
||||
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600
|
||||
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('ui.pagination.next')}</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function BlogPage({ params, searchParams }: Props) {
|
||||
const { locale } = await params;
|
||||
const { page } = await searchParams;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations('blog');
|
||||
|
||||
// Get dynamic posts from markdown files
|
||||
const dynamicPosts = getAllBlogPosts();
|
||||
|
||||
// Get legacy posts (with translations)
|
||||
const legacyPosts = legacyBlogPosts[locale] || legacyBlogPosts.de;
|
||||
|
||||
// Merge: dynamic posts first, then legacy posts (avoiding duplicates)
|
||||
const legacySlugs = new Set(legacyPosts.map(p => p.slug));
|
||||
const allPosts = [
|
||||
...dynamicPosts.filter(p => !legacySlugs.has(p.slug)),
|
||||
...legacyPosts,
|
||||
].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
// Pagination
|
||||
const currentPage = Math.max(1, parseInt(page || '1', 10) || 1);
|
||||
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
|
||||
const validPage = Math.min(currentPage, totalPages || 1);
|
||||
|
||||
const startIndex = (validPage - 1) * POSTS_PER_PAGE;
|
||||
const endIndex = startIndex + POSTS_PER_PAGE;
|
||||
const posts = allPosts.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
||||
{/* Header Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-12 sm:mb-16 lg:mb-20"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
className="flex items-center gap-4 mb-8"
|
||||
>
|
||||
<div className="mb-12 sm:mb-16 lg:mb-20">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<ArrowRight className="h-5 w-5 text-zinc-500" />
|
||||
<h2 className="text-white text-sm tracking-wider">BLOG</h2>
|
||||
</motion.div>
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6"
|
||||
>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
|
||||
{t('meta.header.title')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
className="text-lg sm:text-xl text-zinc-400 max-w-2xl"
|
||||
>
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl text-zinc-400 max-w-2xl">
|
||||
{t('meta.header.subtitle')}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</p>
|
||||
{/* Post count */}
|
||||
<p className="text-sm text-zinc-500 mt-4">
|
||||
{t('ui.pagination.showing', {
|
||||
start: startIndex + 1,
|
||||
end: Math.min(endIndex, allPosts.length),
|
||||
total: allPosts.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Blog Posts Grid */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8"
|
||||
>
|
||||
{posts.map((post, index) => (
|
||||
<motion.div
|
||||
key={post.slug}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 * (index + 4) }}
|
||||
>
|
||||
<BlogPostCard post={post} locale={locale} />
|
||||
</motion.div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
||||
{posts.map((post) => (
|
||||
<BlogPostCard key={post.slug} post={post} locale={locale} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={validPage}
|
||||
totalPages={totalPages}
|
||||
locale={locale}
|
||||
t={(key) => t(key)}
|
||||
/>
|
||||
|
||||
{/* Empty State */}
|
||||
{posts.length === 0 && !loading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
className="text-center py-12"
|
||||
>
|
||||
{posts.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-zinc-400">{t('ui.errors.posts')}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,169 +1,86 @@
|
||||
'use client';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { ContactForm } from '@/components/contact';
|
||||
import { BreadcrumbJsonLd, WebPageJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { Mail, Phone, MapPin } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export default function ContactPage() {
|
||||
const t = useTranslations('pages.contact');
|
||||
const params = useParams();
|
||||
const locale = params.locale as string;
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
message: '',
|
||||
});
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'pages.contact' });
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
|
||||
// Simulate form submission
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setStatus('success');
|
||||
setFormData({ name: '', email: '', message: '' });
|
||||
} catch {
|
||||
setStatus('error');
|
||||
}
|
||||
const titles: Record<string, string> = {
|
||||
de: 'Kontakt - KI Entwickler Köln | Damjan Savić',
|
||||
en: 'Contact - Remote AI Developer | Hire for USA, UK & Europe Projects',
|
||||
sr: 'Kontakt - AI Developer Keln | Damjan Savić',
|
||||
};
|
||||
|
||||
const descriptions: Record<string, string> = {
|
||||
de: 'Kontaktieren Sie Damjan Savić - Ihren AI & Automation Specialist aus Köln. Entwicklung von KI-Agenten, Voice AI, Prozessautomatisierung und SaaS-Lösungen.',
|
||||
en: 'Hire Damjan Savić - Remote AI Developer based in Germany. Available for clients in USA, UK & Europe. Specializing in AI agents, Voice AI, n8n automation and custom SaaS solutions. Free consultation.',
|
||||
sr: 'Kontaktirajte Damjana Savića - AI Developer i Automation Specialist iz Nemačke, srpskog porekla. Razvoj AI agenata, Voice AI, n8n automatizacija i SaaS rešenja za klijente iz Srbije i dijaspore.',
|
||||
};
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
de: ['Kontakt Damjan Savić', 'KI Entwickler kontaktieren', 'Freelancer Köln', 'AI Beratung'],
|
||||
en: ['Hire AI Developer', 'Contact Remote Developer', 'AI Consultant USA', 'AI Developer UK', 'Freelance AI Developer', 'n8n Expert for Hire'],
|
||||
sr: ['Kontakt AI Developer', 'AI Programer Srbija', 'Freelance Programer Beograd', 'AI Konsultant Srbija', 'Softverski Razvoj Srbija', 'Remote Developer Dijaspora', 'n8n Automatizacija Srbija'],
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/contact' : `/${locale}/contact`;
|
||||
|
||||
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}/de/contact`,
|
||||
de: `${BASE_URL}/de/contact`,
|
||||
en: `${BASE_URL}/en/contact`,
|
||||
sr: `${BASE_URL}/sr/contact`,
|
||||
},
|
||||
},
|
||||
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 ContactPage({ params }: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations({ locale, namespace: 'pages.contact' });
|
||||
const commonT = await getTranslations({ locale, namespace: 'common.nav' });
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: commonT('home'), url: `/${locale}` },
|
||||
{ name: commonT('contact'), url: `/${locale}/contact` },
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8">
|
||||
<Link href={`/${locale}`} className="text-zinc-400 hover:text-white transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span className="text-zinc-600 mx-2">/</span>
|
||||
<span className="text-white">{t('breadcrumb')}</span>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="mb-16 text-center">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-4">{t('hero.title')}</h1>
|
||||
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
|
||||
{t('hero.subtitle')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Contact Info */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6">{t('contactInfo.title')}</h2>
|
||||
<p className="text-zinc-400 mb-8">{t('contactInfo.description')}</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-zinc-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<MapPin size={20} className="text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-400 text-sm">{t('contactInfo.address.label')}</p>
|
||||
<p className="text-white">{t('contactInfo.address.value')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-zinc-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Phone size={20} className="text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-400 text-sm">{t('contactInfo.phone.label')}</p>
|
||||
<a href="tel:+491756950979" className="text-white hover:text-blue-400 transition-colors">
|
||||
{t('contactInfo.phone.value')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-zinc-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Mail size={20} className="text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-400 text-sm">{t('contactInfo.email.label')}</p>
|
||||
<a href="mailto:info@damjan-savic.com" className="text-white hover:text-blue-400 transition-colors">
|
||||
{t('contactInfo.email.value')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6">{t('contactForm.title')}</h2>
|
||||
<p className="text-zinc-400 mb-8">{t('contactForm.description')}</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-zinc-300 mb-2">
|
||||
{t('contactForm.name.label')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder={t('contactForm.name.placeholder')}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-zinc-300 mb-2">
|
||||
{t('contactForm.email.label')}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder={t('contactForm.email.placeholder')}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-zinc-300 mb-2">
|
||||
{t('contactForm.message.label')}
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
placeholder={t('contactForm.message.placeholder')}
|
||||
required
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className="w-full px-6 py-4 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
{status === 'loading' ? 'Sending...' : t('contactForm.submit')}
|
||||
</button>
|
||||
|
||||
{status === 'success' && (
|
||||
<p className="text-green-400 text-center">{t('contactForm.successMessage')}</p>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<p className="text-red-400 text-center">{t('contactForm.errorMessage')}</p>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<>
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
<WebPageJsonLd
|
||||
title={t('hero.title')}
|
||||
description={t('hero.subtitle')}
|
||||
url={`/${locale}/contact`}
|
||||
locale={locale as Locale}
|
||||
/>
|
||||
<ContactForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,195 +1,36 @@
|
||||
'use client';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import DashboardContent from '@/components/auth/DashboardContent';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2, BarChart2, Users, Eye, Clock, TrendingUp, TrendingDown, LogOut } from 'lucide-react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
interface MetricCard {
|
||||
title: string;
|
||||
value: string;
|
||||
subtitle: string;
|
||||
trend?: 'up' | 'down';
|
||||
trendValue?: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations('dashboard');
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const supabase = createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login`);
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(user);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [locale, router]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
router.push(`/${locale}`);
|
||||
const titles: Record<string, string> = {
|
||||
de: 'Dashboard | Damjan Savić',
|
||||
en: 'Dashboard | Damjan Savić',
|
||||
sr: 'Dashboard | Damjan Savić',
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
|
||||
<p className="text-zinc-400 text-sm">{t('status.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<p className="text-zinc-400">{t('status.notAuthenticated')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics: MetricCard[] = [
|
||||
{
|
||||
title: t('metrics.pageViews.title'),
|
||||
value: '12,543',
|
||||
subtitle: '45 ' + t('metrics.pageViews.perMinute'),
|
||||
trend: 'up',
|
||||
trendValue: '+12.5%',
|
||||
icon: <Eye className="h-5 w-5" />,
|
||||
return {
|
||||
title: titles[locale] || titles.de,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('metrics.uniqueVisitors.title'),
|
||||
value: '3,721',
|
||||
subtitle: '127 ' + t('metrics.uniqueVisitors.currentlyActive'),
|
||||
trend: 'up',
|
||||
trendValue: '+8.2%',
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: t('metrics.conversions.title'),
|
||||
value: '156',
|
||||
subtitle: '4.2% ' + t('metrics.conversions.rate'),
|
||||
trend: 'down',
|
||||
trendValue: '-2.1%',
|
||||
icon: <BarChart2 className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: t('metrics.timeOnSite.title'),
|
||||
value: '4:32',
|
||||
subtitle: '32% ' + t('metrics.timeOnSite.bounceRate'),
|
||||
trend: 'up',
|
||||
trendValue: '+15.3%',
|
||||
icon: <Clock className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
const topPages = [
|
||||
{ path: '/', views: 4521, change: '+12%' },
|
||||
{ path: '/portfolio', views: 2341, change: '+8%' },
|
||||
{ path: '/about', views: 1823, change: '+5%' },
|
||||
{ path: '/blog', views: 1456, change: '-2%' },
|
||||
{ path: '/contact', views: 892, change: '+3%' },
|
||||
];
|
||||
export default async function DashboardPage({ params }: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-16 sm:py-20 lg:py-24">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">{t('header.title')}</h1>
|
||||
<p className="text-zinc-400 mt-2">
|
||||
127 {t('header.activeUsers')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-zinc-900/50 backdrop-blur-sm rounded-xl p-6 ring-1 ring-zinc-800"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="p-2 bg-zinc-800 rounded-lg text-zinc-400">
|
||||
{metric.icon}
|
||||
</div>
|
||||
{metric.trend && (
|
||||
<div className={`flex items-center gap-1 text-sm ${
|
||||
metric.trend === 'up' ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{metric.trend === 'up' ? (
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4" />
|
||||
)}
|
||||
{metric.trendValue}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-sm text-zinc-400 mb-1">{metric.title}</h3>
|
||||
<p className="text-2xl font-bold text-white">{metric.value}</p>
|
||||
<p className="text-sm text-zinc-500 mt-1">{metric.subtitle}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Top Pages */}
|
||||
<div className="bg-zinc-900/50 backdrop-blur-sm rounded-xl ring-1 ring-zinc-800">
|
||||
<div className="p-6 border-b border-zinc-800">
|
||||
<h2 className="text-xl font-semibold text-white">{t('topPages.title')}</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left text-sm text-zinc-500 border-b border-zinc-800">
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.path')}</th>
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.views')}</th>
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.change')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topPages.map((page, index) => (
|
||||
<tr key={index} className="border-b border-zinc-800/50 last:border-0">
|
||||
<td className="px-6 py-4 text-white font-mono text-sm">{page.path}</td>
|
||||
<td className="px-6 py-4 text-zinc-300">{page.views.toLocaleString()}</td>
|
||||
<td className={`px-6 py-4 ${
|
||||
page.change.startsWith('+') ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{page.change}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
return <DashboardContent />;
|
||||
}
|
||||
|
||||
@@ -6,24 +6,49 @@ type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
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: 'Impressum',
|
||||
en: 'Imprint',
|
||||
sr: 'Impresum',
|
||||
de: 'Impressum | Damjan Savić - KI Entwickler Köln',
|
||||
en: 'Legal Notice | Damjan Savić - AI Developer',
|
||||
sr: 'Impresum | Damjan Savić - AI Developer',
|
||||
};
|
||||
|
||||
const descriptions: Record<string, string> = {
|
||||
de: 'Impressum fuer die Portfolio-Website von Damjan Savic',
|
||||
en: 'Legal notice for Damjan Savic portfolio website',
|
||||
sr: 'Pravno obavestenje za portfolio veb sajt Damjana Savica',
|
||||
de: 'Impressum und rechtliche Informationen für die Portfolio-Website von Damjan Savić, KI Entwickler und Automation Specialist aus Köln.',
|
||||
en: 'Legal notice and contact information for Damjan Savić portfolio website, AI Developer and Automation Specialist.',
|
||||
sr: 'Pravno obavestenje i kontakt informacije za portfolio veb sajt Damjana Savica, AI Developer i Automation Specialist.',
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/imprint' : `/${locale}/imprint`;
|
||||
|
||||
return {
|
||||
title: titles[locale] || titles.de,
|
||||
description: descriptions[locale] || descriptions.de,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/imprint`,
|
||||
de: `${BASE_URL}/imprint`,
|
||||
en: `${BASE_URL}/en/imprint`,
|
||||
sr: `${BASE_URL}/sr/imprint`,
|
||||
},
|
||||
},
|
||||
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')),
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +71,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
contact: {
|
||||
title: 'Kontakt',
|
||||
content: [
|
||||
'Damjan Savic',
|
||||
'Damjan Savić',
|
||||
'Fullstack Developer & Digital Solutions Consultant',
|
||||
'E-Mail: info@damjan-savic.com',
|
||||
'Website: https://damjan-savic.com',
|
||||
@@ -56,18 +81,18 @@ export default async function ImprintPage({ params }: Props) {
|
||||
title: 'Haftungsausschluss',
|
||||
sections: [
|
||||
{
|
||||
title: 'Haftung fuer Inhalte',
|
||||
content: 'Die Inhalte meiner Seiten wurden mit groesster Sorgfalt erstellt. Fuer die Richtigkeit, Vollstaendigkeit und Aktualitaet der Inhalte kann ich jedoch keine Gewaehr uebernehmen.',
|
||||
title: 'Haftung für Inhalte',
|
||||
content: 'Die Inhalte meiner Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen.',
|
||||
},
|
||||
{
|
||||
title: 'Haftung fuer Links',
|
||||
content: 'Meine Website enthaelt Links zu externen Websites Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich fuer diese fremden Inhalte auch keine Gewaehr uebernehmen. Fuer die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.',
|
||||
title: 'Haftung für Links',
|
||||
content: 'Meine Website enthält Links zu externen Websites Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.',
|
||||
},
|
||||
],
|
||||
},
|
||||
copyright: {
|
||||
title: 'Urheberrecht',
|
||||
content: 'Die durch den Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfaeltigung, Bearbeitung, Verbreitung und jede Art der Verwertung ausserhalb der Grenzen des Urheberrechtes beduerfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.',
|
||||
content: 'Die durch den Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.',
|
||||
},
|
||||
images: {
|
||||
title: 'Bildnachweise',
|
||||
@@ -79,12 +104,12 @@ export default async function ImprintPage({ params }: Props) {
|
||||
},
|
||||
privacy: {
|
||||
title: 'Datenschutz',
|
||||
content: 'Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten moeglich. Weitere Informationen zum Datenschutz finden Sie in unserer',
|
||||
link: 'Datenschutzerklaerung',
|
||||
content: 'Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich. Weitere Informationen zum Datenschutz finden Sie in unserer',
|
||||
link: 'Datenschutzerklärung',
|
||||
},
|
||||
technical: {
|
||||
title: 'Technische Umsetzung',
|
||||
design: 'Design & Entwicklung: Damjan Savic',
|
||||
design: 'Design & Entwicklung: Damjan Savić',
|
||||
tech: 'Technologien: Next.js, TypeScript, Tailwind CSS',
|
||||
},
|
||||
copyrightNotice: 'Alle Rechte vorbehalten.',
|
||||
@@ -94,7 +119,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
contact: {
|
||||
title: 'Contact',
|
||||
content: [
|
||||
'Damjan Savic',
|
||||
'Damjan Savić',
|
||||
'Fullstack Developer & Digital Solutions Consultant',
|
||||
'Email: info@damjan-savic.com',
|
||||
'Website: https://damjan-savic.com',
|
||||
@@ -132,7 +157,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
},
|
||||
technical: {
|
||||
title: 'Technical Implementation',
|
||||
design: 'Design & Development: Damjan Savic',
|
||||
design: 'Design & Development: Damjan Savić',
|
||||
tech: 'Technologies: Next.js, TypeScript, Tailwind CSS',
|
||||
},
|
||||
copyrightNotice: 'All rights reserved.',
|
||||
@@ -142,7 +167,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
contact: {
|
||||
title: 'Kontakt',
|
||||
content: [
|
||||
'Damjan Savic',
|
||||
'Damjan Savić',
|
||||
'Fullstack Developer & Digital Solutions Consultant',
|
||||
'Email: info@damjan-savic.com',
|
||||
'Website: https://damjan-savic.com',
|
||||
@@ -180,7 +205,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
},
|
||||
technical: {
|
||||
title: 'Tehnicka implementacija',
|
||||
design: 'Dizajn i razvoj: Damjan Savic',
|
||||
design: 'Dizajn i razvoj: Damjan Savić',
|
||||
tech: 'Tehnologije: Next.js, TypeScript, Tailwind CSS',
|
||||
},
|
||||
copyrightNotice: 'Sva prava zadrzana.',
|
||||
@@ -266,7 +291,7 @@ export default async function ImprintPage({ params }: Props) {
|
||||
{/* Copyright Notice */}
|
||||
<section>
|
||||
<p className="text-zinc-400">
|
||||
© {new Date().getFullYear()} Damjan Savic. {c.copyrightNotice}
|
||||
© {new Date().getFullYear()} Damjan Savić. {c.copyrightNotice}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+56
-20
@@ -2,8 +2,10 @@ import { notFound } from 'next/navigation';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, setRequestLocale } from 'next-intl/server';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { locales, type Locale } from '@/i18n/config';
|
||||
import { locales, localeMetadata, seoKeywords, type Locale } from '@/i18n/config';
|
||||
import type { Metadata } from 'next';
|
||||
import { Header, Footer, GlobalBackground } from '@/components/layout';
|
||||
import { PersonJsonLd, ProfessionalServiceJsonLd, OrganizationJsonLd, WebSiteJsonLd } from '@/components/seo';
|
||||
import '../globals.css';
|
||||
|
||||
const inter = Inter({
|
||||
@@ -23,43 +25,57 @@ export function generateStaticParams() {
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const currentLocale = locale as Locale;
|
||||
const meta = localeMetadata[currentLocale] || localeMetadata.de;
|
||||
const keywords = seoKeywords[currentLocale] || seoKeywords.de;
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const titles: Record<Locale, string> = {
|
||||
de: 'Damjan Savic | AI & Automation Specialist aus Koeln',
|
||||
en: 'Damjan Savic | AI & Automation Specialist from Cologne',
|
||||
sr: 'Damjan Savic | AI & Automation Specialist iz Kelna',
|
||||
de: 'Damjan Savić | AI & Automation Specialist aus Köln',
|
||||
en: 'Damjan Savić | AI & Automation Specialist from Cologne',
|
||||
sr: 'Damjan Savić | AI & Automation Specialist iz Kelna',
|
||||
};
|
||||
|
||||
const descriptions: Record<Locale, string> = {
|
||||
de: 'Entwicklung von KI-Agenten und Automatisierungsloesungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
|
||||
de: 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
|
||||
en: 'Building AI agents and automation solutions. From voice AI platforms to autonomous web agents.',
|
||||
sr: 'Razvoj AI agenata i resenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.',
|
||||
};
|
||||
|
||||
// Generate alternates using localeMetadata
|
||||
const languageAlternates: Record<string, string> = {
|
||||
'x-default': `${BASE_URL}/de`,
|
||||
};
|
||||
locales.forEach((loc) => {
|
||||
languageAlternates[loc] = `${BASE_URL}/${loc}`;
|
||||
});
|
||||
|
||||
// Generate OG alternate locales
|
||||
const allOgLocales = locales.map((loc) => localeMetadata[loc].ogLocale);
|
||||
const alternateOgLocales = allOgLocales.filter((l) => l !== meta.ogLocale);
|
||||
|
||||
return {
|
||||
title: {
|
||||
template: '%s | Damjan Savic',
|
||||
default: titles[locale as Locale] || titles.de,
|
||||
template: '%s | Damjan Savić',
|
||||
default: titles[currentLocale] || titles.de,
|
||||
},
|
||||
description: descriptions[locale as Locale] || descriptions.de,
|
||||
description: descriptions[currentLocale] || descriptions.de,
|
||||
keywords: keywords,
|
||||
alternates: {
|
||||
canonical: `/${locale}`,
|
||||
languages: {
|
||||
de: '/de',
|
||||
en: '/en',
|
||||
sr: '/sr',
|
||||
},
|
||||
canonical: `${BASE_URL}/${locale}`,
|
||||
languages: languageAlternates,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
|
||||
siteName: 'Damjan Savic',
|
||||
locale: meta.ogLocale,
|
||||
alternateLocale: alternateOgLocales,
|
||||
siteName: 'Damjan Savić',
|
||||
images: [
|
||||
{
|
||||
url: '/images/og-image.jpg',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Damjan Savic - AI & Automation Specialist',
|
||||
alt: locale === 'de' ? 'Damjan Savić - KI & Automatisierung Spezialist' : locale === 'sr' ? 'Damjan Savić - AI & Automatizacija Specijalista' : 'Damjan Savić - AI & Automation Specialist',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -82,10 +98,30 @@ export default async function LocaleLayout({ children, params }: Props) {
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={inter.variable}>
|
||||
<body className="bg-zinc-900 text-zinc-50 antialiased">
|
||||
<html lang={locale} className={`${inter.variable} dark`}>
|
||||
<head>
|
||||
{/* Preconnect für bessere Performance */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link rel="dns-prefetch" href="https://mxadgucxhmstlzsbgmoz.supabase.co" />
|
||||
|
||||
{/* Theme Color für mobile Browser */}
|
||||
<meta name="theme-color" content="#18181b" />
|
||||
|
||||
{/* JSON-LD Structured Data */}
|
||||
<PersonJsonLd locale={locale as Locale} />
|
||||
<ProfessionalServiceJsonLd locale={locale as Locale} />
|
||||
<OrganizationJsonLd locale={locale as Locale} />
|
||||
<WebSiteJsonLd locale={locale as Locale} />
|
||||
</head>
|
||||
<body className="min-h-screen bg-zinc-900 text-zinc-50 antialiased">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
<div className="min-h-screen flex flex-col relative">
|
||||
<GlobalBackground />
|
||||
<Header />
|
||||
<main className="flex-1 pt-16 relative z-10">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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.webp"
|
||||
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.webp"
|
||||
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>
|
||||
);
|
||||
}
|
||||
+28
-105
@@ -1,113 +1,36 @@
|
||||
'use client';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import LoginForm from '@/components/auth/LoginForm';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Lock, Loader2 } from 'lucide-react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations('login');
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
router.push(`/${locale}/dashboard`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('form.errors.default'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const titles: Record<string, string> = {
|
||||
de: 'Login | Damjan Savić',
|
||||
en: 'Login | Damjan Savić',
|
||||
sr: 'Prijava | Damjan Savić',
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center py-24 px-4">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<Lock className="mx-auto h-12 w-12 text-orange-500" />
|
||||
<h1 className="mt-6 text-3xl font-bold text-white">{t('header.title')}</h1>
|
||||
</div>
|
||||
return {
|
||||
title: titles[locale] || titles.de,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
export default async function LoginPage({ params }: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white/80 mb-2">
|
||||
{t('form.email.label')}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t('form.email.placeholder')}
|
||||
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-white/80 mb-2">
|
||||
{t('form.password.label')}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('form.password.placeholder')}
|
||||
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{t('form.submit.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('form.submit.default')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-sm text-zinc-500">
|
||||
<p className="flex items-center justify-center gap-2">
|
||||
<svg className="h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('security.secureConnection')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
+125
-257
@@ -1,270 +1,138 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import { Linkedin, Github, Mail } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Hero, Experience, Skills, Projects, About } from '@/components/sections';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
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: 'KI Entwickler Köln | AI Agents & Automatisierung | Damjan Savić',
|
||||
en: 'AI Developer Cologne | AI Agents & Automation | Damjan Savić',
|
||||
sr: 'AI Developer Keln | AI Agenti & Automatizacija | Damjan Savić',
|
||||
};
|
||||
|
||||
const descriptions: Record<string, string> = {
|
||||
de: 'Freelance KI Entwickler aus Köln. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und SaaS-Entwicklung. Über 7 Jahre Erfahrung.',
|
||||
en: 'Remote AI Developer & Automation Expert based in Germany. Building AI agents, Voice AI systems, and n8n automations for clients in USA, UK, and Europe. 7+ years experience. Available for international projects.',
|
||||
sr: 'AI Developer i Automation Specialist iz Nemačke, poreklo iz Srbije. Specijalizovan za AI agente, Voice AI, n8n automatizaciju i SaaS razvoj. Radim sa klijentima iz Srbije, Nemačke i dijaspore. 7+ godina iskustva.',
|
||||
};
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
de: [
|
||||
'KI Entwickler Köln',
|
||||
'AI Developer Deutschland',
|
||||
'KI Entwickler DACH',
|
||||
'AI Agentur Köln',
|
||||
'Prozessautomatisierung',
|
||||
'n8n Entwickler',
|
||||
'Voice AI',
|
||||
'KI-Agenten',
|
||||
'Fullstack Entwickler',
|
||||
'SaaS Entwicklung',
|
||||
'Automatisierung',
|
||||
'GPT-4 Integration',
|
||||
'KI Entwickler Wien',
|
||||
'AI Developer Zürich',
|
||||
'Freelancer Deutschland',
|
||||
'Claude AI Integration',
|
||||
],
|
||||
en: [
|
||||
'AI Developer',
|
||||
'Remote AI Developer',
|
||||
'AI Developer USA',
|
||||
'AI Developer UK',
|
||||
'AI Developer Europe',
|
||||
'Process Automation Expert',
|
||||
'n8n Developer',
|
||||
'Voice AI Developer',
|
||||
'AI Agents Developer',
|
||||
'Fullstack Developer',
|
||||
'SaaS Development',
|
||||
'GPT-4 Integration',
|
||||
'ChatGPT Integration',
|
||||
'Remote Developer',
|
||||
'Freelance AI Developer',
|
||||
'AI Consultant',
|
||||
'Automation Specialist',
|
||||
],
|
||||
sr: [
|
||||
'AI Developer Srbija',
|
||||
'AI Programer Beograd',
|
||||
'AI Developer Novi Sad',
|
||||
'Fullstack Programer Srbija',
|
||||
'Automatizacija procesa',
|
||||
'n8n Automatizacija',
|
||||
'Voice AI Srbija',
|
||||
'AI Agenti',
|
||||
'SaaS Razvoj',
|
||||
'GPT-4 Integracija',
|
||||
'Python Programer Srbija',
|
||||
'React Programer Beograd',
|
||||
'Freelance Developer Srbija',
|
||||
'Remote Programer Nemačka',
|
||||
'Srpski Developer Dijaspora',
|
||||
'Web Razvoj Beograd',
|
||||
],
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '' : `/${locale}`;
|
||||
|
||||
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}/`,
|
||||
de: `${BASE_URL}/`,
|
||||
en: `${BASE_URL}/en`,
|
||||
sr: `${BASE_URL}/sr`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: titles[locale] || titles.de,
|
||||
description: descriptions[locale] || descriptions.de,
|
||||
url: `${BASE_URL}${localePath || '/'}`,
|
||||
siteName: 'Damjan Savić',
|
||||
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')),
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/images/og-image.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Damjan Savić - AI & Automation Specialist',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: titles[locale] || titles.de,
|
||||
description: descriptions[locale] || descriptions.de,
|
||||
images: [`${BASE_URL}/images/og-image.jpg`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function HomePage({ params }: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<HeroSection />
|
||||
<ExperienceSection />
|
||||
<SkillsSection />
|
||||
<AboutSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroSection() {
|
||||
const t = useTranslations('pages.home.hero');
|
||||
|
||||
return (
|
||||
<section id="home" className="relative min-h-screen text-white overflow-hidden">
|
||||
{/* Concentric Circles Background */}
|
||||
<div className="absolute inset-0 pointer-events-none flex items-center justify-center z-[2]">
|
||||
{[1, 2, 3].map((index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full border border-zinc-800/30"
|
||||
style={{
|
||||
width: `${index * 30}%`,
|
||||
height: `${index * 30}%`,
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social Links Bar */}
|
||||
<div className="absolute top-20 left-0 right-0 px-4 sm:px-8 z-40">
|
||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group text-zinc-400 hover:text-white transition-colors"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<Linkedin className="transform transition-transform group-hover:scale-110" size={20} />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/damjan1996"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group text-zinc-400 hover:text-white transition-colors"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<Github className="transform transition-transform group-hover:scale-110" size={20} />
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="mailto:info@damjan-savic.com"
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors uppercase tracking-wider"
|
||||
>
|
||||
{t('cta')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-32 relative z-30">
|
||||
{/* Profile Image */}
|
||||
<div className="w-56 h-56 sm:w-72 sm:h-72 mb-8 rounded-full overflow-hidden border-2 border-zinc-800">
|
||||
<Image
|
||||
src="/portraits/portrait-288.webp"
|
||||
alt={t('name')}
|
||||
width={288}
|
||||
height={288}
|
||||
className="w-full h-full object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title and Name */}
|
||||
<p className="text-zinc-400 text-sm sm:text-base tracking-wider mb-2">
|
||||
{t('title')}
|
||||
</p>
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-3 flex items-center">
|
||||
{t('name')}<span className="animate-pulse ml-1">|</span>
|
||||
</h1>
|
||||
<p className="text-zinc-300 text-sm sm:text-base max-w-xl text-center mb-2 px-4">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-zinc-400 text-xs sm:text-sm mb-8">
|
||||
{t('currentRole')}
|
||||
</p>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 text-white text-base sm:text-lg z-20">
|
||||
{['experience', 'skills', 'projects', 'about'].map((section) => (
|
||||
<Link
|
||||
key={section}
|
||||
href={`#${section}`}
|
||||
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase text-center hover:bg-zinc-700/50 transition-colors"
|
||||
>
|
||||
{t(`navigation.${section}`)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Button Bottom */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-40">
|
||||
<a
|
||||
href="mailto:info@damjan-savic.com"
|
||||
className="w-12 h-12 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition-colors"
|
||||
aria-label="Contact"
|
||||
>
|
||||
<Mail size={20} className="text-white" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ExperienceSection() {
|
||||
const t = useTranslations('pages.home.experience');
|
||||
|
||||
const positions = [
|
||||
{
|
||||
role: 'Process Automation Specialist',
|
||||
company: 'Everlast Consulting GmbH',
|
||||
period: '12/2024 - PRESENT',
|
||||
highlights: [
|
||||
'Development of AI agents with n8n and Zapier',
|
||||
'Building web scraping solutions',
|
||||
'Migration from Power Automate to n8n',
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'Consultant Digital Solutions',
|
||||
company: 'Ritter Digital GmbH',
|
||||
period: '08/2023 - 11/2024',
|
||||
highlights: [
|
||||
'Backend services on dedicated servers',
|
||||
'AI integration via Power Automate',
|
||||
'RFID/IoT solutions with Zebra hardware',
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'ERP Integration Specialist',
|
||||
company: 'Joyce & Girls',
|
||||
period: '01/2023 - 08/2023',
|
||||
highlights: [
|
||||
'ApparelMagic & TradeByte Integration',
|
||||
'MariaDB Middleware Development',
|
||||
'Shopify Optimization',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="experience" className="py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-12 text-center">{t('title')}</h2>
|
||||
<div className="space-y-8">
|
||||
{positions.map((position, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-zinc-800/50 rounded-lg p-6 border border-zinc-700/50"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white">{position.role}</h3>
|
||||
<p className="text-zinc-400">{position.company}</p>
|
||||
</div>
|
||||
<span className="text-zinc-500 text-sm mt-2 sm:mt-0">{position.period}</span>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{position.highlights.map((highlight, i) => (
|
||||
<li key={i} className="text-zinc-300 flex items-start">
|
||||
<span className="text-blue-500 mr-2">-</span>
|
||||
{highlight}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillsSection() {
|
||||
const t = useTranslations('pages.home.skills');
|
||||
|
||||
const skills = [
|
||||
{ name: 'AI & LLMs', level: 90, description: 'GPT-4, Claude API, Vapi Voice AI' },
|
||||
{ name: 'Automation', level: 85, description: 'n8n, Zapier, Power Automate' },
|
||||
{ name: 'Python', level: 90, description: 'Backend, APIs, Automation' },
|
||||
{ name: 'TypeScript', level: 85, description: 'React, Next.js, Node.js' },
|
||||
{ name: 'Database', level: 85, description: 'PostgreSQL, Supabase, MariaDB' },
|
||||
{ name: 'DevOps', level: 75, description: 'Docker, Vercel, Dedicated Servers' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="skills" className="py-20 px-4 bg-zinc-800/30">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-12 text-center">{t('title')}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{skills.map((skill, index) => (
|
||||
<div key={index} className="bg-zinc-900/50 rounded-lg p-6 border border-zinc-700/50">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">{skill.name}</h3>
|
||||
<span className="text-zinc-400 text-sm">{skill.level}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-zinc-700 rounded-full h-2 mb-3">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${skill.level}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-zinc-400 text-sm">{skill.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AboutSection() {
|
||||
const t = useTranslations('pages.home.about');
|
||||
|
||||
return (
|
||||
<section id="about" className="py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-8 text-center">{t('title')}</h2>
|
||||
<div className="bg-zinc-800/50 rounded-lg p-8 border border-zinc-700/50">
|
||||
<p className="text-zinc-300 leading-relaxed whitespace-pre-line">
|
||||
{t('content')}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/about"
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-center"
|
||||
>
|
||||
{t('buttons.learnMore')}
|
||||
</Link>
|
||||
<a
|
||||
href="/damjan_savic_cv_en.pdf"
|
||||
download
|
||||
className="px-6 py-3 bg-zinc-700 hover:bg-zinc-600 text-white rounded-lg transition-colors text-center"
|
||||
>
|
||||
{t('buttons.downloadCV')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<Hero />
|
||||
<Experience />
|
||||
<Skills />
|
||||
<Projects />
|
||||
<About />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,112 +1,443 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { Calendar, Building2, Clock, ArrowLeft, Tag } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { BreadcrumbJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string; slug: string }>;
|
||||
};
|
||||
|
||||
const projects: Record<string, {
|
||||
interface ProjectMeta {
|
||||
date?: string;
|
||||
client?: string;
|
||||
duration?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
technologies: string[];
|
||||
content: string;
|
||||
}> = {
|
||||
technologies?: string[];
|
||||
videoUrl?: string;
|
||||
}
|
||||
|
||||
interface ProjectContent {
|
||||
intro: string;
|
||||
challenge: {
|
||||
title: string;
|
||||
description: string;
|
||||
points?: string[];
|
||||
};
|
||||
solution: {
|
||||
title: string;
|
||||
description: string;
|
||||
content?: string;
|
||||
points?: string[];
|
||||
};
|
||||
technical?: {
|
||||
title: string;
|
||||
description: string;
|
||||
points?: string[];
|
||||
code?: string;
|
||||
};
|
||||
implementation?: {
|
||||
title: string;
|
||||
description: string;
|
||||
points?: string[];
|
||||
};
|
||||
results: {
|
||||
title: string;
|
||||
description: string;
|
||||
points?: string[];
|
||||
};
|
||||
conclusion?: string;
|
||||
}
|
||||
|
||||
interface TranslatedProject {
|
||||
meta: ProjectMeta;
|
||||
content: ProjectContent;
|
||||
}
|
||||
|
||||
// Fallback project data
|
||||
const fallbackProjects: Record<string, TranslatedProject> = {
|
||||
'ai-data-reader': {
|
||||
title: 'AI Data Reader',
|
||||
description: 'KI-gestuetzte PDF-Datenextraktion mit Claude AI und JTL-Integration',
|
||||
category: 'Data Processing',
|
||||
technologies: ['Python', 'FastAPI', 'Claude AI', 'PyPDF', 'Pydantic', 'JTL-Wawi'],
|
||||
content: `Ein KI-gestuetztes System zur automatisierten Extraktion von Produktdaten aus PDF-Dokumenten mit direkter Integration in JTL-Warenwirtschaftssysteme.
|
||||
|
||||
## Die Herausforderung
|
||||
|
||||
Die manuelle Erfassung von Produktdaten aus PDF-Dokumenten war zeitaufwendig und fehleranfaellig:
|
||||
- Grosse Mengen an PDF-Dokumenten mit Produktinformationen
|
||||
- Zeitintensive manuelle Dateneingabe
|
||||
- Inkonsistente Datenqualitaet
|
||||
|
||||
## Die Loesung
|
||||
|
||||
Durch den Einsatz von Claude AI und modernen Python-Technologien wurde eine vollautomatische Loesung geschaffen:
|
||||
- Automatisierte PDF-Verarbeitung
|
||||
- KI-gestuetzte Datenextraktion
|
||||
- Intelligente Datenvalidierung
|
||||
- Nahtlose JTL-Integration
|
||||
|
||||
## Ergebnisse
|
||||
|
||||
- 90% Zeitersparnis bei der Produktdatenpflege
|
||||
- 99% Genauigkeit bei der Datenextraktion
|
||||
- Eliminierung manueller Dateneingaben`,
|
||||
meta: {
|
||||
title: 'AI Document Reader',
|
||||
description: 'KI-gestützte PDF-Datenextraktion mit Claude AI und JTL-Integration',
|
||||
date: '2024-01',
|
||||
client: 'Internal Project',
|
||||
duration: '3 Monate',
|
||||
technologies: ['Python', 'FastAPI', 'Claude AI', 'PyPDF', 'JTL-Wawi'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Ein KI-gestütztes System zur automatisierten Extraktion von Produktdaten aus PDF-Dokumenten mit direkter Integration in JTL-Warenwirtschaftssysteme.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Die manuelle Erfassung von Produktdaten aus PDF-Dokumenten war zeitaufwendig und fehleranfällig.',
|
||||
points: [
|
||||
'Große Mengen an PDF-Dokumenten mit Produktinformationen',
|
||||
'Zeitintensive manuelle Dateneingabe',
|
||||
'Inkonsistente Datenqualität'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Durch den Einsatz von Claude AI und modernen Python-Technologien wurde eine vollautomatische Lösung geschaffen.',
|
||||
points: [
|
||||
'Automatisierte PDF-Verarbeitung',
|
||||
'KI-gestützte Datenextraktion',
|
||||
'Intelligente Datenvalidierung',
|
||||
'Nahtlose JTL-Integration'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Die Implementierung führte zu signifikanten Verbesserungen.',
|
||||
points: [
|
||||
'90% Zeitersparnis bei der Produktdatenpflege',
|
||||
'99% Genauigkeit bei der Datenextraktion',
|
||||
'Eliminierung manueller Dateneingaben'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'smart-warehouse': {
|
||||
title: 'Smart Warehouse',
|
||||
description: 'RFID-basierte Lagerverwaltung mit IoT-Integration',
|
||||
category: 'Integration',
|
||||
technologies: ['Python', 'RFID', 'Zebra Hardware', 'PostgreSQL', 'FastAPI'],
|
||||
content: `Ein intelligentes Lagerverwaltungssystem mit RFID-Technologie und IoT-Integration fuer Echtzeit-Bestandsverfolgung.
|
||||
|
||||
## Features
|
||||
|
||||
- Echtzeit-Bestandsverfolgung mit RFID
|
||||
- Integration mit Zebra-Hardware
|
||||
- Automatische Bestandsaktualisierung
|
||||
- Dashboard fuer Lageranalyse`,
|
||||
meta: {
|
||||
title: 'Smart Warehouse RFID',
|
||||
description: 'RFID-basierte Lagerverwaltung mit IoT-Integration',
|
||||
date: '2024-02',
|
||||
client: 'Enterprise Client',
|
||||
duration: '6 Monate',
|
||||
technologies: ['Python', 'RFID', 'Zebra Hardware', 'PostgreSQL', 'FastAPI'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Ein intelligentes Lagerverwaltungssystem mit RFID-Technologie und IoT-Integration für Echtzeit-Bestandsverfolgung.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Traditionelle Lagerverwaltung mit manueller Bestandsführung führte zu Ineffizienzen.',
|
||||
points: [
|
||||
'Manuelle Bestandszählung war zeitaufwendig',
|
||||
'Bestandsdiskrepanzen durch Fehler',
|
||||
'Keine Echtzeit-Transparenz'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Implementierung eines RFID-basierten Systems mit Zebra-Hardware.',
|
||||
points: [
|
||||
'Echtzeit-Bestandsverfolgung mit RFID',
|
||||
'Integration mit Zebra-Hardware',
|
||||
'Automatische Bestandsaktualisierung',
|
||||
'Dashboard für Lageranalyse'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Das System verbesserte die Lageroperationen erheblich.',
|
||||
points: [
|
||||
'95% Reduzierung der Inventurzeit',
|
||||
'99.9% Bestandsgenauigkeit',
|
||||
'Echtzeit-Sichtbarkeit aller Bestände'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'website-mit-ki': {
|
||||
title: 'KI-Portfolio Website',
|
||||
description: 'Diese Portfolio-Website mit Next.js und modernen Web-Technologien',
|
||||
category: 'Web Development',
|
||||
technologies: ['Next.js', 'TypeScript', 'Tailwind CSS', 'Vercel', 'React'],
|
||||
content: `Eine moderne Portfolio-Website entwickelt mit Next.js 15 und Server-Side Rendering fuer optimale Performance und SEO.
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- Next.js 15 mit App Router
|
||||
- TypeScript fuer Typsicherheit
|
||||
- Tailwind CSS fuer Styling
|
||||
- Server-Side Rendering fuer SEO
|
||||
- Internationalisierung (DE/EN/SR)`,
|
||||
meta: {
|
||||
title: 'Portfolio mit KI',
|
||||
description: 'Moderne Portfolio-Website mit KI-Integration',
|
||||
date: '2024-03',
|
||||
client: 'Personal Project',
|
||||
duration: '2 Monate',
|
||||
technologies: ['Next.js', 'TypeScript', 'Tailwind CSS', 'Supabase', 'React'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Eine moderne Portfolio-Website entwickelt mit Next.js 15 und Server-Side Rendering für optimale Performance und SEO.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Erstellung einer performanten, mehrsprachigen Portfolio-Website.',
|
||||
points: [
|
||||
'SEO-Optimierung für multiple Sprachen',
|
||||
'Schnelle Ladezeiten',
|
||||
'Moderne, responsive Design'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Next.js 15 mit App Router und i18n-Integration.',
|
||||
points: [
|
||||
'Server-Side Rendering für SEO',
|
||||
'Internationalisierung (DE/EN/SR)',
|
||||
'Tailwind CSS für responsives Design',
|
||||
'Supabase für Backend-Services'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Die Website erreicht hervorragende Performance-Werte.',
|
||||
points: [
|
||||
'Lighthouse Score > 95',
|
||||
'Optimale SEO-Rankings',
|
||||
'Schnelle Ladezeiten weltweit'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'power-platform-governance': {
|
||||
title: 'Power Platform Governance',
|
||||
description: 'Enterprise Governance-Loesung fuer Microsoft Power Platform',
|
||||
category: 'Enterprise Software',
|
||||
technologies: ['Power Automate', 'SharePoint', 'Azure', 'TypeScript', 'Power Apps'],
|
||||
content: `Eine umfassende Governance-Loesung fuer Microsoft Power Platform in Unternehmensumgebungen.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatisierte Compliance-Pruefungen
|
||||
- Benutzer- und Ressourcenverwaltung
|
||||
- Audit-Trails und Reporting
|
||||
- Integration mit Azure AD`,
|
||||
meta: {
|
||||
title: 'Power Platform Governance',
|
||||
description: 'Enterprise Governance-Lösung für Microsoft Power Platform',
|
||||
date: '2023-11',
|
||||
client: 'Enterprise Client',
|
||||
duration: '4 Monate',
|
||||
technologies: ['Power Automate', 'SharePoint', 'Azure', 'TypeScript'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Eine umfassende Governance-Lösung für Microsoft Power Platform in Unternehmensumgebungen.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Verwaltung und Kontrolle der Power Platform-Nutzung im Unternehmen.',
|
||||
points: [
|
||||
'Unkontrollierte Citizen Development',
|
||||
'Fehlende Compliance-Prüfungen',
|
||||
'Mangelnde Übersicht über Ressourcen'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Entwicklung einer Governance-Lösung mit automatisierten Prüfungen.',
|
||||
points: [
|
||||
'Automatisierte Compliance-Prüfungen',
|
||||
'Benutzer- und Ressourcenverwaltung',
|
||||
'Audit-Trails und Reporting',
|
||||
'Integration mit Azure AD'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Verbesserte Kontrolle und Compliance.',
|
||||
points: [
|
||||
'100% Compliance-Erfüllung',
|
||||
'Vollständige Ressourcen-Transparenz',
|
||||
'Reduzierte Sicherheitsrisiken'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'automated-ad-creatives': {
|
||||
meta: {
|
||||
title: 'Automated Ad Creatives',
|
||||
description: 'Automatisierte Erstellung von Werbeanzeigen',
|
||||
date: '2023-10',
|
||||
client: 'Marketing Agency',
|
||||
duration: '2 Monate',
|
||||
technologies: ['Python', 'OpenAI', 'Pillow', 'FastAPI'],
|
||||
},
|
||||
content: {
|
||||
intro: 'KI-gestützte Generierung von Marketing-Creatives für Social Media Kampagnen.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Manuelle Erstellung von Werbegrafiken war zeitaufwendig.',
|
||||
points: [
|
||||
'Hoher Zeitaufwand pro Creative',
|
||||
'Begrenzte Varianten möglich',
|
||||
'Manuelle Anpassung an Formate'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Automatisierte Creative-Generierung mit KI.',
|
||||
points: [
|
||||
'KI-generierte Texte und Layouts',
|
||||
'Automatische Formatanpassung',
|
||||
'Batch-Verarbeitung',
|
||||
'A/B-Testing-Varianten'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Signifikante Effizienzsteigerung.',
|
||||
points: [
|
||||
'80% Zeitersparnis',
|
||||
'10x mehr Varianten',
|
||||
'Bessere Ad-Performance'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'kamenpro': {
|
||||
meta: {
|
||||
title: 'KamenPro',
|
||||
description: 'E-Commerce Platform für Steinprodukte',
|
||||
date: '2023-08',
|
||||
client: 'KamenPro',
|
||||
duration: '3 Monate',
|
||||
technologies: ['Shopify', 'JavaScript', 'Liquid', 'ERP Integration'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Shopify-basierte E-Commerce-Lösung mit ERP-Integration für einen Steinprodukte-Händler.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Migration von manuellem Verkauf zu E-Commerce.',
|
||||
points: [
|
||||
'Komplexe Produktkonfiguration',
|
||||
'Integration mit bestehendem ERP',
|
||||
'Mehrsprachiger Shop erforderlich'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'Shopify-Shop mit Custom-Entwicklung.',
|
||||
points: [
|
||||
'Custom Theme-Entwicklung',
|
||||
'Produktkonfigurator',
|
||||
'ERP-Synchronisation',
|
||||
'Mehrsprachige Inhalte'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Erfolgreicher E-Commerce-Launch.',
|
||||
points: [
|
||||
'200% Umsatzsteigerung',
|
||||
'Automatisierte Bestellverarbeitung',
|
||||
'Internationale Kunden'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'ai-music-production': {
|
||||
meta: {
|
||||
title: 'AI Music Production',
|
||||
description: 'KI-gestützte Musikproduktion',
|
||||
date: '2023-06',
|
||||
client: 'Music Label',
|
||||
duration: '4 Monate',
|
||||
technologies: ['Python', 'TensorFlow', 'MIDI', 'Audio Processing'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Automatisierte Musikgenerierung mit Machine Learning für Hintergrundmusik und Jingles.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Bedarf an kostengünstiger, lizenzfreier Musik.',
|
||||
points: [
|
||||
'Hohe Lizenzkosten',
|
||||
'Begrenzte Auswahl',
|
||||
'Lange Produktionszeiten'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'KI-basierte Musikgenerierung.',
|
||||
points: [
|
||||
'Training auf lizenzfreien Samples',
|
||||
'Genre-spezifische Modelle',
|
||||
'MIDI-Export',
|
||||
'Audio-Postprocessing'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Kosteneffiziente Musikproduktion.',
|
||||
points: [
|
||||
'90% Kostenersparnis',
|
||||
'Unbegrenzte Varianten',
|
||||
'Schnelle Generierung'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'cursor-ide': {
|
||||
meta: {
|
||||
title: 'Cursor IDE Integration',
|
||||
description: 'AI-gestützte Entwicklungsumgebung',
|
||||
date: '2023-05',
|
||||
client: 'Internal Project',
|
||||
duration: '2 Monate',
|
||||
technologies: ['TypeScript', 'VS Code API', 'Claude AI', 'GPT-4'],
|
||||
},
|
||||
content: {
|
||||
intro: 'Integration von KI-Assistenten in den Entwicklungsworkflow für produktivere Programmierung.',
|
||||
challenge: {
|
||||
title: 'Die Herausforderung',
|
||||
description: 'Optimierung des Entwicklungsworkflows.',
|
||||
points: [
|
||||
'Wiederholende Coding-Tasks',
|
||||
'Dokumentationserstellung',
|
||||
'Code-Reviews'
|
||||
]
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
description: 'KI-gestützte IDE-Integration.',
|
||||
points: [
|
||||
'Code-Completion mit Claude AI',
|
||||
'Automatische Dokumentation',
|
||||
'Code-Review-Assistent',
|
||||
'Refactoring-Vorschläge'
|
||||
]
|
||||
},
|
||||
results: {
|
||||
title: 'Ergebnisse',
|
||||
description: 'Gesteigerte Entwicklerproduktivität.',
|
||||
points: [
|
||||
'40% schnellere Entwicklung',
|
||||
'Bessere Code-Qualität',
|
||||
'Automatisierte Dokumentation'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate static params for all projects
|
||||
export async function generateStaticParams() {
|
||||
const slugs = Object.keys(projects);
|
||||
const locales = ['de', 'en', 'sr'];
|
||||
const slugs = Object.keys(fallbackProjects);
|
||||
|
||||
return locales.flatMap((locale) =>
|
||||
slugs.map((slug) => ({ locale, slug }))
|
||||
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 { slug } = await params;
|
||||
const project = projects[slug];
|
||||
const { locale, slug } = await params;
|
||||
const project = fallbackProjects[slug];
|
||||
|
||||
if (!project) {
|
||||
return { title: 'Project Not Found' };
|
||||
return {
|
||||
title: 'Project Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: project.title,
|
||||
description: project.description,
|
||||
title: project.meta.title,
|
||||
description: project.meta.description,
|
||||
keywords: project.meta.technologies,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}/${locale}/portfolio/${slug}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/de/portfolio/${slug}`,
|
||||
de: `${BASE_URL}/de/portfolio/${slug}`,
|
||||
en: `${BASE_URL}/en/portfolio/${slug}`,
|
||||
sr: `${BASE_URL}/sr/portfolio/${slug}`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: project.meta.title,
|
||||
description: project.meta.description,
|
||||
type: 'article',
|
||||
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: [`/images/projects/${slug}/cover.jpg`],
|
||||
url: `${BASE_URL}/${locale}/portfolio/${slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,85 +445,209 @@ export default async function ProjectPage({ params }: Props) {
|
||||
const { locale, slug } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const project = projects[slug];
|
||||
const t = await getTranslations('portfolio.ui');
|
||||
const commonT = await getTranslations('common.nav');
|
||||
const project = fallbackProjects[slug];
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: commonT('home'), url: `/${locale}` },
|
||||
{ name: 'Portfolio', url: `/${locale}/portfolio` },
|
||||
{ name: project.meta.title, url: `/${locale}/portfolio/${slug}` },
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8">
|
||||
<Link href={`/${locale}`} className="text-zinc-400 hover:text-white transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span className="text-zinc-600 mx-2">/</span>
|
||||
<Link href={`/${locale}/portfolio`} className="text-zinc-400 hover:text-white transition-colors">
|
||||
Portfolio
|
||||
</Link>
|
||||
<span className="text-zinc-600 mx-2">/</span>
|
||||
<span className="text-white">{project.title}</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<header className="mb-12">
|
||||
<span className="text-blue-400 text-sm font-medium">{project.category}</span>
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mt-2 mb-4">{project.title}</h1>
|
||||
<p className="text-zinc-400 text-lg">{project.description}</p>
|
||||
|
||||
{/* Technologies */}
|
||||
<div className="flex flex-wrap gap-2 mt-6">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="text-sm bg-zinc-800 text-zinc-300 px-3 py-1 rounded-full"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<article className="prose prose-invert prose-zinc max-w-none">
|
||||
{project.content.split('\n\n').map((paragraph, index) => {
|
||||
if (paragraph.startsWith('## ')) {
|
||||
return (
|
||||
<h2 key={index} className="text-2xl font-bold mt-8 mb-4">
|
||||
{paragraph.replace('## ', '')}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
if (paragraph.startsWith('- ')) {
|
||||
const items = paragraph.split('\n').filter((item) => item.startsWith('- '));
|
||||
return (
|
||||
<ul key={index} className="list-disc list-inside space-y-2 text-zinc-300">
|
||||
{items.map((item, i) => (
|
||||
<li key={i}>{item.replace('- ', '')}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p key={index} className="text-zinc-300 leading-relaxed">
|
||||
{paragraph}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</article>
|
||||
|
||||
{/* Back Link */}
|
||||
<div className="mt-12 pt-8 border-t border-zinc-800">
|
||||
<main className="min-h-screen">
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
||||
{/* Back Navigation */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href={`/${locale}/portfolio`}
|
||||
className="text-blue-400 hover:text-blue-300 transition-colors"
|
||||
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors duration-300"
|
||||
>
|
||||
← Back to Portfolio
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span>{t('backToPortfolio')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Header */}
|
||||
<div className="mb-12">
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6 text-zinc-400">
|
||||
{project.meta.date && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<time>{project.meta.date}</time>
|
||||
</div>
|
||||
)}
|
||||
{project.meta.client && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span>{project.meta.client}</span>
|
||||
</div>
|
||||
)}
|
||||
{project.meta.duration && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>{project.meta.duration}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-8">
|
||||
{project.meta.title}
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-zinc-400">
|
||||
{project.meta.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cover Image */}
|
||||
<div className="mb-12">
|
||||
<div className="relative aspect-video rounded-lg overflow-hidden bg-zinc-800">
|
||||
<Image
|
||||
src={`/images/projects/${slug}/cover.jpg`}
|
||||
alt={project.meta.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 900px"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900/50 to-transparent opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Content */}
|
||||
<div className="prose prose-invert max-w-none">
|
||||
{/* Intro */}
|
||||
<div className="mb-12 p-6 bg-zinc-800/30 rounded-lg border border-zinc-800">
|
||||
<p className="text-lg text-zinc-300 m-0">
|
||||
{project.content.intro}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Challenge Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.challenge.title}</h2>
|
||||
<p className="text-zinc-300 mb-4">{project.content.challenge.description}</p>
|
||||
{project.content.challenge.points && (
|
||||
<ul className="space-y-2">
|
||||
{project.content.challenge.points.map((point, index) => (
|
||||
<li key={index} className="text-zinc-300 flex items-start gap-2">
|
||||
<span className="text-zinc-500">•</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Solution Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.solution.title}</h2>
|
||||
<p className="text-zinc-300 mb-4">{project.content.solution.description}</p>
|
||||
{project.content.solution.points && (
|
||||
<ul className="space-y-2">
|
||||
{project.content.solution.points.map((point, index) => (
|
||||
<li key={index} className="text-zinc-300 flex items-start gap-2">
|
||||
<span className="text-zinc-500">•</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Technical Section */}
|
||||
{project.content.technical && (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.technical.title}</h2>
|
||||
<p className="text-zinc-300 mb-4">{project.content.technical.description}</p>
|
||||
{project.content.technical.points && (
|
||||
<ul className="space-y-2">
|
||||
{project.content.technical.points.map((point, index) => (
|
||||
<li key={index} className="text-zinc-300 flex items-start gap-2">
|
||||
<span className="text-zinc-500">•</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{project.content.technical.code && (
|
||||
<pre className="bg-zinc-800/50 p-4 rounded-lg overflow-x-auto mt-4">
|
||||
<code className="text-sm">{project.content.technical.code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Implementation Section */}
|
||||
{project.content.implementation && (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.implementation.title}</h2>
|
||||
<p className="text-zinc-300 mb-4">{project.content.implementation.description}</p>
|
||||
{project.content.implementation.points && (
|
||||
<ul className="space-y-2">
|
||||
{project.content.implementation.points.map((point, index) => (
|
||||
<li key={index} className="text-zinc-300 flex items-start gap-2">
|
||||
<span className="text-zinc-500">•</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Results Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.results.title}</h2>
|
||||
<p className="text-zinc-300 mb-4">{project.content.results.description}</p>
|
||||
{project.content.results.points && (
|
||||
<ul className="space-y-2">
|
||||
{project.content.results.points.map((point, index) => (
|
||||
<li key={index} className="text-zinc-300 flex items-start gap-2">
|
||||
<span className="text-zinc-500">•</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Conclusion */}
|
||||
{project.content.conclusion && (
|
||||
<section className="mb-12 p-6 bg-zinc-800/30 rounded-lg border border-zinc-800">
|
||||
<p className="text-lg text-zinc-300 m-0">
|
||||
{project.content.conclusion}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Technologies */}
|
||||
{project.meta.technologies && (
|
||||
<div className="mt-12 pt-6 border-t border-zinc-800">
|
||||
<div className="flex items-center gap-4">
|
||||
<Tag className="h-4 w-4 text-zinc-400" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.meta.technologies.map((tech, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 bg-zinc-800/50 border border-zinc-800 rounded-full text-sm text-zinc-300"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,160 @@
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Link from 'next/link';
|
||||
import { PortfolioGrid } from '@/components/portfolio';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
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 t = await getTranslations({ locale, namespace: 'portfolio.seo' });
|
||||
|
||||
const keywords: Record<string, string[]> = {
|
||||
de: [
|
||||
'Portfolio Damjan Savić',
|
||||
'KI Projekte',
|
||||
'Automatisierung Projekte',
|
||||
'Python Entwicklung',
|
||||
'React Projekte',
|
||||
'RFID IoT',
|
||||
'n8n Automatisierung',
|
||||
'Web Entwicklung Köln',
|
||||
],
|
||||
en: [
|
||||
'AI Developer Portfolio',
|
||||
'AI Projects',
|
||||
'Automation Projects',
|
||||
'Python Development',
|
||||
'React Projects',
|
||||
'n8n Automation',
|
||||
'Remote Developer Portfolio',
|
||||
'Hire AI Developer',
|
||||
'AI Agent Projects',
|
||||
'Voice AI Projects',
|
||||
'SaaS Development Portfolio',
|
||||
],
|
||||
sr: [
|
||||
'Portfolio Damjan Savić',
|
||||
'AI Projekti',
|
||||
'Automatizacija Projekti',
|
||||
'Python Razvoj',
|
||||
'React Projekti',
|
||||
'RFID IoT',
|
||||
'n8n Automatizacija',
|
||||
'Web Razvoj Keln',
|
||||
],
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/portfolio' : `/${locale}/portfolio`;
|
||||
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
keywords: keywords[locale] || keywords.de,
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/portfolio`,
|
||||
de: `${BASE_URL}/portfolio`,
|
||||
en: `${BASE_URL}/en/portfolio`,
|
||||
sr: `${BASE_URL}/sr/portfolio`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
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')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Project data for SSG
|
||||
const projects = [
|
||||
{
|
||||
slug: 'ai-data-reader',
|
||||
title: 'AI Data Reader',
|
||||
description: 'KI-gestuetzte PDF-Datenextraktion mit Claude AI und JTL-Integration',
|
||||
category: 'Data Processing',
|
||||
technologies: ['Python', 'FastAPI', 'Claude AI', 'JTL-Wawi'],
|
||||
title: 'AI Document Reader',
|
||||
description: 'KI-gestützte Dokumentenanalyse mit OLLAMA und Python',
|
||||
excerpt: 'KI-gestützte PDF-Datenextraktion mit Claude AI und JTL-Integration',
|
||||
category: 'AI Development',
|
||||
date: '2024-01',
|
||||
technologies: ['Python', 'OLLAMA', 'FastAPI', 'React', 'Claude AI', 'JTL-Wawi'],
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
slug: 'smart-warehouse',
|
||||
title: 'Smart Warehouse',
|
||||
description: 'RFID-basierte Lagerverwaltung mit IoT-Integration',
|
||||
category: 'Integration',
|
||||
technologies: ['Python', 'RFID', 'Zebra Hardware', 'PostgreSQL'],
|
||||
title: 'Smart Warehouse RFID',
|
||||
description: 'RFID-basiertes Lagerverwaltungssystem mit IoT-Integration',
|
||||
excerpt: 'RFID-basierte Lagerverwaltung mit Zebra-Hardware und Echtzeit-Tracking',
|
||||
category: 'IoT',
|
||||
date: '2024-02',
|
||||
technologies: ['Python', 'RFID', 'IoT', 'PostgreSQL', 'Zebra Hardware'],
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
slug: 'website-mit-ki',
|
||||
title: 'KI-Portfolio Website',
|
||||
description: 'Diese Portfolio-Website mit Next.js und modernen Web-Technologien',
|
||||
category: 'Web Development',
|
||||
technologies: ['Next.js', 'TypeScript', 'Tailwind CSS', 'Vercel'],
|
||||
title: 'Portfolio mit KI',
|
||||
description: 'Moderne Portfolio-Website mit KI-Integration',
|
||||
excerpt: 'Next.js 15 Portfolio mit SSR, i18n und modernen Web-Technologien',
|
||||
category: 'Full-Stack',
|
||||
date: '2024-03',
|
||||
technologies: ['Next.js', 'TypeScript', 'Tailwind', 'Supabase', 'React'],
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
slug: 'power-platform-governance',
|
||||
title: 'Power Platform Governance',
|
||||
description: 'Enterprise Governance-Loesung fuer Microsoft Power Platform',
|
||||
description: 'Enterprise Governance-Lösung für Microsoft Power Platform',
|
||||
excerpt: 'Automatisierte Compliance-Prüfungen und Ressourcenverwaltung',
|
||||
category: 'Enterprise Software',
|
||||
technologies: ['Power Automate', 'SharePoint', 'Azure', 'TypeScript'],
|
||||
date: '2023-11',
|
||||
technologies: ['Power Automate', 'SharePoint', 'Azure', 'TypeScript', 'Power Apps'],
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
slug: 'automated-ad-creatives',
|
||||
title: 'Automated Ad Creatives',
|
||||
description: 'Automatisierte Erstellung von Werbeanzeigen',
|
||||
excerpt: 'KI-gestützte Generierung von Marketing-Creatives',
|
||||
category: 'AI Development',
|
||||
date: '2023-10',
|
||||
technologies: ['Python', 'OpenAI', 'Pillow', 'FastAPI'],
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
slug: 'kamenpro',
|
||||
title: 'KamenPro',
|
||||
description: 'E-Commerce Platform für Steinprodukte',
|
||||
excerpt: 'Shopify-basierte E-Commerce-Lösung mit ERP-Integration',
|
||||
category: 'E-Commerce',
|
||||
date: '2023-08',
|
||||
technologies: ['Shopify', 'JavaScript', 'Liquid', 'ERP Integration'],
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
slug: 'ai-music-production',
|
||||
title: 'AI Music Production',
|
||||
description: 'KI-gestützte Musikproduktion',
|
||||
excerpt: 'Automatisierte Musikgenerierung mit Machine Learning',
|
||||
category: 'AI Development',
|
||||
date: '2023-06',
|
||||
technologies: ['Python', 'TensorFlow', 'MIDI', 'Audio Processing'],
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
slug: 'cursor-ide',
|
||||
title: 'Cursor IDE Integration',
|
||||
description: 'AI-gestützte Entwicklungsumgebung',
|
||||
excerpt: 'Integration von KI-Assistenten in den Entwicklungsworkflow',
|
||||
category: 'Developer Tools',
|
||||
date: '2023-05',
|
||||
technologies: ['TypeScript', 'VS Code API', 'Claude AI', 'GPT-4'],
|
||||
featured: false,
|
||||
},
|
||||
];
|
||||
@@ -59,65 +166,21 @@ export default async function PortfolioPage({ params }: Props) {
|
||||
const t = await getTranslations('portfolio');
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8">
|
||||
<Link href={`/${locale}`} className="text-zinc-400 hover:text-white transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span className="text-zinc-600 mx-2">/</span>
|
||||
<span className="text-white">Portfolio</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<section className="mb-16">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-4">{t('sections.title')}</h1>
|
||||
<p className="text-zinc-400 text-lg max-w-2xl">
|
||||
{t('sections.subtitle')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<section>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.slug}
|
||||
href={`/${locale}/portfolio/${project.slug}`}
|
||||
className="group bg-zinc-800/50 rounded-lg overflow-hidden border border-zinc-700/50 hover:border-zinc-600 transition-colors"
|
||||
>
|
||||
<div className="aspect-video bg-zinc-700/50 flex items-center justify-center">
|
||||
<span className="text-zinc-500 text-sm">{project.category}</span>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h2 className="text-xl font-semibold text-white group-hover:text-blue-400 transition-colors">
|
||||
{project.title}
|
||||
</h2>
|
||||
{project.featured && (
|
||||
<span className="text-xs bg-blue-600/20 text-blue-400 px-2 py-1 rounded">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-zinc-400 mb-4">{project.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.technologies.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="text-xs bg-zinc-700/50 text-zinc-300 px-2 py-1 rounded"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<div className="relative min-h-screen">
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-3xl font-bold text-white mb-4">
|
||||
{t('sections.title')}
|
||||
</h1>
|
||||
<p className="text-zinc-400 max-w-2xl mx-auto">
|
||||
{t('sections.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<PortfolioGrid projects={projects} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,38 @@ type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
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 t = await getTranslations({ locale, namespace: 'pages.terms.seo' });
|
||||
|
||||
const localePath = locale === 'de' ? '/terms' : `/${locale}/terms`;
|
||||
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
alternates: {
|
||||
canonical: `${BASE_URL}${localePath}`,
|
||||
languages: {
|
||||
'x-default': `${BASE_URL}/terms`,
|
||||
de: `${BASE_URL}/terms`,
|
||||
en: `${BASE_URL}/en/terms`,
|
||||
sr: `${BASE_URL}/sr/terms`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
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')),
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +73,7 @@ export default async function TermsPage({ params }: Props) {
|
||||
<h2>2. Intellectual Property</h2>
|
||||
<p>
|
||||
All content on this website, including text, graphics, logos, images, and software,
|
||||
is the property of Damjan Savic and is protected by intellectual property laws.
|
||||
is the property of Damjan Savić and is protected by intellectual property laws.
|
||||
</p>
|
||||
|
||||
<h2>3. User Responsibilities</h2>
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Damjan Savic',
|
||||
default: 'Damjan Savic | AI & Automation Specialist',
|
||||
template: '%s | Damjan Savić',
|
||||
default: 'Damjan Savić | AI & Automation Specialist',
|
||||
},
|
||||
description: 'AI & Automation Specialist - Building AI agents and automation solutions.',
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/login', '/dashboard', '/api/', '/admin', '/.env'],
|
||||
crawlDelay: 1,
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { locales } from '@/i18n/config';
|
||||
import { getAllCitySlugs } from '@/data/cities';
|
||||
import { getAllServiceSlugs } from '@/data/services';
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
// Blog slugs
|
||||
const blogSlugs = [
|
||||
'n8n-automatisierung-tutorial',
|
||||
'geo-generative-engine-optimization',
|
||||
'gpt-api-integration-praxisguide',
|
||||
'ki-agenten-unternehmen-guide',
|
||||
'n8n-make-zapier-vergleich',
|
||||
'voice-ai-kundenservice-trends',
|
||||
'saas-mvp-entwicklung-4-wochen',
|
||||
'erp-integration-breuninger',
|
||||
];
|
||||
|
||||
// Portfolio slugs
|
||||
const portfolioSlugs = [
|
||||
'ai-data-reader',
|
||||
'smart-warehouse',
|
||||
'website-mit-ki',
|
||||
'power-platform-governance',
|
||||
'automated-ad-creatives',
|
||||
'kamenpro',
|
||||
'ai-music-production',
|
||||
'cursor-ide',
|
||||
];
|
||||
|
||||
function generateAlternates(path: string) {
|
||||
const alternates: { [key: string]: string } = {
|
||||
// x-default zeigt auf deutsche Version als Fallback
|
||||
'x-default': `${BASE_URL}${path || '/'}`,
|
||||
};
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? path : `/${locale}${path}`;
|
||||
alternates[locale] = `${BASE_URL}${localePath}`;
|
||||
});
|
||||
return alternates;
|
||||
}
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const sitemap: MetadataRoute.Sitemap = [];
|
||||
|
||||
// Static pages
|
||||
const staticPages = [
|
||||
{ path: '', priority: 1.0, changeFrequency: 'weekly' as const },
|
||||
{ path: '/about', priority: 0.8, changeFrequency: 'monthly' as const },
|
||||
{ path: '/portfolio', priority: 0.9, changeFrequency: 'weekly' as const },
|
||||
{ path: '/blog', priority: 0.9, changeFrequency: 'weekly' as const },
|
||||
{ path: '/contact', priority: 0.8, changeFrequency: 'monthly' as const },
|
||||
{ path: '/leistungen', priority: 0.9, changeFrequency: 'weekly' as const },
|
||||
{ path: '/privacy', priority: 0.3, changeFrequency: 'yearly' as const },
|
||||
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly' as const },
|
||||
{ path: '/imprint', priority: 0.3, changeFrequency: 'yearly' as const },
|
||||
];
|
||||
|
||||
// Add static pages for all locales
|
||||
staticPages.forEach((page) => {
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? page.path : `/${locale}${page.path}`;
|
||||
sitemap.push({
|
||||
url: `${BASE_URL}${localePath || '/'}`,
|
||||
lastModified: today,
|
||||
changeFrequency: page.changeFrequency,
|
||||
priority: page.priority,
|
||||
alternates: {
|
||||
languages: generateAlternates(page.path),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add service pages
|
||||
const serviceSlugs = getAllServiceSlugs();
|
||||
serviceSlugs.forEach((slug) => {
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`;
|
||||
sitemap.push({
|
||||
url: `${BASE_URL}${localePath}`,
|
||||
lastModified: today,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
alternates: {
|
||||
languages: generateAlternates(`/leistungen/${slug}`),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add city pages
|
||||
const citySlugs = getAllCitySlugs();
|
||||
citySlugs.forEach((slug) => {
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? `/leistungen/standort/${slug}` : `/${locale}/leistungen/standort/${slug}`;
|
||||
sitemap.push({
|
||||
url: `${BASE_URL}${localePath}`,
|
||||
lastModified: today,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: {
|
||||
languages: generateAlternates(`/leistungen/standort/${slug}`),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add blog posts
|
||||
blogSlugs.forEach((slug) => {
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? `/blog/${slug}` : `/${locale}/blog/${slug}`;
|
||||
sitemap.push({
|
||||
url: `${BASE_URL}${localePath}`,
|
||||
lastModified: today,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: {
|
||||
languages: generateAlternates(`/blog/${slug}`),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add portfolio projects
|
||||
portfolioSlugs.forEach((slug) => {
|
||||
locales.forEach((locale) => {
|
||||
const localePath = locale === 'de' ? `/portfolio/${slug}` : `/${locale}/portfolio/${slug}`;
|
||||
sitemap.push({
|
||||
url: `${BASE_URL}${localePath}`,
|
||||
lastModified: today,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
alternates: {
|
||||
languages: generateAlternates(`/portfolio/${slug}`),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return sitemap;
|
||||
}
|
||||
Reference in New Issue
Block a user