Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
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 }>;
|
||||
};
|
||||
|
||||
interface ProjectMeta {
|
||||
date?: string;
|
||||
client?: string;
|
||||
duration?: string;
|
||||
title: string;
|
||||
description: 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': {
|
||||
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': {
|
||||
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': {
|
||||
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': {
|
||||
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 locales = ['de', 'en', 'sr'];
|
||||
const slugs = Object.keys(fallbackProjects);
|
||||
|
||||
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 project = fallbackProjects[slug];
|
||||
|
||||
if (!project) {
|
||||
return {
|
||||
title: 'Project Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
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.avif`],
|
||||
url: `${BASE_URL}/${locale}/portfolio/${slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProjectPage({ params }: Props) {
|
||||
const { locale, slug } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
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">
|
||||
<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="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors duration-300"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span>{t('backToPortfolio')}</span>
|
||||
</Link>
|
||||
</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.avif`}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { PortfolioGrid, CategoryFilter } from '@/components/portfolio';
|
||||
import { BreadcrumbJsonLd, CollectionPageJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams: Promise<{ category?: 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}`,
|
||||
siteName: 'Damjan Savić',
|
||||
type: 'website',
|
||||
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
|
||||
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/images/og-image.avif`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Damjan Savić - AI & Automation Specialist',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
images: [`${BASE_URL}/images/og-image.avif`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Project data for SSG
|
||||
const projects = [
|
||||
{
|
||||
slug: 'ai-data-reader',
|
||||
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 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: '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-Lösung für Microsoft Power Platform',
|
||||
excerpt: 'Automatisierte Compliance-Prüfungen und Ressourcenverwaltung',
|
||||
category: 'Enterprise Software',
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export default async function PortfolioPage({ params, searchParams }: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations('portfolio');
|
||||
|
||||
// Get category filter from URL params
|
||||
const { category } = await searchParams;
|
||||
|
||||
// Filter projects by category if specified
|
||||
const filteredProjects = category
|
||||
? projects.filter((project) => project.category === category)
|
||||
: projects;
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
|
||||
{ name: 'Portfolio', url: `/${locale}/portfolio` },
|
||||
];
|
||||
|
||||
// Transform projects for JSON-LD schema
|
||||
const portfolioProjects = projects.map((project) => ({
|
||||
name: project.title,
|
||||
description: project.description,
|
||||
url: `/${locale}/portfolio/${project.slug}`,
|
||||
dateCreated: project.date,
|
||||
technologies: project.technologies,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen">
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
<CollectionPageJsonLd locale={locale as Locale} projects={portfolioProjects} />
|
||||
|
||||
<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>
|
||||
|
||||
<CategoryFilter />
|
||||
|
||||
<PortfolioGrid projects={filteredProjects} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user