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:
2026-01-19 23:33:28 +01:00
co-authored by Claude Opus 4.5
parent b1ec7b4d61
commit 87c7ebc5e3
75 changed files with 10271 additions and 1314 deletions
+51
View File
@@ -53,8 +53,59 @@ const nextConfig: NextConfig = {
},
],
},
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
// Content-Language Headers für jede Sprache (wichtig für Bing)
{
source: '/de/:path*',
headers: [
{
key: 'Content-Language',
value: 'de-DE',
},
],
},
{
source: '/en/:path*',
headers: [
{
key: 'Content-Language',
value: 'en-US',
},
],
},
{
source: '/sr/:path*',
headers: [
{
key: 'Content-Language',
value: 'sr-RS',
},
],
},
];
},
// Compiler-Optimierungen
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
const withMDX = createMDX({
+4 -1
View File
@@ -6,12 +6,15 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "next lint --strict",
"build:images": "node scripts/optimize-images.js",
"generate:images": "node scripts/generate-blog-images.mjs",
"generate:images:dry": "node scripts/generate-blog-images.mjs --dry-run",
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"openai": "^4.77.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.0",
+121 -9
View File
@@ -50,6 +50,9 @@ importers:
next-mdx-remote:
specifier: ^5.0.0
version: 5.0.0(@types/react@19.2.8)(acorn@8.15.0)(react@19.2.3)
openai:
specifier: ^4.77.0
version: 4.104.0(ws@8.18.3)
react:
specifier: ^19.0.0
version: 19.2.3
@@ -831,6 +834,12 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
@@ -1034,6 +1043,10 @@ packages:
'@vitest/utils@1.6.1':
resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1052,6 +1065,10 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1618,6 +1635,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@@ -1685,10 +1706,17 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -1826,6 +1854,9 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -2341,6 +2372,20 @@ packages:
sass:
optional: true
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -2399,6 +2444,18 @@ packages:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
openai@4.104.0:
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2987,6 +3044,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -3121,6 +3181,10 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
@@ -3815,6 +3879,15 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 22.19.7
form-data: 4.0.4
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@22.19.7':
dependencies:
undici-types: 6.21.0
@@ -4020,6 +4093,10 @@ snapshots:
loupe: 2.3.7
pretty-format: 29.7.0
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -4033,6 +4110,10 @@ snapshots:
agent-base@7.1.4:
optional: true
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4144,8 +4225,7 @@ snapshots:
async-function@1.0.0: {}
asynckit@0.4.0:
optional: true
asynckit@0.4.0: {}
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
@@ -4289,7 +4369,6 @@ snapshots:
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
optional: true
comma-separated-tokens@2.0.3: {}
@@ -4378,8 +4457,7 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
delayed-stream@1.0.0:
optional: true
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
@@ -4850,6 +4928,8 @@ snapshots:
esutils@2.0.3: {}
event-target-shim@5.0.1: {}
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -4927,6 +5007,8 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data-encoder@1.7.2: {}
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
@@ -4934,7 +5016,11 @@ snapshots:
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
optional: true
formdata-node@4.4.1:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
fraction.js@4.3.7: {}
@@ -5120,6 +5206,10 @@ snapshots:
human-signals@5.0.0: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -5744,13 +5834,11 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0:
optional: true
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
optional: true
mimic-fn@4.0.0: {}
@@ -5842,6 +5930,12 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
node-domexception@1.0.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.19: {}
normalize-path@3.0.0: {}
@@ -5903,6 +5997,20 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
openai@4.104.0(ws@8.18.3):
dependencies:
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
optionalDependencies:
ws: 8.18.3
transitivePeerDependencies:
- encoding
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -6648,6 +6756,8 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
undici-types@5.26.5: {}
undici-types@6.21.0: {}
unified@11.0.5:
@@ -6836,6 +6946,8 @@ snapshots:
xml-name-validator: 5.0.0
optional: true
web-streams-polyfill@4.0.0-beta.3: {}
web-vitals@5.1.0: {}
webidl-conversions@3.0.1: {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+53
View File
@@ -0,0 +1,53 @@
import sharp from 'sharp';
import { readdir, mkdir } from 'fs/promises';
import path from 'path';
const SOURCE_DIR = './photos';
const OUTPUT_DIR = './public/images/gallery';
const imageMapping = {
'0c477daf-538e-42b8-9171-3be0f32b052b.jpg': 'thinker-dark.webp',
'8f1df0fa-ff21-4d10-8f4e-67807405405d.jpg': 'studio-seated.webp',
'11da5e53-100b-406f-9ca7-1c84ff193a0a.jpg': 'profile-white.webp',
'e1af9b8e-f857-4df3-8e05-8f51966677f9.jpg': 'editorial-dark.webp',
'Gemini_Generated_Image_4y9h4z4y9h4z4y9h.png': 'walking-phone.webp',
'Gemini_Generated_Image_6i8jtc6i8jtc6i8j.png': 'handshake.webp',
'Gemini_Generated_Image_8x4pc88x4pc88x4p.png': 'speaking-dark.webp',
'Gemini_Generated_Image_b79h3hb79h3hb79h.png': 'meeting-boardroom.webp',
'Gemini_Generated_Image_bhiyhrbhiyhrbhiy.png': 'editorial-side.webp',
'Gemini_Generated_Image_d7l8ihd7l8ihd7l8.png': 'over-shoulder-smile.webp',
'Gemini_Generated_Image_dn15mrdn15mrdn15.png': 'frontal-smile.webp',
'Gemini_Generated_Image_kri03okri03okri0.png': 'phone-skyline.webp',
'Gemini_Generated_Image_lxg2aplxg2aplxg2.png': 'gesturing-dark.webp',
'Gemini_Generated_Image_vz86wsvz86wsvz86.png': 'video-call.webp',
'Gemini_Generated_Image_x4fuuix4fuuix4fu.png': 'studio-seated-white.webp',
};
async function optimizeImages() {
try {
await mkdir(OUTPUT_DIR, { recursive: true });
for (const [source, target] of Object.entries(imageMapping)) {
const sourcePath = path.join(SOURCE_DIR, source);
const targetPath = path.join(OUTPUT_DIR, target);
console.log(`Processing: ${source} -> ${target}`);
await sharp(sourcePath)
.resize(1200, null, {
withoutEnlargement: true,
fit: 'inside'
})
.webp({ quality: 80 })
.toFile(targetPath);
console.log(`✓ Saved: ${target}`);
}
console.log('\n✅ All images optimized successfully!');
} catch (error) {
console.error('Error:', error);
}
}
optimizeImages();
+100 -50
View File
@@ -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 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
<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>
<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>
{/* 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>
</main>
);
}
File diff suppressed because it is too large Load Diff
+497 -111
View File
@@ -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,9 +239,75 @@ const blogPostsData: Record<string, BlogPost[]> = {
],
};
function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const getFormattedDate = (date: string) => {
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',
@@ -68,15 +319,51 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
month: 'long',
day: 'numeric',
});
};
}
const getImagePath = (coverImage: string) => {
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 }) {
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);
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 (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;
};
loadPosts();
}, [locale]);
if (loading) {
if (totalPages <= 1) return null;
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"
<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>
</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>
+79 -162
View File
@@ -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';
export default function ContactPage() {
const t = useTranslations('pages.contact');
const params = useParams();
const locale = params.locale as string;
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
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');
}
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.contact' });
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"
<>
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<WebPageJsonLd
title={t('hero.title')}
description={t('hero.subtitle')}
url={`/${locale}/contact`}
locale={locale as Locale}
/>
</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>
<ContactForm />
</>
);
}
+26 -185
View File
@@ -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';
interface MetricCard {
title: string;
value: string;
subtitle: string;
trend?: 'up' | 'down';
trendValue?: string;
icon: React.ReactNode;
}
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);
type Props = {
params: Promise<{ locale: string }>;
};
checkAuth();
}, [locale, router]);
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
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>
);
return {
title: titles[locale] || titles.de,
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
},
};
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-zinc-400">{t('status.notAuthenticated')}</p>
</div>
);
}
export default async function DashboardPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
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" />,
},
{
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%' },
];
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 />;
}
+45 -20
View File
@@ -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">
&copy; {new Date().getFullYear()} Damjan Savic. {c.copyrightNotice}
&copy; {new Date().getFullYear()} Damjan Savić. {c.copyrightNotice}
</p>
</section>
</div>
+56 -20
View File
@@ -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>
+257
View File
@@ -0,0 +1,257 @@
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';
import Link from 'next/link';
import { ArrowLeft, ArrowRight, CheckCircle, Brain, Mic, Workflow, Cloud, Code, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
import { notFound } from 'next/navigation';
import { allServices, getServiceBySlug, getAllServiceSlugs } from '@/data/services';
import { BreadcrumbJsonLd, ServiceJsonLd } from '@/components/seo';
import { type Locale, locales } from '@/i18n/config';
type Props = {
params: Promise<{ locale: string; slug: string }>;
};
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Brain,
Mic,
Workflow,
Cloud,
Code,
Bot,
Building,
ShoppingCart,
Factory,
};
export async function generateStaticParams() {
const slugs = getAllServiceSlugs();
return locales.flatMap((locale) =>
slugs.map((slug) => ({
locale,
slug,
}))
);
}
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, slug } = await params;
const service = getServiceBySlug(slug);
if (!service) {
return { title: 'Service Not Found' };
}
const title = locale === 'de'
? `${service.name.de} - Köln & Deutschland | Damjan Savić`
: locale === 'sr'
? `${service.name.sr} - Keln & Nemačka | Damjan Savić`
: `${service.name.en} | Remote AI Developer for USA, UK & Europe`;
const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`;
// Enhanced keywords for English
const enhancedKeywords = locale === 'en'
? [...service.technologies, 'Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK'].join(', ')
: service.technologies.join(', ');
return {
title,
description: service.description[locale as Locale] || service.description.de,
keywords: enhancedKeywords,
alternates: {
canonical: `${BASE_URL}${localePath}`,
languages: {
'x-default': `${BASE_URL}/de/leistungen/${slug}`,
de: `${BASE_URL}/de/leistungen/${slug}`,
en: `${BASE_URL}/en/leistungen/${slug}`,
sr: `${BASE_URL}/sr/leistungen/${slug}`,
},
},
openGraph: {
title,
description: service.shortDescription[locale as Locale] || service.shortDescription.de,
url: `${BASE_URL}${localePath}`,
type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
},
};
}
export default async function ServicePage({ params }: Props) {
const { locale, slug } = await params;
setRequestLocale(locale);
const service = getServiceBySlug(slug);
if (!service) {
notFound();
}
const IconComponent = iconMap[service.icon] || Brain;
const breadcrumbItems = [
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
{ name: locale === 'de' ? 'Leistungen' : locale === 'sr' ? 'Usluge' : 'Services', url: `/${locale}/leistungen` },
{ name: service.name[locale as Locale], url: `/${locale}/leistungen/${slug}` },
];
// Get related services (excluding current)
const relatedServices = allServices.filter((s) => s.slug !== slug).slice(0, 3);
return (
<div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<ServiceJsonLd
name={service.name[locale as Locale]}
description={service.description[locale as Locale]}
url={`/${locale}/leistungen/${slug}`}
locale={locale as Locale}
/>
{/* Hero Section */}
<section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Back Link */}
<Link
href={`/${locale}/leistungen`}
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-8"
>
<ArrowLeft className="h-4 w-4" />
{locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'}
</Link>
<div className="flex items-center gap-4 mb-6">
<div className="p-4 bg-zinc-800/50 rounded-xl">
<IconComponent className="h-8 w-8 text-white" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
{service.name[locale as Locale]}
</h1>
<p className="text-xl text-zinc-400 mb-8">
{service.description[locale as Locale]}
</p>
{/* Technologies */}
<div className="flex flex-wrap gap-2">
{service.technologies.map((tech) => (
<span
key={tech}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-700 text-sm text-zinc-300 rounded-full"
>
{tech}
</span>
))}
</div>
</div>
</section>
{/* Features Section */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">
{locale === 'de' ? 'Was ich anbiete' : locale === 'sr' ? 'Šta nudim' : 'What I offer'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{service.features[locale as Locale].map((feature, index) => (
<div
key={index}
className="flex items-start gap-3 p-4 bg-zinc-800/30 rounded-lg"
>
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-300">{feature}</span>
</div>
))}
</div>
</div>
</section>
{/* Use Cases Section */}
<section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">
{locale === 'de' ? 'Anwendungsbeispiele' : locale === 'sr' ? 'Primeri primene' : 'Use Cases'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{service.useCases[locale as Locale].map((useCase, index) => (
<div
key={index}
className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-zinc-700 rounded-full flex items-center justify-center text-sm font-semibold text-white">
{index + 1}
</div>
<span className="text-zinc-200">{useCase}</span>
</div>
</div>
))}
</div>
</div>
</section>
{/* Related Services */}
<section className="py-16 bg-zinc-800/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">
{locale === 'de' ? 'Weitere Leistungen' : locale === 'sr' ? 'Ostale usluge' : 'Other Services'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedServices.map((relatedService) => {
const RelatedIcon = iconMap[relatedService.icon] || Brain;
return (
<Link
key={relatedService.slug}
href={`/${locale}/leistungen/${relatedService.slug}`}
className="group p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-all duration-300"
>
<div className="p-2 bg-zinc-800/50 rounded-lg w-fit mb-3">
<RelatedIcon className="h-5 w-5 text-zinc-400 group-hover:text-white transition-colors" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">
{relatedService.name[locale as Locale]}
</h3>
<p className="text-sm text-zinc-400">
{relatedService.shortDescription[locale as Locale]}
</p>
</Link>
);
})}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-white mb-4">
{locale === 'de'
? `${service.name.de} für Ihr Unternehmen?`
: locale === 'sr'
? `${service.name.sr} za vašu kompaniju?`
: `${service.name.en} for your business?`}
</h2>
<p className="text-zinc-400 mb-8">
{locale === 'de'
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
: locale === 'sr'
? 'Razgovarajmo o tome kako vam mogu pomoći.'
: "Let's discuss how I can help you."}
</p>
<Link
href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors"
>
{locale === 'de' ? 'Projekt besprechen' : locale === 'sr' ? 'Razgovarajte o projektu' : 'Discuss project'}
<ArrowRight className="h-5 w-5 ml-2" />
</Link>
</div>
</section>
</div>
);
}
+479
View File
@@ -0,0 +1,479 @@
import { setRequestLocale } from 'next-intl/server';
import { getTranslations } from 'next-intl/server';
import type { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import { Brain, Mic, Workflow, Cloud, Code, ArrowRight, MapPin, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
import { services, techServices, industryServices } from '@/data/services';
import { cities } from '@/data/cities';
import { BreadcrumbJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale } from '@/i18n/config';
type Props = {
params: Promise<{ locale: string }>;
};
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Brain,
Mic,
Workflow,
Cloud,
Code,
Bot,
Building,
ShoppingCart,
Factory,
};
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const titles: Record<string, string> = {
de: 'Leistungen - KI-Entwicklung, Voice AI & Automatisierung | Damjan Savić',
en: 'AI Development Services | Voice AI, Automation & SaaS | Remote Developer',
sr: 'Usluge - AI Razvoj, Voice AI & Automatizacija | Damjan Savić',
};
const descriptions: Record<string, string> = {
de: 'Professionelle KI-Entwicklung, Voice AI, Prozessautomatisierung und SaaS-Entwicklung. Maßgeschneiderte Lösungen für Unternehmen in Köln, Düsseldorf, Frankfurt und ganz Deutschland.',
en: 'Expert AI development services for businesses in USA, UK & Europe. Specialized in AI agents, Voice AI, n8n automation, and custom SaaS solutions. Remote-first, German engineering quality. Get a free consultation.',
sr: 'Profesionalni AI razvoj, Voice AI, n8n automatizacija i SaaS razvoj. Prilagođena rešenja za kompanije iz Srbije (Beograd, Novi Sad, Niš) i srpsku dijasporu u Nemačkoj, Austriji i Švajcarskoj.',
};
const keywords: Record<string, string[]> = {
de: [
'KI-Entwicklung',
'Voice AI',
'Prozessautomatisierung',
'SaaS Entwicklung',
'n8n',
'GPT-4 Integration',
'KI-Agenten',
'Automatisierung Köln',
'AI Developer Deutschland',
],
en: [
'AI Development Services',
'Voice AI Developer',
'Process Automation',
'SaaS Development',
'n8n Automation',
'GPT-4 Integration',
'AI Agents',
'ChatGPT Integration',
'Remote AI Developer',
'AI Developer USA',
'AI Developer UK',
'AI Developer London',
'AI Developer New York',
'Freelance AI Developer',
'AI Consultant',
'LLM Integration',
'Custom AI Solutions',
],
sr: [
'AI Razvoj Srbija',
'Voice AI Srbija',
'Automatizacija procesa',
'SaaS Razvoj Srbija',
'n8n Automatizacija',
'GPT-4 Integracija',
'AI Agenti Beograd',
'Fullstack Programer Srbija',
'AI Developer Novi Sad',
'Python Programer Srbija',
'Remote Developer Dijaspora',
'Softverski Razvoj Beograd',
],
};
const localePath = locale === 'de' ? '/leistungen' : `/${locale}/leistungen`;
return {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de,
keywords: keywords[locale] || keywords.de,
alternates: {
canonical: `${BASE_URL}${localePath}`,
languages: {
'x-default': `${BASE_URL}/leistungen`,
de: `${BASE_URL}/leistungen`,
en: `${BASE_URL}/en/leistungen`,
sr: `${BASE_URL}/sr/leistungen`,
},
},
openGraph: {
title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de,
url: `${BASE_URL}${localePath}`,
type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
},
};
}
export default async function ServicesPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const breadcrumbItems = [
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
{ name: locale === 'de' ? 'Leistungen' : locale === 'sr' ? 'Usluge' : 'Services', url: `/${locale}/leistungen` },
];
const faqItems = locale === 'de' ? [
{
question: 'Welche KI-Technologien setzen Sie ein?',
answer: 'Ich arbeite mit führenden LLM-Anbietern wie OpenAI (GPT-4, GPT-4o), Anthropic (Claude) und Open-Source-Modellen wie Llama. Für Voice AI nutze ich Vapi, ElevenLabs und Whisper.',
},
{
question: 'Wie lange dauert die Entwicklung eines MVP?',
answer: 'Ein typisches MVP kann in 4-8 Wochen entwickelt werden, abhängig vom Umfang und den Anforderungen des Projekts. Komplexere SaaS-Produkte benötigen 8-12 Wochen.',
},
{
question: 'Arbeiten Sie auch mit Unternehmen außerhalb von Köln?',
answer: 'Ja, ich arbeite remote mit Unternehmen in ganz Deutschland (Düsseldorf, Frankfurt, München, Berlin, Hamburg, Stuttgart) und international. Die meisten meiner Projekte werden vollständig remote abgewickelt.',
},
{
question: 'Was kostet eine KI-Automatisierung?',
answer: 'Die Kosten variieren je nach Komplexität. Einfache n8n-Workflows starten ab ca. 1.500€, während komplexe KI-Agenten-Systeme zwischen 5.000€ und 25.000€ kosten können. Ich erstelle gerne ein individuelles Angebot.',
},
{
question: 'Sind Ihre Lösungen DSGVO-konform?',
answer: 'Ja, Datenschutz hat höchste Priorität. Ich setze auf Self-Hosted-Lösungen (n8n, Supabase) und sichere API-Anbindungen. Bei Bedarf können alle Daten in deutschen Rechenzentren verarbeitet werden.',
},
{
question: 'Bieten Sie auch Support und Wartung an?',
answer: 'Ja, ich biete flexible Support-Pakete für Wartung, Updates und Weiterentwicklung. Nach dem Launch bin ich weiterhin für Sie da, um Ihre Lösung optimal zu betreuen.',
},
] : locale === 'en' ? [
{
question: 'Do you work with clients in the USA and UK?',
answer: 'Yes! I work remotely with clients worldwide, including the USA (New York, Los Angeles, San Francisco, Miami, Chicago) and UK (London). Most of my international projects are handled completely remotely with regular video calls to accommodate different time zones.',
},
{
question: 'What AI technologies do you use?',
answer: 'I work with leading LLM providers like OpenAI (GPT-4, GPT-4o), Anthropic (Claude) and open-source models like Llama. For Voice AI, I use Vapi, ElevenLabs and Whisper. I also specialize in n8n for workflow automation.',
},
{
question: 'How long does MVP development take?',
answer: 'A typical MVP can be developed in 4-8 weeks, depending on the scope and requirements of the project. More complex SaaS products require 8-12 weeks. I follow agile methodologies with weekly demos.',
},
{
question: 'What are your rates for international clients?',
answer: 'I offer competitive rates for international clients. Simple n8n automations start at around $1,600, while complex AI agent systems range from $5,500 to $27,000. I provide detailed proposals with fixed-price options for most projects.',
},
{
question: 'How do you handle time zone differences?',
answer: 'I am flexible with communication and can accommodate US Eastern, US Pacific, and UK time zones. I typically overlap 4-6 hours with US clients and have full overlap with UK working hours. Async communication via Slack/Email works great for most projects.',
},
{
question: 'Are your solutions GDPR and data privacy compliant?',
answer: 'Yes, data protection is a top priority. I am well-versed in GDPR, CCPA, and international data privacy regulations. I use self-hosted solutions (n8n, Supabase) and can ensure data stays in specific regions (US, EU) as needed.',
},
{
question: 'What makes you different from US/UK-based developers?',
answer: 'I combine German engineering precision with competitive rates and strong English communication. You get enterprise-quality solutions at freelancer prices, plus experience working with international Fortune 500 companies and startups alike.',
},
{
question: 'Do you offer ongoing support and maintenance?',
answer: 'Yes, I offer flexible support packages including 24/7 monitoring for critical systems, regular maintenance windows, and priority support SLAs. I can work within your existing ticketing and communication systems.',
},
] : [
{
question: 'Koje AI tehnologije koristite?',
answer: 'Radim sa vodećim LLM provajderima kao što su OpenAI (GPT-4, GPT-4o), Anthropic (Claude) i open-source modeli kao Llama. Za Voice AI koristim Vapi, ElevenLabs i Whisper.',
},
{
question: 'Koliko traje razvoj MVP-a?',
answer: 'Tipičan MVP može se razviti za 4-8 nedelja, u zavisnosti od obima i zahteva projekta. Kompleksniji SaaS proizvodi zahtevaju 8-12 nedelja.',
},
{
question: 'Da li radite i sa kompanijama van Kelna?',
answer: 'Da, radim remote sa kompanijama širom Nemačke (Diseldorf, Frankfurt, Minhen, Berlin, Hamburg, Štutgart) i međunarodno. Većina mojih projekata se obrađuje potpuno remote.',
},
{
question: 'Koliko košta AI automatizacija?',
answer: 'Troškovi variraju u zavisnosti od kompleksnosti. Jednostavni n8n radni tokovi počinju od oko 1.500€, dok kompleksni sistemi AI agenata mogu koštati između 5.000€ i 25.000€. Rado ću vam dati individualizovanu ponudu.',
},
{
question: 'Da li su vaša rešenja usklađena sa GDPR-om?',
answer: 'Da, zaštita podataka ima najviši prioritet. Oslanjam se na self-hosted rešenja (n8n, Supabase) i sigurne API konekcije. Po potrebi, svi podaci mogu se obrađivati u nemačkim data centrima.',
},
{
question: 'Da li nudite podršku i održavanje?',
answer: 'Da, nudim fleksibilne pakete podrške za održavanje, ažuriranja i dalji razvoj. Nakon lansiranja, tu sam za vas da optimalno podržim vaše rešenje.',
},
];
const pageTitle = locale === 'de' ? 'Meine Leistungen' : locale === 'sr' ? 'Moje Usluge' : 'My Services';
const pageSubtitle = locale === 'de'
? 'Von KI-Entwicklung über Voice AI bis zur Prozessautomatisierung - maßgeschneiderte Lösungen für Ihr Unternehmen.'
: locale === 'sr'
? 'Od AI razvoja preko Voice AI do automatizacije procesa - prilagođena rešenja za vašu kompaniju.'
: 'From AI development to Voice AI to process automation - customized solutions for your business.';
const citiesTitle = locale === 'de' ? 'Standorte' : locale === 'sr' ? 'Lokacije' : 'Locations';
const citiesSubtitle = locale === 'de'
? 'Ich arbeite mit Unternehmen in folgenden Städten:'
: locale === 'sr'
? 'Radim sa kompanijama u sledećim gradovima:'
: 'I work with companies in the following cities:';
// Section titles for service categories
const coreServicesTitle = locale === 'de' ? 'Kern-Leistungen' : locale === 'sr' ? 'Osnovne Usluge' : 'Core Services';
const techServicesTitle = locale === 'de' ? 'Technologie-Spezialisierungen' : locale === 'sr' ? 'Tehnološke Specijalizacije' : 'Technology Specializations';
const industryServicesTitle = locale === 'de' ? 'Branchen-Lösungen' : locale === 'sr' ? 'Industrijska Rešenja' : 'Industry Solutions';
return (
<div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<FAQJsonLd items={faqItems} locale={locale as Locale} />
{/* Hero Section */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="text-center lg:text-left">
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
{pageTitle}
</h1>
<p className="text-xl text-zinc-400 max-w-2xl">
{pageSubtitle}
</p>
</div>
<div className="relative aspect-video rounded-2xl overflow-hidden">
<Image
src="/images/gallery/walking-phone.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
View File
@@ -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';
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);
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);
}
type Props = {
params: Promise<{ locale: string }>;
};
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>
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
<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>
)}
const titles: Record<string, string> = {
de: 'Login | Damjan Savić',
en: 'Login | Damjan Savić',
sr: 'Prijava | Damjan Savić',
};
<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>
return {
title: titles[locale] || titles.de,
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
},
};
}
<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>
export default async function LoginPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
<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
View File
@@ -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 />
</>
);
}
+594 -139
View File
@@ -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',
meta: {
title: 'Smart Warehouse RFID',
description: 'RFID-basierte Lagerverwaltung mit IoT-Integration',
category: 'Integration',
date: '2024-02',
client: 'Enterprise Client',
duration: '6 Monate',
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`,
},
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': {
meta: {
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`,
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();
}
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>
const breadcrumbItems = [
{ name: commonT('home'), url: `/${locale}` },
{ name: 'Portfolio', url: `/${locale}/portfolio` },
{ name: project.meta.title, url: `/${locale}/portfolio/${slug}` },
];
{/* 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>
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.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 */}
<div className="flex flex-wrap gap-2 mt-6">
{project.technologies.map((tech) => (
{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={tech}
className="text-sm bg-zinc-800 text-zinc-300 px-3 py-1 rounded-full"
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>
</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>
);
})}
</div>
</div>
)}
</article>
{/* Back Link */}
<div className="mt-12 pt-8 border-t border-zinc-800">
<Link
href={`/${locale}/portfolio`}
className="text-blue-400 hover:text-blue-300 transition-colors"
>
&larr; Back to Portfolio
</Link>
</div>
</div>
</main>
);
}
+132 -69
View File
@@ -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">
<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>
</section>
</div>
{/* 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>
))}
<PortfolioGrid projects={projects} />
</div>
</section>
</div>
</main>
);
}
+26 -1
View File
@@ -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
View File
@@ -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'),
+17
View File
@@ -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`,
};
}
+145
View File
@@ -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;
}
+119
View File
@@ -0,0 +1,119 @@
'use client';
import { motion } from 'framer-motion';
import { Terminal, Globe2, Code2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
const Hero = () => {
const t = useTranslations('pages.about.hero');
const getLanguages = () => {
try {
const languages = t.raw('languages');
return typeof languages === 'object' ? Object.entries(languages) : [];
} catch {
return [];
}
};
const scrollToSkills = () => {
const skillsSection = document.getElementById('skills');
if (skillsSection) {
skillsSection.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<section className="relative min-h-screen text-white overflow-hidden">
<div className="flex flex-col items-center justify-center min-h-screen px-4 relative z-10">
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-white text-3xl font-bold tracking-wider mb-4"
>
{t('title')}
</motion.h1>
{/* Description */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="max-w-3xl text-center mb-12"
>
<p className="text-zinc-400 text-lg mb-8">
{t('description')}
</p>
</motion.div>
{/* Expertise Areas */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12"
>
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50">
<Terminal className="h-8 w-8 mb-4 text-white" />
<h3 className="text-lg font-semibold mb-2">
{t('expertise.processAutomation.title')}
</h3>
<p className="text-zinc-400 text-center text-sm">
{t('expertise.processAutomation.description')}
</p>
</div>
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50">
<Globe2 className="h-8 w-8 mb-4 text-white" />
<h3 className="text-lg font-semibold mb-2">
{t('expertise.systemIntegration.title')}
</h3>
<p className="text-zinc-400 text-center text-sm">
{t('expertise.systemIntegration.description')}
</p>
</div>
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50">
<Code2 className="h-8 w-8 mb-4 text-white" />
<h3 className="text-lg font-semibold mb-2">
{t('expertise.customDevelopment.title')}
</h3>
<p className="text-zinc-400 text-center text-sm">
{t('expertise.customDevelopment.description')}
</p>
</div>
</motion.div>
{/* Languages */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4"
>
{getLanguages().map(([key, value]) => (
<div
key={key}
className="flex flex-col items-center p-3 border border-zinc-800 rounded-lg bg-zinc-900/50 hover:bg-zinc-800/50 transition-colors"
>
<span className="text-white font-medium">{value as string}</span>
</div>
))}
</motion.div>
{/* About Me Link */}
<motion.button
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
onClick={scrollToSkills}
className="mt-8 px-6 py-3 bg-white text-zinc-900 rounded-lg font-medium hover:bg-zinc-100 transition-colors"
>
{t('aboutMe')}
</motion.button>
</div>
</section>
);
};
export default Hero;
+142
View File
@@ -0,0 +1,142 @@
import { getTranslations } from 'next-intl/server';
import { Briefcase, GraduationCap, Globe } from 'lucide-react';
interface WorkPosition {
title: string;
period: string;
company: string;
location: string;
highlights: string[];
}
interface LanguageItem {
name: string;
level: string;
}
const Journey = async () => {
const t = await getTranslations('pages.about.journey');
let workPositions: WorkPosition[] = [];
let languageItems: [string, LanguageItem][] = [];
try {
const positions = t.raw('work.positions') as WorkPosition[];
if (Array.isArray(positions)) {
workPositions = positions;
}
} catch {
workPositions = [];
}
try {
const items = t.raw('languages.items') as Record<string, LanguageItem>;
if (typeof items === 'object') {
languageItems = Object.entries(items);
}
} catch {
languageItems = [];
}
return (
<section className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-white mb-4">
{t('title')}
</h2>
<p className="text-zinc-400 max-w-2xl mx-auto">
{t('subtitle')}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Education Column */}
<div className="space-y-6">
<div className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg">
<div className="flex items-center gap-3 mb-6">
<GraduationCap className="h-5 w-5 text-zinc-400" />
<h3 className="text-lg font-medium text-white">
{t('education.title')}
</h3>
</div>
<div className="space-y-4">
<div>
<h4 className="text-white font-medium">
{t('education.degrees.masters.degree')}
</h4>
<p className="text-zinc-400 text-sm">
{t('education.degrees.masters.university')}
</p>
<p className="text-zinc-500 text-sm">
{t('education.degrees.masters.period')}
</p>
</div>
<div>
<h4 className="text-white font-medium">
{t('education.degrees.bachelors.degree')}
</h4>
<p className="text-zinc-400 text-sm">
{t('education.degrees.bachelors.university')}
</p>
<p className="text-zinc-500 text-sm">
{t('education.degrees.bachelors.period')}
</p>
</div>
</div>
</div>
{/* Languages */}
<div className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg">
<div className="flex items-center gap-3 mb-6">
<Globe className="h-5 w-5 text-zinc-400" />
<h3 className="text-lg font-medium text-white">
{t('languages.title')}
</h3>
</div>
<div className="grid grid-cols-2 gap-4">
{languageItems.map(([key, item]) => (
<div key={key}>
<div className="text-zinc-300">{item.name}</div>
<div className="text-zinc-500 text-sm">{item.level}</div>
</div>
))}
</div>
</div>
</div>
{/* Work History */}
<div className="lg:col-span-2 space-y-6">
{workPositions.map((job, index) => (
<div
key={index}
className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-white">{job.title}</h3>
<span className="text-zinc-500 text-sm">{job.period}</span>
</div>
<div className="flex items-center gap-2 mb-4">
<Briefcase className="h-4 w-4 text-zinc-400" />
<p className="text-zinc-300">{job.company}</p>
<span className="text-zinc-500"> {job.location}</span>
</div>
<ul className="space-y-2">
{job.highlights.map((highlight, idx) => (
<li key={idx} className="text-zinc-400 text-sm flex items-start gap-2">
<span className="text-zinc-500"></span>
{highlight}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default Journey;
+131
View File
@@ -0,0 +1,131 @@
'use client';
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { Database, Server, Store, Code2, Box } from 'lucide-react';
interface SkillItem {
name: string;
level: number;
}
interface SkillGroup {
category: string;
items: SkillItem[];
}
const iconMap: Record<string, React.ReactNode> = {
'Python': <Code2 className="w-8 h-8" />,
'Server': <Server className="w-8 h-8" />,
'Google Ads': <Store className="w-8 h-8" />,
'Meta Ads': <Store className="w-8 h-8" />,
'Shopware': <Store className="w-8 h-8" />,
'Shopify': <Store className="w-8 h-8" />,
'WooCommerce': <Store className="w-8 h-8" />,
'JTL WAWI': <Box className="w-8 h-8" />,
'AirFlow': <Database className="w-8 h-8" />
};
const Skills = () => {
const t = useTranslations('pages.about.skills');
const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
const [allSkills, setAllSkills] = useState<SkillItem[]>([]);
useEffect(() => {
try {
const skillGroups = t.raw('skillGroups') as SkillGroup[];
if (Array.isArray(skillGroups)) {
const skills = skillGroups.flatMap(group => group.items);
setAllSkills(skills);
}
} catch {
setAllSkills([]);
}
}, [t]);
if (allSkills.length === 0) {
return null;
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Skills Progress Bars */}
<div className="space-y-6">
{allSkills.map((skill) => (
<motion.div
key={skill.name}
className="space-y-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="text-white text-sm font-medium">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs">
{skill.level}% Proficiency
</span>
)}
</div>
<span className="text-white text-sm">
{skill.level}%
</span>
</div>
<div
className="h-2 bg-zinc-800 rounded-full overflow-hidden"
role="progressbar"
aria-valuenow={skill.level}
aria-valuemin={0}
aria-valuemax={100}
>
<motion.div
className={`h-full rounded-full ${
selectedSkill === skill.name ? 'bg-zinc-500' : 'bg-zinc-600'
}`}
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
/>
</div>
</motion.div>
))}
</div>
{/* Skills Icons Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{allSkills.map((skill) => (
<motion.div
key={skill.name}
className={`p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500' : ''
}`}
whileHover={{ scale: 1.05 }}
onClick={() => setSelectedSkill(skill.name === selectedSkill ? null : skill.name)}
role="button"
aria-pressed={selectedSkill === skill.name}
>
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
{iconMap[skill.name] || <Box className="w-8 h-8" />}
</div>
<span className="text-white text-sm text-center">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs text-center mt-1">
{skill.level}%
</span>
)}
</motion.div>
))}
</div>
</div>
);
};
export default Skills;
+56
View File
@@ -0,0 +1,56 @@
import { getTranslations } from 'next-intl/server';
interface WorkflowStep {
number: string;
title: string;
}
const Workflow = async () => {
const t = await getTranslations('pages.about.workflow');
let workflowSteps: WorkflowStep[] = [];
try {
const steps = t.raw('steps') as WorkflowStep[];
if (Array.isArray(steps)) {
workflowSteps = steps;
}
} catch {
workflowSteps = [];
}
if (workflowSteps.length === 0) {
return null;
}
const totalSteps = workflowSteps.length;
return (
<div className="max-w-7xl mx-auto" role="list">
{workflowSteps.map((step) => (
<div
key={step.number}
className="group relative"
role="listitem"
aria-label={`Step ${step.number} of ${totalSteps}: ${step.title}`}
>
<div className="flex items-center py-6 border-b border-zinc-800 group-hover:border-zinc-700 transition-colors duration-300">
<div className="w-12 sm:w-20 text-sm text-zinc-600 font-light">
{step.number}
</div>
<h3 className="text-sm sm:text-base text-zinc-400 group-hover:text-white transition-colors duration-300">
{step.title}
</h3>
</div>
{/* Line Indicator */}
<div
className="absolute left-0 bottom-0 w-0 h-px bg-white group-hover:w-full transition-all duration-500 ease-out"
aria-hidden="true"
/>
</div>
))}
</div>
);
};
export default Workflow;
+4
View File
@@ -0,0 +1,4 @@
export { default as Hero } from './Hero';
export { default as Skills } from './Skills';
export { default as Workflow } from './Workflow';
export { default as Journey } from './Journey';
+195
View File
@@ -0,0 +1,195 @@
'use client';
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';
interface MetricCard {
title: string;
value: string;
subtitle: string;
trend?: 'up' | 'down';
trendValue?: string;
icon: React.ReactNode;
}
export default function DashboardContent() {
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}`);
};
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" />,
},
{
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%' },
];
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>
);
}
+113
View File
@@ -0,0 +1,113 @@
'use client';
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';
export default function LoginForm() {
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);
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);
}
};
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>
<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>
)}
<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" aria-hidden="true">
<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>
);
}
+2
View File
@@ -0,0 +1,2 @@
export { default as LoginForm } from './LoginForm';
export { default as DashboardContent } from './DashboardContent';
+328
View File
@@ -0,0 +1,328 @@
'use client';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { MapPin, Phone, Mail, Send, Loader2, CheckCircle2 } from 'lucide-react';
import Image from 'next/image';
interface FormData {
name: string;
email: string;
message: string;
}
export function ContactForm() {
const t = useTranslations('pages.contact');
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const validateForm = () => {
const newErrors: Partial<FormData> = {};
if (!formData.name.trim()) {
newErrors.name = t('contactForm.errors.nameRequired');
}
if (!formData.email.trim()) {
newErrors.email = t('contactForm.errors.emailRequired');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = t('contactForm.errors.emailInvalid');
}
if (!formData.message.trim()) {
newErrors.message = t('contactForm.errors.messageRequired');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name as keyof FormData]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
setSubmitStatus('idle');
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
setSubmitStatus('success');
setFormData({ name: '', email: '', message: '' });
} catch {
setSubmitStatus('error');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen">
{/* Hero Section */}
<div className="py-24 text-center">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-4xl md:text-5xl font-bold text-white mb-4"
>
{t('hero.title')}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-lg text-zinc-400 max-w-2xl mx-auto px-4"
>
{t('hero.subtitle')}
</motion.p>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{/* Contact Info */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="space-y-8"
>
{/* Headshot */}
<div className="flex items-center gap-6">
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-zinc-700 flex-shrink-0">
<Image
src="/images/headshot.webp"
alt="Damjan Savić"
fill
sizes="96px"
className="object-cover"
/>
</div>
<div>
<h2 className="text-2xl font-bold text-white mb-1">
{t('contactInfo.title')}
</h2>
<p className="text-zinc-400">
{t('contactInfo.description')}
</p>
</div>
</div>
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<MapPin className="h-6 w-6 text-zinc-400" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
{t('contactInfo.address.label')}
</h3>
<p className="text-zinc-400">
{t('contactInfo.address.value')}
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<Phone className="h-6 w-6 text-zinc-400" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
{t('contactInfo.phone.label')}
</h3>
<a
href="tel:+491756950979"
className="text-zinc-400 hover:text-white transition-colors"
>
{t('contactInfo.phone.value')}
</a>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<Mail className="h-6 w-6 text-zinc-400" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
{t('contactInfo.email.label')}
</h3>
<a
href="mailto:info@damjan-savic.com"
className="text-zinc-400 hover:text-white transition-colors"
>
{t('contactInfo.email.value')}
</a>
</div>
</motion.div>
</div>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="text-sm text-zinc-500"
>
{t('contactInfo.availabilityNote')}
</motion.p>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
>
<div className="bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 md:p-8">
<div className="flex items-center gap-2 mb-2">
<Mail className="h-5 w-5 text-zinc-400" />
<h2 className="text-xl font-semibold text-white">
{t('contactForm.title')}
</h2>
</div>
<p className="text-zinc-400 mb-6">
{t('contactForm.description')}
</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="name" className="block text-sm text-zinc-300">
{t('contactForm.name.label')}
</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleChange}
placeholder={t('contactForm.name.placeholder')}
disabled={isSubmitting}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
/>
{errors.name && (
<p className="text-sm text-red-400">{errors.name}</p>
)}
</div>
<div className="space-y-2">
<label htmlFor="email" className="block text-sm text-zinc-300">
{t('contactForm.email.label')}
</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder={t('contactForm.email.placeholder')}
disabled={isSubmitting}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
/>
{errors.email && (
<p className="text-sm text-red-400">{errors.email}</p>
)}
</div>
<div className="space-y-2">
<label htmlFor="message" className="block text-sm text-zinc-300">
{t('contactForm.message.label')}
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
placeholder={t('contactForm.message.placeholder')}
rows={6}
disabled={isSubmitting}
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50 resize-none"
/>
{errors.message && (
<p className="text-sm text-red-400">{errors.message}</p>
)}
</div>
</div>
{submitStatus === 'success' && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-2 p-3 bg-green-900/20 border border-green-900/50 rounded-lg"
>
<CheckCircle2 className="h-4 w-4 text-green-500" />
<p className="text-green-400 text-sm">
{t('contactForm.successMessage')}
</p>
</motion.div>
)}
{submitStatus === 'error' && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-900/20 border border-red-900/50 rounded-lg"
>
<p className="text-red-400 text-sm">
{t('contactForm.errorMessage')}
</p>
</motion.div>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full h-12 bg-zinc-100 hover:bg-white text-zinc-900 font-medium rounded-lg transition-colors duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<>
<Send className="h-5 w-5" />
{t('contactForm.submit')}
</>
)}
</button>
</form>
</div>
</motion.div>
</div>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { ContactForm } from './ContactForm';
+46
View File
@@ -0,0 +1,46 @@
'use client';
import { Phone, Mail, ArrowRight } from 'lucide-react';
import Link from 'next/link';
import { useLocale, useTranslations } from 'next-intl';
export function ContactInfo() {
const locale = useLocale();
const t = useTranslations('footer.sections.contact');
return (
<div className="pb-6 px-6 pt-4 bg-zinc-800/30 border border-zinc-800 rounded-lg mx-4 mt-2">
<div className="space-y-4">
<h3 className="text-sm font-medium text-white tracking-wider uppercase">
{t('title')}
</h3>
<a
href="tel:+491756950979"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group"
>
<Phone className="h-4 w-4 text-zinc-400 group-hover:text-white transition-colors duration-200" />
<span>+49 175 695 0979</span>
</a>
<a
href="mailto:info@damjan-savic.com"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group"
>
<Mail className="h-4 w-4 text-zinc-400 group-hover:text-white transition-colors duration-200" />
<span>{t('email')}</span>
</a>
<Link
href={`/${locale}/contact`}
className="flex items-center justify-between mt-6 px-4 py-2 bg-zinc-800/50
hover:bg-zinc-800 rounded-full text-sm text-zinc-400 hover:text-white
transition-all duration-200 group border border-zinc-800 hover:border-zinc-700"
>
<span className="tracking-wide">Kontakt aufnehmen</span>
<ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform duration-200" />
</Link>
</div>
</div>
);
}
+128
View File
@@ -0,0 +1,128 @@
import Link from 'next/link';
import { getLocale, getTranslations } from 'next-intl/server';
import { Mail, Linkedin, Github, MapPin } from 'lucide-react';
const Footer = async () => {
const locale = await getLocale();
const t = await getTranslations('footer');
const currentYear = new Date().getFullYear();
return (
<footer className="relative bg-zinc-800/50 backdrop-blur-sm border-t border-zinc-700/30 pt-8 pb-4 px-4">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col space-y-8">
{/* Contact Section */}
<div>
<h3 className="text-white text-base font-semibold mb-3">
{t('sections.contact.title')}
</h3>
<div className="space-y-2">
<a
href={`mailto:${t('sections.contact.email')}`}
className="group flex items-center gap-2 text-zinc-400 hover:text-white transition-colors text-sm w-fit"
>
<Mail className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors flex-shrink-0" />
<span>{t('sections.contact.email')}</span>
</a>
<div className="flex items-center gap-2 text-zinc-400 text-sm w-fit">
<MapPin className="h-4 w-4 text-zinc-500 flex-shrink-0" />
<span>{t('sections.contact.location')}</span>
</div>
</div>
</div>
{/* Navigation Section */}
<div>
<h3 className="text-white text-base font-semibold mb-3 text-left">
{t('sections.navigation.title')}
</h3>
<nav className="flex flex-col space-y-2">
<Link
href={`/${locale}/portfolio`}
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit"
>
{t('sections.navigation.links.portfolio')}
</Link>
<Link
href={`/${locale}/blog`}
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit"
>
{t('sections.navigation.links.blog')}
</Link>
<Link
href={`/${locale}/about`}
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit"
>
{t('sections.navigation.links.about')}
</Link>
<Link
href={`/${locale}/contact`}
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit"
>
{t('sections.navigation.links.contact')}
</Link>
</nav>
</div>
{/* Social Media Section */}
<div>
<h3 className="text-white text-base font-semibold mb-3 text-left">
{t('sections.social.title')}
</h3>
<div className="flex space-x-4">
<a
href="https://www.linkedin.com/in/damjan-savić-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>
</div>
{/* Legal Section */}
<div className="border-t border-zinc-700/30 pt-4">
<div className="flex flex-col space-y-4">
<p className="text-zinc-400 text-xs text-left">
&copy; {currentYear} Damjan Savić. {t('legal.copyright')}
</p>
<div className="flex space-x-4">
<Link
href={`/${locale}/privacy`}
className="text-zinc-400 hover:text-white text-xs transition-colors"
>
{t('legal.links.privacy')}
</Link>
<Link
href={`/${locale}/imprint`}
className="text-zinc-400 hover:text-white text-xs transition-colors"
>
{t('legal.links.imprint')}
</Link>
<Link
href={`/${locale}/terms`}
className="text-zinc-400 hover:text-white text-xs transition-colors"
>
{t('legal.links.terms')}
</Link>
</div>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;
+112
View File
@@ -0,0 +1,112 @@
const GlobalBackground = () => {
return (
<>
{/* Background layer */}
<div
className="fixed inset-0 bg-zinc-900"
style={{ zIndex: -2 }}
/>
{/* Gradient overlay */}
<div
className="fixed inset-0"
style={{
zIndex: -1,
background: 'linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 50%, #1e1e1e 100%)',
opacity: 0.9
}}
/>
{/* Animated lines */}
<div
className="fixed inset-0 pointer-events-none"
style={{ zIndex: -1 }}
>
<svg
className="w-full h-full"
preserveAspectRatio="none"
style={{ opacity: 0.3 }}
>
<line
x1="0%"
y1="20%"
x2="100%"
y2="20%"
stroke="white"
strokeWidth="1"
opacity="0.2"
>
<animate
attributeName="y1"
values="20%;80%;20%"
dur="10s"
repeatCount="indefinite"
/>
<animate
attributeName="y2"
values="20%;80%;20%"
dur="10s"
repeatCount="indefinite"
/>
</line>
<line
x1="0%"
y1="50%"
x2="100%"
y2="50%"
stroke="white"
strokeWidth="1"
opacity="0.15"
>
<animate
attributeName="x1"
values="0%;100%;0%"
dur="15s"
repeatCount="indefinite"
/>
</line>
<line
x1="20%"
y1="0%"
x2="20%"
y2="100%"
stroke="white"
strokeWidth="1"
opacity="0.1"
>
<animate
attributeName="x1"
values="20%;80%;20%"
dur="12s"
repeatCount="indefinite"
/>
<animate
attributeName="x2"
values="20%;80%;20%"
dur="12s"
repeatCount="indefinite"
/>
</line>
</svg>
</div>
{/* Grid overlay */}
<div
className="fixed inset-0 pointer-events-none"
style={{
zIndex: -1,
backgroundImage: `
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
opacity: 0.5
}}
/>
</>
);
};
export default GlobalBackground;
+210
View File
@@ -0,0 +1,210 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import {
Menu,
X,
Home,
User,
Briefcase,
BookOpen,
Phone,
Cog,
} from 'lucide-react';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { NavLink } from './NavLink';
import LanguageSwitcher from './LanguageSwitcher';
import { ContactInfo } from './ContactInfo';
const Header = () => {
const t = useTranslations('navigation');
const locale = useLocale();
const pathname = usePathname();
const [scrolled, setScrolled] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Close menu on route change
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);
// Scroll handler
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 0);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Prevent body scroll when menu is open
useEffect(() => {
if (isMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isMenuOpen]);
const navigationLinks = [
{
path: `/${locale}`,
label: t('main.home'),
icon: <Home className="h-5 w-5" />,
},
{
path: `/${locale}/about`,
label: t('main.about'),
icon: <User className="h-5 w-5" />,
},
{
path: `/${locale}/leistungen`,
label: t('main.services'),
icon: <Cog className="h-5 w-5" />,
},
{
path: `/${locale}/portfolio`,
label: t('main.portfolio'),
icon: <Briefcase className="h-5 w-5" />,
},
{
path: `/${locale}/blog`,
label: t('main.blog'),
icon: <BookOpen className="h-5 w-5" />,
},
{
path: `/${locale}/contact`,
label: t('main.contact'),
icon: <Phone className="h-5 w-5" />,
},
];
return (
<>
{/* Navigation */}
<nav
className={`
fixed w-full z-40 transition-all duration-300
bg-zinc-800/50 backdrop-blur-sm border-b border-zinc-700/30
${scrolled
? 'bg-zinc-800/70 backdrop-blur-md shadow-lg shadow-zinc-900/30'
: ''
}
`}
role="navigation"
aria-label="Main navigation"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link href={`/${locale}`} className="flex items-center space-x-2 group shrink-0">
<Image
src="/header-logo.svg"
alt="Damjan Savić Logo"
className="h-8 w-auto transition-transform duration-200 hover:scale-105"
width={120}
height={32}
priority
/>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-2">
{navigationLinks.map(({ path, label, icon }) => (
<NavLink
key={path}
href={path}
icon={icon}
label={label}
className="text-sm"
/>
))}
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700">
<LanguageSwitcher />
</div>
</div>
{/* Mobile Menu Button */}
<button
className="md:hidden relative z-50 p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-expanded={isMenuOpen}
aria-controls="mobile-menu"
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
>
{isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
</div>
</div>
</nav>
{/* Mobile Menu Backdrop */}
<div
className={`fixed inset-0 bg-black/50 md:hidden z-30 transition-opacity duration-200 ${
isMenuOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
onClick={() => setIsMenuOpen(false)}
/>
{/* Mobile Sidebar */}
<div
id="mobile-menu"
className={`fixed right-0 top-0 h-full w-80 bg-zinc-900/95 backdrop-blur-md shadow-xl md:hidden z-50 border-l border-zinc-800 transition-transform duration-300 ease-out ${
isMenuOpen ? 'translate-x-0' : 'translate-x-full'
}`}
>
{/* Sidebar Header */}
<div className="flex items-center justify-between px-4 h-16 border-b border-zinc-800">
<span className="text-white font-semibold">Menu</span>
<button
onClick={() => setIsMenuOpen(false)}
aria-label="Close sidebar"
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Scrollable Sidebar Content */}
<div className="h-[calc(100vh-4rem)] overflow-y-auto">
<div className="flex flex-col h-full">
{/* Contact Info */}
<ContactInfo />
{/* Navigation Links */}
<div className="py-4 px-4">
{navigationLinks.map(({ path, label, icon }) => (
<NavLink
key={path}
href={path}
icon={icon}
label={label}
onClick={() => setIsMenuOpen(false)}
className="flex items-center w-full mb-1"
/>
))}
</div>
{/* Language Switcher */}
<div className="px-4 py-3 border-t border-zinc-800">
<div className="flex items-center justify-between">
<span className="text-sm text-zinc-400">
Sprache ändern
</span>
<LanguageSwitcher />
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default Header;
+107
View File
@@ -0,0 +1,107 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useLocale } from 'next-intl';
import { Globe } from 'lucide-react';
const LanguageSwitcher = () => {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const languages = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'sr', name: 'Srpski' },
];
const currentLanguage = languages.find(lang => lang.code === locale)?.name || 'Language';
const handleLanguageChange = (newLocale: string) => {
// Replace the locale in the pathname
const pathWithoutLocale = pathname.replace(/^\/(de|en|sr)/, '');
const newPath = `/${newLocale}${pathWithoutLocale || ''}`;
router.push(newPath);
setIsOpen(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('resize', checkMobile);
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative inline-block" ref={dropdownRef}>
<button
className="flex items-center space-x-2 text-zinc-400 hover:text-white
px-4 py-2 rounded-full transition-all duration-200
hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800
hover:scale-105 active:scale-95"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label="Select language"
>
<Globe className="h-5 w-5" aria-hidden="true" />
<span className="text-sm">{currentLanguage}</span>
</button>
<div
className={`absolute ${isMobile ? 'bottom-full mb-2' : 'top-full mt-2'} right-0 w-48 rounded-lg overflow-hidden
border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm shadow-lg
transition-all duration-200 origin-top
${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`}
role="listbox"
aria-label="Languages"
onKeyDown={handleKeyDown}
>
<div className="py-1">
{languages.map((lang) => (
<button
key={lang.code}
className={`w-full text-left px-4 py-2 text-sm transition-colors duration-200
hover:bg-zinc-800/50
${locale === lang.code
? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white'
}`}
onClick={() => handleLanguageChange(lang.code)}
role="option"
aria-selected={locale === lang.code}
>
{lang.name}
</button>
))}
</div>
</div>
</div>
);
};
export default LanguageSwitcher;
+38
View File
@@ -0,0 +1,38 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ReactNode } from 'react';
interface NavLinkProps {
href: string;
icon: ReactNode;
label: string;
onClick?: () => void;
className?: string;
}
export function NavLink({ href, icon, label, onClick, className }: NavLinkProps) {
const pathname = usePathname();
// Check if path matches (accounting for locale prefix)
const pathWithoutLocale = pathname.replace(/^\/(de|en|sr)/, '');
const hrefWithoutLocale = href.replace(/^\/(de|en|sr)/, '');
const isActive = pathWithoutLocale === hrefWithoutLocale ||
(hrefWithoutLocale === '' && pathWithoutLocale === '');
return (
<Link
href={href}
onClick={onClick}
className={`
relative flex items-center gap-2 rounded-full py-2 px-4
transition-all duration-200 ease-in-out
${isActive ? 'text-white bg-zinc-700/60' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'}
${className || ''}
`}
>
{icon}
<span className="text-sm tracking-wide">{label}</span>
</Link>
);
}
+6
View File
@@ -0,0 +1,6 @@
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as GlobalBackground } from './GlobalBackground';
export { default as LanguageSwitcher } from './LanguageSwitcher';
export { NavLink } from './NavLink';
export { ContactInfo } from './ContactInfo';
+125
View File
@@ -0,0 +1,125 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { motion } from 'framer-motion';
import { ExternalLink, Calendar, Tag } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
interface Project {
slug: string;
title: string;
description: string;
excerpt?: string;
technologies: string[];
category?: string;
date?: string;
featured?: boolean;
}
interface PortfolioCardProps {
project: Project;
}
const PortfolioCard = ({ project }: PortfolioCardProps) => {
const locale = useLocale();
const t = useTranslations('portfolio.ui');
const displayTags = project.technologies;
return (
<Link
href={`/${locale}/portfolio/${project.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700
transition-all duration-500 focus:outline-none focus:ring-2
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 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="relative overflow-hidden rounded-xl"
>
{/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900">
<Image
src={`/images/projects/${project.slug}/cover.jpg`}
alt={project.title}
fill
sizes="(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px"
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
</div>
{/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10">
{/* Project Info */}
<div className="flex items-center gap-4 mb-4 text-zinc-500">
{project.date && (
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" />
<span>{project.date}</span>
</div>
)}
{displayTags && displayTags.length > 0 && (
<div className="flex items-center gap-2 text-sm">
<Tag className="h-4 w-4" />
<span>{displayTags.length} {t('technologies')}</span>
</div>
)}
</div>
{/* Title & Description */}
<h3 className="text-xl font-semibold text-white mb-3
group-hover:text-zinc-100 transition-colors duration-300">
{project.title}
</h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4
group-hover:text-zinc-300 transition-colors duration-300">
{project.excerpt || project.description}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{displayTags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="px-3 py-1 bg-zinc-800/50
text-sm text-zinc-400 rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300
transition-all duration-300"
>
{tag}
</span>
))}
{displayTags.length > 3 && (
<span className="text-sm text-zinc-600">
+{displayTags.length - 3} more
</span>
)}
</div>
{/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50
opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0
transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" />
</div>
</div>
{/* Subtle Hover Border */}
<div className="absolute inset-0 rounded-xl pointer-events-none
ring-1 ring-zinc-600/0 group-hover:ring-zinc-600/30
transition-all duration-500" />
</motion.div>
</Link>
);
};
export default PortfolioCard;
@@ -0,0 +1,57 @@
'use client';
import { motion } from 'framer-motion';
import PortfolioCard from './PortfolioCard';
interface Project {
slug: string;
title: string;
description: string;
excerpt?: string;
technologies: string[];
category?: string;
date?: string;
featured?: boolean;
}
interface PortfolioGridProps {
projects: Project[];
}
const PortfolioGrid = ({ projects }: PortfolioGridProps) => {
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 md:grid-cols-2 gap-8"
>
{projects.map((project) => (
<motion.div
key={project.slug}
variants={itemVariants}
transition={{ duration: 0.5 }}
className="group"
>
<PortfolioCard project={project} />
</motion.div>
))}
</motion.div>
);
};
export default PortfolioGrid;
+2
View File
@@ -0,0 +1,2 @@
export { default as PortfolioCard } from './PortfolioCard';
export { default as PortfolioGrid } from './PortfolioGrid';
+94
View File
@@ -0,0 +1,94 @@
import { getTranslations, getLocale } from 'next-intl/server';
import { MapPin, Briefcase, Code, Award, GraduationCap, Download } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
const About = async () => {
const t = await getTranslations('pages.home.about');
const locale = await getLocale();
const getCvUrl = () => {
if (locale === 'de') return '/damjan_savic_cv_de.pdf';
return '/damjan_savic_cv_en.pdf';
};
const highlights = [
{ icon: <MapPin className="w-4 h-4" />, label: 'Köln, Deutschland' },
{ icon: <Briefcase className="w-4 h-4" />, label: '5+ Jahre Erfahrung' },
{ icon: <Code className="w-4 h-4" />, label: 'Full-Stack Developer' },
{ icon: <Award className="w-4 h-4" />, label: 'ERP Spezialist' }
];
return (
<section id="about" className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Content Section */}
<div className="space-y-6 order-2 lg:order-1">
{/* Title */}
<div>
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-2">
{t('title')}
</h2>
<div className="h-1 w-20 bg-gradient-to-r from-zinc-600 to-zinc-800 rounded-full" />
</div>
{/* Highlight Pills */}
<div className="flex flex-wrap gap-3">
{highlights.map((item, index) => (
<div
key={index}
className="flex items-center gap-2 px-3 py-1.5 bg-zinc-800/30 backdrop-blur-sm rounded-full border border-zinc-700/50 hover:border-zinc-600 transition-colors"
>
<span className="text-zinc-400">{item.icon}</span>
<span className="text-zinc-300 text-sm">{item.label}</span>
</div>
))}
</div>
{/* Main Content */}
<div className="space-y-4">
<p className="text-zinc-300 leading-relaxed text-lg">
{t('content')}
</p>
</div>
{/* Call to Action Buttons */}
<div className="flex flex-wrap gap-4 pt-4">
<Link
href={`/${locale}/about`}
className="px-6 py-3 bg-zinc-800/50 backdrop-blur-sm hover:bg-zinc-700/50 text-white rounded-xl font-medium transition-colors duration-300 flex items-center gap-2"
>
<GraduationCap className="w-4 h-4" />
{t('buttons.learnMore')}
</Link>
<a
href={getCvUrl()}
download
className="px-6 py-3 bg-transparent hover:bg-zinc-800/30 text-zinc-300 border border-zinc-700/50 rounded-xl font-medium transition-colors duration-300 flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t('buttons.downloadCV')}
</a>
</div>
</div>
{/* Image Section */}
<div className="relative order-1 lg:order-2">
<div className="relative rounded-3xl overflow-hidden aspect-square bg-zinc-800/30 backdrop-blur-sm">
<Image
src="/images/gallery/frontal-smile.webp"
alt="Damjan Savić"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover object-bottom"
/>
</div>
</div>
</div>
</div>
</section>
);
};
export default About;
+178
View File
@@ -0,0 +1,178 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion, AnimatePresence } from 'framer-motion';
interface ExperiencePosition {
role: string;
company: string;
period: string;
highlights: string[];
}
const Experience = () => {
const t = useTranslations('pages.home.experience');
const [currentPage, setCurrentPage] = useState(0);
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchEnd, setTouchEnd] = useState<number | null>(null);
const [itemsPerPage, setItemsPerPage] = useState(2);
const containerRef = useRef<HTMLDivElement>(null);
const experiences = t.raw('positions') as ExperiencePosition[];
useEffect(() => {
const updateItemsPerPage = () => {
setItemsPerPage(window.innerWidth >= 768 ? 2 : 1);
};
updateItemsPerPage();
window.addEventListener('resize', updateItemsPerPage);
return () => window.removeEventListener('resize', updateItemsPerPage);
}, []);
const totalPages = Math.ceil((Array.isArray(experiences) ? experiences.length : 0) / itemsPerPage);
const handleTouchStart = (e: React.TouchEvent) => {
setTouchStart(e.touches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.touches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > 50;
const isRightSwipe = distance < -50;
if (isLeftSwipe && currentPage < totalPages - 1) {
setCurrentPage(prev => prev + 1);
}
if (isRightSwipe && currentPage > 0) {
setCurrentPage(prev => prev - 1);
}
setTouchStart(null);
setTouchEnd(null);
};
const getCurrentPageItems = () => {
if (!Array.isArray(experiences)) return [];
const startIndex = currentPage * itemsPerPage;
return experiences.slice(startIndex, startIndex + itemsPerPage);
};
return (
<section id="experience" className="py-12 md:py-24 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<h2 className="text-3xl font-bold text-white text-center mb-12">
{t('title')}
</h2>
<div
ref={containerRef}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
className="relative"
>
<AnimatePresence mode="wait">
<motion.div
key={currentPage}
initial={{ opacity: 0, x: 100, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: -100, scale: 0.95 }}
transition={{ duration: 0.5, ease: [0.43, 0.13, 0.23, 0.96] }}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
>
{getCurrentPageItems().map((exp, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.15, ease: [0.25, 0.46, 0.45, 0.94] }}
whileHover={{ scale: 1.02, y: -5, transition: { duration: 0.3 } }}
className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-all duration-300 hover:shadow-xl hover:shadow-zinc-900/50"
>
<h3 className="text-lg md:text-xl font-semibold text-white mb-1">
{exp.role}
</h3>
<h4 className="text-base md:text-lg text-zinc-300 mb-2">
{exp.company}
</h4>
<p className="text-sm text-zinc-400 mb-6">
{exp.period}
</p>
<ul className="space-y-3 md:space-y-4">
{exp.highlights?.map((highlight: string, hIndex: number) => (
<motion.li
key={hIndex}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: 0.3 + (hIndex * 0.1), ease: 'easeOut' }}
className="text-zinc-400 text-sm leading-relaxed flex items-start"
>
<span className="text-white/40 mr-2 mt-0.5"></span>
<span>{highlight}</span>
</motion.li>
))}
</ul>
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
{/* Navigation Dots */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-4 mt-8">
<button
onClick={() => currentPage > 0 && setCurrentPage(prev => prev - 1)}
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === 0}
aria-label="Previous page"
>
</button>
<div className="flex items-center gap-1">
{[...Array(totalPages)].map((_, index) => (
<button
key={index}
onClick={() => setCurrentPage(index)}
className="relative flex items-center justify-center transition-all duration-300"
style={{ width: '24px', height: '24px' }}
aria-label={`Page ${index + 1}`}
>
<span
className={`rounded-full transition-all duration-300 ${
index === currentPage ? 'bg-white' : 'bg-zinc-600 hover:bg-zinc-400'
}`}
style={{ width: '8px', height: '8px' }}
/>
</button>
))}
</div>
<button
onClick={() => currentPage < totalPages - 1 && setCurrentPage(prev => prev + 1)}
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === totalPages - 1}
aria-label="Next page"
>
</button>
</div>
)}
</motion.div>
</div>
</section>
);
};
export default Experience;
+114
View File
@@ -0,0 +1,114 @@
import { getTranslations } from 'next-intl/server';
import { Linkedin, Github, ChevronDown } from 'lucide-react';
import Image from 'next/image';
const Hero = async () => {
const t = await getTranslations('pages.home.hero');
return (
<section id="home" className="relative h-screen max-h-screen text-white overflow-hidden">
{/* Hero Image - Full Width */}
<div className="absolute inset-0">
<Image
src="/hero-portrait.jpg"
alt="Damjan Savić"
fill
sizes="100vw"
className="object-cover object-top md:object-left"
priority
/>
</div>
{/* Desktop: Gradient on left side */}
<div className="hidden md:block absolute inset-y-0 left-0 w-1/2 bg-gradient-to-r from-zinc-950 via-zinc-950 via-60% to-transparent z-10" />
{/* Mobile: Gradient at bottom for text */}
<div className="md:hidden absolute inset-x-0 bottom-0 h-[60%] bg-gradient-to-t from-zinc-950 via-zinc-950/80 via-40% to-transparent z-10 pointer-events-none" />
{/* Social Links - Top */}
<div className="absolute top-8 left-6 sm:left-12 lg:left-20 flex gap-4 z-20">
<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={22} />
</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={22} />
</a>
</div>
{/* Main Content - Bottom on mobile, left on desktop */}
<div className="relative z-20 h-full flex flex-col justify-end md:justify-center items-center md:items-start px-6 sm:px-12 lg:px-20 pb-24 md:pb-0">
<div className="max-w-3xl space-y-4 md:space-y-6 text-center md:text-left">
<p className="text-zinc-400 text-sm sm:text-base tracking-[0.3em] uppercase">
{t('title')}
</p>
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold tracking-tight">
{t('name')}
<span className="animate-pulse text-white/80">|</span>
</h1>
<p className="text-zinc-300 text-base sm:text-lg lg:text-xl max-w-lg leading-relaxed">
{t('description')}
</p>
{/* CTA Buttons */}
<div className="flex flex-wrap justify-center md:justify-start gap-3 sm:gap-4 pt-2 sm:pt-4">
<a
href="mailto:info@damjan-savic.com"
className="px-6 sm:px-8 py-2.5 sm:py-3 bg-white text-zinc-900 font-medium rounded-full hover:bg-zinc-200 transition-colors text-sm sm:text-base"
>
{t('cta')}
</a>
<a
href="#projects"
className="px-6 sm:px-8 py-2.5 sm:py-3 border border-zinc-600 text-white rounded-full hover:bg-zinc-800 transition-colors text-sm sm:text-base"
>
{t('navigation.projects')}
</a>
</div>
{/* Navigation Links - Hidden on mobile */}
<nav className="hidden sm:flex gap-8 pt-4 text-sm uppercase tracking-wider">
<a
href="#experience"
className="text-zinc-400 hover:text-white transition-colors"
>
{t('navigation.experience')}
</a>
<a
href="#skills"
className="text-zinc-400 hover:text-white transition-colors"
>
{t('navigation.skills')}
</a>
<a
href="#about"
className="text-zinc-400 hover:text-white transition-colors"
>
{t('navigation.about')}
</a>
</nav>
</div>
</div>
{/* Scroll Indicator - Hidden on mobile */}
<div className="hidden sm:block absolute bottom-8 left-1/2 -translate-x-1/2 z-20 animate-bounce">
<ChevronDown className="text-zinc-400" size={28} />
</div>
</section>
);
};
export default Hero;
+139
View File
@@ -0,0 +1,139 @@
import { getTranslations, getLocale } from 'next-intl/server';
import Link from 'next/link';
import Image from 'next/image';
import { Calendar, Tag, ExternalLink } from 'lucide-react';
// Project data embedded directly for SSG
const projectsData = [
{
slug: 'ai-data-reader',
title: 'AI Document Reader',
excerpt: 'KI-gestützte Dokumentenanalyse mit OLLAMA und Python',
date: '2024-01',
technologies: ['Python', 'OLLAMA', 'FastAPI', 'React'],
category: 'AI Development',
},
{
slug: 'smart-warehouse',
title: 'Smart Warehouse RFID',
excerpt: 'RFID-basiertes Lagerverwaltungssystem mit IoT-Integration',
date: '2024-02',
technologies: ['Python', 'RFID', 'IoT', 'PostgreSQL'],
category: 'IoT',
},
{
slug: 'website-mit-ki',
title: 'Portfolio mit KI',
excerpt: 'Moderne Portfolio-Website mit KI-Integration',
date: '2024-03',
technologies: ['Next.js', 'TypeScript', 'Tailwind', 'Supabase'],
category: 'Full-Stack',
},
];
const Projects = async () => {
const t = await getTranslations('pages.home.projects');
const locale = await getLocale();
return (
<section id="projects" className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center mb-12">
<h2 className="text-3xl font-bold text-zinc-100">
{t('title')}
</h2>
<Link
href={`/${locale}/portfolio`}
className="text-zinc-400 hover:text-white transition-colors duration-300"
>
{t('viewAll')}
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projectsData.map((project) => (
<Link
key={project.slug}
href={`/${locale}/portfolio/${project.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700
transition-all duration-500 focus:outline-none focus:ring-2
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950
transform hover:-translate-y-1"
>
<div className="relative overflow-hidden rounded-xl">
{/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900">
<Image
src={`/images/projects/${project.slug}/cover.jpg`}
alt={project.title}
fill
sizes="(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px"
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
</div>
{/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10">
<div className="flex items-center gap-4 mb-4 text-zinc-500">
{project.date && (
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" />
<span>{project.date}</span>
</div>
)}
{project.technologies.length > 0 && (
<div className="flex items-center gap-2 text-sm">
<Tag className="h-4 w-4" />
<span>{project.technologies.length} Technologies</span>
</div>
)}
</div>
<h3 className="text-xl font-semibold text-white mb-3 group-hover:text-zinc-100 transition-colors duration-300">
{project.title}
</h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4 group-hover:text-zinc-300 transition-colors duration-300">
{project.excerpt}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{project.technologies.slice(0, 3).map((tag, tagIndex) => (
<span
key={tagIndex}
className="px-3 py-1 bg-zinc-800/50 text-sm text-zinc-400 rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300
transition-all duration-300"
>
{tag}
</span>
))}
{project.technologies.length > 3 && (
<span className="text-sm text-zinc-600">
+{project.technologies.length - 3} more
</span>
)}
</div>
{/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50
opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0
transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" />
</div>
</div>
</div>
</Link>
))}
</div>
</div>
</section>
);
};
export default Projects;
+157
View File
@@ -0,0 +1,157 @@
'use client';
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion, AnimatePresence } from 'framer-motion';
import { Database, Server, Code2, Bot, Workflow, FileCode2 } from 'lucide-react';
interface Skill {
name: string;
level: number;
description: string;
}
const iconMap: Record<string, React.ReactNode> = {
'AI & LLMs': <Bot className="w-8 h-8" />,
'Automation': <Workflow className="w-8 h-8" />,
'Automatizacija': <Workflow className="w-8 h-8" />,
'Python': <Code2 className="w-8 h-8" />,
'TypeScript': <FileCode2 className="w-8 h-8" />,
'Datenbank': <Database className="w-8 h-8" />,
'Database': <Database className="w-8 h-8" />,
'Baze podataka': <Database className="w-8 h-8" />,
'DevOps': <Server className="w-8 h-8" />
};
const Skills = () => {
const t = useTranslations('pages.home.skills');
const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
const [skills, setSkills] = useState<Skill[]>([]);
useEffect(() => {
try {
const translatedSkills = t.raw('skills') as Skill[];
if (Array.isArray(translatedSkills)) {
setSkills(translatedSkills);
}
} catch (error) {
console.error('Error loading skills:', error);
setSkills([]);
}
}, [t]);
if (skills.length === 0) {
return <div className="py-24 text-center text-zinc-400">Loading skills...</div>;
}
return (
<section id="skills" className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold text-white mb-12">
{t('title')}
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Skills Progress Bars */}
<div className="space-y-6">
{skills.map((skill, idx) => (
<motion.div
key={skill.name}
className="space-y-2"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
>
<div className="flex justify-between items-center">
<span className="text-white text-sm font-medium">{skill.name}</span>
<span className="text-white text-sm">{skill.level}%</span>
</div>
<div
className="h-2 bg-zinc-800/50 rounded-full overflow-hidden"
role="progressbar"
aria-valuenow={skill.level}
aria-valuemin={0}
aria-valuemax={100}
>
<motion.div
className="h-full rounded-full bg-gradient-to-r from-zinc-600 to-zinc-500"
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
viewport={{ once: true }}
transition={{ duration: 1, delay: idx * 0.1 }}
/>
</div>
</motion.div>
))}
</div>
{/* Skills Icons Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{skills.map((skill, idx) => (
<motion.div
key={skill.name}
className="relative"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
>
<motion.div
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
role="button"
>
<motion.div
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
animate={{
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
scale: selectedSkill === skill.name ? 1.1 : 1
}}
transition={{ duration: 0.4 }}
>
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</motion.div>
<span className="text-white text-sm text-center">{skill.name}</span>
<AnimatePresence>
{selectedSkill === skill.name && (
<motion.div
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
style={{
whiteSpace: 'nowrap',
transform: 'translate(-50%, calc(-100% - 12px))',
zIndex: 999
}}
>
{skill.description}
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800" />
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
))}
</div>
</div>
</motion.div>
</div>
</section>
);
};
export default Skills;
+5
View File
@@ -0,0 +1,5 @@
export { default as Hero } from './Hero';
export { default as Experience } from './Experience';
export { default as Skills } from './Skills';
export { default as Projects } from './Projects';
export { default as About } from './About';
+758
View File
@@ -0,0 +1,758 @@
import { type Locale } from '@/i18n/config';
interface JsonLdProps {
locale: Locale;
}
export function PersonJsonLd({ locale }: JsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'Person',
'@id': `${baseUrl}/#person`,
name: 'Damjan Savić',
givenName: 'Damjan',
familyName: 'Savić',
jobTitle: locale === 'de'
? 'AI & Automation Specialist | Fullstack Entwickler'
: locale === 'sr'
? 'AI & Automation Specialist | Fullstack Developer'
: 'AI & Automation Specialist | Fullstack Developer',
description: locale === 'de'
? 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.'
: locale === 'sr'
? 'Razvoj AI agenata i rešenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.'
: 'Remote AI developer building AI agents and automation solutions for clients in USA, UK and Europe. From voice AI platforms to autonomous web agents.',
url: baseUrl,
image: `${baseUrl}/images/og-image.jpg`,
email: 'info@damjan-savic.com',
telephone: '+491756950979',
address: {
'@type': 'PostalAddress',
addressLocality: locale === 'sr' ? 'Keln' : locale === 'en' ? 'Cologne' : 'Köln',
addressRegion: 'NRW',
addressCountry: 'DE',
},
sameAs: [
'https://www.linkedin.com/in/damjansavic/',
'https://github.com/damjansavic',
],
knowsAbout: [
'Artificial Intelligence',
'Machine Learning',
'Process Automation',
'Voice AI',
'Web Development',
'Python',
'TypeScript',
'React',
'Next.js',
'n8n',
'SaaS Development',
],
alumniOf: [
{
'@type': 'EducationalOrganization',
name: 'FOM Hochschule für Ökonomie & Management',
},
],
hasCredential: [
{
'@type': 'EducationalOccupationalCredential',
name: 'M.A. Software Development',
credentialCategory: 'degree',
},
{
'@type': 'EducationalOccupationalCredential',
name: 'B.Sc. Wirtschaftsinformatik',
credentialCategory: 'degree',
},
],
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export function ProfessionalServiceJsonLd({ locale }: JsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const serviceNames = {
de: [
'KI-Entwicklung',
'Prozessautomatisierung',
'Voice AI Entwicklung',
'SaaS Entwicklung',
'Fullstack Webentwicklung',
'n8n Automatisierung',
],
en: [
'AI Development',
'Process Automation',
'Voice AI Development',
'SaaS Development',
'Fullstack Web Development',
'n8n Automation',
],
sr: [
'AI Razvoj',
'Automatizacija procesa',
'Voice AI Razvoj',
'SaaS Razvoj',
'Fullstack Web Razvoj',
'n8n Automatizacija',
],
};
const schema = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
'@id': `${baseUrl}/#business`,
name: 'Damjan Savić - AI & Automation Specialist',
description: locale === 'de'
? 'Freelance AI & Automation Specialist aus Köln. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung und SaaS-Entwicklung.'
: locale === 'sr'
? 'Freelance AI & Automation Specialist iz Kelna. Specijalizovan za AI agente, Voice AI, automatizaciju procesa i SaaS razvoj.'
: 'Remote AI & Automation Specialist based in Germany, serving clients in USA, UK and Europe. Specialized in AI agents, Voice AI, n8n process automation and custom SaaS development.',
url: baseUrl,
logo: `${baseUrl}/images/og-image.jpg`,
image: `${baseUrl}/images/og-image.jpg`,
email: 'info@damjan-savic.com',
telephone: '+491756950979',
priceRange: '€€€',
address: {
'@type': 'PostalAddress',
streetAddress: 'Rotdornallee',
addressLocality: locale === 'sr' ? 'Keln' : locale === 'en' ? 'Cologne' : 'Köln',
addressRegion: 'NRW',
postalCode: '50769',
addressCountry: 'DE',
},
geo: {
'@type': 'GeoCoordinates',
latitude: 50.9375,
longitude: 6.9603,
},
areaServed: [
// Germany
{ '@type': 'City', name: 'Cologne' },
{ '@type': 'City', name: 'Düsseldorf' },
{ '@type': 'City', name: 'Frankfurt' },
{ '@type': 'City', name: 'Munich' },
{ '@type': 'City', name: 'Berlin' },
{ '@type': 'City', name: 'Hamburg' },
{ '@type': 'Country', name: 'Germany' },
// USA
{ '@type': 'City', name: 'New York' },
{ '@type': 'City', name: 'Los Angeles' },
{ '@type': 'City', name: 'San Francisco' },
{ '@type': 'City', name: 'Miami' },
{ '@type': 'City', name: 'Chicago' },
{ '@type': 'Country', name: 'United States' },
// UK
{ '@type': 'City', name: 'London' },
{ '@type': 'Country', name: 'United Kingdom' },
// Europe
{ '@type': 'Country', name: 'Austria' },
{ '@type': 'Country', name: 'Switzerland' },
],
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: locale === 'de' ? 'Dienstleistungen' : locale === 'sr' ? 'Usluge' : 'Services',
itemListElement: serviceNames[locale].map((service, index) => ({
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: service,
},
position: index + 1,
})),
},
founder: {
'@id': `${baseUrl}/#person`,
},
sameAs: [
'https://www.linkedin.com/in/damjansavic/',
'https://github.com/damjansavic',
],
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export function BreadcrumbJsonLd({
items,
locale,
}: {
items: { name: string; url: string }[];
locale: Locale;
}) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url.startsWith('http') ? item.url : `${baseUrl}${item.url}`,
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export function WebPageJsonLd({
title,
description,
url,
locale,
}: {
title: string;
description: string;
url: string;
locale: Locale;
}) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description: description,
url: url.startsWith('http') ? url : `${baseUrl}${url}`,
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
isPartOf: {
'@type': 'WebSite',
name: 'Damjan Savić',
url: baseUrl,
},
author: {
'@id': `${baseUrl}/#person`,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
interface FAQItem {
question: string;
answer: string;
}
export function FAQJsonLd({ items, locale }: { items: FAQItem[]; locale?: Locale }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
inLanguage: locale ? (locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US') : 'de-DE',
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
interface ServiceJsonLdProps {
name: string;
description: string;
url: string;
locale: Locale;
areaServed?: string[];
}
export function ServiceJsonLd({
name,
description,
url,
locale,
areaServed = ['Köln', 'Düsseldorf', 'Frankfurt', 'Deutschland'],
}: ServiceJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'Service',
name: name,
description: description,
url: url.startsWith('http') ? url : `${baseUrl}${url}`,
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
availableLanguage: ['de-DE', 'en-US', 'sr-RS'],
provider: {
'@id': `${baseUrl}/#business`,
},
areaServed: areaServed.map((area) => ({
'@type': 'City',
name: area,
})),
serviceType: name,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Organization Schema
export function OrganizationJsonLd({ locale }: JsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${baseUrl}/#organization`,
name: 'Damjan Savić - AI & Automation Specialist',
alternateName: 'Damjan Savić',
url: baseUrl,
logo: {
'@type': 'ImageObject',
url: `${baseUrl}/images/og-image.jpg`,
width: 1200,
height: 630,
},
image: `${baseUrl}/images/og-image.jpg`,
description: locale === 'de'
? 'Freelance KI-Entwickler und Automatisierungsspezialist aus Köln. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und SaaS-Entwicklung.'
: locale === 'sr'
? 'Freelance AI developer i specijalista za automatizaciju iz Kelna. Specijalizovan za AI agente, Voice AI, automatizaciju procesa sa n8n i SaaS razvoj.'
: 'Remote AI developer and automation specialist based in Germany. Working with clients in USA, UK & Europe. Specialized in AI agents, Voice AI, n8n automation and SaaS development.',
email: 'info@damjan-savic.com',
telephone: '+491756950979',
address: {
'@type': 'PostalAddress',
streetAddress: 'Rotdornallee',
addressLocality: locale === 'sr' ? 'Keln' : locale === 'en' ? 'Cologne' : 'Köln',
addressRegion: 'NRW',
postalCode: '50769',
addressCountry: 'DE',
},
geo: {
'@type': 'GeoCoordinates',
latitude: 50.9375,
longitude: 6.9603,
},
areaServed: [
// DACH Region
{ '@type': 'Country', name: 'Germany' },
{ '@type': 'Country', name: 'Austria' },
{ '@type': 'Country', name: 'Switzerland' },
// International (Remote)
{ '@type': 'Country', name: 'United States' },
{ '@type': 'Country', name: 'United Kingdom' },
{ '@type': 'Country', name: 'Serbia' },
// Major International Cities
{ '@type': 'City', name: 'New York' },
{ '@type': 'City', name: 'London' },
{ '@type': 'City', name: 'San Francisco' },
{ '@type': 'City', name: 'Los Angeles' },
],
founder: {
'@id': `${baseUrl}/#person`,
},
sameAs: [
'https://www.linkedin.com/in/damjansavic/',
'https://github.com/damjansavic',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'customer service',
email: 'info@damjan-savic.com',
telephone: '+491756950979',
availableLanguage: ['German', 'English', 'Serbian'],
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Article Schema for Blog Posts
interface ArticleJsonLdProps {
title: string;
description: string;
url: string;
imageUrl: string;
datePublished: string;
dateModified?: string;
authorName?: string;
tags?: string[];
locale: Locale;
}
export function ArticleJsonLd({
title,
description,
url,
imageUrl,
datePublished,
dateModified,
authorName = 'Damjan Savić',
tags = [],
locale,
}: ArticleJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: title,
description: description,
image: imageUrl.startsWith('http') ? imageUrl : `${baseUrl}${imageUrl}`,
url: url.startsWith('http') ? url : `${baseUrl}${url}`,
datePublished: datePublished,
dateModified: dateModified || datePublished,
author: {
'@type': 'Person',
'@id': `${baseUrl}/#person`,
name: authorName,
url: baseUrl,
},
publisher: {
'@type': 'Organization',
'@id': `${baseUrl}/#organization`,
name: 'Damjan Savić',
logo: {
'@type': 'ImageObject',
url: `${baseUrl}/images/og-image.jpg`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url.startsWith('http') ? url : `${baseUrl}${url}`,
},
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
keywords: tags.join(', '),
articleSection: 'Technology',
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// HowTo Schema for Tutorials
interface HowToStep {
name: string;
text: string;
url?: string;
image?: string;
}
interface HowToJsonLdProps {
name: string;
description: string;
steps: HowToStep[];
totalTime?: string; // ISO 8601 duration format, e.g., "PT30M" for 30 minutes
estimatedCost?: { currency: string; value: string };
tools?: string[];
supplies?: string[];
image?: string;
locale: Locale;
}
export function HowToJsonLd({
name,
description,
steps,
totalTime,
estimatedCost,
tools,
supplies,
image,
locale,
}: HowToJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: name,
description: description,
step: steps.map((step, index) => ({
'@type': 'HowToStep',
position: index + 1,
name: step.name,
text: step.text,
...(step.url && { url: step.url.startsWith('http') ? step.url : `${baseUrl}${step.url}` }),
...(step.image && { image: step.image.startsWith('http') ? step.image : `${baseUrl}${step.image}` }),
})),
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
};
if (totalTime) {
schema.totalTime = totalTime;
}
if (estimatedCost) {
schema.estimatedCost = {
'@type': 'MonetaryAmount',
currency: estimatedCost.currency,
value: estimatedCost.value,
};
}
if (tools && tools.length > 0) {
schema.tool = tools.map((tool) => ({
'@type': 'HowToTool',
name: tool,
}));
}
if (supplies && supplies.length > 0) {
schema.supply = supplies.map((supply) => ({
'@type': 'HowToSupply',
name: supply,
}));
}
if (image) {
schema.image = image.startsWith('http') ? image : `${baseUrl}${image}`;
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// ProfilePage Schema for About Page
interface ProfilePageJsonLdProps {
locale: Locale;
}
export function ProfilePageJsonLd({ locale }: ProfilePageJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
dateCreated: '2024-01-01',
dateModified: new Date().toISOString().split('T')[0],
mainEntity: {
'@id': `${baseUrl}/#person`,
},
name: locale === 'de'
? 'Über Damjan Savić - KI-Entwickler & Automatisierungsspezialist'
: locale === 'sr'
? 'O Damjanu Saviću - AI Developer & Specijalista za automatizaciju'
: 'About Damjan Savić - AI Developer & Automation Specialist',
description: locale === 'de'
? 'Erfahren Sie mehr über Damjan Savić, Freelance KI-Entwickler und Automatisierungsspezialist aus Köln. Expertise in KI-Agenten, Voice AI, n8n und SaaS-Entwicklung.'
: locale === 'sr'
? 'Saznajte više o Damjanu Saviću, freelance AI developeru i specijalisti za automatizaciju iz Kelna. Ekspertiza u AI agentima, Voice AI, n8n i SaaS razvoju.'
: 'Learn more about Damjan Savić, freelance AI developer and automation specialist from Cologne. Expertise in AI agents, Voice AI, n8n and SaaS development.',
url: `${baseUrl}/${locale}/about`,
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// VideoObject Schema for embedded videos
interface VideoJsonLdProps {
name: string;
description: string;
thumbnailUrl: string;
uploadDate: string;
duration?: string; // ISO 8601 format, e.g., "PT1M30S"
contentUrl?: string;
embedUrl?: string;
locale: Locale;
}
export function VideoJsonLd({
name,
description,
thumbnailUrl,
uploadDate,
duration,
contentUrl,
embedUrl,
locale,
}: VideoJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'VideoObject',
name: name,
description: description,
thumbnailUrl: thumbnailUrl.startsWith('http') ? thumbnailUrl : `${baseUrl}${thumbnailUrl}`,
uploadDate: uploadDate,
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
author: {
'@id': `${baseUrl}/#person`,
},
};
if (duration) {
schema.duration = duration;
}
if (contentUrl) {
schema.contentUrl = contentUrl;
}
if (embedUrl) {
schema.embedUrl = embedUrl;
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// SoftwareApplication Schema for tools/projects
interface SoftwareAppJsonLdProps {
name: string;
description: string;
applicationCategory: string;
operatingSystem?: string;
offers?: {
price: string;
priceCurrency: string;
};
aggregateRating?: {
ratingValue: string;
ratingCount: string;
};
locale: Locale;
}
export function SoftwareAppJsonLd({
name,
description,
applicationCategory,
operatingSystem = 'Web',
offers,
aggregateRating,
locale,
}: SoftwareAppJsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: name,
description: description,
applicationCategory: applicationCategory,
operatingSystem: operatingSystem,
author: {
'@id': `${baseUrl}/#person`,
},
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
};
if (offers) {
schema.offers = {
'@type': 'Offer',
price: offers.price,
priceCurrency: offers.priceCurrency,
};
}
if (aggregateRating) {
schema.aggregateRating = {
'@type': 'AggregateRating',
ratingValue: aggregateRating.ratingValue,
ratingCount: aggregateRating.ratingCount,
};
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// WebSite Schema for Search Box
export function WebSiteJsonLd({ locale }: JsonLdProps) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const schema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': `${baseUrl}/#website`,
name: 'Damjan Savić',
alternateName: locale === 'de'
? 'Damjan Savić - KI Entwickler Köln'
: locale === 'sr'
? 'Damjan Savić - AI Developer Keln'
: 'Damjan Savić - AI Developer Cologne',
url: baseUrl,
description: locale === 'de'
? 'Portfolio und Blog von Damjan Savić - KI-Entwickler und Automatisierungsspezialist aus Köln'
: locale === 'sr'
? 'Portfolio i blog Damjana Savića - AI developer i specijalista za automatizaciju iz Kelna'
: 'Portfolio and blog of Damjan Savić - AI Developer and Automation Specialist from Cologne',
publisher: {
'@id': `${baseUrl}/#organization`,
},
inLanguage: [
{ '@type': 'Language', name: 'German', alternateName: 'de' },
{ '@type': 'Language', name: 'English', alternateName: 'en' },
{ '@type': 'Language', name: 'Serbian', alternateName: 'sr' },
],
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
+15
View File
@@ -0,0 +1,15 @@
export {
PersonJsonLd,
ProfessionalServiceJsonLd,
BreadcrumbJsonLd,
WebPageJsonLd,
FAQJsonLd,
ServiceJsonLd,
OrganizationJsonLd,
ArticleJsonLd,
WebSiteJsonLd,
HowToJsonLd,
ProfilePageJsonLd,
VideoJsonLd,
SoftwareAppJsonLd,
} from './JsonLd';
+556
View File
@@ -0,0 +1,556 @@
export interface CityData {
slug: string;
name: {
de: string;
en: string;
sr: string;
};
region: string;
description: {
de: string;
en: string;
sr: string;
};
keywords: {
de: string[];
en: string[];
sr: string[];
};
nearbyAreas: string[];
}
export const cities: CityData[] = [
{
slug: 'koeln',
name: {
de: 'Köln',
en: 'Cologne',
sr: 'Keln',
},
region: 'NRW',
description: {
de: 'Als KI-Entwickler und Automatisierungsexperte aus Köln entwickle ich maßgeschneiderte Lösungen für Unternehmen in der Rheinmetropole. Von Voice AI bis Prozessautomatisierung - ich bringe Ihr Unternehmen auf das nächste Level.',
en: 'As an AI developer and automation expert from Cologne, I develop customized solutions for companies in the Rhine metropolis. From Voice AI to process automation - I take your business to the next level.',
sr: 'Kao AI developer i ekspert za automatizaciju iz Kelna, razvijam prilagođena rešenja za kompanije u rajnskoj metropoli. Od Voice AI do automatizacije procesa - vodim vaš posao na sledeći nivo.',
},
keywords: {
de: ['KI Entwickler Köln', 'AI Agentur Köln', 'Fullstack Entwickler Köln', 'Webentwickler Köln', 'Python Entwickler Köln'],
en: ['AI Developer Cologne', 'AI Agency Cologne', 'Fullstack Developer Cologne', 'Web Developer Cologne', 'Python Developer Cologne'],
sr: ['AI Developer Keln', 'Fullstack Developer Keln', 'Web Developer Keln'],
},
nearbyAreas: ['Bonn', 'Düsseldorf', 'Leverkusen', 'Bergisch Gladbach'],
},
{
slug: 'duesseldorf',
name: {
de: 'Düsseldorf',
en: 'Düsseldorf',
sr: 'Diseldorf',
},
region: 'NRW',
description: {
de: 'Für Unternehmen in Düsseldorf biete ich professionelle KI-Entwicklung und Automatisierungslösungen. Die Landeshauptstadt NRWs ist bekannt für ihre Innovations- und Wirtschaftskraft - und ich helfe Ihnen, diese durch smarte Technologie zu nutzen.',
en: 'For companies in Düsseldorf, I offer professional AI development and automation solutions. The capital of NRW is known for its innovation and economic power - and I help you leverage this through smart technology.',
sr: 'Za kompanije u Diseldorfu nudim profesionalni AI razvoj i rešenja za automatizaciju. Glavni grad NRW-a poznat je po inovativnosti i ekonomskoj snazi - a ja vam pomažem da to iskoristite kroz pametnu tehnologiju.',
},
keywords: {
de: ['KI Entwickler Düsseldorf', 'AI Agentur Düsseldorf', 'Fullstack Entwickler Düsseldorf', 'Webentwickler Düsseldorf', 'Automatisierung Düsseldorf'],
en: ['AI Developer Düsseldorf', 'AI Agency Düsseldorf', 'Fullstack Developer Düsseldorf', 'Web Developer Düsseldorf', 'Automation Düsseldorf'],
sr: ['AI Developer Diseldorf', 'Fullstack Developer Diseldorf'],
},
nearbyAreas: ['Köln', 'Essen', 'Duisburg', 'Wuppertal'],
},
{
slug: 'frankfurt',
name: {
de: 'Frankfurt am Main',
en: 'Frankfurt',
sr: 'Frankfurt',
},
region: 'Hessen',
description: {
de: 'Frankfurt als Finanzmetropole Deutschlands hat besondere Anforderungen an Technologie und Automatisierung. Ich entwickle KI-Lösungen und SaaS-Anwendungen, die den hohen Standards der Finanzbranche entsprechen.',
en: 'Frankfurt as Germany\'s financial metropolis has special requirements for technology and automation. I develop AI solutions and SaaS applications that meet the high standards of the financial industry.',
sr: 'Frankfurt kao finansijska metropola Nemačke ima posebne zahteve za tehnologiju i automatizaciju. Razvijam AI rešenja i SaaS aplikacije koje ispunjavaju visoke standarde finansijske industrije.',
},
keywords: {
de: ['KI Entwickler Frankfurt', 'SaaS Entwicklung Frankfurt', 'Fullstack Entwickler Frankfurt', 'Fintech Entwickler Frankfurt', 'Automatisierung Frankfurt'],
en: ['AI Developer Frankfurt', 'SaaS Development Frankfurt', 'Fullstack Developer Frankfurt', 'Fintech Developer Frankfurt', 'Automation Frankfurt'],
sr: ['AI Developer Frankfurt', 'SaaS Razvoj Frankfurt'],
},
nearbyAreas: ['Wiesbaden', 'Mainz', 'Darmstadt', 'Offenbach'],
},
{
slug: 'muenchen',
name: {
de: 'München',
en: 'Munich',
sr: 'Minhen',
},
region: 'Bayern',
description: {
de: 'München ist ein führender Technologie-Standort in Deutschland. Als Remote-Freelancer unterstütze ich Münchner Unternehmen bei der Entwicklung von KI-Agenten, Voice AI und Automatisierungslösungen.',
en: 'Munich is a leading technology location in Germany. As a remote freelancer, I support Munich-based companies in developing AI agents, Voice AI and automation solutions.',
sr: 'Minhen je vodeća tehnološka lokacija u Nemačkoj. Kao remote freelancer, podržavam kompanije iz Minhena u razvoju AI agenata, Voice AI i rešenja za automatizaciju.',
},
keywords: {
de: ['KI Entwickler München', 'AI Agentur München', 'Fullstack Entwickler München', 'SaaS Entwicklung München', 'Automatisierung München'],
en: ['AI Developer Munich', 'AI Agency Munich', 'Fullstack Developer Munich', 'SaaS Development Munich', 'Automation Munich'],
sr: ['AI Developer Minhen', 'Fullstack Developer Minhen'],
},
nearbyAreas: ['Augsburg', 'Ingolstadt', 'Regensburg', 'Nürnberg'],
},
{
slug: 'berlin',
name: {
de: 'Berlin',
en: 'Berlin',
sr: 'Berlin',
},
region: 'Berlin',
description: {
de: 'Die Startup-Hauptstadt Deutschlands braucht innovative Technologie-Partner. Ich unterstütze Berliner Startups und Unternehmen bei der Entwicklung von KI-Lösungen, MVPs und skalierbaren SaaS-Produkten.',
en: 'Germany\'s startup capital needs innovative technology partners. I support Berlin startups and companies in developing AI solutions, MVPs and scalable SaaS products.',
sr: 'Startap prestonica Nemačke treba inovativne tehnološke partnere. Podržavam berlinske startape i kompanije u razvoju AI rešenja, MVP-ova i skalabilnih SaaS proizvoda.',
},
keywords: {
de: ['KI Entwickler Berlin', 'Startup Entwickler Berlin', 'SaaS Entwicklung Berlin', 'MVP Entwicklung Berlin', 'Automatisierung Berlin'],
en: ['AI Developer Berlin', 'Startup Developer Berlin', 'SaaS Development Berlin', 'MVP Development Berlin', 'Automation Berlin'],
sr: ['AI Developer Berlin', 'Startup Developer Berlin'],
},
nearbyAreas: ['Potsdam', 'Leipzig', 'Dresden', 'Hamburg'],
},
{
slug: 'hamburg',
name: {
de: 'Hamburg',
en: 'Hamburg',
sr: 'Hamburg',
},
region: 'Hamburg',
description: {
de: 'Hamburg als Handels- und Medienstandort hat vielfältige Anforderungen an digitale Lösungen. Ich entwickle KI-Agenten und Automatisierungen für E-Commerce, Logistik und Medienunternehmen.',
en: 'Hamburg as a trade and media location has diverse requirements for digital solutions. I develop AI agents and automations for e-commerce, logistics and media companies.',
sr: 'Hamburg kao trgovinsko i medijsko središte ima raznovrsne zahteve za digitalna rešenja. Razvijam AI agente i automatizacije za e-commerce, logistiku i medijske kompanije.',
},
keywords: {
de: ['KI Entwickler Hamburg', 'E-Commerce Entwickler Hamburg', 'Fullstack Entwickler Hamburg', 'Automatisierung Hamburg', 'SaaS Hamburg'],
en: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg', 'Fullstack Developer Hamburg', 'Automation Hamburg', 'SaaS Hamburg'],
sr: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg'],
},
nearbyAreas: ['Bremen', 'Lübeck', 'Kiel', 'Hannover'],
},
{
slug: 'bonn',
name: {
de: 'Bonn',
en: 'Bonn',
sr: 'Bon',
},
region: 'NRW',
description: {
de: 'Bonn als ehemalige Bundeshauptstadt und Sitz vieler internationaler Organisationen benötigt professionelle IT-Lösungen. Ich biete KI-Entwicklung und Automatisierung für Bonner Unternehmen.',
en: 'Bonn as the former federal capital and seat of many international organizations needs professional IT solutions. I offer AI development and automation for Bonn-based companies.',
sr: 'Bon kao bivši federalni glavni grad i sedište mnogih međunarodnih organizacija treba profesionalna IT rešenja. Nudim AI razvoj i automatizaciju za kompanije iz Bona.',
},
keywords: {
de: ['KI Entwickler Bonn', 'Fullstack Entwickler Bonn', 'Webentwickler Bonn', 'Automatisierung Bonn', 'IT Freelancer Bonn'],
en: ['AI Developer Bonn', 'Fullstack Developer Bonn', 'Web Developer Bonn', 'Automation Bonn', 'IT Freelancer Bonn'],
sr: ['AI Developer Bon', 'Fullstack Developer Bon'],
},
nearbyAreas: ['Köln', 'Koblenz', 'Siegburg', 'Bad Godesberg'],
},
{
slug: 'stuttgart',
name: {
de: 'Stuttgart',
en: 'Stuttgart',
sr: 'Štutgart',
},
region: 'Baden-Württemberg',
description: {
de: 'Stuttgart ist das Herz der deutschen Automobilindustrie und Industrie 4.0. Ich entwickle KI-Lösungen und Automatisierungen für Maschinenbau, Automotive und produzierende Unternehmen in der Region.',
en: 'Stuttgart is the heart of the German automotive industry and Industry 4.0. I develop AI solutions and automations for mechanical engineering, automotive and manufacturing companies in the region.',
sr: 'Štutgart je srce nemačke automobilske industrije i Industrije 4.0. Razvijam AI rešenja i automatizacije za mašinstvo, automotive i proizvodne kompanije u regionu.',
},
keywords: {
de: ['KI Entwickler Stuttgart', 'Industrie 4.0 Stuttgart', 'Automatisierung Stuttgart', 'KI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Entwickler Stuttgart'],
en: ['AI Developer Stuttgart', 'Industry 4.0 Stuttgart', 'Automation Stuttgart', 'AI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Developer Stuttgart'],
sr: ['AI Developer Štutgart', 'Industrie 4.0 Štutgart', 'Automatizacija Štutgart'],
},
nearbyAreas: ['Esslingen', 'Ludwigsburg', 'Böblingen', 'Sindelfingen', 'Karlsruhe'],
},
{
slug: 'essen',
name: {
de: 'Essen',
en: 'Essen',
sr: 'Esen',
},
region: 'NRW',
description: {
de: 'Essen im Herzen des Ruhrgebiets wandelt sich vom Industriestandort zum Digitalzentrum. Ich unterstütze Essener Unternehmen bei der digitalen Transformation mit KI-Agenten und Prozessautomatisierung.',
en: 'Essen in the heart of the Ruhr area is transforming from an industrial location to a digital center. I support Essen-based companies in digital transformation with AI agents and process automation.',
sr: 'Esen u srcu Rurske oblasti transformiše se od industrijskog u digitalni centar. Podržavam kompanije iz Esena u digitalnoj transformaciji sa AI agentima i automatizacijom procesa.',
},
keywords: {
de: ['KI Entwickler Essen', 'Automatisierung Essen', 'Digitalisierung Essen', 'Fullstack Entwickler Essen', 'n8n Entwickler Essen', 'Prozessautomatisierung Essen'],
en: ['AI Developer Essen', 'Automation Essen', 'Digitalization Essen', 'Fullstack Developer Essen', 'n8n Developer Essen', 'Process Automation Essen'],
sr: ['AI Developer Esen', 'Automatizacija Esen', 'Digitalizacija Esen'],
},
nearbyAreas: ['Dortmund', 'Duisburg', 'Bochum', 'Gelsenkirchen', 'Düsseldorf'],
},
{
slug: 'dortmund',
name: {
de: 'Dortmund',
en: 'Dortmund',
sr: 'Dortmund',
},
region: 'NRW',
description: {
de: 'Dortmund entwickelt sich zum führenden Tech-Hub im Ruhrgebiet. Mit der TU Dortmund und zahlreichen Startups biete ich hier KI-Entwicklung, SaaS-Lösungen und Automatisierung für innovative Unternehmen.',
en: 'Dortmund is developing into a leading tech hub in the Ruhr area. With TU Dortmund and numerous startups, I offer AI development, SaaS solutions and automation for innovative companies here.',
sr: 'Dortmund se razvija u vodeći tech hub u Rurskoj oblasti. Sa TU Dortmund i brojnim startapima, nudim AI razvoj, SaaS rešenja i automatizaciju za inovativne kompanije.',
},
keywords: {
de: ['KI Entwickler Dortmund', 'Startup Entwickler Dortmund', 'SaaS Entwicklung Dortmund', 'Fullstack Entwickler Dortmund', 'Automatisierung Dortmund', 'Tech Freelancer Dortmund'],
en: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'SaaS Development Dortmund', 'Fullstack Developer Dortmund', 'Automation Dortmund', 'Tech Freelancer Dortmund'],
sr: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'SaaS Razvoj Dortmund'],
},
nearbyAreas: ['Essen', 'Bochum', 'Hagen', 'Münster', 'Köln'],
},
{
slug: 'leipzig',
name: {
de: 'Leipzig',
en: 'Leipzig',
sr: 'Lajpcig',
},
region: 'Sachsen',
description: {
de: 'Leipzig ist das aufstrebende Tech-Zentrum Ostdeutschlands. Ich unterstütze Leipziger Startups und etablierte Unternehmen mit KI-Entwicklung, Voice AI und modernen Automatisierungslösungen.',
en: 'Leipzig is the emerging tech center of Eastern Germany. I support Leipzig startups and established companies with AI development, Voice AI and modern automation solutions.',
sr: 'Lajpcig je rastući tech centar istočne Nemačke. Podržavam startape i etablirane kompanije iz Lajpciga sa AI razvojem, Voice AI i modernim rešenjima za automatizaciju.',
},
keywords: {
de: ['KI Entwickler Leipzig', 'Startup Entwickler Leipzig', 'Voice AI Leipzig', 'Fullstack Entwickler Leipzig', 'Automatisierung Leipzig', 'n8n Entwickler Leipzig'],
en: ['AI Developer Leipzig', 'Startup Developer Leipzig', 'Voice AI Leipzig', 'Fullstack Developer Leipzig', 'Automation Leipzig', 'n8n Developer Leipzig'],
sr: ['AI Developer Lajpcig', 'Startup Developer Lajpcig', 'Voice AI Lajpcig'],
},
nearbyAreas: ['Dresden', 'Halle', 'Berlin', 'Chemnitz', 'Magdeburg'],
},
// DACH region - Austria
{
slug: 'wien',
name: {
de: 'Wien',
en: 'Vienna',
sr: 'Beč',
},
region: 'Österreich',
description: {
de: 'Wien als Hauptstadt Österreichs ist ein bedeutendes Wirtschafts- und Technologiezentrum in der DACH-Region. Ich biete KI-Entwicklung und Automatisierungslösungen für Wiener Unternehmen.',
en: 'Vienna as the capital of Austria is a major economic and technology center in the DACH region. I offer AI development and automation solutions for Vienna-based companies.',
sr: 'Beč kao glavni grad Austrije je značajan ekonomski i tehnološki centar u DACH regionu. Nudim razvoj AI i rešenja za automatizaciju za bečke kompanije.',
},
keywords: {
de: ['KI Entwickler Wien', 'AI Agentur Wien', 'Fullstack Entwickler Wien', 'Automatisierung Wien', 'n8n Entwickler Österreich', 'Voice AI Wien'],
en: ['AI Developer Vienna', 'AI Agency Vienna', 'Fullstack Developer Vienna', 'Automation Vienna', 'n8n Developer Austria'],
sr: ['AI Developer Beč', 'Fullstack Developer Austrija'],
},
nearbyAreas: ['Graz', 'Linz', 'Salzburg', 'Bratislava'],
},
{
slug: 'graz',
name: {
de: 'Graz',
en: 'Graz',
sr: 'Grac',
},
region: 'Österreich',
description: {
de: 'Graz ist die zweitgrößte Stadt Österreichs und ein wichtiger Standort für Technologie und Innovation. Ich unterstütze Grazer Unternehmen mit KI-Lösungen und moderner Prozessautomatisierung.',
en: 'Graz is Austria\'s second-largest city and an important location for technology and innovation. I support Graz-based companies with AI solutions and modern process automation.',
sr: 'Grac je drugi najveći grad Austrije i važna lokacija za tehnologiju i inovacije. Podržavam kompanije iz Graca sa AI rešenjima i modernom automatizacijom procesa.',
},
keywords: {
de: ['KI Entwickler Graz', 'Fullstack Entwickler Graz', 'Automatisierung Graz', 'SaaS Entwicklung Graz', 'Tech Freelancer Graz'],
en: ['AI Developer Graz', 'Fullstack Developer Graz', 'Automation Graz', 'SaaS Development Graz'],
sr: ['AI Developer Grac', 'Fullstack Developer Grac'],
},
nearbyAreas: ['Wien', 'Klagenfurt', 'Maribor', 'Ljubljana'],
},
// DACH region - Switzerland
{
slug: 'zuerich',
name: {
de: 'Zürich',
en: 'Zurich',
sr: 'Cirih',
},
region: 'Schweiz',
description: {
de: 'Zürich ist das Finanz- und Wirtschaftszentrum der Schweiz. Ich biete KI-Entwicklung und Automatisierungslösungen für Schweizer Unternehmen mit höchsten Qualitätsstandards.',
en: 'Zurich is the financial and economic center of Switzerland. I offer AI development and automation solutions for Swiss companies with the highest quality standards.',
sr: 'Cirih je finansijski i ekonomski centar Švajcarske. Nudim razvoj AI i rešenja za automatizaciju za švajcarske kompanije sa najvišim standardima kvaliteta.',
},
keywords: {
de: ['KI Entwickler Zürich', 'AI Agentur Zürich', 'Fullstack Entwickler Schweiz', 'Automatisierung Zürich', 'Fintech Entwickler Zürich', 'SaaS Entwicklung Schweiz'],
en: ['AI Developer Zurich', 'AI Agency Zurich', 'Fullstack Developer Switzerland', 'Automation Zurich', 'Fintech Developer Zurich'],
sr: ['AI Developer Cirih', 'Fullstack Developer Švajcarska'],
},
nearbyAreas: ['Basel', 'Bern', 'Luzern', 'Winterthur'],
},
{
slug: 'basel',
name: {
de: 'Basel',
en: 'Basel',
sr: 'Bazel',
},
region: 'Schweiz',
description: {
de: 'Basel ist ein wichtiger Pharma- und Life-Sciences-Standort in der Schweiz. Ich unterstütze Basler Unternehmen mit KI-Lösungen für Industrie 4.0 und Prozessautomatisierung.',
en: 'Basel is an important pharmaceutical and life sciences location in Switzerland. I support Basel-based companies with AI solutions for Industry 4.0 and process automation.',
sr: 'Bazel je važna farmaceutska i life sciences lokacija u Švajcarskoj. Podržavam kompanije iz Bazela sa AI rešenjima za Industriju 4.0 i automatizaciju procesa.',
},
keywords: {
de: ['KI Entwickler Basel', 'Pharma IT Basel', 'Fullstack Entwickler Basel', 'Automatisierung Basel', 'Life Sciences IT Basel'],
en: ['AI Developer Basel', 'Pharma IT Basel', 'Fullstack Developer Basel', 'Automation Basel', 'Life Sciences IT Basel'],
sr: ['AI Developer Bazel', 'Fullstack Developer Bazel'],
},
nearbyAreas: ['Zürich', 'Bern', 'Freiburg', 'Strasbourg'],
},
// UK cities for international market
{
slug: 'london',
name: {
de: 'London',
en: 'London',
sr: 'London',
},
region: 'United Kingdom',
description: {
de: 'London ist eines der weltweit führenden Finanzzentren und Tech-Hubs. Ich biete Remote AI-Entwicklung und Automatisierungslösungen für Londoner Unternehmen mit deutscher Ingenieursqualität.',
en: 'London is one of the world\'s leading financial centers and tech hubs. I offer remote AI development and automation solutions for London-based companies with German engineering quality.',
sr: 'London je jedan od vodećih svetskih finansijskih centara i tech habova. Nudim remote AI razvoj i rešenja za automatizaciju za londonske kompanije sa nemačkim inženjerskim kvalitetom.',
},
keywords: {
de: ['KI Entwickler London', 'Remote Developer UK', 'AI Freelancer London', 'Automatisierung London'],
en: ['AI Developer London', 'Remote Developer UK', 'AI Freelancer London', 'Process Automation London', 'n8n Developer London', 'Voice AI London', 'Python Developer London', 'React Developer UK'],
sr: ['AI Developer London', 'Remote Programer UK'],
},
nearbyAreas: ['Manchester', 'Birmingham', 'Cambridge', 'Oxford'],
},
// US cities for international market
{
slug: 'new-york',
name: {
de: 'New York',
en: 'New York',
sr: 'Njujork',
},
region: 'United States',
description: {
de: 'New York City ist das wirtschaftliche Herz der USA. Ich unterstütze New Yorker Startups und Unternehmen remote mit KI-Agenten, Voice AI und Prozessautomatisierung.',
en: 'New York City is the economic heart of the USA. I support NYC startups and enterprises remotely with AI agents, Voice AI, and process automation solutions.',
sr: 'Njujork je ekonomsko srce SAD-a. Podržavam njujorške startape i preduzeća na daljinu sa AI agentima, Voice AI i rešenjima za automatizaciju procesa.',
},
keywords: {
de: ['KI Entwickler New York', 'Remote Developer USA', 'AI Freelancer NYC', 'Automatisierung New York'],
en: ['AI Developer New York', 'Remote Developer NYC', 'AI Freelancer New York', 'Process Automation NYC', 'n8n Developer New York', 'Voice AI NYC', 'Python Developer New York', 'Fullstack Developer NYC'],
sr: ['AI Developer Njujork', 'Remote Programer SAD'],
},
nearbyAreas: ['Brooklyn', 'Manhattan', 'New Jersey', 'Connecticut'],
},
{
slug: 'los-angeles',
name: {
de: 'Los Angeles',
en: 'Los Angeles',
sr: 'Los Anđeles',
},
region: 'United States',
description: {
de: 'Los Angeles ist ein führendes Zentrum für Technologie, Entertainment und Innovation. Ich biete Remote KI-Entwicklung und Automatisierungslösungen für LA-basierte Unternehmen.',
en: 'Los Angeles is a leading center for technology, entertainment, and innovation. I offer remote AI development and automation solutions for LA-based companies.',
sr: 'Los Anđeles je vodeći centar za tehnologiju, zabavu i inovacije. Nudim remote AI razvoj i rešenja za automatizaciju za kompanije iz LA.',
},
keywords: {
de: ['KI Entwickler Los Angeles', 'Remote Developer Kalifornien', 'AI Freelancer LA', 'Tech Entwickler LA'],
en: ['AI Developer Los Angeles', 'Remote Developer California', 'AI Freelancer LA', 'Process Automation Los Angeles', 'Tech Developer LA', 'Python Developer Los Angeles', 'SaaS Developer California'],
sr: ['AI Developer Los Anđeles', 'Remote Programer Kalifornija'],
},
nearbyAreas: ['San Francisco', 'San Diego', 'Silicon Valley', 'Orange County'],
},
{
slug: 'miami',
name: {
de: 'Miami',
en: 'Miami',
sr: 'Majami',
},
region: 'United States',
description: {
de: 'Miami entwickelt sich zum aufstrebenden Tech-Hub der USA mit Fokus auf Fintech und Krypto. Ich unterstütze Miami-basierte Unternehmen mit KI-Lösungen und moderner Prozessautomatisierung.',
en: 'Miami is emerging as a rising tech hub in the USA with focus on fintech and crypto. I support Miami-based companies with AI solutions and modern process automation.',
sr: 'Majami se razvija kao rastući tech hab u SAD sa fokusom na fintech i kripto. Podržavam kompanije iz Majamija sa AI rešenjima i modernom automatizacijom procesa.',
},
keywords: {
de: ['KI Entwickler Miami', 'Remote Developer Florida', 'AI Freelancer Miami', 'Fintech Entwickler Miami'],
en: ['AI Developer Miami', 'Remote Developer Florida', 'AI Freelancer Miami', 'Process Automation Miami', 'Fintech Developer Miami', 'Python Developer Miami', 'Startup Developer Miami'],
sr: ['AI Developer Majami', 'Remote Programer Florida'],
},
nearbyAreas: ['Fort Lauderdale', 'Tampa', 'Orlando', 'West Palm Beach'],
},
{
slug: 'san-francisco',
name: {
de: 'San Francisco',
en: 'San Francisco',
sr: 'San Francisko',
},
region: 'United States',
description: {
de: 'San Francisco und das Silicon Valley sind das globale Zentrum für Technologie und Innovation. Ich biete Remote-Entwicklungsdienstleistungen für SF-basierte Startups und Tech-Unternehmen.',
en: 'San Francisco and Silicon Valley are the global center for technology and innovation. I offer remote development services for SF-based startups and tech companies.',
sr: 'San Francisko i Silikonska dolina su globalni centar za tehnologiju i inovacije. Nudim remote razvojne usluge za startape i tech kompanije iz SF.',
},
keywords: {
de: ['KI Entwickler San Francisco', 'Remote Developer Silicon Valley', 'AI Freelancer SF', 'Startup Entwickler Bay Area'],
en: ['AI Developer San Francisco', 'Remote Developer Silicon Valley', 'AI Freelancer SF', 'Process Automation San Francisco', 'Startup Developer Bay Area', 'Python Developer SF', 'SaaS Developer Silicon Valley'],
sr: ['AI Developer San Francisko', 'Remote Programer Silikonska Dolina'],
},
nearbyAreas: ['Silicon Valley', 'Palo Alto', 'Oakland', 'San Jose'],
},
{
slug: 'chicago',
name: {
de: 'Chicago',
en: 'Chicago',
sr: 'Čikago',
},
region: 'United States',
description: {
de: 'Chicago ist ein bedeutendes Wirtschaftszentrum im Mittleren Westen der USA. Ich unterstütze Chicagoer Unternehmen remote mit KI-Entwicklung und Automatisierungslösungen.',
en: 'Chicago is a major economic center in the Midwest USA. I support Chicago-based companies remotely with AI development and automation solutions.',
sr: 'Čikago je značajan ekonomski centar na Srednjem zapadu SAD-a. Podržavam kompanije iz Čikaga na daljinu sa AI razvojem i rešenjima za automatizaciju.',
},
keywords: {
de: ['KI Entwickler Chicago', 'Remote Developer Midwest', 'AI Freelancer Chicago', 'Automatisierung Chicago'],
en: ['AI Developer Chicago', 'Remote Developer Midwest', 'AI Freelancer Chicago', 'Process Automation Chicago', 'Python Developer Chicago', 'Fullstack Developer Chicago'],
sr: ['AI Developer Čikago', 'Remote Programer Midwest'],
},
nearbyAreas: ['Milwaukee', 'Indianapolis', 'Detroit', 'Minneapolis'],
},
// Serbian cities for local market targeting
{
slug: 'beograd',
name: {
de: 'Belgrad',
en: 'Belgrade',
sr: 'Beograd',
},
region: 'Srbija',
description: {
de: 'Belgrad ist das wirtschaftliche und technologische Zentrum Serbiens. Ich biete KI-Entwicklung und Automatisierungslösungen für serbische Unternehmen mit deutscher Präzision und Qualität.',
en: 'Belgrade is the economic and technological center of Serbia. I offer AI development and automation solutions for Serbian companies with German precision and quality.',
sr: 'Beograd je ekonomski i tehnološki centar Srbije. Nudim razvoj AI i rešenja za automatizaciju za srpske kompanije sa nemačkom preciznošću i kvalitetom.',
},
keywords: {
de: ['KI Entwickler Belgrad', 'AI Entwicklung Serbien', 'Fullstack Entwickler Belgrad', 'Automatisierung Serbien'],
en: ['AI Developer Belgrade', 'AI Development Serbia', 'Fullstack Developer Belgrade', 'Automation Serbia'],
sr: ['AI Developer Beograd', 'Softverski Razvoj Beograd', 'Fullstack Programer Beograd', 'Automatizacija Procesa Srbija', 'n8n Automatizacija Beograd', 'Voice AI Beograd'],
},
nearbyAreas: ['Novi Sad', 'Niš', 'Kragujevac', 'Subotica'],
},
{
slug: 'novi-sad',
name: {
de: 'Novi Sad',
en: 'Novi Sad',
sr: 'Novi Sad',
},
region: 'Vojvodina',
description: {
de: 'Novi Sad ist das aufstrebende IT-Zentrum Serbiens und Europäische Kulturhauptstadt 2022. Ich unterstütze die wachsende Tech-Szene mit KI-Lösungen und moderner Prozessautomatisierung.',
en: 'Novi Sad is Serbia\'s emerging IT center and European Capital of Culture 2022. I support the growing tech scene with AI solutions and modern process automation.',
sr: 'Novi Sad je rastući IT centar Srbije i Evropska prestonica kulture 2022. Podržavam rastuću tech scenu sa AI rešenjima i modernom automatizacijom procesa.',
},
keywords: {
de: ['KI Entwickler Novi Sad', 'IT Freelancer Serbien', 'Fullstack Entwickler Novi Sad', 'Automatisierung Vojvodina'],
en: ['AI Developer Novi Sad', 'IT Freelancer Serbia', 'Fullstack Developer Novi Sad', 'Automation Vojvodina'],
sr: ['AI Developer Novi Sad', 'Softverski Razvoj Novi Sad', 'Fullstack Programer Novi Sad', 'IT Freelancer Novi Sad', 'Web Razvoj Novi Sad', 'Startup Developer Novi Sad'],
},
nearbyAreas: ['Beograd', 'Subotica', 'Zrenjanin', 'Sombor'],
},
{
slug: 'nis',
name: {
de: 'Niš',
en: 'Niš',
sr: 'Niš',
},
region: 'Srbija',
description: {
de: 'Niš ist die drittgrößte Stadt Serbiens und ein wachsendes IT-Zentrum im Süden des Landes. Ich biete AI-Entwicklung und Automatisierungslösungen für Unternehmen aus Niš und Südserbien.',
en: 'Niš is Serbia\'s third-largest city and a growing IT center in the south of the country. I offer AI development and automation solutions for companies from Niš and southern Serbia.',
sr: 'Niš je treći najveći grad Srbije i rastući IT centar na jugu zemlje. Nudim AI razvoj i rešenja za automatizaciju za kompanije iz Niša i južne Srbije.',
},
keywords: {
de: ['KI Entwickler Niš', 'IT Freelancer Südserbien', 'Fullstack Entwickler Niš', 'Automatisierung Niš'],
en: ['AI Developer Niš', 'IT Freelancer South Serbia', 'Fullstack Developer Niš', 'Automation Niš'],
sr: ['AI Developer Niš', 'Softverski Razvoj Niš', 'Fullstack Programer Niš', 'IT Freelancer Niš', 'Web Razvoj Niš', 'Python Programer Niš'],
},
nearbyAreas: ['Beograd', 'Kragujevac', 'Leskovac', 'Vranje'],
},
{
slug: 'kragujevac',
name: {
de: 'Kragujevac',
en: 'Kragujevac',
sr: 'Kragujevac',
},
region: 'Srbija',
description: {
de: 'Kragujevac ist das industrielle Zentrum Zentralserbiens mit starker Automobilindustrie. Ich unterstütze Unternehmen aus Kragujevac mit modernen AI- und Automatisierungslösungen.',
en: 'Kragujevac is the industrial center of Central Serbia with a strong automotive industry. I support companies from Kragujevac with modern AI and automation solutions.',
sr: 'Kragujevac je industrijski centar centralne Srbije sa jakom automobilskom industrijom. Podržavam kompanije iz Kragujevca sa modernim AI i rešenjima za automatizaciju.',
},
keywords: {
de: ['KI Entwickler Kragujevac', 'IT Freelancer Zentralserbien', 'Fullstack Entwickler Kragujevac', 'Automatisierung Kragujevac'],
en: ['AI Developer Kragujevac', 'IT Freelancer Central Serbia', 'Fullstack Developer Kragujevac', 'Automation Kragujevac'],
sr: ['AI Developer Kragujevac', 'Softverski Razvoj Kragujevac', 'Fullstack Programer Kragujevac', 'IT Freelancer Kragujevac', 'Web Razvoj Kragujevac', 'Automatizacija Procesa Kragujevac'],
},
nearbyAreas: ['Beograd', 'Niš', 'Čačak', 'Kraljevo'],
},
{
slug: 'subotica',
name: {
de: 'Subotica',
en: 'Subotica',
sr: 'Subotica',
},
region: 'Vojvodina',
description: {
de: 'Subotica liegt im Norden Serbiens nahe der ungarischen Grenze und ist bekannt für seine multikulturelle Geschichte. Ich biete AI-Entwicklung und Automatisierung für die wachsende Tech-Szene.',
en: 'Subotica is located in northern Serbia near the Hungarian border and is known for its multicultural history. I offer AI development and automation for the growing tech scene.',
sr: 'Subotica se nalazi na severu Srbije blizu mađarske granice i poznata je po svojoj multikulturalnoj istoriji. Nudim AI razvoj i automatizaciju za rastuću tech scenu.',
},
keywords: {
de: ['KI Entwickler Subotica', 'IT Freelancer Nordserbien', 'Fullstack Entwickler Subotica', 'Automatisierung Subotica'],
en: ['AI Developer Subotica', 'IT Freelancer North Serbia', 'Fullstack Developer Subotica', 'Automation Subotica'],
sr: ['AI Developer Subotica', 'Softverski Razvoj Subotica', 'Fullstack Programer Subotica', 'IT Freelancer Subotica', 'Web Razvoj Subotica', 'Remote Programer Subotica'],
},
nearbyAreas: ['Novi Sad', 'Sombor', 'Szeged', 'Beograd'],
},
];
export function getCityBySlug(slug: string): CityData | undefined {
return cities.find((city) => city.slug === slug);
}
export function getAllCitySlugs(): string[] {
return cities.map((city) => city.slug);
}
+781
View File
@@ -0,0 +1,781 @@
export interface ServiceData {
slug: string;
icon: string;
name: {
de: string;
en: string;
sr: string;
};
shortDescription: {
de: string;
en: string;
sr: string;
};
description: {
de: string;
en: string;
sr: string;
};
features: {
de: string[];
en: string[];
sr: string[];
};
technologies: string[];
useCases: {
de: string[];
en: string[];
sr: string[];
};
}
export const services: ServiceData[] = [
{
slug: 'ki-entwicklung',
icon: 'Brain',
name: {
de: 'KI-Entwicklung',
en: 'AI Development',
sr: 'AI Razvoj',
},
shortDescription: {
de: 'Maßgeschneiderte KI-Agenten und LLM-Integrationen für Ihr Unternehmen',
en: 'Custom AI agents and LLM integrations for your business',
sr: 'Prilagođeni AI agenti i LLM integracije za vaš posao',
},
description: {
de: 'Ich entwickle intelligente KI-Agenten, die Ihre Geschäftsprozesse automatisieren und optimieren. Von Chatbots über Dokumentenverarbeitung bis hin zu autonomen Agenten - ich setze modernste LLM-Technologie für Ihre Anforderungen ein.',
en: 'I develop intelligent AI agents that automate and optimize your business processes. From chatbots to document processing to autonomous agents - I use cutting-edge LLM technology for your requirements.',
sr: 'Razvijam inteligentne AI agente koji automatizuju i optimizuju vaše poslovne procese. Od chatbotova preko obrade dokumenata do autonomnih agenata - koristim najsavremeniju LLM tehnologiju za vaše potrebe.',
},
features: {
de: [
'Custom GPT & Claude Integrationen',
'Autonome KI-Agenten für komplexe Aufgaben',
'RAG-Systeme für Wissensdatenbanken',
'Multi-Agent-Systeme',
'Fine-Tuning von Sprachmodellen',
],
en: [
'Custom GPT & Claude integrations',
'Autonomous AI agents for complex tasks',
'RAG systems for knowledge bases',
'Multi-agent systems',
'Fine-tuning of language models',
],
sr: [
'Custom GPT & Claude integracije',
'Autonomni AI agenti za kompleksne zadatke',
'RAG sistemi za baze znanja',
'Multi-agent sistemi',
'Fine-tuning jezičkih modela',
],
},
technologies: ['OpenAI API', 'Claude API', 'LangChain', 'LlamaIndex', 'Python', 'FastAPI'],
useCases: {
de: [
'Kundenservice-Automatisierung',
'Dokumentenanalyse und -extraktion',
'Code-Review-Assistenten',
'Wissensmanagementsysteme',
],
en: [
'Customer service automation',
'Document analysis and extraction',
'Code review assistants',
'Knowledge management systems',
],
sr: [
'Automatizacija korisničke službe',
'Analiza i ekstrakcija dokumenata',
'Asistenti za pregled koda',
'Sistemi za upravljanje znanjem',
],
},
},
{
slug: 'voice-ai',
icon: 'Mic',
name: {
de: 'Voice AI',
en: 'Voice AI',
sr: 'Voice AI',
},
shortDescription: {
de: 'Intelligente Sprachassistenten und Voice-Bots für Telefon und Web',
en: 'Intelligent voice assistants and voice bots for phone and web',
sr: 'Inteligentni glasovni asistenti i voice botovi za telefon i web',
},
description: {
de: 'Voice AI revolutioniert die Kundenkommunikation. Ich entwickle natürlich klingende Sprachassistenten, die Anrufe entgegennehmen, Termine buchen und komplexe Gespräche führen können - 24/7 und in mehreren Sprachen.',
en: 'Voice AI is revolutionizing customer communication. I develop natural-sounding voice assistants that can answer calls, book appointments and conduct complex conversations - 24/7 and in multiple languages.',
sr: 'Voice AI revolucionira komunikaciju sa kupcima. Razvijam prirodno zvučeće glasovne asistente koji mogu primati pozive, rezervisati termine i voditi kompleksne razgovore - 24/7 i na više jezika.',
},
features: {
de: [
'Telefon-Bots für Inbound & Outbound',
'Natürliche Sprachverarbeitung (NLP)',
'Mehrsprachige Unterstützung',
'Integration mit CRM & Kalendern',
'Echtzeit-Transkription',
],
en: [
'Phone bots for inbound & outbound',
'Natural Language Processing (NLP)',
'Multi-language support',
'Integration with CRM & calendars',
'Real-time transcription',
],
sr: [
'Telefonski botovi za dolazne i odlazne pozive',
'Obrada prirodnog jezika (NLP)',
'Višejezična podrška',
'Integracija sa CRM i kalendarima',
'Transkripcija u realnom vremenu',
],
},
technologies: ['Vapi', 'Twilio', 'ElevenLabs', 'Whisper', 'Python', 'WebRTC'],
useCases: {
de: [
'Automatisierte Terminbuchung',
'Kundenservice-Hotlines',
'Lead-Qualifizierung',
'Umfragen und Feedback',
],
en: [
'Automated appointment booking',
'Customer service hotlines',
'Lead qualification',
'Surveys and feedback',
],
sr: [
'Automatsko zakazivanje termina',
'Hotline za korisničku službu',
'Kvalifikacija lead-ova',
'Ankete i feedback',
],
},
},
{
slug: 'prozessautomatisierung',
icon: 'Workflow',
name: {
de: 'Prozessautomatisierung',
en: 'Process Automation',
sr: 'Automatizacija Procesa',
},
shortDescription: {
de: 'Workflow-Automatisierung mit n8n, Make und Python für maximale Effizienz',
en: 'Workflow automation with n8n, Make and Python for maximum efficiency',
sr: 'Automatizacija radnih tokova sa n8n, Make i Python za maksimalnu efikasnost',
},
description: {
de: 'Ich automatisiere repetitive Aufgaben und verbinde Ihre Systeme miteinander. Von einfachen Workflows bis zu komplexen Multi-System-Integrationen - ich spare Ihnen Zeit und reduziere Fehler durch intelligente Automatisierung.',
en: 'I automate repetitive tasks and connect your systems together. From simple workflows to complex multi-system integrations - I save you time and reduce errors through intelligent automation.',
sr: 'Automatizujem repetitivne zadatke i povezujem vaše sisteme. Od jednostavnih radnih tokova do kompleksnih multi-sistem integracija - štedim vam vreme i smanjujem greške kroz inteligentnu automatizaciju.',
},
features: {
de: [
'n8n & Make Workflows',
'API-Integrationen',
'Web Scraping & Data Mining',
'E-Mail-Automatisierung',
'Datenbank-Synchronisation',
],
en: [
'n8n & Make workflows',
'API integrations',
'Web scraping & data mining',
'Email automation',
'Database synchronization',
],
sr: [
'n8n & Make radni tokovi',
'API integracije',
'Web scraping & data mining',
'Automatizacija e-pošte',
'Sinhronizacija baza podataka',
],
},
technologies: ['n8n', 'Make', 'Zapier', 'Python', 'Playwright', 'Puppeteer'],
useCases: {
de: [
'Lead-Erfassung und CRM-Updates',
'Rechnungsverarbeitung',
'Social Media Posting',
'Bestandsmanagement',
],
en: [
'Lead capture and CRM updates',
'Invoice processing',
'Social media posting',
'Inventory management',
],
sr: [
'Prikupljanje lead-ova i ažuriranje CRM-a',
'Obrada faktura',
'Objavljivanje na društvenim mrežama',
'Upravljanje zalihama',
],
},
},
{
slug: 'saas-entwicklung',
icon: 'Cloud',
name: {
de: 'SaaS-Entwicklung',
en: 'SaaS Development',
sr: 'SaaS Razvoj',
},
shortDescription: {
de: 'Von der Idee zum skalierbaren SaaS-Produkt mit Next.js und React',
en: 'From idea to scalable SaaS product with Next.js and React',
sr: 'Od ideje do skalabilnog SaaS proizvoda sa Next.js i React',
},
description: {
de: 'Ich entwickle moderne SaaS-Anwendungen von Grund auf. Von der ersten MVP-Version bis zur skalierbaren Plattform - ich begleite Sie durch den gesamten Entwicklungsprozess mit bewährten Technologien und Best Practices.',
en: 'I develop modern SaaS applications from scratch. From the first MVP version to the scalable platform - I accompany you through the entire development process with proven technologies and best practices.',
sr: 'Razvijam moderne SaaS aplikacije od nule. Od prve MVP verzije do skalabilne platforme - pratim vas kroz ceo proces razvoja sa proverenim tehnologijama i najboljim praksama.',
},
features: {
de: [
'MVP-Entwicklung in 4-8 Wochen',
'Skalierbare Architektur',
'Multi-Tenant Systeme',
'Subscription & Billing',
'Admin Dashboards',
],
en: [
'MVP development in 4-8 weeks',
'Scalable architecture',
'Multi-tenant systems',
'Subscription & billing',
'Admin dashboards',
],
sr: [
'MVP razvoj u 4-8 nedelja',
'Skalabilna arhitektura',
'Multi-tenant sistemi',
'Pretplata i naplata',
'Admin kontrolne table',
],
},
technologies: ['Next.js', 'React', 'TypeScript', 'PostgreSQL', 'Prisma', 'Stripe', 'Vercel'],
useCases: {
de: [
'Interne Tools & Dashboards',
'Kunden-Portale',
'Marketplace-Plattformen',
'Analytics-Lösungen',
],
en: [
'Internal tools & dashboards',
'Customer portals',
'Marketplace platforms',
'Analytics solutions',
],
sr: [
'Interni alati i kontrolne table',
'Portali za kupce',
'Marketplace platforme',
'Analytics rešenja',
],
},
},
{
slug: 'fullstack-entwicklung',
icon: 'Code',
name: {
de: 'Fullstack-Entwicklung',
en: 'Fullstack Development',
sr: 'Fullstack Razvoj',
},
shortDescription: {
de: 'Moderne Web-Anwendungen mit React, Next.js, Python und Node.js',
en: 'Modern web applications with React, Next.js, Python and Node.js',
sr: 'Moderne web aplikacije sa React, Next.js, Python i Node.js',
},
description: {
de: 'Als Fullstack-Entwickler decke ich die gesamte Bandbreite moderner Webentwicklung ab. Frontend, Backend, Datenbank, DevOps - ich liefere komplette Lösungen aus einer Hand.',
en: 'As a fullstack developer, I cover the entire spectrum of modern web development. Frontend, backend, database, DevOps - I deliver complete solutions from a single source.',
sr: 'Kao fullstack developer, pokrivam ceo spektar modernog web razvoja. Frontend, backend, baza podataka, DevOps - isporučujem kompletna rešenja iz jednog izvora.',
},
features: {
de: [
'React & Next.js Frontend',
'Python & Node.js Backend',
'REST & GraphQL APIs',
'PostgreSQL & MongoDB',
'Docker & CI/CD',
],
en: [
'React & Next.js frontend',
'Python & Node.js backend',
'REST & GraphQL APIs',
'PostgreSQL & MongoDB',
'Docker & CI/CD',
],
sr: [
'React & Next.js frontend',
'Python & Node.js backend',
'REST & GraphQL API-ji',
'PostgreSQL & MongoDB',
'Docker & CI/CD',
],
},
technologies: ['React', 'Next.js', 'TypeScript', 'Python', 'FastAPI', 'Node.js', 'PostgreSQL', 'Docker'],
useCases: {
de: [
'Web-Anwendungen',
'E-Commerce Plattformen',
'Unternehmensportale',
'Custom CMS',
],
en: [
'Web applications',
'E-commerce platforms',
'Enterprise portals',
'Custom CMS',
],
sr: [
'Web aplikacije',
'E-commerce platforme',
'Poslovni portali',
'Custom CMS',
],
},
},
];
// Technology-specific services
export const techServices: ServiceData[] = [
{
slug: 'n8n-entwicklung',
icon: 'Workflow',
name: {
de: 'n8n Entwicklung',
en: 'n8n Development',
sr: 'n8n Razvoj',
},
shortDescription: {
de: 'Professionelle n8n Workflows und Automatisierungen für Ihr Unternehmen',
en: 'Professional n8n workflows and automations for your business',
sr: 'Profesionalni n8n radni tokovi i automatizacije za vaš posao',
},
description: {
de: 'Als zertifizierter n8n-Experte entwickle ich maßgeschneiderte Workflows, die Ihre Geschäftsprozesse revolutionieren. Von der Migration bestehender Zapier/Make-Workflows bis zu komplexen Multi-System-Integrationen - n8n bietet unbegrenzte Möglichkeiten bei voller Datenkontrolle.',
en: 'As a certified n8n expert, I develop customized workflows that revolutionize your business processes. From migrating existing Zapier/Make workflows to complex multi-system integrations - n8n offers unlimited possibilities with full data control.',
sr: 'Kao sertifikovani n8n ekspert, razvijam prilagođene radne tokove koji revolucioniraju vaše poslovne procese. Od migracije postojećih Zapier/Make radnih tokova do kompleksnih multi-sistem integracija - n8n nudi neograničene mogućnosti uz punu kontrolu podataka.',
},
features: {
de: [
'Custom n8n Workflow-Entwicklung',
'Migration von Zapier/Make zu n8n',
'Self-Hosted n8n Setup & Hosting',
'API-Integrationen & Webhooks',
'KI-Integration (OpenAI, Claude) in n8n',
'Schulung & Support',
],
en: [
'Custom n8n workflow development',
'Migration from Zapier/Make to n8n',
'Self-hosted n8n setup & hosting',
'API integrations & webhooks',
'AI integration (OpenAI, Claude) in n8n',
'Training & support',
],
sr: [
'Custom n8n razvoj radnih tokova',
'Migracija sa Zapier/Make na n8n',
'Self-hosted n8n setup & hosting',
'API integracije & webhooks',
'AI integracija (OpenAI, Claude) u n8n',
'Obuka & podrška',
],
},
technologies: ['n8n', 'Docker', 'PostgreSQL', 'REST APIs', 'Webhooks', 'OpenAI', 'Python'],
useCases: {
de: [
'CRM-Automatisierung (HubSpot, Salesforce)',
'E-Commerce Workflows (Shopify, WooCommerce)',
'Slack/Teams Benachrichtigungen',
'Daten-Synchronisation zwischen Systemen',
'Lead-Enrichment & Scoring',
],
en: [
'CRM automation (HubSpot, Salesforce)',
'E-commerce workflows (Shopify, WooCommerce)',
'Slack/Teams notifications',
'Data synchronization between systems',
'Lead enrichment & scoring',
],
sr: [
'CRM automatizacija (HubSpot, Salesforce)',
'E-commerce radni tokovi (Shopify, WooCommerce)',
'Slack/Teams obaveštenja',
'Sinhronizacija podataka između sistema',
'Lead enrichment & scoring',
],
},
},
{
slug: 'gpt-integration',
icon: 'Brain',
name: {
de: 'GPT & LLM Integration',
en: 'GPT & LLM Integration',
sr: 'GPT & LLM Integracija',
},
shortDescription: {
de: 'ChatGPT, Claude und andere LLMs nahtlos in Ihre Systeme integrieren',
en: 'Seamlessly integrate ChatGPT, Claude and other LLMs into your systems',
sr: 'Besprekorno integrisanje ChatGPT, Claude i drugih LLM-ova u vaše sisteme',
},
description: {
de: 'Ich integriere modernste Sprachmodelle wie GPT-4, Claude und Gemini in Ihre bestehenden Systeme. Von einfachen Chatbots bis zu komplexen RAG-Systemen - ich bringe die Power der KI in Ihr Unternehmen, DSGVO-konform und sicher.',
en: 'I integrate state-of-the-art language models like GPT-4, Claude and Gemini into your existing systems. From simple chatbots to complex RAG systems - I bring the power of AI to your business, GDPR-compliant and secure.',
sr: 'Integrisem najsavremenije jezičke modele kao što su GPT-4, Claude i Gemini u vaše postojeće sisteme. Od jednostavnih chatbotova do kompleksnih RAG sistema - donosim snagu AI u vaš posao, GDPR-usklađeno i sigurno.',
},
features: {
de: [
'OpenAI API Integration (GPT-4, GPT-4o)',
'Anthropic Claude Integration',
'RAG-Systeme mit Vektor-Datenbanken',
'Fine-Tuning für Ihre Daten',
'DSGVO-konforme Implementierung',
'Multi-LLM Orchestrierung',
],
en: [
'OpenAI API integration (GPT-4, GPT-4o)',
'Anthropic Claude integration',
'RAG systems with vector databases',
'Fine-tuning for your data',
'GDPR-compliant implementation',
'Multi-LLM orchestration',
],
sr: [
'OpenAI API integracija (GPT-4, GPT-4o)',
'Anthropic Claude integracija',
'RAG sistemi sa vektorskim bazama',
'Fine-tuning za vaše podatke',
'GDPR-usklađena implementacija',
'Multi-LLM orkestracija',
],
},
technologies: ['OpenAI API', 'Claude API', 'LangChain', 'Pinecone', 'Weaviate', 'Python', 'FastAPI'],
useCases: {
de: [
'Intelligente Kundensupport-Chatbots',
'Automatische E-Mail-Beantwortung',
'Dokumenten-Analyse & Zusammenfassung',
'Code-Generierung & Review',
'Wissensmanagement-Systeme',
],
en: [
'Intelligent customer support chatbots',
'Automatic email response',
'Document analysis & summarization',
'Code generation & review',
'Knowledge management systems',
],
sr: [
'Inteligentni chatbotovi za korisničku podršku',
'Automatski odgovor na e-poštu',
'Analiza i sumarizacija dokumenata',
'Generisanje i pregled koda',
'Sistemi za upravljanje znanjem',
],
},
},
{
slug: 'ki-agenten',
icon: 'Bot',
name: {
de: 'KI-Agenten',
en: 'AI Agents',
sr: 'AI Agenti',
},
shortDescription: {
de: 'Autonome KI-Agenten, die komplexe Aufgaben selbstständig erledigen',
en: 'Autonomous AI agents that complete complex tasks independently',
sr: 'Autonomni AI agenti koji samostalno obavljaju kompleksne zadatke',
},
description: {
de: 'Die Zukunft der Automatisierung sind KI-Agenten - autonome Systeme, die eigenständig Entscheidungen treffen und Aufgaben ausführen. Ich entwickle maßgeschneiderte Agenten, die Ihre Workflows revolutionieren und menschliche Produktivität potenzieren.',
en: 'The future of automation is AI agents - autonomous systems that make independent decisions and execute tasks. I develop custom agents that revolutionize your workflows and multiply human productivity.',
sr: 'Budućnost automatizacije su AI agenti - autonomni sistemi koji samostalno donose odluke i izvršavaju zadatke. Razvijam prilagođene agente koji revolucionišu vaše radne tokove i umnožavaju ljudsku produktivnost.',
},
features: {
de: [
'Autonome Task-Ausführung',
'Tool-Use & API-Aufrufe',
'Multi-Agent-Systeme',
'Reasoning & Planung',
'Memory & Kontext-Management',
'Menschliche Aufsicht & Kontrolle',
],
en: [
'Autonomous task execution',
'Tool use & API calls',
'Multi-agent systems',
'Reasoning & planning',
'Memory & context management',
'Human oversight & control',
],
sr: [
'Autonomno izvršavanje zadataka',
'Korišćenje alata & API pozivi',
'Multi-agent sistemi',
'Rasuđivanje & planiranje',
'Memorija & upravljanje kontekstom',
'Ljudski nadzor & kontrola',
],
},
technologies: ['LangChain', 'AutoGen', 'CrewAI', 'OpenAI', 'Claude', 'Python', 'n8n'],
useCases: {
de: [
'Research-Agenten für Marktanalyse',
'Sales-Agenten für Lead-Qualifizierung',
'Support-Agenten für Ticketing',
'DevOps-Agenten für Monitoring',
'Content-Agenten für Marketing',
],
en: [
'Research agents for market analysis',
'Sales agents for lead qualification',
'Support agents for ticketing',
'DevOps agents for monitoring',
'Content agents for marketing',
],
sr: [
'Research agenti za analizu tržišta',
'Sales agenti za kvalifikaciju lead-ova',
'Support agenti za ticketing',
'DevOps agenti za monitoring',
'Content agenti za marketing',
],
},
},
];
// Industry-specific services
export const industryServices: ServiceData[] = [
{
slug: 'ki-mittelstand',
icon: 'Building',
name: {
de: 'KI für den Mittelstand',
en: 'AI for SMEs',
sr: 'AI za Srednja Preduzeća',
},
shortDescription: {
de: 'Bezahlbare KI-Lösungen speziell für kleine und mittlere Unternehmen',
en: 'Affordable AI solutions specifically for small and medium enterprises',
sr: 'Pristupačna AI rešenja posebno za mala i srednja preduzeća',
},
description: {
de: 'Der deutsche Mittelstand braucht keine Enterprise-Lösungen - sondern pragmatische, bezahlbare KI-Tools. Ich entwickle maßgeschneiderte Automatisierungen, die sich schnell rechnen und ohne großes IT-Team betrieben werden können.',
en: 'German SMEs don\'t need enterprise solutions - but pragmatic, affordable AI tools. I develop customized automations that pay for themselves quickly and can be operated without a large IT team.',
sr: 'Nemačka srednja preduzeća ne trebaju enterprise rešenja - već pragmatične, pristupačne AI alate. Razvijam prilagođene automatizacije koje se brzo isplate i mogu se upravljati bez velikog IT tima.',
},
features: {
de: [
'DSGVO-konforme Lösungen',
'Schneller ROI (< 6 Monate)',
'Einfache Bedienung ohne IT-Kenntnisse',
'Deutsche Cloud-Anbieter',
'Persönliche Betreuung & Schulung',
'Transparente Festpreise',
],
en: [
'GDPR-compliant solutions',
'Fast ROI (< 6 months)',
'Easy operation without IT knowledge',
'German cloud providers',
'Personal support & training',
'Transparent fixed prices',
],
sr: [
'GDPR-usklađena rešenja',
'Brz ROI (< 6 meseci)',
'Lako upravljanje bez IT znanja',
'Nemački cloud provajderi',
'Lična podrška & obuka',
'Transparentne fiksne cene',
],
},
technologies: ['n8n', 'Make', 'OpenAI', 'Supabase', 'Next.js', 'Python'],
useCases: {
de: [
'Automatische Angebotserstellung',
'Kundenkommunikation mit KI',
'Rechnungsverarbeitung',
'Terminbuchung per Telefon-Bot',
'Dokumenten-Digitalisierung',
],
en: [
'Automatic quote generation',
'Customer communication with AI',
'Invoice processing',
'Appointment booking via phone bot',
'Document digitization',
],
sr: [
'Automatsko generisanje ponuda',
'Komunikacija sa kupcima sa AI',
'Obrada faktura',
'Zakazivanje termina putem telefonskog bota',
'Digitalizacija dokumenata',
],
},
},
{
slug: 'ki-ecommerce',
icon: 'ShoppingCart',
name: {
de: 'KI für E-Commerce',
en: 'AI for E-Commerce',
sr: 'AI za E-Commerce',
},
shortDescription: {
de: 'Automatisierung und KI-Tools für Online-Shops und Marktplätze',
en: 'Automation and AI tools for online shops and marketplaces',
sr: 'Automatizacija i AI alati za online prodavnice i marketplace',
},
description: {
de: 'E-Commerce lebt von Effizienz und Kundenerlebnis. Ich entwickle KI-gestützte Lösungen für Produktbeschreibungen, Kundenservice, Bestandsmanagement und Marketing-Automatisierung - für Shopify, WooCommerce und mehr.',
en: 'E-commerce thrives on efficiency and customer experience. I develop AI-powered solutions for product descriptions, customer service, inventory management and marketing automation - for Shopify, WooCommerce and more.',
sr: 'E-commerce živi od efikasnosti i korisničkog iskustva. Razvijam AI-powered rešenja za opise proizvoda, korisničku službu, upravljanje zalihama i automatizaciju marketinga - za Shopify, WooCommerce i više.',
},
features: {
de: [
'KI-Produktbeschreibungen',
'Chatbots für Kundenservice',
'Automatische Preisanpassung',
'Bestandsvorhersage mit ML',
'Personalisierte Empfehlungen',
'Bewertungs-Management',
],
en: [
'AI product descriptions',
'Customer service chatbots',
'Automatic price adjustment',
'Inventory forecasting with ML',
'Personalized recommendations',
'Review management',
],
sr: [
'AI opisi proizvoda',
'Chatbotovi za korisničku službu',
'Automatsko prilagođavanje cena',
'Predviđanje zaliha sa ML',
'Personalizovane preporuke',
'Upravljanje recenzijama',
],
},
technologies: ['Shopify', 'WooCommerce', 'OpenAI', 'n8n', 'Python', 'PostgreSQL'],
useCases: {
de: [
'Automatische SEO-Texte für Produkte',
'Multi-Channel-Bestandssync',
'24/7 Kundenservice-Bot',
'Retargeting-Automatisierung',
'Dynamische Rabattaktionen',
],
en: [
'Automatic SEO texts for products',
'Multi-channel inventory sync',
'24/7 customer service bot',
'Retargeting automation',
'Dynamic discount campaigns',
],
sr: [
'Automatski SEO tekstovi za proizvode',
'Multi-channel sinhronizacija zaliha',
'24/7 korisnička služba bot',
'Automatizacija retargetinga',
'Dinamičke popust kampanje',
],
},
},
{
slug: 'ki-industrie',
icon: 'Factory',
name: {
de: 'KI für Industrie 4.0',
en: 'AI for Industry 4.0',
sr: 'AI za Industriju 4.0',
},
shortDescription: {
de: 'IoT, RFID und KI-Lösungen für die produzierende Industrie',
en: 'IoT, RFID and AI solutions for the manufacturing industry',
sr: 'IoT, RFID i AI rešenja za proizvodnu industriju',
},
description: {
de: 'Die Industrie 4.0 erfordert intelligente Vernetzung von Maschinen, Sensoren und Software. Ich entwickle IoT-Lösungen mit RFID, Predictive Maintenance und KI-gestützter Qualitätskontrolle für produzierende Unternehmen.',
en: 'Industry 4.0 requires intelligent networking of machines, sensors and software. I develop IoT solutions with RFID, predictive maintenance and AI-powered quality control for manufacturing companies.',
sr: 'Industrija 4.0 zahteva inteligentno umrežavanje mašina, senzora i softvera. Razvijam IoT rešenja sa RFID, prediktivnim održavanjem i AI-powered kontrolom kvaliteta za proizvodne kompanije.',
},
features: {
de: [
'RFID Asset-Tracking',
'Predictive Maintenance',
'KI-Qualitätskontrolle',
'Echzeit-Monitoring',
'Datenintegration (ERP, MES)',
'Digital Twin Konzepte',
],
en: [
'RFID asset tracking',
'Predictive maintenance',
'AI quality control',
'Real-time monitoring',
'Data integration (ERP, MES)',
'Digital twin concepts',
],
sr: [
'RFID praćenje sredstava',
'Prediktivno održavanje',
'AI kontrola kvaliteta',
'Praćenje u realnom vremenu',
'Integracija podataka (ERP, MES)',
'Digital twin koncepti',
],
},
technologies: ['Python', 'RFID', 'IoT Sensors', 'PostgreSQL', 'Grafana', 'Docker', 'MQTT'],
useCases: {
de: [
'Lagerverwaltung mit RFID',
'Maschinenüberwachung',
'Qualitätsprüfung mit Computer Vision',
'Produktionsdaten-Analyse',
'Automatische Bestellvorschläge',
],
en: [
'Warehouse management with RFID',
'Machine monitoring',
'Quality inspection with computer vision',
'Production data analysis',
'Automatic order suggestions',
],
sr: [
'Upravljanje skladištem sa RFID',
'Nadzor mašina',
'Kontrola kvaliteta sa computer vision',
'Analiza proizvodnih podataka',
'Automatski predlozi narudžbi',
],
},
},
];
// Combine all services for easy access
export const allServices: ServiceData[] = [...services, ...techServices, ...industryServices];
export function getServiceBySlug(slug: string): ServiceData | undefined {
return allServices.find((service) => service.slug === slug);
}
export function getAllServiceSlugs(): string[] {
return allServices.map((service) => service.slug);
}
+93
View File
@@ -14,3 +14,96 @@ export const localeFlags: Record<Locale, string> = {
en: '🇬🇧',
sr: '🇷🇸',
};
// Extended locale metadata for SEO
export const localeMetadata: Record<Locale, {
language: string;
region: string;
hreflang: string;
ogLocale: string;
script: string;
territory: string;
currency: string;
}> = {
de: {
language: 'de',
region: 'DE',
hreflang: 'de-DE',
ogLocale: 'de_DE',
script: 'Latn',
territory: 'Germany',
currency: 'EUR',
},
en: {
language: 'en',
region: 'US',
hreflang: 'en-US',
ogLocale: 'en_US',
script: 'Latn',
territory: 'United States',
currency: 'USD',
},
sr: {
language: 'sr',
region: 'RS',
hreflang: 'sr-RS',
ogLocale: 'sr_RS',
script: 'Latn', // Using Latin script for web compatibility (Cyrillic: Cyrl)
territory: 'Serbia',
currency: 'RSD',
},
};
// SEO keywords per locale
export const seoKeywords: Record<Locale, string[]> = {
de: [
'Fullstack Developer',
'KI Entwickler',
'AI Agentenentwicklung',
'Prozessautomatisierung',
'Voice AI',
'n8n Automatisierung',
'Köln',
'Deutschland',
],
en: [
'Fullstack Developer',
'AI Developer',
'AI Agent Development',
'Process Automation Expert',
'Voice AI Developer',
'n8n Automation',
'Remote Developer Europe',
'Remote Developer USA',
'Remote Developer UK',
'Python Developer',
'React Developer',
'SaaS Development',
'Freelance AI Developer',
'London',
'New York',
'Los Angeles',
'Miami',
'San Francisco',
],
sr: [
'Fullstack Programer',
'AI Programer Srbija',
'AI Developer Beograd',
'Razvoj AI Agenata',
'Automatizacija Procesa',
'Voice AI Srbija',
'n8n Automatizacija',
'Beograd',
'Novi Sad',
'Niš',
'Kragujevac',
'Subotica',
'Srbija',
'Softverski Razvoj Srbija',
'Python Programer Srbija',
'React Programer Beograd',
'Remote Developer Dijaspora',
'Freelance Programer Srbija',
],
};
+164
View File
@@ -0,0 +1,164 @@
import fs from 'fs';
import path from 'path';
export interface BlogPost {
slug: string;
title: string;
date: string;
excerpt: string;
coverImage: string;
category?: string;
tags?: string[];
content: string;
}
const BLOG_POSTS_DIR = path.join(process.cwd(), 'blog-posts');
// Category mapping based on filename patterns
const categoryMap: Record<string, string> = {
'agentic': 'KI-Agenten',
'ai-agent': 'KI-Agenten',
'deepseek': 'KI-Entwicklung',
'openai': 'KI-Entwicklung',
'gpt': 'KI-Entwicklung',
'claude': 'KI-Entwicklung',
'langchain': 'KI-Entwicklung',
'langgraph': 'KI-Entwicklung',
'rag': 'KI-Entwicklung',
'llm': 'KI-Entwicklung',
'voice': 'Voice AI',
'vapi': 'Voice AI',
'speech': 'Voice AI',
'elevenlabs': 'Voice AI',
'whisper': 'Voice AI',
'n8n': 'Automatisierung',
'make': 'Automatisierung',
'zapier': 'Automatisierung',
'automation': 'Automatisierung',
'workflow': 'Automatisierung',
'scraping': 'Web Scraping',
'playwright': 'Web Scraping',
'puppeteer': 'Web Scraping',
'nextjs': 'Web Development',
'react': 'Web Development',
'typescript': 'Web Development',
'api': 'Backend',
'database': 'Backend',
'postgres': 'Backend',
'prisma': 'Backend',
'supabase': 'Backend',
'saas': 'SaaS',
'stripe': 'SaaS',
'subscription': 'SaaS',
'seo': 'SEO',
'performance': 'Performance',
'iot': 'IoT',
'raspberry': 'IoT',
'mqtt': 'IoT',
'home-assistant': 'Smart Home',
};
function detectCategory(filename: string, content: string): string {
const lowerFilename = filename.toLowerCase();
const lowerContent = content.toLowerCase().slice(0, 500);
for (const [keyword, category] of Object.entries(categoryMap)) {
if (lowerFilename.includes(keyword) || lowerContent.includes(keyword)) {
return category;
}
}
return 'Technologie';
}
function parseMarkdownPost(filename: string, content: string): BlogPost | null {
try {
// Extract title from first H1
const titleMatch = content.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : filename.replace(/\.md$/, '');
// Extract meta description
const metaMatch = content.match(/\*\*Meta-Description:\*\*\s*(.+)/i);
const metaDescription = metaMatch ? metaMatch[1].trim() : '';
// Extract keywords
const keywordsMatch = content.match(/\*\*Keywords?:\*\*\s*(.+)/i);
const keywordsStr = keywordsMatch ? keywordsMatch[1].trim() : '';
const tags = keywordsStr ? keywordsStr.split(',').map(k => k.trim()).slice(0, 6) : [];
// Extract excerpt from introduction or meta description
let excerpt = metaDescription;
if (!excerpt) {
const introMatch = content.match(/## Einführung\s*\n\n(.+?)(?:\n\n|$)/s);
if (introMatch) {
excerpt = introMatch[1].trim().slice(0, 200) + '...';
}
}
// Generate slug from filename (remove number prefix)
const slug = filename
.replace(/\.md$/, '')
.replace(/^\d+-/, '');
// Extract date from filename number (approximate)
const numMatch = filename.match(/^(\d+)-/);
const postNum = numMatch ? parseInt(numMatch[1], 10) : 1;
// Generate dates starting from 2026-01-19 going backwards
const baseDate = new Date('2026-01-19');
baseDate.setDate(baseDate.getDate() - (postNum - 1));
const date = baseDate.toISOString().split('T')[0];
// Detect category
const category = detectCategory(filename, content);
// Cover image path
const coverImage = `/images/posts/${slug}/cover.jpg`;
return {
slug,
title,
date,
excerpt: excerpt || title,
coverImage,
category,
tags,
content,
};
} catch (error) {
console.error(`Error parsing ${filename}:`, error);
return null;
}
}
export function getAllBlogPosts(): BlogPost[] {
if (!fs.existsSync(BLOG_POSTS_DIR)) {
console.warn('Blog posts directory not found:', BLOG_POSTS_DIR);
return [];
}
const files = fs.readdirSync(BLOG_POSTS_DIR)
.filter(file => file.endsWith('.md'))
.sort();
const posts: BlogPost[] = [];
for (const file of files) {
const filePath = path.join(BLOG_POSTS_DIR, file);
const content = fs.readFileSync(filePath, 'utf-8');
const post = parseMarkdownPost(file, content);
if (post) {
posts.push(post);
}
}
// Sort by date descending (newest first)
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
export function getBlogPostBySlug(slug: string): BlogPost | null {
const posts = getAllBlogPosts();
return posts.find(post => post.slug === slug) || null;
}
export function getBlogPostSlugs(): string[] {
return getAllBlogPosts().map(post => post.slug);
}
+311
View File
@@ -0,0 +1,311 @@
import React from 'react';
function parseInlineMarkdown(text: string): React.ReactNode {
const parts: React.ReactNode[] = [];
let remaining = text;
let keyIndex = 0;
while (remaining.length > 0) {
// Inline code
const codeMatch = remaining.match(/^`([^`]+)`/);
if (codeMatch) {
parts.push(
<code key={keyIndex++} className="bg-zinc-800 px-1.5 py-0.5 rounded text-[#697565] text-sm font-mono">
{codeMatch[1]}
</code>
);
remaining = remaining.slice(codeMatch[0].length);
continue;
}
// Bold
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
if (boldMatch) {
parts.push(<strong key={keyIndex++} className="font-semibold text-white">{boldMatch[1]}</strong>);
remaining = remaining.slice(boldMatch[0].length);
continue;
}
// Italic
const italicMatch = remaining.match(/^\*([^*]+)\*/);
if (italicMatch) {
parts.push(<em key={keyIndex++} className="italic">{italicMatch[1]}</em>);
remaining = remaining.slice(italicMatch[0].length);
continue;
}
// Links
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
if (linkMatch) {
parts.push(
<a key={keyIndex++} href={linkMatch[2]} className="text-[#697565] hover:text-[#8a9a7e] underline" target="_blank" rel="noopener noreferrer">
{linkMatch[1]}
</a>
);
remaining = remaining.slice(linkMatch[0].length);
continue;
}
// Regular character
const nextSpecial = remaining.search(/[`*\[]/);
if (nextSpecial === -1) {
parts.push(remaining);
break;
} else if (nextSpecial === 0) {
parts.push(remaining[0]);
remaining = remaining.slice(1);
} else {
parts.push(remaining.slice(0, nextSpecial));
remaining = remaining.slice(nextSpecial);
}
}
return parts.length === 1 ? parts[0] : <>{parts}</>;
}
export function renderMarkdown(content: string): React.ReactNode {
const lines = content.split('\n');
const elements: React.ReactNode[] = [];
let i = 0;
let listItems: { type: 'ul' | 'ol'; items: React.ReactNode[] } | null = null;
let codeBlock: { language: string; lines: string[] } | null = null;
let tableRows: string[][] | null = null;
const flushList = () => {
if (listItems) {
if (listItems.type === 'ul') {
elements.push(
<ul key={`ul-${elements.length}`} className="list-disc list-inside space-y-2 my-4 text-zinc-300">
{listItems.items}
</ul>
);
} else {
elements.push(
<ol key={`ol-${elements.length}`} className="list-decimal list-inside space-y-2 my-4 text-zinc-300">
{listItems.items}
</ol>
);
}
listItems = null;
}
};
const flushTable = () => {
if (tableRows && tableRows.length > 0) {
const header = tableRows[0];
const body = tableRows.slice(2); // Skip header separator row
elements.push(
<div key={`table-${elements.length}`} className="overflow-x-auto my-6">
<table className="min-w-full border-collapse">
<thead>
<tr className="border-b border-zinc-700">
{header.map((cell, idx) => (
<th key={idx} className="px-4 py-2 text-left text-zinc-200 font-semibold">
{cell.trim()}
</th>
))}
</tr>
</thead>
<tbody>
{body.map((row, rowIdx) => (
<tr key={rowIdx} className="border-b border-zinc-800">
{row.map((cell, cellIdx) => (
<td key={cellIdx} className="px-4 py-2 text-zinc-300">
{cell.trim()}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
tableRows = null;
}
};
while (i < lines.length) {
const line = lines[i];
// Code block start/end
if (line.startsWith('```')) {
if (codeBlock) {
// End code block
elements.push(
<pre key={`code-${elements.length}`} className="bg-zinc-900 border border-zinc-800 rounded-lg p-4 overflow-x-auto my-6">
<code className="text-sm font-mono text-zinc-300">
{codeBlock.lines.join('\n')}
</code>
</pre>
);
codeBlock = null;
} else {
// Start code block
flushList();
flushTable();
const language = line.slice(3).trim();
codeBlock = { language, lines: [] };
}
i++;
continue;
}
// Inside code block
if (codeBlock) {
codeBlock.lines.push(line);
i++;
continue;
}
// Table row
if (line.startsWith('|') && line.endsWith('|')) {
flushList();
const cells = line.slice(1, -1).split('|');
if (!tableRows) {
tableRows = [];
}
tableRows.push(cells);
i++;
continue;
} else if (tableRows) {
flushTable();
}
// Headers
if (line.startsWith('#### ')) {
flushList();
elements.push(
<h4 key={`h4-${elements.length}`} className="text-lg font-semibold text-white mt-5 mb-2">
{parseInlineMarkdown(line.slice(5))}
</h4>
);
i++;
continue;
}
if (line.startsWith('### ')) {
flushList();
elements.push(
<h3 key={`h3-${elements.length}`} className="text-xl font-semibold text-white mt-6 mb-3">
{parseInlineMarkdown(line.slice(4))}
</h3>
);
i++;
continue;
}
if (line.startsWith('## ')) {
flushList();
elements.push(
<h2 key={`h2-${elements.length}`} className="text-2xl font-bold text-white mt-8 mb-4">
{parseInlineMarkdown(line.slice(3))}
</h2>
);
i++;
continue;
}
if (line.startsWith('# ')) {
flushList();
elements.push(
<h1 key={`h1-${elements.length}`} className="text-3xl font-bold text-white mt-8 mb-4">
{parseInlineMarkdown(line.slice(2))}
</h1>
);
i++;
continue;
}
// Unordered list items
if (line.match(/^[-*]\s/)) {
if (!listItems || listItems.type !== 'ul') {
flushList();
listItems = { type: 'ul', items: [] };
}
listItems.items.push(
<li key={`li-${listItems.items.length}`}>
{parseInlineMarkdown(line.replace(/^[-*]\s/, ''))}
</li>
);
i++;
continue;
}
// Ordered list items
if (line.match(/^\d+\.\s/)) {
if (!listItems || listItems.type !== 'ol') {
flushList();
listItems = { type: 'ol', items: [] };
}
listItems.items.push(
<li key={`li-${listItems.items.length}`}>
{parseInlineMarkdown(line.replace(/^\d+\.\s/, ''))}
</li>
);
i++;
continue;
}
// Checkbox list items
if (line.match(/^[-*]\s\[[ x]\]/i)) {
if (!listItems || listItems.type !== 'ul') {
flushList();
listItems = { type: 'ul', items: [] };
}
const checked = line.match(/^[-*]\s\[x\]/i);
const itemContent = line.replace(/^[-*]\s\[[ x]\]\s?/i, '');
listItems.items.push(
<li key={`li-${listItems.items.length}`} className="flex items-center gap-2">
<span className={checked ? 'text-green-500' : 'text-zinc-500'}>
{checked ? '✓' : '○'}
</span>
{parseInlineMarkdown(itemContent)}
</li>
);
i++;
continue;
}
// Horizontal rule
if (line.match(/^[-*_]{3,}$/)) {
flushList();
elements.push(<hr key={`hr-${elements.length}`} className="border-zinc-700 my-8" />);
i++;
continue;
}
// Blockquote
if (line.startsWith('> ')) {
flushList();
elements.push(
<blockquote key={`quote-${elements.length}`} className="border-l-4 border-[#697565] pl-4 my-4 text-zinc-400 italic">
{parseInlineMarkdown(line.slice(2))}
</blockquote>
);
i++;
continue;
}
// Empty line
if (!line.trim()) {
flushList();
i++;
continue;
}
// Regular paragraph
flushList();
elements.push(
<p key={`p-${elements.length}`} className="text-zinc-300 mb-4 leading-relaxed">
{parseInlineMarkdown(line)}
</p>
);
i++;
}
// Flush any remaining items
flushList();
flushTable();
return <div className="prose prose-invert prose-zinc max-w-none">{elements}</div>;
}
+214 -71
View File
@@ -1,15 +1,15 @@
{
"meta": {
"site": {
"name": "Damjan Savic",
"title": "Damjan Savic | Fullstack-, IoT- & KI-Entwickler",
"subtitle": "Skalierbare Softwareloesungen mit IoT- und KI-Integration",
"description": "Als Fullstack-Entwickler aus Koeln spezialisiert sich Damjan Savic auf die Entwicklung skalierbarer Softwareloesungen mit IoT- und KI-Integration."
"name": "Damjan Savić",
"title": "Damjan Savić | Fullstack-, IoT- & KI-Entwickler",
"subtitle": "Skalierbare Softwarelösungen mit IoT- und KI-Integration",
"description": "Als Fullstack-Entwickler aus Köln spezialisiert sich Damjan Savić auf die Entwicklung skalierbarer Softwarelösungen mit IoT- und KI-Integration."
},
"author": {
"name": "Damjan Savic",
"name": "Damjan Savić",
"role": "Senior Fullstack Developer & Digital Solutions Consultant",
"location": "Koeln, Deutschland",
"location": "Köln, Deutschland",
"email": "info@damjan-savic.com"
},
"social": {
@@ -19,11 +19,11 @@
}
},
"common": {
"siteTitle": "Damjan Savic - Digital Solutions & JTL Integration",
"siteDescription": "Entwickler individueller Loesungen zur Automatisierung von Prozessen",
"siteTitle": "Damjan Savić - Digital Solutions & JTL Integration",
"siteDescription": "Entwickler individueller Lösungen zur Automatisierung von Prozessen",
"nav": {
"home": "Start",
"about": "Ueber mich",
"about": "Über mich",
"services": "Leistungen",
"portfolio": "Portfolio",
"blog": "Blog",
@@ -33,32 +33,33 @@
"readMore": "Weiterlesen",
"viewMore": "Mehr anzeigen",
"viewAll": "Alle anzeigen",
"back": "Zurueck",
"close": "Schliessen",
"back": "Zurück",
"close": "Schließen",
"submit": "Absenden",
"loading": "Laedt..."
"loading": "Lädt..."
},
"errors": {
"generic": "Ein Fehler ist aufgetreten",
"notFound": "Seite nicht gefunden",
"loading": "Fehler beim Laden",
"return": "Zurueck zur Startseite"
"return": "Zurück zur Startseite"
},
"cookies": {
"message": "Diese Website verwendet Cookies, um Ihr Browsing-Erlebnis zu verbessern. Die Daten werden nicht an Dritte weitergegeben.",
"accept": "Cookies akzeptieren",
"decline": "Ablehnen",
"necessary": "Notwendige Cookies",
"necessaryDesc": "Fuer grundlegende Funktionen der Website",
"necessaryDesc": "Für grundlegende Funktionen der Website",
"analytics": "Analyse-Cookies",
"analyticsDesc": "Fuer Analysen zur Verbesserung der Website",
"analyticsDesc": "Für Analysen zur Verbesserung der Website",
"learnMore": "Mehr erfahren"
}
},
"navigation": {
"main": {
"home": "Startseite",
"about": "Ueber mich",
"about": "Über mich",
"services": "Leistungen",
"portfolio": "Portfolio",
"blog": "Blog",
"contact": "Kontakt"
@@ -76,9 +77,9 @@
}
},
"mobile": {
"menu": "Menue",
"close": "Schliessen",
"open": "Menue oeffnen"
"menu": "Menü",
"close": "Schließen",
"open": "Menü öffnen"
}
},
"footer": {
@@ -86,14 +87,14 @@
"contact": {
"title": "Kontakt",
"email": "info@damjan-savic.com",
"location": "Koeln, Deutschland"
"location": "Köln, Deutschland"
},
"navigation": {
"title": "Navigation",
"links": {
"portfolio": "Portfolio",
"blog": "Blog",
"about": "Ueber mich",
"about": "Über mich",
"contact": "Kontakt"
}
},
@@ -113,22 +114,26 @@
"pages": {
"home": {
"hero": {
"title": "AI & AUTOMATION SPECIALIST",
"title": "FULLSTACK DEVELOPER",
"name": "DAMJAN SAVIC",
"subtitle": "KI-AGENTEN | VOICE AI | PROZESSAUTOMATISIERUNG",
"description": "Entwicklung von KI-Agenten und Automatisierungsloesungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.",
"description": "Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.",
"currentRole": "Aktuell @ Everlast Consulting GmbH",
"cta": "KONTAKT AUFNEHMEN",
"navigation": {
"experience": "ERFAHRUNG",
"skills": "FAEHIGKEITEN",
"skills": "FÄHIGKEITEN",
"projects": "PROJEKTE",
"about": "UEBER MICH"
"about": "ÜBER MICH"
}
},
"gallery": {
"title": "Einblicke",
"description": "Professionelle Momente aus meinem Arbeitsalltag"
},
"about": {
"title": "Ueber Damjan Savic - Senior Fullstack Entwickler & Digital Solutions Consultant",
"content": "Damjan Savic ist AI & Automation Specialist mit Fokus auf Voice AI, autonome Agenten und Prozessautomatisierung. Mit einem M.A. in Software Development und ueber 7 Jahren Erfahrung in der Entwicklung digitaler Loesungen verbindet er technische Expertise mit strategischem Geschaeftsverstaendnis.",
"title": "Über Damjan Savić - Senior Fullstack Entwickler & Digital Solutions Consultant",
"content": "Damjan Savić ist AI & Automation Specialist mit Fokus auf Voice AI, autonome Agenten und Prozessautomatisierung. Mit einem M.A. in Software Development und über 7 Jahren Erfahrung in der Entwicklung digitaler Lösungen verbindet er technische Expertise mit strategischem Geschäftsverständnis.",
"buttons": {
"learnMore": "Mehr erfahren",
"downloadCV": "CV herunterladen"
@@ -143,7 +148,7 @@
"period": "12/2024 - HEUTE",
"highlights": [
"Entwicklung von KI-Agenten mit n8n und Zapier",
"Aufbau von Web-Scraping-Loesungen",
"Aufbau von Web-Scraping-Lösungen",
"Migration von Power Automate zu n8n"
]
},
@@ -154,7 +159,7 @@
"highlights": [
"Backend-Services auf dedizierten Servern",
"KI-Integration via Power Automate",
"RFID/IoT-Loesungen mit Zebra-Hardware"
"RFID/IoT-Lösungen mit Zebra-Hardware"
]
},
{
@@ -164,14 +169,13 @@
"highlights": [
"ApparelMagic & TradeByte Integration",
"MariaDB Middleware-Entwicklung",
"Windows Server Administration",
"Shopify-Optimierung"
]
}
]
},
"skills": {
"title": "Faehigkeiten",
"title": "Fähigkeiten",
"skills": [
{
"name": "AI & LLMs",
@@ -212,27 +216,158 @@
},
"about": {
"seo": {
"title": "Ueber Mich - Damjan Savic | Fullstack Entwickler & KI-Spezialist",
"description": "Erfahren Sie mehr ueber Damjan Savic - AI & Automation Specialist mit Fokus auf Voice AI, autonome Agenten und Prozessautomatisierung aus Koeln."
"title": "Über Mich - Damjan Savić | Fullstack Entwickler & KI-Spezialist",
"description": "Erfahren Sie mehr über Damjan Savić - AI & Automation Specialist mit Fokus auf Voice AI, autonome Agenten und Prozessautomatisierung aus Köln."
},
"hero": {
"aboutMe": "FULLSTACK ENTWICKLER KOELN - DAMJAN SAVIC",
"title": "Python Entwickler Koeln & React Developer - Damjan Savic",
"description": "Als Fullstack Entwickler Koeln ist Damjan Savic Ihr Freelance Developer fuer Python Automatisierung und React Entwicklung."
"aboutMe": "Über mich",
"title": "FULLSTACK ENTWICKLER KÖLN",
"description": "Als AI & Automation Specialist mit über 7 Jahren Erfahrung entwickle ich skalierbare Lösungen für komplexe Geschäftsprozesse. Mein Fokus liegt auf Voice AI, autonomen Agenten und Prozessautomatisierung.",
"expertise": {
"processAutomation": {
"title": "Prozessautomatisierung",
"description": "Automatisierung komplexer Geschäftsprozesse mit n8n, Zapier und Python"
},
"systemIntegration": {
"title": "System-Integration",
"description": "Nahtlose Integration von ERP, CRM und E-Commerce Systemen"
},
"customDevelopment": {
"title": "Individuelle Entwicklung",
"description": "Maßgeschneiderte Softwarelösungen mit Python, TypeScript und React"
}
},
"languages": {
"python": "Python",
"typescript": "TypeScript",
"react": "React",
"nextjs": "Next.js",
"nodejs": "Node.js",
"sql": "SQL"
}
},
"skills": {
"title": "Fähigkeiten & Expertise",
"description": "Meine technischen Fähigkeiten und Kompetenzen, die ich in über 7 Jahren Berufserfahrung erworben habe.",
"skillGroups": [
{
"category": "Backend & Automation",
"items": [
{ "name": "Python", "level": 90 },
{ "name": "n8n / Zapier", "level": 85 },
{ "name": "Node.js", "level": 80 },
{ "name": "FastAPI", "level": 85 }
]
},
{
"category": "Frontend",
"items": [
{ "name": "React", "level": 85 },
{ "name": "TypeScript", "level": 85 },
{ "name": "Next.js", "level": 80 },
{ "name": "Tailwind CSS", "level": 90 }
]
},
{
"category": "Datenbanken & Cloud",
"items": [
{ "name": "PostgreSQL", "level": 85 },
{ "name": "Supabase", "level": 80 },
{ "name": "Docker", "level": 75 },
{ "name": "Azure", "level": 70 }
]
}
]
},
"workflow": {
"title": "Mein Arbeitsablauf",
"description": "Strukturierter Prozess von der Anforderungsanalyse bis zur erfolgreichen Implementierung.",
"steps": [
{ "number": "01", "title": "Anforderungsanalyse & Beratung" },
{ "number": "02", "title": "Technische Konzeption & Architektur" },
{ "number": "03", "title": "Agile Entwicklung & Testing" },
{ "number": "04", "title": "Deployment & Integration" },
{ "number": "05", "title": "Monitoring & Optimierung" },
{ "number": "06", "title": "Support & Wartung" }
]
},
"journey": {
"title": "Mein Werdegang",
"subtitle": "Ausbildung, Sprachen und berufliche Erfahrung",
"education": {
"title": "Ausbildung",
"degrees": {
"masters": {
"degree": "M.A. Software Development",
"university": "FOM Hochschule für Ökonomie & Management",
"period": "2021 - 2023"
},
"bachelors": {
"degree": "B.Sc. Wirtschaftsinformatik",
"university": "FOM Hochschule für Ökonomie & Management",
"period": "2017 - 2021"
}
}
},
"languages": {
"title": "Sprachen",
"items": {
"german": { "name": "Deutsch", "level": "Muttersprache" },
"english": { "name": "Englisch", "level": "Fließend (C1)" },
"serbian": { "name": "Serbisch", "level": "Muttersprache" }
}
},
"work": {
"positions": [
{
"title": "Process Automation Specialist",
"period": "12/2024 - Heute",
"company": "Everlast Consulting GmbH",
"location": "Köln",
"highlights": [
"Entwicklung von KI-Agenten mit n8n und Zapier",
"Aufbau von Web-Scraping-Lösungen",
"Migration von Power Automate zu n8n"
]
},
{
"title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024",
"company": "Ritter Digital GmbH",
"location": "Köln",
"highlights": [
"Backend-Services auf dedizierten Servern",
"KI-Integration via Power Automate",
"RFID/IoT-Lösungen mit Zebra-Hardware"
]
},
{
"title": "ERP-Integrationsspezialist",
"period": "01/2023 - 08/2023",
"company": "Joyce & Girls",
"location": "Köln",
"highlights": [
"ApparelMagic & TradeByte Integration",
"MariaDB Middleware-Entwicklung",
"Shopify-Optimierung"
]
}
]
}
}
},
"contact": {
"breadcrumb": "KONTAKT",
"hero": {
"title": "Lassen Sie uns zusammenarbeiten",
"subtitle": "Sie haben ein Projekt im Sinn? Lassen Sie uns besprechen, wie wir Ihnen bei der Erreichung Ihrer Ziele helfen koennen."
"subtitle": "Sie haben ein Projekt im Sinn? Lassen Sie uns besprechen, wie wir Ihnen bei der Erreichung Ihrer Ziele helfen können."
},
"contactInfo": {
"title": "Kontaktinformationen",
"description": "Sie koennen mich ueber folgende Wege erreichen",
"description": "Sie können mich über folgende Wege erreichen",
"address": {
"label": "Buerostandort",
"value": "Rotdornallee, Koeln"
"label": "Bürostandort",
"value": "Rotdornallee, Köln"
},
"phone": {
"label": "Telefon",
@@ -241,14 +376,15 @@
"email": {
"label": "E-Mail",
"value": "info@damjan-savic.com"
}
},
"availabilityNote": "Ich antworte in der Regel innerhalb von 24 Stunden."
},
"contactForm": {
"title": "Kontaktieren Sie mich",
"description": "Haben Sie Fragen oder moechten Sie zusammenarbeiten? Schreiben Sie mir eine Nachricht.",
"description": "Haben Sie Fragen oder möchten Sie zusammenarbeiten? Schreiben Sie mir eine Nachricht.",
"name": {
"label": "Name",
"placeholder": "Ihr vollstaendiger Name"
"placeholder": "Ihr vollständiger Name"
},
"email": {
"label": "E-Mail",
@@ -259,44 +395,44 @@
"placeholder": "Ihre Nachricht an mich..."
},
"submit": "Nachricht senden",
"successMessage": "Vielen Dank fuer Ihre Nachricht! Ich werde mich so schnell wie moeglich bei Ihnen melden.",
"successMessage": "Vielen Dank für Ihre Nachricht! Ich werde mich so schnell wie möglich bei Ihnen melden.",
"errorMessage": "Entschuldigung, beim Senden Ihrer Nachricht ist ein Fehler aufgetreten."
}
},
"privacy": {
"seo": {
"title": "Datenschutzerklaerung",
"description": "Datenschutzerklaerung fuer die Portfolio-Website von Damjan Savic"
"title": "Datenschutzerklärung",
"description": "Datenschutzerklärung für die Portfolio-Website von Damjan Savić"
},
"content": {
"title": "Datenschutzerklaerung",
"lastUpdated": "Zuletzt aktualisiert: 10. Maerz 2024"
"title": "Datenschutzerklärung",
"lastUpdated": "Zuletzt aktualisiert: 10. März 2024"
}
},
"terms": {
"seo": {
"title": "Allgemeine Geschaeftsbedingungen",
"description": "Allgemeine Geschaeftsbedingungen fuer die Portfolio-Website von Damjan Savic"
"title": "Allgemeine Geschäftsbedingungen",
"description": "Allgemeine Geschäftsbedingungen für die Portfolio-Website von Damjan Savić"
},
"content": {
"title": "Allgemeine Geschaeftsbedingungen",
"lastUpdated": "Zuletzt aktualisiert: 10. Maerz 2024"
"title": "Allgemeine Geschäftsbedingungen",
"lastUpdated": "Zuletzt aktualisiert: 10. März 2024"
}
},
"notfound": {
"title": "Seite nicht gefunden",
"description": "Die angeforderte Seite existiert nicht oder wurde verschoben.",
"backHome": "Zurueck zur Startseite"
"backHome": "Zurück zur Startseite"
}
},
"portfolio": {
"seo": {
"title": "Portfolio - Fullstack Entwickler Koeln Damjan Savic | Python & React Projekte",
"description": "Portfolio von Freelance Developer Koeln Damjan Savic: Python Automatisierung, OLLAMA KI Integration, ERP System Integration, E-Commerce Entwicklung."
"title": "Portfolio - Fullstack Entwickler Köln Damjan Savić | Python & React Projekte",
"description": "Portfolio von Freelance Developer Köln Damjan Savić: Python Automatisierung, OLLAMA KI Integration, ERP System Integration, E-Commerce Entwicklung."
},
"sections": {
"title": "Software Entwickler Koeln - Portfolio Damjan Savic",
"subtitle": "Python Entwickler Koeln & React Developer - Erfolgreiche Projekte"
"title": "Software Entwickler Köln - Portfolio Damjan Savić",
"subtitle": "Python Entwickler Köln & React Developer - Erfolgreiche Projekte"
},
"filters": {
"all": "Alle",
@@ -312,39 +448,46 @@
"loading": "Projekte werden geladen...",
"noProjects": "Keine Projekte gefunden",
"viewProject": "Projekt ansehen",
"backToPortfolio": "Zurueck zum Portfolio",
"backToPortfolio": "Zurück zum Portfolio",
"technologies": "Technologien"
}
},
"blog": {
"meta": {
"title": "Blog von Damjan Savic - JTL Integration & Digital Solutions",
"description": "Insights und Best Practices von Damjan Savic zu JTL-WaWi, E-Commerce und digitaler Transformation.",
"title": "Blog von Damjan Savić - JTL Integration & Digital Solutions",
"description": "Insights und Best Practices von Damjan Savić zu JTL-WaWi, E-Commerce und digitaler Transformation.",
"header": {
"title": "Insights & Best Practices von Damjan Savic",
"subtitle": "Damjan Savic teilt Insights und Best Practices zu JTL-WaWi, E-Commerce und digitaler Transformation"
"title": "Insights & Best Practices von Damjan Savić",
"subtitle": "Damjan Savić teilt Insights und Best Practices zu JTL-WaWi, E-Commerce und digitaler Transformation"
}
},
"ui": {
"readMore": "Weiterlesen",
"loading": {
"posts": "Blogbeitraege werden geladen...",
"posts": "Blogbeiträge werden geladen...",
"post": "Blogbeitrag wird geladen..."
},
"errors": {
"posts": "Fehler beim Laden der Blogbeitraege",
"posts": "Fehler beim Laden der Blogbeiträge",
"post": "Der gesuchte Blogbeitrag existiert nicht oder wurde verschoben."
},
"backToBlog": "Zurueck zum Blog",
"publishedOn": "Veroeffentlicht am",
"backToBlog": "Zurück zum Blog",
"publishedOn": "Veröffentlicht am",
"category": "Kategorie",
"tags": "Tags"
"tags": "Tags",
"pagination": {
"previous": "Zurück",
"next": "Weiter",
"page": "Seite",
"of": "von",
"showing": "Zeige {start}-{end} von {total} Artikeln"
}
}
},
"login": {
"seo": {
"title": "Login",
"description": "Anmelden fuer Zugriff auf das Dashboard"
"description": "Anmelden für Zugriff auf das Dashboard"
},
"header": {
"title": "Im Dashboard anmelden"
@@ -360,11 +503,11 @@
},
"submit": {
"default": "Anmelden",
"loading": "Anmeldung laeuft..."
"loading": "Anmeldung läuft..."
},
"errors": {
"default": "Ein Fehler ist aufgetreten",
"invalidCredentials": "Ungueltige Anmeldedaten"
"invalidCredentials": "Ungültige Anmeldedaten"
}
},
"security": {
@@ -399,7 +542,7 @@
"columns": {
"path": "Pfad",
"views": "Aufrufe",
"change": "Aenderung"
"change": "Änderung"
}
},
"status": {
+182 -24
View File
@@ -1,13 +1,27 @@
{
"meta": {
"site": {
"name": "Damjan Savic",
"title": "Damjan Savic | Fullstack, IoT & AI Developer",
"name": "Damjan Savić",
"title": "Damjan Savić | Fullstack, IoT & AI Developer",
"subtitle": "Scalable Software Solutions with IoT and AI Integration",
"description": "As a fullstack developer from Cologne, Damjan Savic specializes in developing scalable software solutions with IoT and AI integration."
"description": "As a fullstack developer from Cologne, Damjan Savić specializes in developing scalable software solutions with IoT and AI integration.",
"keywords": [
"Fullstack Developer",
"AI Developer Germany",
"AI Agent Development",
"Process Automation",
"Voice AI Developer",
"n8n Automation Expert",
"Python Developer",
"React Developer",
"TypeScript Developer",
"Remote Developer Europe",
"Cologne Germany Developer",
"SaaS Development"
]
},
"author": {
"name": "Damjan Savic",
"name": "Damjan Savić",
"role": "Senior Fullstack Developer & Digital Solutions Consultant",
"location": "Cologne, Germany",
"email": "info@damjan-savic.com"
@@ -19,7 +33,7 @@
}
},
"common": {
"siteTitle": "Damjan Savic - Digital Solutions & JTL Integration",
"siteTitle": "Damjan Savić - Digital Solutions & JTL Integration",
"siteDescription": "Developer of custom solutions for process automation",
"nav": {
"home": "Home",
@@ -59,6 +73,7 @@
"main": {
"home": "Home",
"about": "About",
"services": "Services",
"portfolio": "Portfolio",
"blog": "Blog",
"contact": "Contact"
@@ -113,7 +128,7 @@
"pages": {
"home": {
"hero": {
"title": "AI & AUTOMATION SPECIALIST",
"title": "FULLSTACK DEVELOPER",
"name": "DAMJAN SAVIC",
"subtitle": "AI AGENTS | VOICE AI | PROCESS AUTOMATION",
"description": "Building AI agents and automation solutions. From voice AI platforms to autonomous web agents.",
@@ -126,9 +141,13 @@
"about": "ABOUT"
}
},
"gallery": {
"title": "Insights",
"description": "Professional moments from my daily work"
},
"about": {
"title": "About Damjan Savic - Senior Fullstack Developer & Digital Solutions Consultant",
"content": "Damjan Savic is an AI & Automation Specialist focusing on Voice AI, autonomous agents, and process automation. With an M.A. in Software Development and over 7 years of experience developing digital solutions, he combines technical expertise with strategic business understanding.",
"title": "About Damjan Savić - Senior Fullstack Developer & Digital Solutions Consultant",
"content": "Damjan Savić is an AI & Automation Specialist focusing on Voice AI, autonomous agents, and process automation. With an M.A. in Software Development and over 7 years of experience developing digital solutions, he combines technical expertise with strategic business understanding.",
"buttons": {
"learnMore": "Learn More",
"downloadCV": "Download CV"
@@ -212,13 +231,144 @@
},
"about": {
"seo": {
"title": "About Me - Damjan Savic | Fullstack Developer & AI Specialist",
"description": "Learn more about Damjan Savic - AI & Automation Specialist focusing on Voice AI, autonomous agents, and process automation from Cologne."
"title": "About Me - Damjan Savić | Fullstack Developer & AI Specialist",
"description": "Learn more about Damjan Savić - AI & Automation Specialist focusing on Voice AI, autonomous agents, and process automation from Cologne."
},
"hero": {
"aboutMe": "FULLSTACK DEVELOPER COLOGNE - DAMJAN SAVIC",
"title": "Python Developer Cologne & React Developer - Damjan Savic",
"description": "As a Fullstack Developer in Cologne, Damjan Savic is your Freelance Developer for Python automation and React development."
"aboutMe": "About Me",
"title": "FULLSTACK DEVELOPER COLOGNE",
"description": "As an AI & Automation Specialist with over 7 years of experience, I develop scalable solutions for complex business processes. My focus is on Voice AI, autonomous agents, and process automation.",
"expertise": {
"processAutomation": {
"title": "Process Automation",
"description": "Automation of complex business processes with n8n, Zapier and Python"
},
"systemIntegration": {
"title": "System Integration",
"description": "Seamless integration of ERP, CRM and e-commerce systems"
},
"customDevelopment": {
"title": "Custom Development",
"description": "Tailored software solutions with Python, TypeScript and React"
}
},
"languages": {
"python": "Python",
"typescript": "TypeScript",
"react": "React",
"nextjs": "Next.js",
"nodejs": "Node.js",
"sql": "SQL"
}
},
"skills": {
"title": "Skills & Expertise",
"description": "My technical skills and competencies acquired over 7+ years of professional experience.",
"skillGroups": [
{
"category": "Backend & Automation",
"items": [
{ "name": "Python", "level": 90 },
{ "name": "n8n / Zapier", "level": 85 },
{ "name": "Node.js", "level": 80 },
{ "name": "FastAPI", "level": 85 }
]
},
{
"category": "Frontend",
"items": [
{ "name": "React", "level": 85 },
{ "name": "TypeScript", "level": 85 },
{ "name": "Next.js", "level": 80 },
{ "name": "Tailwind CSS", "level": 90 }
]
},
{
"category": "Databases & Cloud",
"items": [
{ "name": "PostgreSQL", "level": 85 },
{ "name": "Supabase", "level": 80 },
{ "name": "Docker", "level": 75 },
{ "name": "Azure", "level": 70 }
]
}
]
},
"workflow": {
"title": "My Workflow",
"description": "Structured process from requirements analysis to successful implementation.",
"steps": [
{ "number": "01", "title": "Requirements Analysis & Consulting" },
{ "number": "02", "title": "Technical Concept & Architecture" },
{ "number": "03", "title": "Agile Development & Testing" },
{ "number": "04", "title": "Deployment & Integration" },
{ "number": "05", "title": "Monitoring & Optimization" },
{ "number": "06", "title": "Support & Maintenance" }
]
},
"journey": {
"title": "My Journey",
"subtitle": "Education, languages and professional experience",
"education": {
"title": "Education",
"degrees": {
"masters": {
"degree": "M.A. Software Development",
"university": "FOM University of Applied Sciences",
"period": "2021 - 2023"
},
"bachelors": {
"degree": "B.Sc. Business Informatics",
"university": "FOM University of Applied Sciences",
"period": "2017 - 2021"
}
}
},
"languages": {
"title": "Languages",
"items": {
"german": { "name": "German", "level": "Native" },
"english": { "name": "English", "level": "Fluent (C1)" },
"serbian": { "name": "Serbian", "level": "Native" }
}
},
"work": {
"positions": [
{
"title": "Process Automation Specialist",
"period": "12/2024 - Present",
"company": "Everlast Consulting GmbH",
"location": "Cologne",
"highlights": [
"Development of AI agents with n8n and Zapier",
"Building web scraping solutions",
"Migration from Power Automate to n8n"
]
},
{
"title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024",
"company": "Ritter Digital GmbH",
"location": "Cologne",
"highlights": [
"Backend services on dedicated servers",
"AI integration via Power Automate",
"RFID/IoT solutions with Zebra hardware"
]
},
{
"title": "ERP Integration Specialist",
"period": "01/2023 - 08/2023",
"company": "Joyce & Girls",
"location": "Cologne",
"highlights": [
"ApparelMagic & TradeByte Integration",
"MariaDB Middleware Development",
"Shopify Optimization"
]
}
]
}
}
},
"contact": {
@@ -241,7 +391,8 @@
"email": {
"label": "Email",
"value": "info@damjan-savic.com"
}
},
"availabilityNote": "I usually respond within 24 hours."
},
"contactForm": {
"title": "Contact Me",
@@ -266,7 +417,7 @@
"privacy": {
"seo": {
"title": "Privacy Policy",
"description": "Privacy Policy for the portfolio website of Damjan Savic"
"description": "Privacy Policy for the portfolio website of Damjan Savić"
},
"content": {
"title": "Privacy Policy",
@@ -276,7 +427,7 @@
"terms": {
"seo": {
"title": "Terms and Conditions",
"description": "Terms and Conditions for the portfolio website of Damjan Savic"
"description": "Terms and Conditions for the portfolio website of Damjan Savić"
},
"content": {
"title": "Terms and Conditions",
@@ -291,11 +442,11 @@
},
"portfolio": {
"seo": {
"title": "Portfolio - Fullstack Developer Cologne Damjan Savic | Python & React Projects",
"description": "Portfolio of Freelance Developer Cologne Damjan Savic: Python Automation, OLLAMA AI Integration, ERP System Integration, E-Commerce Development."
"title": "Portfolio - Fullstack Developer Cologne Damjan Savić | Python & React Projects",
"description": "Portfolio of Freelance Developer Cologne Damjan Savić: Python Automation, OLLAMA AI Integration, ERP System Integration, E-Commerce Development."
},
"sections": {
"title": "Software Developer Cologne - Portfolio Damjan Savic",
"title": "Software Developer Cologne - Portfolio Damjan Savić",
"subtitle": "Python Developer Cologne & React Developer - Successful Projects"
},
"filters": {
@@ -318,11 +469,11 @@
},
"blog": {
"meta": {
"title": "Blog by Damjan Savic - JTL Integration & Digital Solutions",
"description": "Insights and best practices from Damjan Savic on JTL-WaWi, e-commerce, and digital transformation.",
"title": "Blog by Damjan Savić - JTL Integration & Digital Solutions",
"description": "Insights and best practices from Damjan Savić on JTL-WaWi, e-commerce, and digital transformation.",
"header": {
"title": "Insights & Best Practices from Damjan Savic",
"subtitle": "Damjan Savic shares insights and best practices on JTL-WaWi, e-commerce, and digital transformation"
"title": "Insights & Best Practices from Damjan Savić",
"subtitle": "Damjan Savić shares insights and best practices on JTL-WaWi, e-commerce, and digital transformation"
}
},
"ui": {
@@ -338,7 +489,14 @@
"backToBlog": "Back to Blog",
"publishedOn": "Published on",
"category": "Category",
"tags": "Tags"
"tags": "Tags",
"pagination": {
"previous": "Previous",
"next": "Next",
"page": "Page",
"of": "of",
"showing": "Showing {start}-{end} of {total} articles"
}
}
},
"login": {
+182 -17
View File
@@ -1,13 +1,34 @@
{
"meta": {
"site": {
"name": "Damjan Savic",
"title": "Damjan Savic | Fullstack, IoT & AI Programer",
"name": "Damjan Savić",
"title": "Damjan Savić | Fullstack, IoT & AI Programer",
"subtitle": "Skalabilna softverska resenja sa IoT i AI integracijom",
"description": "Kao fullstack programer iz Kelna, Damjan Savic se specijalizuje za razvoj skalabilnih softverskih resenja sa IoT i AI integracijom."
"description": "Kao fullstack programer iz Kelna, Damjan Savić se specijalizuje za razvoj skalabilnih softverskih resenja sa IoT i AI integracijom.",
"keywords": [
"Fullstack Programer Srbija",
"AI Programer Srbija",
"AI Developer Beograd",
"AI Developer Novi Sad",
"Razvoj AI Agenata",
"Automatizacija Procesa",
"Voice AI Srbija",
"n8n Automatizacija",
"Softverski Razvoj Beograd",
"Softverski Razvoj Novi Sad",
"Softverski Razvoj Niš",
"Python Programer Srbija",
"React Programer Srbija",
"TypeScript Programer",
"Web Razvoj Srbija",
"Remote Developer Dijaspora",
"Freelance Programer Srbija",
"SaaS Razvoj Srbija",
"GPT-4 Integracija Srbija"
]
},
"author": {
"name": "Damjan Savic",
"name": "Damjan Savić",
"role": "Senior Fullstack Developer & Digital Solutions Consultant",
"location": "Keln, Nemacka",
"email": "info@damjan-savic.com"
@@ -19,7 +40,7 @@
}
},
"common": {
"siteTitle": "Damjan Savic - Digitalna resenja & JTL integracija",
"siteTitle": "Damjan Savić - Digitalna resenja & JTL integracija",
"siteDescription": "Programer individualnih resenja za automatizaciju procesa",
"nav": {
"home": "Pocetna",
@@ -59,6 +80,7 @@
"main": {
"home": "Pocetna",
"about": "O meni",
"services": "Usluge",
"portfolio": "Portfolio",
"blog": "Blog",
"contact": "Kontakt"
@@ -113,7 +135,7 @@
"pages": {
"home": {
"hero": {
"title": "AI & AUTOMATION SPECIALIST",
"title": "FULLSTACK DEVELOPER",
"name": "DAMJAN SAVIC",
"subtitle": "AI AGENTI | VOICE AI | AUTOMATIZACIJA PROCESA",
"description": "Razvoj AI agenata i resenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.",
@@ -126,9 +148,13 @@
"about": "O MENI"
}
},
"gallery": {
"title": "Uvidi",
"description": "Profesionalni momenti iz mog svakodnevnog rada"
},
"about": {
"title": "O Damjanu Savicu - Senior Fullstack Developer & Digital Solutions Consultant",
"content": "Damjan Savic je AI & Automation Specialist sa fokusom na Voice AI, autonomne agente i automatizaciju procesa. Sa MA diplomom iz razvoja softvera i preko 7 godina iskustva u razvoju digitalnih resenja, kombinuje tehnicku ekspertizu sa strateskim razumevanjem poslovanja.",
"content": "Damjan Savić je AI & Automation Specialist sa fokusom na Voice AI, autonomne agente i automatizaciju procesa. Sa MA diplomom iz razvoja softvera i preko 7 godina iskustva u razvoju digitalnih resenja, kombinuje tehnicku ekspertizu sa strateskim razumevanjem poslovanja.",
"buttons": {
"learnMore": "Saznaj vise",
"downloadCV": "Preuzmi CV"
@@ -212,13 +238,144 @@
},
"about": {
"seo": {
"title": "O meni - Damjan Savic | Fullstack Developer & AI Specijalista",
"description": "Saznajte vise o Damjanu Savicu - AI & Automation Specialist sa fokusom na Voice AI, autonomne agente i automatizaciju procesa iz Kelna."
"title": "O meni - Damjan Savić | Fullstack Developer & AI Specijalista",
"description": "Saznajte više o Damjanu Saviću - AI & Automation Specialist sa fokusom na Voice AI, autonomne agente i automatizaciju procesa iz Kelna."
},
"hero": {
"aboutMe": "FULLSTACK DEVELOPER KELN - DAMJAN SAVIC",
"title": "Python Developer Keln & React Developer - Damjan Savic",
"description": "Kao Fullstack Developer u Kelnu, Damjan Savic je vas Freelance Developer za Python automatizaciju i React razvoj."
"aboutMe": "O meni",
"title": "FULLSTACK DEVELOPER KELN",
"description": "Kao AI & Automation Specialist sa preko 7 godina iskustva, razvijam skalabilna rešenja za kompleksne poslovne procese. Moj fokus je na Voice AI, autonomnim agentima i automatizaciji procesa.",
"expertise": {
"processAutomation": {
"title": "Automatizacija procesa",
"description": "Automatizacija kompleksnih poslovnih procesa sa n8n, Zapier i Python"
},
"systemIntegration": {
"title": "Sistemska integracija",
"description": "Besprekorna integracija ERP, CRM i e-commerce sistema"
},
"customDevelopment": {
"title": "Prilagođeni razvoj",
"description": "Prilagođena softverska rešenja sa Python, TypeScript i React"
}
},
"languages": {
"python": "Python",
"typescript": "TypeScript",
"react": "React",
"nextjs": "Next.js",
"nodejs": "Node.js",
"sql": "SQL"
}
},
"skills": {
"title": "Veštine i ekspertiza",
"description": "Moje tehničke veštine i kompetencije stečene kroz 7+ godina profesionalnog iskustva.",
"skillGroups": [
{
"category": "Backend i automatizacija",
"items": [
{ "name": "Python", "level": 90 },
{ "name": "n8n / Zapier", "level": 85 },
{ "name": "Node.js", "level": 80 },
{ "name": "FastAPI", "level": 85 }
]
},
{
"category": "Frontend",
"items": [
{ "name": "React", "level": 85 },
{ "name": "TypeScript", "level": 85 },
{ "name": "Next.js", "level": 80 },
{ "name": "Tailwind CSS", "level": 90 }
]
},
{
"category": "Baze podataka i Cloud",
"items": [
{ "name": "PostgreSQL", "level": 85 },
{ "name": "Supabase", "level": 80 },
{ "name": "Docker", "level": 75 },
{ "name": "Azure", "level": 70 }
]
}
]
},
"workflow": {
"title": "Moj radni proces",
"description": "Strukturiran proces od analize zahteva do uspešne implementacije.",
"steps": [
{ "number": "01", "title": "Analiza zahteva i konsalting" },
{ "number": "02", "title": "Tehnički koncept i arhitektura" },
{ "number": "03", "title": "Agilni razvoj i testiranje" },
{ "number": "04", "title": "Deployment i integracija" },
{ "number": "05", "title": "Monitoring i optimizacija" },
{ "number": "06", "title": "Podrška i održavanje" }
]
},
"journey": {
"title": "Moj put",
"subtitle": "Obrazovanje, jezici i profesionalno iskustvo",
"education": {
"title": "Obrazovanje",
"degrees": {
"masters": {
"degree": "M.A. Razvoj softvera",
"university": "FOM Univerzitet primenjenih nauka",
"period": "2021 - 2023"
},
"bachelors": {
"degree": "B.Sc. Poslovna informatika",
"university": "FOM Univerzitet primenjenih nauka",
"period": "2017 - 2021"
}
}
},
"languages": {
"title": "Jezici",
"items": {
"german": { "name": "Nemački", "level": "Maternji" },
"english": { "name": "Engleski", "level": "Tečno (C1)" },
"serbian": { "name": "Srpski", "level": "Maternji" }
}
},
"work": {
"positions": [
{
"title": "Process Automation Specialist",
"period": "12/2024 - Danas",
"company": "Everlast Consulting GmbH",
"location": "Keln",
"highlights": [
"Razvoj AI agenata sa n8n i Zapier",
"Izgradnja web scraping rešenja",
"Migracija sa Power Automate na n8n"
]
},
{
"title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024",
"company": "Ritter Digital GmbH",
"location": "Keln",
"highlights": [
"Backend servisi na namenskim serverima",
"AI integracija preko Power Automate",
"RFID/IoT rešenja sa Zebra hardverom"
]
},
{
"title": "ERP Integration Specialist",
"period": "01/2023 - 08/2023",
"company": "Joyce & Girls",
"location": "Keln",
"highlights": [
"ApparelMagic & TradeByte integracija",
"MariaDB Middleware razvoj",
"Shopify optimizacija"
]
}
]
}
}
},
"contact": {
@@ -241,7 +398,8 @@
"email": {
"label": "E-mail",
"value": "info@damjan-savic.com"
}
},
"availabilityNote": "Obično odgovaram u roku od 24 sata."
},
"contactForm": {
"title": "Kontaktirajte me",
@@ -291,11 +449,11 @@
},
"portfolio": {
"seo": {
"title": "Portfolio - Fullstack Developer Keln Damjan Savic | Python & React Projekti",
"title": "Portfolio - Fullstack Developer Keln Damjan Savić | Python & React Projekti",
"description": "Portfolio Freelance Developera iz Kelna Damjana Savica: Python automatizacija, OLLAMA AI integracija, ERP sistem integracija, E-Commerce razvoj."
},
"sections": {
"title": "Software Developer Keln - Portfolio Damjan Savic",
"title": "Software Developer Keln - Portfolio Damjan Savić",
"subtitle": "Python Developer Keln & React Developer - Uspesni projekti"
},
"filters": {
@@ -322,7 +480,7 @@
"description": "Uvidi i najbolje prakse Damjana Savica o JTL-WaWi, e-trgovini i digitalnoj transformaciji.",
"header": {
"title": "Uvidi & Najbolje prakse Damjana Savica",
"subtitle": "Damjan Savic deli uvide i najbolje prakse o JTL-WaWi, e-trgovini i digitalnoj transformaciji"
"subtitle": "Damjan Savić deli uvide i najbolje prakse o JTL-WaWi, e-trgovini i digitalnoj transformaciji"
}
},
"ui": {
@@ -338,7 +496,14 @@
"backToBlog": "Nazad na Blog",
"publishedOn": "Objavljeno",
"category": "Kategorija",
"tags": "Oznake"
"tags": "Oznake",
"pagination": {
"previous": "Prethodna",
"next": "Sledeća",
"page": "Stranica",
"of": "od",
"showing": "Prikazano {start}-{end} od {total} članaka"
}
}
},
"login": {