auto-claude: subtask-2-1 - Delete src/components-vite directory (299KB, 50 fi

This commit is contained in:
2026-01-25 06:28:08 +01:00
parent 26955a7b64
commit 3b4a10c90e
51 changed files with 4 additions and 4765 deletions
+4 -4
View File
@@ -3,13 +3,13 @@
"spec": "029-remove-dead-code-components-vite-and-pages-vite-fo", "spec": "029-remove-dead-code-components-vite-and-pages-vite-fo",
"state": "building", "state": "building",
"subtasks": { "subtasks": {
"completed": 1, "completed": 2,
"total": 8, "total": 8,
"in_progress": 1, "in_progress": 1,
"failed": 0 "failed": 0
}, },
"phase": { "phase": {
"current": "Pre-Deletion Safety Verification", "current": "Delete Legacy Directories",
"id": null, "id": null,
"total": 2 "total": 2
}, },
@@ -18,8 +18,8 @@
"max": 1 "max": 1
}, },
"session": { "session": {
"number": 3, "number": 4,
"started_at": "2026-01-25T06:18:19.396966" "started_at": "2026-01-25T06:18:19.396966"
}, },
"last_update": "2026-01-25T06:23:54.704974" "last_update": "2026-01-25T06:27:40.436575"
} }
-43
View File
@@ -1,43 +0,0 @@
// Alert.tsx
import React from 'react';
import { motion } from 'framer-motion';
import { CheckCircle2, XCircle } from 'lucide-react';
interface AlertProps {
variant?: 'success' | 'error';
children: React.ReactNode;
}
export const Alert: React.FC<AlertProps> = ({ variant = 'success', children }) => {
const variants = {
success: {
bg: 'bg-primary/10',
border: 'border-primary',
text: 'text-primary',
icon: <CheckCircle2 className="h-5 w-5" />
},
error: {
bg: 'bg-red-500/10',
border: 'border-red-500',
text: 'text-red-400',
icon: <XCircle className="h-5 w-5" />
}
};
const style = variants[variant];
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`${style.bg} ${style.border} ${style.text} border rounded-lg p-4 flex items-start space-x-3`}
>
<span className="flex-shrink-0">{style.icon}</span>
<div className="flex-1">{children}</div>
</motion.div>
);
};
export const AlertDescription: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="text-sm">{children}</div>;
};
-41
View File
@@ -1,41 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
// Fügen Sie diese Funktion direkt in die Datei ein
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
-77
View File
@@ -1,77 +0,0 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
interface BreadcrumbsProps {
currentTitle?: string;
}
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ currentTitle }) => {
const location = useLocation();
// Definiere die feste Struktur der Breadcrumbs
const generateBreadcrumbs = () => {
const segments = location.pathname.split('/').filter(Boolean);
// Basis-Breadcrumb-Struktur
const breadcrumbs = [];
// Portfolio ist immer der erste Level nach Home
if (segments.includes('portfolio')) {
breadcrumbs.push({
title: 'Portfolio',
path: '/portfolio',
isLast: segments.length === 1
});
}
// Wenn wir in einem Projekt sind, füge den Projekttitel hinzu
if (segments.length > 1 && currentTitle) {
breadcrumbs.push({
title: currentTitle,
path: location.pathname,
isLast: true
});
}
return breadcrumbs;
};
const breadcrumbs = generateBreadcrumbs();
// Wenn wir nur auf der Homepage sind, zeigen wir keine Breadcrumbs
if (location.pathname === '/') return null;
return (
<nav className="flex items-center gap-2 py-4 sm:py-6">
{/* Home ist immer der erste Link */}
<Link
to="/"
className="text-zinc-400 hover:text-white transition-colors duration-200"
>
Home
</Link>
{breadcrumbs.map((crumb) => (
<React.Fragment key={crumb.path}>
<ChevronRight className="h-4 w-4 text-zinc-600" />
{crumb.isLast ? (
<span className="text-white font-medium">
{crumb.title}
</span>
) : (
<Link
to={crumb.path}
className="text-zinc-400 hover:text-white transition-colors duration-200"
>
{crumb.title}
</Link>
)}
</React.Fragment>
))}
</nav>
);
};
export default Breadcrumbs;
-60
View File
@@ -1,60 +0,0 @@
// Button.tsx
import React from 'react';
import { Loader2 } from 'lucide-react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({
className = '',
variant = 'default',
size = 'md',
isLoading,
leftIcon,
rightIcon,
children,
disabled,
...props
}, ref) => {
const baseStyles = 'inline-flex items-center justify-center rounded-lg font-medium transition-all focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
default: 'bg-primary text-white hover:bg-primary/90',
outline: 'border border-primary text-primary hover:bg-primary hover:text-white',
ghost: 'text-primary hover:bg-primary/10'
};
const sizes = {
sm: 'h-9 px-4 text-sm',
md: 'h-12 px-6 text-base',
lg: 'h-14 px-8 text-lg'
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
disabled={isLoading || disabled}
{...props}
>
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<>
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</>
)}
</button>
);
}
);
Button.displayName = 'Button';
-84
View File
@@ -1,84 +0,0 @@
import * as React from "react"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
// Fügen Sie diese Funktion direkt in die Datei ein
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
-36
View File
@@ -1,36 +0,0 @@
"use client"
import { Phone, Mail, ArrowRight } from "lucide-react"
import { Link } from "react-router-dom"
import { motion } from "framer-motion"
export function ContactBar() {
return (
<motion.div
initial={{ y: -50 }}
animate={{ y: 0 }}
className="bg-gradient-to-r from-cyan-900 to-slate-900 text-white py-2 px-4"
>
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="flex items-center space-x-6 text-sm">
<a href="tel:+1234567890" className="flex items-center space-x-2 hover:text-cyan-400 transition-colors">
<Phone className="h-4 w-4" />
<span>+1 234 567 890</span>
</a>
<a
href="mailto:info@damjan-savic.com"
className="flex items-center space-x-2 hover:text-cyan-400 transition-colors"
>
<Mail className="h-4 w-4" />
<span>info@damjan-savic.com</span>
</a>
</div>
<Link to="/contact" className="flex items-center space-x-1 text-sm hover:text-cyan-400 transition-colors group">
<span>Contact Us</span>
<ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</motion.div>
)
}
-125
View File
@@ -1,125 +0,0 @@
import React, { useState } from 'react';
import { Send, AlertCircle } from 'lucide-react';
import { getCsrfToken } from '../utils/csrf';
import { rateLimiter } from '../utils/rateLimiting';
interface ContactFormProps {
onSubmit: (data: FormData) => Promise<void>;
}
const ContactForm: React.FC<ContactFormProps> = ({ onSubmit }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
// Check rate limiting
const clientIp = '127.0.0.1'; // In production, get this from the request
if (rateLimiter.isRateLimited(clientIp)) {
const timeToReset = Math.ceil(rateLimiter.getTimeToReset(clientIp) / 1000 / 60);
throw new Error(`Too many attempts. Please try again in ${timeToReset} minutes.`);
}
// Validate CSRF token
const formData = new FormData();
formData.append('name', name);
formData.append('email', email);
formData.append('message', message);
formData.append('csrf_token', getCsrfToken());
await onSubmit(formData);
setSuccess(true);
setName('');
setEmail('');
setMessage('');
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg flex items-center">
<AlertCircle className="h-5 w-5 mr-2" />
<span>{error}</span>
</div>
)}
{success && (
<div className="bg-green-500/10 border border-green-500/20 text-green-500 p-4 rounded-lg">
Message sent successfully!
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-white/80 mb-2">Name</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-gray-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"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-white/80 mb-2">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-gray-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"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-white/80 mb-2">Message</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={6}
className="w-full bg-gray-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"
required
></textarea>
</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"
>
{loading ? (
<span className="flex items-center">
<svg className="animate-spin h-5 w-5 mr-2" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Sending...
</span>
) : (
<span className="flex items-center">
<Send className="h-5 w-5 mr-2" />
Send Message
</span>
)}
</button>
</form>
);
};
export default ContactForm;
-40
View File
@@ -1,40 +0,0 @@
import { Phone, Mail, ArrowRight } from "lucide-react"
import { Link } from "react-router-dom"
export function ContactInfo() {
return (
<div className="pb-6 px-6 pt-4 bg-zinc-800/30 border border-zinc-800 rounded-lg mx-4 mt-2 animate-fade-in">
<div className="space-y-4">
<h3 className="text-sm font-medium text-white tracking-wider uppercase">
Kontakt
</h3>
<a
href="tel:+1234567890"
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>+1 234 567 890</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>info@damjan-savic.com</span>
</a>
<Link
to="/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>
)
}
-490
View File
@@ -1,490 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { initGA, logPageView, initFBPixel, initFunctionalCookies } from '../utils/analytics';
import { loadGoogleTagManager } from '../services/gtm';
// Feste Übersetzungen direkt in der Komponente
const translations = {
de: {
title: "Cookie-Einstellungen",
message: "Wir nutzen Cookies zur Verbesserung der Website. Ohne Ihre Zustimmung keine Datenweitergabe an Dritte. ",
acceptAll: "Alle akzeptieren",
save: "Einstellungen speichern",
decline: "Ablehnen",
necessary: "Notwendige Cookies",
necessaryDesc: "Für grundlegende Funktionen der Website",
analytics: "Analyse-Cookies",
analyticsDesc: "Für Analysen zur Verbesserung der Website",
marketing: "Marketing-Cookies",
marketingDesc: "Für personalisierte Werbung und Inhalte",
functional: "Funktionale Cookies",
functionalDesc: "Für erweiterte Funktionen und Personalisierung",
learnMore: "Mehr erfahren",
alwaysActive: "Immer aktiv"
},
en: {
title: "Cookie Settings",
message: "This website uses cookies to enhance your browsing experience. Data will not be shared with third parties without your consent.",
acceptAll: "Accept All",
save: "Save Settings",
decline: "Decline All",
necessary: "Necessary Cookies",
necessaryDesc: "For basic website functions",
analytics: "Analytics Cookies",
analyticsDesc: "For analyzing website usage",
marketing: "Marketing Cookies",
marketingDesc: "For personalized ads and content",
functional: "Functional Cookies",
functionalDesc: "For enhanced functionality and personalization",
learnMore: "Learn more",
alwaysActive: "Always active"
},
sr: {
title: "Podešavanja kolačića",
message: "Ovaj sajt koristi kolačiće za bolje iskustvo pretraživanja. Podaci neće biti deljeni sa trećim licima bez vaše saglasnosti.",
acceptAll: "Prihvati sve",
save: "Sačuvaj podešavanja",
decline: "Odbij sve",
necessary: "Neophodni kolačići",
necessaryDesc: "Za osnovne funkcije sajta",
analytics: "Analitički kolačići",
analyticsDesc: "Za analizu korišćenja sajta",
marketing: "Marketing kolačići",
marketingDesc: "Za personalizovane reklame i sadržaj",
functional: "Funkcionalni kolačići",
functionalDesc: "Za napredne funkcije i personalizaciju",
learnMore: "Saznajte više",
alwaysActive: "Uvek aktivno"
}
};
// Definition der verschiedenen Services
const cookieServices = {
essentiell: {
category: 'necessary',
services: [
{
name: 'Session Cookies',
provider: 'Eigentümer der Website',
purpose: 'Speichert Ihre Sitzungsinformationen'
}
]
},
funktional: {
category: 'functional',
services: [
{
name: 'Spracheinstellungen',
provider: 'Eigentümer der Website',
purpose: 'Speichert Ihre bevorzugte Sprache'
},
{
name: 'Google Fonts',
provider: 'Google LLC',
purpose: 'Anzeige von Webschriften'
}
]
},
analyse: {
category: 'analytics',
services: [
{
name: 'Google Analytics',
provider: 'Google LLC',
purpose: 'Analysiert Webseitennutzung und Nutzerverhalten'
}
]
},
marketing: {
category: 'marketing',
services: [
{
name: 'Google Ads',
provider: 'Google LLC',
purpose: 'Personalisierte Werbung'
},
{
name: 'Facebook Pixel',
provider: 'Meta Platforms Inc.',
purpose: 'Tracking von Werbekonversionen'
}
]
}
};
interface CookieSettings {
necessary: boolean;
analytics: boolean;
marketing: boolean;
functional: boolean;
}
// Entfernt externe Google Fonts, falls keine Zustimmung vorliegt
const removeExternalGoogleFonts = () => {
// Suche nach allen Google Fonts Link-Elementen und entferne sie
const linkElements = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
linkElements.forEach(link => {
link.parentNode?.removeChild(link);
});
// Suche nach allen Google Fonts Script-Elementen und entferne sie
const scriptElements = document.querySelectorAll('script[src*="fonts.googleapis.com"]');
scriptElements.forEach(script => {
script.parentNode?.removeChild(script);
});
};
// Funktion zum Blockieren von Google Analytics falls noch keine Zustimmung vorliegt
const blockGoogleAnalytics = () => {
// Entferne alle bestehenden GA Cookies
document.cookie.split(';').forEach(function(c) {
if (c.trim().indexOf('_ga') === 0) {
document.cookie = c.trim().split('=')[0] + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;';
}
});
};
const CookieBanner = () => {
const { i18n } = useTranslation();
const [currentLang, setCurrentLang] = useState<'de' | 'en' | 'sr'>('de');
const [showBanner, setShowBanner] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [cookieSettings, setCookieSettings] = useState<CookieSettings>({
necessary: true, // Always true and disabled
analytics: false,
marketing: false,
functional: false
});
// Initialisiere Dienste basierend auf den Cookie-Einstellungen
const initializeServices = useCallback((settings: CookieSettings) => {
// Blockieren von Diensten, wenn nicht zugestimmt wurde
if (!settings.functional) {
removeExternalGoogleFonts();
}
if (!settings.analytics) {
blockGoogleAnalytics();
} else {
// Initialisiere Google Analytics nur wenn ausdrücklich zugestimmt wurde
initGA();
logPageView();
}
// Marketing-Dienste nur mit Zustimmung
if (settings.marketing) {
initFBPixel();
loadGoogleTagManager();
}
// Funktionale Dienste nur mit Zustimmung
if (settings.functional) {
initFunctionalCookies();
}
}, []);
// Aktuelle Sprache erkennen
useEffect(() => {
const lang = i18n.language.substring(0, 2);
if (lang === 'de' || lang === 'en' || lang === 'sr') {
setCurrentLang(lang);
} else {
setCurrentLang('en'); // Fallback to English
}
}, [i18n.language]);
// Get text based on current language
const getText = (key: keyof typeof translations.en) => {
return translations[currentLang][key];
};
// DSGVO: Unmittelbar beim ersten Laden blockiere alle nicht-essentiellen Dienste
useEffect(() => {
// Blockiere externe Dienste bis Zustimmung vorliegt
removeExternalGoogleFonts();
blockGoogleAnalytics();
}, []);
// Prüfe beim Laden, ob bereits Zustimmung vorhanden ist
useEffect(() => {
try {
const storedSettings = localStorage.getItem('cookieSettings');
const hasConsent = localStorage.getItem('cookieConsent');
if (storedSettings && hasConsent) {
const parsedSettings = JSON.parse(storedSettings) as CookieSettings;
setCookieSettings(parsedSettings);
// Lade die entsprechenden Dienste basierend auf den Einstellungen
initializeServices(parsedSettings);
setShowBanner(false);
} else {
// Zeige Banner wenn keine Zustimmung vorhanden ist
setShowBanner(true);
}
} catch (error) {
console.error('Error loading cookie settings:', error);
setShowBanner(true);
}
}, [initializeServices]);
const handleSaveSettings = () => {
localStorage.setItem('cookieSettings', JSON.stringify(cookieSettings));
localStorage.setItem('cookieConsent', 'true');
localStorage.setItem('cookieConsentDate', new Date().toISOString());
// Initialisiere Dienste basierend auf den ausgewählten Einstellungen
initializeServices(cookieSettings);
setShowSettings(false);
setShowBanner(false);
};
const handleAcceptAll = () => {
const allSettings: CookieSettings = {
necessary: true,
analytics: true,
marketing: true,
functional: true
};
localStorage.setItem('cookieSettings', JSON.stringify(allSettings));
localStorage.setItem('cookieConsent', 'true');
localStorage.setItem('cookieConsentDate', new Date().toISOString());
setCookieSettings(allSettings);
// Initialisiere alle Dienste
initializeServices(allSettings);
setShowSettings(false);
setShowBanner(false);
};
const handleDeclineAll = () => {
const minimalSettings: CookieSettings = {
necessary: true,
analytics: false,
marketing: false,
functional: false
};
localStorage.setItem('cookieSettings', JSON.stringify(minimalSettings));
localStorage.setItem('cookieConsent', 'true');
localStorage.setItem('cookieConsentDate', new Date().toISOString());
setCookieSettings(minimalSettings);
// Block all non-essential services
initializeServices(minimalSettings);
setShowSettings(false);
setShowBanner(false);
};
const toggleSetting = (setting: keyof Omit<CookieSettings, 'necessary'>) => {
setCookieSettings({
...cookieSettings,
[setting]: !cookieSettings[setting]
});
};
// If banner should not be shown, return null
if (!showBanner) {
return null;
}
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
<div
className="bg-[rgba(18,18,18,0.95)] backdrop-blur-md border-t border-white/5 text-white p-4 md:p-6"
style={{ fontFamily: 'Inter, sans-serif' }}
>
<div className="max-w-7xl mx-auto">
{!showSettings ? (
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex-1">
<p className="text-sm md:text-base font-light">{getText('message')}</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3 mt-3 md:mt-0 w-full sm:w-auto">
<button
onClick={() => setShowSettings(true)}
className="w-full sm:w-auto px-4 py-2 text-xs uppercase tracking-wider font-light text-white border border-white/30 hover:bg-white/10 transition-all"
>
{getText('learnMore')}
</button>
<button
onClick={handleDeclineAll}
className="w-full sm:w-auto px-4 py-2 text-xs uppercase tracking-wider font-light text-white border border-white/30 hover:bg-white/10 transition-all"
>
{getText('decline')}
</button>
<button
onClick={handleAcceptAll}
className="w-full sm:w-auto px-4 py-2 text-xs uppercase tracking-wider font-light bg-white text-black hover:bg-gray-200 transition-all"
>
{getText('acceptAll')}
</button>
</div>
</div>
) : (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">{getText('title')}</h3>
<button
onClick={() => setShowSettings(false)}
className="text-white/70 hover:text-white"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
<div className="mb-6">
<p className="text-sm text-white/80 mb-4">{getText('message')}</p>
{/* Cookie categories */}
<div className="space-y-4">
{/* Necessary cookies - always enabled */}
<div className="p-3 border border-white/10 bg-white/5">
<div className="flex items-center justify-between mb-2">
<div className="font-medium">{getText('necessary')}</div>
<div className="text-xs text-white/60">{getText('alwaysActive')}</div>
</div>
<p className="text-xs text-white/70">{getText('necessaryDesc')}</p>
{/* Services List */}
<div className="mt-2 border-t border-white/10 pt-2">
{cookieServices.essentiell.services.map((service, index) => (
<div key={index} className="mt-2">
<div className="text-xs font-medium text-white/80">{service.name}</div>
<div className="text-xs text-white/60">Anbieter: {service.provider}</div>
<div className="text-xs text-white/60">Zweck: {service.purpose}</div>
</div>
))}
</div>
</div>
{/* Analytics cookies */}
<div className="p-3 border border-white/10 bg-white/5">
<div className="flex items-center justify-between mb-2">
<div className="font-medium">{getText('analytics')}</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={cookieSettings.analytics}
onChange={() => toggleSetting('analytics')}
/>
<div className="w-9 h-5 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-white/50"></div>
</label>
</div>
<p className="text-xs text-white/70">{getText('analyticsDesc')}</p>
{/* Services List */}
<div className="mt-2 border-t border-white/10 pt-2">
{cookieServices.analyse.services.map((service, index) => (
<div key={index} className="mt-2">
<div className="text-xs font-medium text-white/80">{service.name}</div>
<div className="text-xs text-white/60">Anbieter: {service.provider}</div>
<div className="text-xs text-white/60">Zweck: {service.purpose}</div>
</div>
))}
</div>
</div>
{/* Marketing cookies */}
<div className="p-3 border border-white/10 bg-white/5">
<div className="flex items-center justify-between mb-2">
<div className="font-medium">{getText('marketing')}</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={cookieSettings.marketing}
onChange={() => toggleSetting('marketing')}
/>
<div className="w-9 h-5 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-white/50"></div>
</label>
</div>
<p className="text-xs text-white/70">{getText('marketingDesc')}</p>
{/* Services List */}
<div className="mt-2 border-t border-white/10 pt-2">
{cookieServices.marketing.services.map((service, index) => (
<div key={index} className="mt-2">
<div className="text-xs font-medium text-white/80">{service.name}</div>
<div className="text-xs text-white/60">Anbieter: {service.provider}</div>
<div className="text-xs text-white/60">Zweck: {service.purpose}</div>
</div>
))}
</div>
</div>
{/* Functional cookies */}
<div className="p-3 border border-white/10 bg-white/5">
<div className="flex items-center justify-between mb-2">
<div className="font-medium">{getText('functional')}</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={cookieSettings.functional}
onChange={() => toggleSetting('functional')}
/>
<div className="w-9 h-5 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-white/50"></div>
</label>
</div>
<p className="text-xs text-white/70">{getText('functionalDesc')}</p>
{/* Services List */}
<div className="mt-2 border-t border-white/10 pt-2">
{cookieServices.funktional.services.map((service, index) => (
<div key={index} className="mt-2">
<div className="text-xs font-medium text-white/80">{service.name}</div>
<div className="text-xs text-white/60">Anbieter: {service.provider}</div>
<div className="text-xs text-white/60">Zweck: {service.purpose}</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-3">
<button
onClick={handleDeclineAll}
className="px-4 py-2 text-xs uppercase tracking-wider font-light text-white border border-white/30 hover:bg-white/10 transition-all"
>
{getText('decline')}
</button>
<button
onClick={handleSaveSettings}
className="px-4 py-2 text-xs uppercase tracking-wider font-light bg-white text-black hover:bg-gray-200 transition-all"
>
{getText('save')}
</button>
</div>
<div className="mt-4 text-center space-x-4">
<a
href="/privacy"
className="text-xs text-white/70 hover:text-white underline"
>
Datenschutzerklärung
</a>
<a
href="/imprint"
className="text-xs text-white/70 hover:text-white underline"
>
Impressum
</a>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default CookieBanner;
-41
View File
@@ -1,41 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useScrollContext } from './ScrollContext';
interface KeyboardEvent {
key: string;
}
const DebugOverlay: React.FC = () => {
const [showDebug, setShowDebug] = useState<boolean>(false);
const { isTransitioning } = useScrollContext();
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'd') {
setShowDebug((prev: boolean) => !prev);
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
if (!showDebug) return null;
return (
<div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded-lg z-[100] font-mono text-sm">
<div>Transitioning: {isTransitioning ? 'Yes' : 'No'}</div>
<div>Body Overflow: {document.body.style.overflow}</div>
<div>Scroll Position: {window.scrollY}</div>
<div>View Height: {window.innerHeight}</div>
<button
onClick={() => document.body.style.overflow = ''}
className="bg-blue-500 px-2 py-1 rounded mt-2"
>
Reset Overflow
</button>
</div>
);
};
export default DebugOverlay;
-61
View File
@@ -1,61 +0,0 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
private handleRetry = () => {
this.setState({ hasError: false, error: undefined });
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[400px] flex items-center justify-center p-4">
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-orange-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-4">Something went wrong</h2>
<p className="text-white/70 mb-6">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={this.handleRetry}
className="inline-flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-lg transition-colors"
>
<RefreshCw className="h-4 w-4" />
Try Again
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
-99
View File
@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { trackFAQInteraction } from '../services/gtm';
export function FAQSection() {
const { t } = useTranslation();
const [openItem, setOpenItem] = useState<number | null>(null);
const faqs = t('faq.questions', { returnObjects: true }) as Array<{
question: string;
answer: string;
category?: string;
}>;
const toggleItem = (index: number) => {
const isOpening = openItem !== index;
const question = faqs[index]?.question || '';
// Track FAQ interaction
trackFAQInteraction(question, isOpening ? 'open' : 'close');
setOpenItem(prev => prev === index ? null : index);
};
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqs.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
};
return (
<section className="faq-section py-16 px-4 max-w-4xl mx-auto">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
<h2 className="text-3xl font-bold text-white mb-8 text-center">
{t('faq.title')}
</h2>
<div className="space-y-4">
{faqs.map((faq, index) => (
<div
key={index}
className="faq-item bg-zinc-800/50 border border-zinc-700 rounded-lg overflow-hidden"
>
<button
onClick={() => toggleItem(index)}
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-zinc-800/70 transition-colors"
aria-expanded={openItem === index}
aria-controls={`faq-answer-${index}`}
>
<h3 className="text-lg font-medium text-white pr-4">
{faq.question}
</h3>
{openItem === index ? (
<ChevronUp className="h-5 w-5 text-zinc-400 flex-shrink-0" />
) : (
<ChevronDown className="h-5 w-5 text-zinc-400 flex-shrink-0" />
)}
</button>
<div
id={`faq-answer-${index}`}
className={`overflow-hidden transition-all duration-300 ${
openItem === index ? 'max-h-96' : 'max-h-0'
}`}
>
<div className="px-6 py-4 text-zinc-300 border-t border-zinc-700">
<p>{faq.answer}</p>
</div>
</div>
</div>
))}
</div>
<div className="mt-8 text-center">
<p className="text-zinc-400">
{t('faq.moreQuestions')}{' '}
<a
href="/contact"
className="text-white hover:text-zinc-300 underline transition-colors"
>
{t('faq.contactUs')}
</a>
</p>
</div>
</section>
);
}
-46
View File
@@ -1,46 +0,0 @@
import { motion } from "framer-motion"
export function FloatingPaths({ position }: { position: number }) {
const paths = Array.from({ length: 36 }, (_, i) => ({
id: i,
d: `M-${380 - i * 5 * position} -${189 + i * 6}C-${
380 - i * 5 * position
} -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${
152 - i * 5 * position
} ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${
684 - i * 5 * position
} ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`,
color: `rgba(var(--accent),${0.1 + i * 0.01})`,
width: 0.5 + i * 0.03,
}))
return (
<div className="absolute inset-0 pointer-events-none floating-paths">
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none" style={{ transform: 'translateZ(0)' }}>
<title>Background Paths</title>
{paths.map((path) => (
<motion.path
key={path.id}
d={path.d}
stroke="currentColor"
strokeWidth={path.width}
strokeOpacity={0.1 + path.id * 0.01}
initial={{ pathLength: 0.3, opacity: 0.6 }}
animate={{
pathLength: 1,
opacity: [0.3, 0.6, 0.3],
pathOffset: [0, 1, 0],
}}
transition={{
duration: 20 + Math.random() * 10,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
style={{ willChange: 'auto' }}
/>
))}
</svg>
</div>
)
}
-129
View File
@@ -1,129 +0,0 @@
// components/Footer.tsx
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Mail, Linkedin, Github, MapPin } from 'lucide-react';
const Footer = () => {
const { t } = useTranslation();
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('footer.sections.contact.title')}
</h3>
<div className="space-y-2">
<a
href={`mailto:${t('footer.sections.contact.email')}`}
className="group flex items-center gap-2 text-zinc-400 hover:text-white transition-colors text-sm !p-0 !min-h-0 !min-w-0 w-fit"
aria-label={t('footer.sections.contact.aria.emailLink')}
>
<Mail className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors flex-shrink-0" size={16} />
<span>{t('footer.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" size={16} />
<span>{t('footer.sections.contact.location')}</span>
</div>
</div>
</div>
{/* Navigation Section */}
<div>
<h3 className="text-white text-base font-semibold mb-3 text-left">
{t('footer.sections.navigation.title')}
</h3>
<nav className="flex flex-col space-y-2">
<Link
to="/portfolio"
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit !p-0 !min-h-0 !min-w-0"
>
{t('footer.sections.navigation.links.portfolio')}
</Link>
<Link
to="/blog"
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit !p-0 !min-h-0 !min-w-0"
>
{t('footer.sections.navigation.links.blog')}
</Link>
<Link
to="/about"
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit !p-0 !min-h-0 !min-w-0"
>
{t('footer.sections.navigation.links.about')}
</Link>
<Link
to="/contact"
className="text-zinc-400 hover:text-white transition-colors text-sm w-fit !p-0 !min-h-0 !min-w-0"
>
{t('footer.sections.navigation.links.contact')}
</Link>
</nav>
</div>
{/* Social Media Section */}
<div>
<h3 className="text-white text-base font-semibold mb-3 text-left">
{t('footer.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 !p-0 !min-h-0 !min-w-0"
aria-label={t('footer.sections.social.aria.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 !p-0 !min-h-0 !min-w-0"
aria-label={t('footer.sections.social.aria.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('footer.legal.copyright')}
</p>
<div className="flex space-x-4">
<Link
to="/privacy"
className="text-zinc-400 hover:text-white text-xs transition-colors !p-0 !min-h-0 !min-w-0"
>
{t('footer.legal.links.privacy')}
</Link>
<Link
to="/imprint"
className="text-zinc-400 hover:text-white text-xs transition-colors !p-0 !min-h-0 !min-w-0"
>
{t('footer.legal.links.imprint')}
</Link>
<Link
to="/terms"
className="text-zinc-400 hover:text-white text-xs transition-colors !p-0 !min-h-0 !min-w-0"
>
{t('footer.legal.links.terms')}
</Link>
</div>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;
-159
View File
@@ -1,159 +0,0 @@
import { useEffect, useState, lazy, Suspense } from 'react';
// Lazy load FloatingPaths - it's just decoration and shouldn't block initial render
const FloatingPaths = lazy(() => import('./FloatingPaths').then(m => ({ default: m.FloatingPaths })));
const GlobalBackground = () => {
const [scrollPosition, setScrollPosition] = useState(0);
const [showPaths, setShowPaths] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY * 0.001);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Delay loading FloatingPaths until after initial render
useEffect(() => {
const timer = setTimeout(() => setShowPaths(true), 2000);
return () => clearTimeout(timer);
}, []);
return (
<>
{/* Background layer - behind everything */}
<div
className="fixed inset-0 bg-zinc-900"
style={{
zIndex: -2,
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100%',
height: '100%'
}}
/>
{/* Gradient overlay */}
<div
className="fixed inset-0"
style={{
zIndex: -1,
background: 'linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 50%, #1e1e1e 100%)',
opacity: 0.9
}}
/>
{/* FloatingPaths layer - lazy loaded after 2s */}
{showPaths && (
<div
className="fixed inset-0"
style={{
zIndex: -1,
pointerEvents: 'none'
}}
>
<Suspense fallback={null}>
<FloatingPaths position={scrollPosition} />
</Suspense>
</div>
)}
{/* Simple animated lines with CSS */}
<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;
-66
View File
@@ -1,66 +0,0 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
export function HreflangTags() {
const { i18n } = useTranslation();
const location = window.location;
const currentPath = location.pathname;
const siteUrl = 'https://damjan-savic.com';
const supportedLanguages = ['de', 'en', 'sr'];
const defaultLanguage = 'de';
// Extract current language from URL
const pathSegments = currentPath.split('/').filter(Boolean);
const currentLang = supportedLanguages.includes(pathSegments[0]) ? pathSegments[0] : defaultLanguage;
// Get path without language prefix
const pathWithoutLang = currentLang === defaultLanguage
? currentPath
: currentPath.replace(`/${currentLang}`, '') || '/';
// Generate hreflang URLs
const generateHreflangUrl = (lang: string) => {
if (lang === defaultLanguage) {
return `${siteUrl}${pathWithoutLang}`;
}
return `${siteUrl}/${lang}${pathWithoutLang}`;
};
return (
<Helmet>
{/* Hreflang tags for all supported languages */}
{supportedLanguages.map((lang) => (
<link
key={lang}
rel="alternate"
hrefLang={lang}
href={generateHreflangUrl(lang)}
/>
))}
{/* x-default hreflang */}
<link
rel="alternate"
hrefLang="x-default"
href={generateHreflangUrl(defaultLanguage)}
/>
{/* Additional language meta tags */}
<html lang={currentLang} />
<meta property="og:locale" content={currentLang === 'de' ? 'de_DE' : currentLang === 'en' ? 'en_US' : 'sr_RS'} />
{/* Alternate og:locale tags */}
{supportedLanguages
.filter(lang => lang !== currentLang)
.map(lang => (
<meta
key={`og-locale-${lang}`}
property="og:locale:alternate"
content={lang === 'de' ? 'de_DE' : lang === 'en' ? 'en_US' : 'sr_RS'}
/>
))}
</Helmet>
);
}
-61
View File
@@ -1,61 +0,0 @@
import { useState } from 'react';
import { ImageOff } from 'lucide-react';
declare module 'react' {
interface ImgHTMLAttributes<T> extends React.HTMLAttributes<T> {
fetchpriority?: 'high' | 'low' | 'auto';
}
}
interface ImageWithFallbackProps {
src: string;
alt: string;
className?: string;
sizes?: string;
loading?: 'lazy' | 'eager';
fetchpriority?: 'high' | 'low' | 'auto';
}
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
src,
alt,
className = '',
sizes = '100vw',
loading = 'lazy',
fetchpriority = 'auto'
}) => {
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
if (error) {
return (
<div className={`flex items-center justify-center bg-gray-900 ${className}`}>
<div className="text-center p-4">
<ImageOff className="h-8 w-8 mx-auto mb-2 text-gray-500" />
<span className="text-sm text-gray-500">{alt}</span>
</div>
</div>
);
}
return (
<div className="relative">
{isLoading && (
<div className={`absolute inset-0 bg-gray-900/50 animate-pulse ${className}`} />
)}
<img
src={src}
alt={alt}
className={`${className} ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`}
loading={loading}
fetchpriority={fetchpriority}
sizes={sizes}
onLoad={() => setIsLoading(false)}
onError={() => setError(true)}
decoding="async"
/>
</div>
);
};
export default ImageWithFallback;
-25
View File
@@ -1,25 +0,0 @@
// Input.tsx
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: string;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className = '', error, ...props }, ref) => {
return (
<div className="w-full">
<input
className={`w-full h-12 bg-zinc-900/50 border border-white/10 rounded-lg px-4 text-white
placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary transition-all
disabled:opacity-50 disabled:cursor-not-allowed ${error ? 'border-red-500' : ''} ${className}`}
ref={ref}
{...props}
/>
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
</div>
);
}
);
Input.displayName = 'Input';
-15
View File
@@ -1,15 +0,0 @@
// Label.tsx
import React from 'react';
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean;
}
export const Label: React.FC<LabelProps> = ({ children, required, className = '', ...props }) => {
return (
<label className={`block text-sm font-medium text-white/80 mb-2 ${className}`} {...props}>
{children}
{required && <span className="text-red-400 ml-1">*</span>}
</label>
);
};
-98
View File
@@ -1,98 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react';
const LanguageSwitcher: React.FC = () => {
const { i18n } = useTranslation();
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 === i18n.language)?.name || 'Language';
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>
{/* Dropdown with CSS transitions */}
<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
${i18n.language === lang.code
? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white'
}`}
onClick={() => {
i18n.changeLanguage(lang.code);
setIsOpen(false);
}}
role="option"
aria-selected={i18n.language === lang.code}
>
{lang.name}
</button>
))}
</div>
</div>
</div>
);
};
export default LanguageSwitcher;
-274
View File
@@ -1,274 +0,0 @@
// Layout.tsx
import { useEffect, useState, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import {
Menu,
X,
Home,
User,
Briefcase,
BookOpen,
Phone,
LayoutDashboard,
} from "lucide-react";
// Supabase wird lazy geladen um Initial Bundle zu reduzieren
import { ContactInfo } from "./ContactInfo";
import { NavLink } from "./NavLink";
import LanguageSwitcher from "./LanguageSwitcher";
import { Link, useLocation } from "react-router-dom";
import Footer from "./Footer";
import { useScrollContext } from "./ScrollContext";
import { PerformanceMonitor } from "./PerformanceMonitor";
import GlobalBackground from "./GlobalBackground";
interface LayoutProps {
children: ReactNode;
}
const Layout = ({ children }: LayoutProps) => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const location = useLocation();
const { isMenuOpen, setIsMenuOpen } = useScrollContext();
// Initial mount with immediate execution
useEffect(() => {
// Use requestAnimationFrame to ensure smooth initial render
requestAnimationFrame(() => {
setIsMounted(true);
});
}, []);
// Schließe das Menü beim Routenwechsel
useEffect(() => {
setIsMenuOpen(false);
}, [location.pathname, setIsMenuOpen]);
// Toggle Menu
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
// Scroll-Handler
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 0);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Prüfe Admin-Status verzögert und lazy (nach Initial Render)
useEffect(() => {
const checkAdmin = async () => {
// Dynamischer Import von Supabase - wird erst geladen wenn benötigt
const { supabase } = await import("../utils/supabaseClient");
const { data, error } = await supabase.auth.getSession();
if (error) {
console.error("Error checking admin status:", error);
return;
}
if (data.session) {
const { user } = data.session;
const { data: adminData, error: adminError } = await supabase
.from("profiles")
.select("isAdmin")
.eq("id", user.id)
.single();
if (adminError) {
console.error("Error checking admin role:", adminError);
return;
}
setIsAdmin(adminData.isAdmin);
}
};
// Verzögere Admin-Check deutlich (5s) um kritischen Pfad nicht zu blockieren
const timeoutId = setTimeout(checkAdmin, 5000);
return () => clearTimeout(timeoutId);
}, []);
const navigationLinks = [
{
path: "/",
label: t("navigation.main.home"),
icon: <Home className="h-5 w-5" />,
},
{
path: "/about",
label: t("navigation.main.about"),
icon: <User className="h-5 w-5" />,
},
{
path: "/portfolio",
label: t("navigation.main.portfolio"),
icon: <Briefcase className="h-5 w-5" />,
},
{
path: "/blog",
label: t("navigation.main.blog"),
icon: <BookOpen className="h-5 w-5" />,
},
{
path: "/contact",
label: t("navigation.main.contact"),
icon: <Phone className="h-5 w-5" />,
},
...(isAdmin
? [
{
path: "/dashboard",
label: t("navigation.admin.dashboard"),
icon: <LayoutDashboard className="h-5 w-5" />,
},
]
: []),
];
// Render with opacity 0 on initial mount to prevent flicker
if (!isMounted) {
return (
<div className="min-h-screen bg-transparent">
<GlobalBackground />
{children}
</div>
);
}
return (
<div className="min-h-screen flex flex-col relative">
{/* Global Background */}
<GlobalBackground />
{/* Navigation oberste Ebene */}
<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 to="/" className="flex items-center space-x-2 group shrink-0">
<img
src="/header-logo.svg"
alt="Damjan Savić Logo"
className="h-8 w-auto transition-transform duration-200 hover:scale-105"
width={120}
height={32}
/>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-2">
{navigationLinks.map(({ path, label, icon }) => (
<NavLink
key={path}
to={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={toggleMenu}
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 - CSS-only Animationen */}
{/* Backdrop */}
<div
className={`fixed inset-0 bg-black/50 md:hidden z-20 pointer-events-none transition-opacity duration-200 ${
isMenuOpen ? "opacity-100" : "opacity-0"
}`}
/>
{/* Sidebar */}
<div
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"
}`}
style={{ willChange: 'transform' }}
>
{/* Sidebar Header mit Close-Button */}
<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>
{/* Scrollbarer Sidebar-Inhalt */}
<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}
to={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>
{/* Hauptinhalt */}
<main className="flex-1 pt-2 relative z-10 bg-transparent">{children}</main>
{/* Footer */}
<Footer />
{/* Performance Monitor */}
<PerformanceMonitor />
</div>
);
};
export default Layout;
-17
View File
@@ -1,17 +0,0 @@
import React, { Suspense } from 'react';
import LoadingSpinner from './LoadingSpinner';
interface LazyComponentProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
const LazyComponent: React.FC<LazyComponentProps> = ({ children, fallback = <LoadingSpinner /> }) => {
return (
<Suspense fallback={fallback}>
{children}
</Suspense>
);
};
export default LazyComponent;
-102
View File
@@ -1,102 +0,0 @@
import { motion } from 'framer-motion';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const LoadingSpinner = ({ size = 'md', className = '' }: LoadingSpinnerProps) => {
const sizes = {
sm: 'w-8 h-8',
md: 'w-12 h-12',
lg: 'w-16 h-16'
};
return (
<div className={`flex items-center justify-center min-h-[200px] ${className}`}>
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
className={`relative ${sizes[size]}`}
>
{/* Base Triangle */}
<svg
viewBox="0 0 100 100"
className="absolute inset-0 w-full h-full"
>
<path
d="M50 10 L90 80 L10 80 Z"
className="fill-none stroke-zinc-800"
strokeWidth="4"
/>
</svg>
{/* Animated Triangle Segments */}
<svg
viewBox="0 0 100 100"
className="absolute inset-0 w-full h-full"
>
<motion.path
d="M50 10 L90 80"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.path
d="M90 80 L10 80"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
delay: 0.66
}}
/>
<motion.path
d="M10 80 L50 10"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
delay: 1.33
}}
/>
</svg>
{/* Optional inner gradient effect */}
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900/50 to-transparent rounded-full" />
</motion.div>
</div>
);
};
export default LoadingSpinner;
-28
View File
@@ -1,28 +0,0 @@
// src/components/MDXProvider.tsx
import React from 'react';
import { MDXProvider } from '@mdx-js/react';
const components = {
h1: (props: React.ComponentProps<'h1'>) => (
<h1 className="text-3xl font-bold my-6" {...props} />
),
h2: (props: React.ComponentProps<'h2'>) => (
<h2 className="text-2xl font-bold my-4" {...props} />
),
h3: (props: React.ComponentProps<'h3'>) => (
<h3 className="text-xl font-bold my-3" {...props} />
),
p: (props: React.ComponentProps<'p'>) => (
<p className="my-4 text-muted-foreground" {...props} />
),
code: (props: React.ComponentProps<'code'>) => (
<code className="bg-muted px-1.5 py-0.5 rounded text-sm" {...props} />
),
pre: (props: React.ComponentProps<'pre'>) => (
<pre className="bg-muted p-4 rounded-lg my-4 overflow-x-auto" {...props} />
),
};
export function MDXLayout({ children }: { children: React.ReactNode }) {
return <MDXProvider components={components}>{children}</MDXProvider>;
}
-32
View File
@@ -1,32 +0,0 @@
// components/NavLink.tsx
import { Link, useLocation } from 'react-router-dom'
import React from 'react'
interface NavLinkProps {
to: string
icon: React.ReactNode
label: string
onClick?: () => void
className?: string
}
export function NavLink({ to, icon, label, onClick, className }: NavLinkProps) {
const location = useLocation()
const isActive = location.pathname === to
return (
<Link
to={to}
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>
)
}
-20
View File
@@ -1,20 +0,0 @@
// components/PageContainer.tsx
import React, { Suspense } from 'react';
interface PageContainerProps {
children: React.ReactNode;
}
const PageContainer = ({ children }: PageContainerProps) => {
return (
<div className="flex flex-col min-h-screen">
<Suspense fallback={null}>
<main className="flex-1 pt-16">
{children}
</main>
</Suspense>
</div>
);
};
export default PageContainer;
-25
View File
@@ -1,25 +0,0 @@
"use client"
import React, { useEffect, useRef } from "react"
import { useLocation } from "react-router-dom"
interface PageTransitionProps {
children: React.ReactNode
}
// Simplified page transition - no loading delay for better LCP
const PageTransition = ({ children }: PageTransitionProps) => {
const location = useLocation()
const previousPathRef = useRef<string | null>(null)
useEffect(() => {
// Only scroll on path change, not on initial mount
if (previousPathRef.current && location.pathname !== previousPathRef.current) {
window.scrollTo(0, 0)
}
previousPathRef.current = location.pathname
}, [location.pathname])
return <>{children}</>
}
export default PageTransition
-199
View File
@@ -1,199 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Activity, TrendingUp, AlertCircle, CheckCircle } from 'lucide-react';
interface WebVitalMetric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
timestamp: number;
}
interface PerformanceMetrics {
lcp: WebVitalMetric[];
inp: WebVitalMetric[];
cls: WebVitalMetric[];
fcp: WebVitalMetric[];
ttfb: WebVitalMetric[];
}
const THRESHOLDS = {
LCP: { good: 2500, poor: 4000 },
INP: { good: 200, poor: 500 },
CLS: { good: 0.1, poor: 0.25 },
FCP: { good: 1800, poor: 3000 },
TTFB: { good: 800, poor: 1800 }
};
export function PerformanceMonitor() {
const [metrics, setMetrics] = useState<PerformanceMetrics>({
lcp: [],
inp: [],
cls: [],
fcp: [],
ttfb: []
});
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Listen for performance metrics from webVitals
const handlePerformanceData = (event: CustomEvent) => {
const metric = event.detail as WebVitalMetric;
setMetrics(prev => ({
...prev,
[metric.name.toLowerCase()]: [...(prev[metric.name.toLowerCase() as keyof PerformanceMetrics] || []), metric].slice(-10)
}));
};
window.addEventListener('web-vitals', handlePerformanceData as EventListener);
return () => {
window.removeEventListener('web-vitals', handlePerformanceData as EventListener);
};
}, []);
const getLatestMetric = (metricName: keyof PerformanceMetrics) => {
const metricArray = metrics[metricName];
return metricArray[metricArray.length - 1];
};
const getRatingColor = (rating?: string) => {
switch (rating) {
case 'good': return 'text-green-500';
case 'needs-improvement': return 'text-yellow-500';
case 'poor': return 'text-red-500';
default: return 'text-zinc-400';
}
};
const getRatingIcon = (rating?: string) => {
switch (rating) {
case 'good': return <CheckCircle className="h-4 w-4" />;
case 'needs-improvement': return <AlertCircle className="h-4 w-4" />;
case 'poor': return <AlertCircle className="h-4 w-4" />;
default: return null;
}
};
const formatValue = (name: string, value?: number) => {
if (value === undefined) return '-';
if (name === 'CLS') return value.toFixed(3);
return `${Math.round(value)}ms`;
};
const calculateScore = () => {
const latestLCP = getLatestMetric('lcp');
const latestINP = getLatestMetric('inp');
const latestCLS = getLatestMetric('cls');
if (!latestLCP || !latestINP || !latestCLS) return null;
let score = 100;
// LCP scoring
if (latestLCP.value > THRESHOLDS.LCP.poor) score -= 33;
else if (latestLCP.value > THRESHOLDS.LCP.good) score -= 16;
// INP scoring
if (latestINP.value > THRESHOLDS.INP.poor) score -= 33;
else if (latestINP.value > THRESHOLDS.INP.good) score -= 16;
// CLS scoring
if (latestCLS.value > THRESHOLDS.CLS.poor) score -= 34;
else if (latestCLS.value > THRESHOLDS.CLS.good) score -= 18;
return Math.max(0, Math.round(score));
};
// Only show in development or with special flag
if (process.env.NODE_ENV === 'production' && !window.location.search.includes('debug=true')) {
return null;
}
return (
<>
{/* Toggle Button */}
<button
onClick={() => setIsVisible(!isVisible)}
className="fixed bottom-4 right-4 z-50 p-3 bg-zinc-800 border border-zinc-700 rounded-full shadow-lg hover:bg-zinc-700 transition-all"
aria-label="Toggle Performance Monitor"
>
<Activity className="h-5 w-5 text-white" />
</button>
{/* Performance Panel */}
{isVisible && (
<div className="fixed bottom-20 right-4 z-50 w-80 bg-zinc-900 border border-zinc-800 rounded-lg shadow-xl p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Activity className="h-5 w-5" />
Core Web Vitals
</h3>
<button
onClick={() => setIsVisible(false)}
className="text-zinc-400 hover:text-white"
>
×
</button>
</div>
{/* Overall Score */}
{calculateScore() !== null && (
<div className="mb-4 p-3 bg-zinc-800 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-zinc-400">Performance Score</span>
<span className={`text-2xl font-bold ${
calculateScore()! >= 90 ? 'text-green-500' :
calculateScore()! >= 50 ? 'text-yellow-500' :
'text-red-500'
}`}>
{calculateScore()}
</span>
</div>
</div>
)}
{/* Metrics */}
<div className="space-y-3">
{[
{ key: 'lcp', label: 'Largest Contentful Paint', threshold: THRESHOLDS.LCP },
{ key: 'inp', label: 'Interaction to Next Paint', threshold: THRESHOLDS.INP },
{ key: 'cls', label: 'Cumulative Layout Shift', threshold: THRESHOLDS.CLS },
{ key: 'fcp', label: 'First Contentful Paint', threshold: THRESHOLDS.FCP },
{ key: 'ttfb', label: 'Time to First Byte', threshold: THRESHOLDS.TTFB }
].map(({ key, label }) => {
const latest = getLatestMetric(key as keyof PerformanceMetrics);
return (
<div key={key} className="flex items-center justify-between p-2 bg-zinc-800/50 rounded">
<div className="flex-1">
<div className="text-sm text-zinc-300">{label}</div>
<div className="text-xs text-zinc-500 uppercase">{key}</div>
</div>
<div className={`flex items-center gap-2 ${getRatingColor(latest?.rating)}`}>
{getRatingIcon(latest?.rating)}
<span className="font-mono text-sm">
{formatValue(key.toUpperCase(), latest?.value)}
</span>
</div>
</div>
);
})}
</div>
{/* Info */}
<div className="mt-4 pt-4 border-t border-zinc-800">
<p className="text-xs text-zinc-500">
Real user metrics Updates live
<a
href="https://web.dev/vitals/"
target="_blank"
rel="noopener noreferrer"
className="ml-1 text-blue-400 hover:text-blue-300"
>
Learn more
</a>
</p>
</div>
</div>
)}
</>
);
}
-55
View File
@@ -1,55 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, ArrowRight } from 'lucide-react';
interface NavigationItem {
title: string;
slug: string;
}
interface PostNavigationProps {
previous?: NavigationItem;
next?: NavigationItem;
basePath: string;
}
const PostNavigation: React.FC<PostNavigationProps> = ({ previous, next, basePath }) => {
if (!previous && !next) return null;
return (
<nav className="border-t border-white/10 mt-12 pt-8">
<div className="flex justify-between">
{previous ? (
<Link
to={`${basePath}/${previous.slug}`}
className="group flex items-center text-white/60 hover:text-white transition-colors"
>
<ArrowLeft className="h-4 w-4 mr-2 transition-transform group-hover:-translate-x-1" />
<div>
<div className="text-sm text-white/40">Previous</div>
<div className="line-clamp-1">{previous.title}</div>
</div>
</Link>
) : (
<div />
)}
{next ? (
<Link
to={`${basePath}/${next.slug}`}
className="group flex items-center text-right text-white/60 hover:text-white transition-colors"
>
<div>
<div className="text-sm text-white/40">Next</div>
<div className="line-clamp-1">{next.title}</div>
</div>
<ArrowRight className="h-4 w-4 ml-2 transition-transform group-hover:translate-x-1" />
</Link>
) : (
<div />
)}
</div>
</nav>
);
};
export default PostNavigation
@@ -1,171 +0,0 @@
// components/ProjectContentTemplate.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Calendar, Building2, Clock, Tag } from 'lucide-react';
interface Section {
title: string;
description?: string;
content?: string;
points?: string[];
code?: string;
image?: string;
}
interface ProjectTranslation {
meta: {
title: string;
description: string;
date: string;
client: string;
duration: string;
technologies: string[];
};
content: {
intro: string;
challenge: Section;
solution: Section;
technical: Section;
implementation: Section;
results: Section;
conclusion: string;
};
}
interface ProjectContentTemplateProps {
translationKey: string;
fallbackData?: ProjectTranslation;
}
const ProjectContentTemplate: React.FC<ProjectContentTemplateProps> = ({
translationKey,
fallbackData
}) => {
// Nur 't' aus dem Hook extrahieren, da 'i18n' nicht verwendet wird
const { t } = useTranslation();
// Übersetzten Inhalt abrufen
const translatedContent = t(`${translationKey}`, {
returnObjects: true,
defaultValue: fallbackData
}) as ProjectTranslation;
const renderSection = (section: Section) => {
if (!section) return null;
return (
<div className="mb-12">
<h2 className="text-2xl font-bold text-white mb-4">
{section.title}
</h2>
{section.description && (
<p className="text-zinc-400 mb-6">
{section.description}
</p>
)}
{section.content && (
<div className="prose prose-invert max-w-none mb-6">
{section.content}
</div>
)}
{section.points && (
<ul className="list-disc list-inside text-zinc-300 space-y-2">
{section.points.map((point, index) => (
<li key={index} className="ml-4">
{point}
</li>
))}
</ul>
)}
{section.code && (
<pre className="bg-zinc-800/50 p-4 rounded-lg overflow-x-auto border border-zinc-800">
<code className="text-zinc-300">
{section.code}
</code>
</pre>
)}
{section.image && (
<div className="mt-6">
<img
src={section.image}
alt={section.title}
className="rounded-lg w-full"
loading="lazy"
/>
</div>
)}
</div>
);
};
return (
<article className="max-w-4xl mx-auto">
{/* Project Meta Information */}
<div className="mb-12">
<div className="flex flex-wrap items-center gap-4 mb-6 text-zinc-400">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
<time>{translatedContent.meta.date}</time>
</div>
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4" />
<span>{translatedContent.meta.client}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>{translatedContent.meta.duration}</span>
</div>
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4">
{translatedContent.meta.title}
</h1>
<p className="text-xl text-zinc-400">
{translatedContent.meta.description}
</p>
</div>
{/* Introduction */}
<div className="prose prose-invert max-w-none mb-12">
<p>{translatedContent.content.intro}</p>
</div>
{/* Main Sections */}
{renderSection(translatedContent.content.challenge)}
{renderSection(translatedContent.content.solution)}
{renderSection(translatedContent.content.technical)}
{renderSection(translatedContent.content.implementation)}
{renderSection(translatedContent.content.results)}
{/* Conclusion */}
{translatedContent.content.conclusion && (
<div className="prose prose-invert max-w-none mb-12">
<h2 className="text-2xl font-bold text-white mb-4">
{t('portfolio.sections.conclusion')}
</h2>
<p>{translatedContent.content.conclusion}</p>
</div>
)}
{/* 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">
{translatedContent.meta.technologies.map((tech) => (
<span
key={tech}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-800 rounded-full text-sm text-zinc-300"
>
{t(`technologies.${tech}`, tech)}
</span>
))}
</div>
</div>
</div>
</article>
);
};
export default ProjectContentTemplate;
-161
View File
@@ -1,161 +0,0 @@
import React from 'react';
import { ExternalLink, Github, Globe, Zap, TrendingUp, Users } from 'lucide-react';
import { ProjectSchema } from './schemas/ProjectSchema';
interface ProjectShowcaseProps {
project: {
id: string;
title: string;
description: string;
technicalChallenge: string;
businessImpact: {
roi?: string;
performanceGain?: string;
usersImpacted?: string;
};
technologies: string[];
category: string;
dateCreated: string;
liveUrl?: string;
githubUrl?: string;
screenshotUrl?: string;
features: string[];
codeExample?: string;
};
}
export function ProjectShowcase({ project }: ProjectShowcaseProps) {
return (
<article className="project-showcase bg-zinc-900 rounded-lg overflow-hidden">
<ProjectSchema project={project} />
{/* Hero Section with Screenshot */}
{project.screenshotUrl && (
<div className="relative aspect-video">
<img
src={project.screenshotUrl}
alt={`Screenshot of ${project.title}`}
className="w-full h-full object-cover"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900 via-transparent to-transparent" />
</div>
)}
<div className="p-8 space-y-8">
{/* Title and Description */}
<header>
<h2 className="text-3xl font-bold text-white mb-4">{project.title}</h2>
<p className="text-lg text-zinc-300 leading-relaxed">{project.description}</p>
</header>
{/* For Recruiters: Technical Challenge */}
<section className="technical-challenge">
<div className="flex items-center gap-2 mb-3">
<Zap className="h-5 w-5 text-blue-500" />
<h3 className="text-xl font-semibold text-white">Technical Challenge</h3>
</div>
<p className="text-zinc-300 bg-zinc-800/50 p-4 rounded-lg border border-zinc-700">
{project.technicalChallenge}
</p>
</section>
{/* For Clients: Business Impact */}
<section className="business-impact">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="h-5 w-5 text-green-500" />
<h3 className="text-xl font-semibold text-white">Business Impact</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{project.businessImpact.roi && (
<div className="bg-zinc-800/50 p-4 rounded-lg border border-zinc-700">
<p className="text-sm text-zinc-400 mb-1">ROI</p>
<p className="text-2xl font-bold text-green-400">{project.businessImpact.roi}</p>
</div>
)}
{project.businessImpact.performanceGain && (
<div className="bg-zinc-800/50 p-4 rounded-lg border border-zinc-700">
<p className="text-sm text-zinc-400 mb-1">Performance</p>
<p className="text-2xl font-bold text-blue-400">{project.businessImpact.performanceGain}</p>
</div>
)}
{project.businessImpact.usersImpacted && (
<div className="bg-zinc-800/50 p-4 rounded-lg border border-zinc-700">
<p className="text-sm text-zinc-400 mb-1">Users Impacted</p>
<p className="text-2xl font-bold text-purple-400">{project.businessImpact.usersImpacted}</p>
</div>
)}
</div>
</section>
{/* Technology Stack with Keywords */}
<section className="tech-stack">
<h3 className="text-xl font-semibold text-white mb-3">Technologies Used</h3>
<div className="flex flex-wrap gap-2">
{project.technologies.map(tech => (
<span
key={tech}
className="tech-tag px-3 py-1 bg-zinc-800 border border-zinc-700 text-zinc-300 rounded-full text-sm hover:border-zinc-600 transition-colors"
>
{tech}
</span>
))}
</div>
</section>
{/* Key Features */}
{project.features && project.features.length > 0 && (
<section className="features">
<h3 className="text-xl font-semibold text-white mb-3">Key Features</h3>
<ul className="space-y-2">
{project.features.map((feature, index) => (
<li key={index} className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span className="text-zinc-300">{feature}</span>
</li>
))}
</ul>
</section>
)}
{/* Code Quality Showcase */}
{project.codeExample && (
<section className="code-showcase">
<h3 className="text-xl font-semibold text-white mb-3">Code Example</h3>
<pre className="bg-zinc-950 p-4 rounded-lg overflow-x-auto border border-zinc-800">
<code className="text-sm text-zinc-300 font-mono">
{project.codeExample}
</code>
</pre>
</section>
)}
{/* Links */}
<div className="project-links flex gap-4 pt-4">
{project.liveUrl && (
<a
href={project.liveUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
<Globe className="h-4 w-4" />
View Live Project
</a>
)}
{project.githubUrl && (
<a
href={project.githubUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-white border border-zinc-700 rounded-lg transition-colors"
>
<Github className="h-4 w-4" />
View on GitHub
</a>
)}
</div>
</div>
</article>
);
}
-44
View File
@@ -1,44 +0,0 @@
interface ResponsiveImageProps {
src: string;
alt: string;
className?: string;
sizes?: string;
loading?: 'lazy' | 'eager';
fetchpriority?: 'high' | 'low' | 'auto';
}
export default function ResponsiveImage({
src,
alt,
className = '',
sizes = '100vw',
loading = 'lazy',
fetchpriority = 'auto'
}: ResponsiveImageProps) {
// Generate Unsplash responsive URLs
const generateSrcSet = (url: string) => {
if (!url.includes('unsplash.com')) return undefined;
const widths = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
return widths
.map((w) => {
const modifiedUrl = url.replace(/w=\d+/, `w=${w}`);
return `${modifiedUrl} ${w}w`;
})
.join(', ');
};
const srcSet = generateSrcSet(src);
return (
<img
src={src}
alt={alt}
className={className}
loading={loading}
fetchpriority={fetchpriority}
sizes={sizes}
srcSet={srcSet}
/>
);
}
@@ -1,52 +0,0 @@
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom';
import { AlertTriangle, Home, ArrowLeft } from 'lucide-react';
const RouteErrorBoundary = () => {
const error = useRouteError();
const navigate = useNavigate();
let title = 'Something went wrong';
let message = 'An unexpected error occurred';
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
title = 'Page not found';
message = "The page you're looking for doesn't exist or has been moved.";
} else {
title = `Error ${error.status}`;
message = error.statusText || 'An error occurred while processing your request.';
}
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
}
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-black">
<div className="max-w-md w-full text-center">
<AlertTriangle className="h-12 w-12 text-orange-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold mb-4">{title}</h1>
<p className="text-white/70 mb-8">{message}</p>
<div className="flex items-center justify-center gap-4">
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 bg-gray-800 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-black"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</button>
<button
onClick={() => navigate('/')}
className="inline-flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-black"
>
<Home className="h-4 w-4" />
Home Page
</button>
</div>
</div>
</div>
);
};
export default RouteErrorBoundary;
-183
View File
@@ -1,183 +0,0 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { HreflangTags } from './HreflangTags';
import { seoContent as seoContentDe } from '../i18n/locales/de/seo-content';
import { seoContent as seoContentEn } from '../i18n/locales/en/seo-content';
import { seoContent as seoContentSr } from '../i18n/locales/sr/seo-content';
import { meta as metaDe } from '../i18n/locales/de/meta';
import { meta as metaEn } from '../i18n/locales/en/meta';
import { meta as metaSr } from '../i18n/locales/sr/meta';
import { AutoBreadcrumbs } from './schemas/BreadcrumbSchema';
interface SEOProps {
title?: string;
description?: string;
image?: string;
article?: boolean;
schema?: object;
keywords?: string;
author?: string;
}
const SEO: React.FC<SEOProps> = ({
title,
description,
image = '/portrait.jpg',
article = false,
schema,
keywords,
author = 'Damjan Savić',
}) => {
// Nur i18n wird benötigt t wurde entfernt, da es nicht genutzt wird.
const { i18n } = useTranslation();
const siteTitle = 'Damjan Savić';
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
const siteUrl = 'https://damjan-savic.com';
const currentUrl = typeof window !== 'undefined' ? window.location.href : siteUrl;
const currentLanguage = i18n.language;
// SEO-Content basierend auf Sprache auswählen
const seoContent = currentLanguage === 'de' ? seoContentDe :
currentLanguage === 'sr' ? seoContentSr :
seoContentEn;
const meta = currentLanguage === 'de' ? metaDe :
currentLanguage === 'sr' ? metaSr :
metaEn;
const defaultDescription = description || seoContent.hero.description;
const defaultKeywords = keywords || meta.site.keywords;
// Default schema für die Website
const defaultSchema = {
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Damjan Savić',
alternateName: 'Damjan Savic',
url: siteUrl,
image: `${siteUrl}${image}`,
description: defaultDescription,
sameAs: [
'https://linkedin.com/in/damjansavic',
'https://github.com/damjansavic'
],
jobTitle: currentLanguage === 'de' ?
['Senior Fullstack Entwickler', 'Digital Solutions Consultant', 'Software Architekt', 'KI/AI Spezialist'] :
currentLanguage === 'sr' ?
['Старији програмер пуног стека', 'Консултант за дигитална решења', 'Софтверски архитекта'] :
['Senior Fullstack Developer', 'Digital Solutions Consultant', 'Software Architect', 'AI Specialist'],
worksFor: {
'@type': 'Organization',
name: 'CoderConda'
},
knowsAbout: [
'Python Development',
'JavaScript Development',
'React.js',
'Next.js',
'TypeScript',
'Electron Desktop Applications',
'Künstliche Intelligenz (KI/AI)',
'OLLAMA AI/ML',
'ERP Systems Integration',
'E-Commerce Development',
'Process Automation',
'Backend Development',
'Frontend Development',
'Full Stack Development'
],
hasSkill: [
{
'@type': 'DefinedTerm',
name: 'Python Development',
description: 'Expert-level Python programming for automation, backend development, and AI/ML applications'
},
{
'@type': 'DefinedTerm',
name: 'JavaScript/TypeScript Development',
description: 'Full-stack JavaScript development with React, Next.js, Node.js, and TypeScript'
},
{
'@type': 'DefinedTerm',
name: 'AI/ML with OLLAMA',
description: 'Implementation of AI solutions using OLLAMA for local language models'
},
{
'@type': 'DefinedTerm',
name: 'ERP & E-Commerce Integration',
description: 'Custom ERP system development and e-commerce platform integration'
}
],
address: {
'@type': 'PostalAddress',
addressCountry: 'DE',
addressLocality: 'Köln'
}
};
// Sicherstellen, dass supportedLngs ein Array ist, bevor .filter verwendet wird
const alternateUrls: Array<{ hrefLang: string; href: string }> = Array.isArray(i18n.options.supportedLngs)
? i18n.options.supportedLngs
.filter((lng: string) => lng !== 'cimode')
.map((lng: string) => ({
hrefLang: lng === 'de' ? 'de-DE' : lng === 'sr' ? 'sr-RS' : 'en-US',
href: lng === 'de' ?
`${siteUrl}${window.location.pathname}` :
`${siteUrl}/${lng}${window.location.pathname}`,
}))
: [];
return (
<>
<HreflangTags />
<AutoBreadcrumbs />
<Helmet>
{/* Basic meta tags */}
<html lang={currentLanguage} />
<title>{fullTitle}</title>
<meta name="description" content={defaultDescription} />
<meta name="image" content={image} />
<link rel="canonical" href={currentUrl} />
{/* Open Graph meta tags */}
<meta property="og:url" content={currentUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={defaultDescription} />
<meta property="og:image" content={image} />
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:site_name" content={siteTitle} />
<meta property="og:locale" content={
currentLanguage === 'de' ? 'de_DE' :
currentLanguage === 'sr' ? 'sr_RS' :
'en_US'
} />
{alternateUrls?.map(
({ hrefLang }: { hrefLang: string; href: string }) => (
<meta key={hrefLang} property="og:locale:alternate" content={hrefLang} />
)
)}
{/* Twitter Card meta tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={defaultDescription} />
<meta name="twitter:image" content={image} />
{/* Additional meta tags */}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta
name="keywords"
content={defaultKeywords}
/>
<meta name="author" content={author} />
{/* Schema.org markup */}
<script type="application/ld+json">
{JSON.stringify(schema || defaultSchema)}
</script>
</Helmet>
</>
);
};
export default SEO;
-44
View File
@@ -1,44 +0,0 @@
import { motion, useAnimation } from "framer-motion"
import { useEffect, useRef } from "react"
interface ScrollAnimationProps {
children: React.ReactNode
delay?: number
}
const ScrollAnimation: React.FC<ScrollAnimationProps> = ({ children, delay = 0 }) => {
const controls = useAnimation()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
controls.start({ opacity: 1, y: 0, transition: { duration: 0.5, delay: delay } })
}
},
{
threshold: 0.5,
},
)
if (ref.current) {
observer.observe(ref.current)
}
return () => {
if (ref.current) {
observer.unobserve(ref.current)
}
}
}, [controls, delay])
return (
<motion.div ref={ref} initial={{ opacity: 0, y: 20 }} animate={controls}>
{children}
</motion.div>
)
}
export default ScrollAnimation
-56
View File
@@ -1,56 +0,0 @@
// ScrollContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';
interface ScrollContextType {
isTransitioning: boolean;
setIsTransitioning: (value: boolean) => void;
isMenuOpen: boolean;
setIsMenuOpen: (value: boolean) => void;
lockScroll: () => void;
unlockScroll: () => void;
}
const ScrollContext = createContext<ScrollContextType | undefined>(undefined);
export const ScrollProvider = ({ children }: { children: React.ReactNode }) => {
const [isTransitioning, setIsTransitioning] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const lockScroll = useCallback(() => {
const scrollY = window.scrollY;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.width = '100%';
}, []);
const unlockScroll = useCallback(() => {
const scrollY = document.body.style.top;
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
window.scrollTo(0, parseInt(scrollY || '0') * -1);
}, []);
return (
<ScrollContext.Provider
value={{
isTransitioning,
setIsTransitioning,
isMenuOpen,
setIsMenuOpen,
lockScroll,
unlockScroll
}}
>
{children}
</ScrollContext.Provider>
);
};
export const useScrollContext = () => {
const context = useContext(ScrollContext);
if (context === undefined) {
throw new Error('useScrollContext must be used within a ScrollProvider');
}
return context;
};
-104
View File
@@ -1,104 +0,0 @@
import React, { useState, useRef } from 'react';
import { Share2, X, Twitter, Facebook, Linkedin, Link as LinkIcon } from 'lucide-react';
import { useOnClickOutside } from '../hooks/useOnClickOutside';
interface ShareMenuProps {
title: string;
url: string;
description: string;
}
const ShareMenu: React.FC<ShareMenuProps> = ({ title, url, description }) => {
const [isOpen, setIsOpen] = useState(false);
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied'>('idle');
const menuRef = useRef<HTMLDivElement>(null);
useOnClickOutside(menuRef, () => setIsOpen(false));
const shareLinks = [
{
name: 'Twitter',
icon: Twitter,
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
label: 'Share on Twitter'
},
{
name: 'Facebook',
icon: Facebook,
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
label: 'Share on Facebook'
},
{
name: 'LinkedIn',
icon: Linkedin,
href: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent(description)}`,
label: 'Share on LinkedIn'
},
];
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(url);
setCopyStatus('copied');
setTimeout(() => setCopyStatus('idle'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-gray-300 hover:text-white focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-black transition-colors p-2 rounded-full hover:bg-white/10"
aria-expanded={isOpen}
aria-haspopup="menu"
aria-label="Share this content"
>
{isOpen ? <X className="h-5 w-5" aria-hidden="true" /> : <Share2 className="h-5 w-5" aria-hidden="true" />}
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-48 rounded-lg bg-gray-900 shadow-lg ring-1 ring-black ring-opacity-5 z-50"
role="menu"
aria-orientation="vertical"
onKeyDown={handleKeyDown}
>
<div className="py-1">
{shareLinks.map((link) => (
<a
key={link.name}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
aria-label={link.label}
>
<link.icon className="h-4 w-4 mr-3" aria-hidden="true" />
{link.name}
</a>
))}
<button
onClick={copyToClipboard}
className="flex items-center w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
>
<LinkIcon className="h-4 w-4 mr-3" aria-hidden="true" />
{copyStatus === 'copied' ? 'Copied!' : 'Copy Link'}
</button>
</div>
</div>
)}
</div>
);
};
export default ShareMenu
-52
View File
@@ -1,52 +0,0 @@
import React from 'react';
interface SkeletonProps {
className?: string;
}
const Skeleton: React.FC<SkeletonProps> = ({ className }) => (
<div className={`animate-pulse bg-gray-700/50 rounded ${className}`}></div>
);
export const BlogPostSkeleton = () => (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-3/4 mb-4" />
<Skeleton className="h-4 w-1/4 mb-8" />
<Skeleton className="h-[400px] w-full mb-8 rounded-xl" />
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
</div>
);
export const ProjectSkeleton = () => (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-2/3 mb-4" />
<div className="flex gap-4 mb-8">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="h-[400px] w-full mb-8 rounded-xl" />
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
</div>
);
export const PortfolioSkeleton = () => (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-48 mx-auto mb-12" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div key={i} className="aspect-[4/3]">
<Skeleton className="w-full h-full rounded-xl" />
</div>
))}
</div>
</div>
);
-60
View File
@@ -1,60 +0,0 @@
import React from 'react';
import { motion } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
interface Skill {
name: string;
level: number;
category: string;
description: string;
}
interface SkillsMatrixProps {
skills: Skill[];
}
const SkillsMatrix: React.FC<SkillsMatrixProps> = ({ skills }) => {
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.1,
});
const categories = Array.from(new Set(skills.map(skill => skill.category)));
return (
<div ref={ref} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{categories.map((category, categoryIndex) => (
<div key={category} className="bg-gray-900/50 p-6 rounded-xl">
<h3 className="text-xl font-semibold mb-6">{category}</h3>
<div className="space-y-6">
{skills
.filter(skill => skill.category === category)
.map((skill, skillIndex) => (
<div key={skill.name}>
<div className="flex justify-between mb-2">
<div className="group relative">
<span>{skill.name}</span>
<div className="absolute bottom-full left-0 mb-2 w-64 p-4 bg-gray-800 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<p className="text-sm text-white/80">{skill.description}</p>
</div>
</div>
<span>{skill.level}%</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<motion.div
className="h-full bg-orange-500"
initial={{ width: 0 }}
animate={inView ? { width: `${skill.level}%` } : { width: 0 }}
transition={{ duration: 1, delay: categoryIndex * 0.2 + skillIndex * 0.1 }}
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
);
};
export default SkillsMatrix;
-23
View File
@@ -1,23 +0,0 @@
import React, { useState } from 'react';
const SkipToContent: React.FC = () => {
const [isFocused, setIsFocused] = useState(false);
return (
<a
href="#main-content"
className={`
fixed top-4 left-4 px-4 py-2 bg-orange-500 text-white rounded-lg
transform transition-transform duration-200
${isFocused ? 'translate-y-0' : '-translate-y-full'}
focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2
`}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
Skip to main content
</a>
);
};
export default SkipToContent
-51
View File
@@ -1,51 +0,0 @@
import React from 'react';
import { Star, Quote } from 'lucide-react';
import ImageWithFallback from './ImageWithFallback';
interface TestimonialProps {
name: string;
role: string;
company: string;
image: string;
content: string;
rating: number;
}
const TestimonialCard: React.FC<TestimonialProps> = ({
name,
role,
company,
image,
content,
rating
}) => {
return (
<div className="bg-gray-900/50 p-6 rounded-xl relative">
<Quote className="absolute top-4 right-4 h-8 w-8 text-orange-500/20" />
<div className="flex items-center space-x-4 mb-4">
<ImageWithFallback
src={image}
alt={name}
className="w-12 h-12 rounded-full"
loading="lazy"
/>
<div>
<h3 className="font-semibold">{name}</h3>
<p className="text-sm text-white/60">{role} at {company}</p>
</div>
</div>
<div className="flex mb-4">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${i < rating ? 'text-orange-500' : 'text-gray-600'}`}
fill={i < rating ? 'currentColor' : 'none'}
/>
))}
</div>
<p className="text-white/80">{content}</p>
</div>
);
};
export default TestimonialCard;
-29
View File
@@ -1,29 +0,0 @@
import React from 'react';
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: string;
}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className = '', error, ...props }, ref) => {
return (
<div className="w-full">
<textarea
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-primary transition-all resize-none
disabled:opacity-50 disabled:cursor-not-allowed
${error ? 'border-red-500' : ''}
${className}`}
ref={ref}
{...props}
/>
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
</div>
);
}
);
Textarea.displayName = 'Textarea';
export default Textarea;
@@ -1,102 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
interface BreadcrumbItem {
name: string;
url: string;
}
interface BreadcrumbSchemaProps {
items: BreadcrumbItem[];
}
export function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": items.map((item, index) => ({
"@type": "ListItem",
"position": index + 1,
"name": item.name,
"item": item.url
}))
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Hook to generate breadcrumb items based on current route
export function useBreadcrumbs(pathname: string) {
const { t, i18n } = useTranslation();
const siteUrl = 'https://damjan-savic.com';
const currentLang = i18n.language;
const segments = pathname.split('/').filter(Boolean);
const breadcrumbs: BreadcrumbItem[] = [];
// Always add home
breadcrumbs.push({
name: t('navigation.home', 'Home'),
url: currentLang === 'de' ? siteUrl : `${siteUrl}/${currentLang}`
});
// Build breadcrumbs from path segments
let currentPath = currentLang === 'de' ? '' : `/${currentLang}`;
segments.forEach((segment, index) => {
// Skip language segment
if (index === 0 && ['en', 'de', 'sr'].includes(segment)) {
return;
}
currentPath += `/${segment}`;
// Map segment to readable name
let name = segment;
switch (segment) {
case 'portfolio':
name = t('navigation.portfolio', 'Portfolio');
break;
case 'blog':
name = t('navigation.blog', 'Blog');
break;
case 'about':
name = t('navigation.about', 'About');
break;
case 'contact':
name = t('navigation.contact', 'Contact');
break;
default:
// For dynamic segments like project names, format them nicely
name = segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
breadcrumbs.push({
name,
url: `${siteUrl}${currentPath}`
});
});
return breadcrumbs;
}
// Component with automatic breadcrumb generation
export function AutoBreadcrumbs() {
const pathname = window.location.pathname;
const breadcrumbs = useBreadcrumbs(pathname);
// Don't show breadcrumbs on home page
if (breadcrumbs.length <= 1) {
return null;
}
return <BreadcrumbSchema items={breadcrumbs} />;
}
-160
View File
@@ -1,160 +0,0 @@
import React from 'react';
interface HowToStep {
name: string;
text: string;
image?: string;
url?: string;
}
interface HowToSchemaProps {
title: string;
description: string;
totalTime?: string;
estimatedCost?: {
value: string;
currency: string;
};
supply?: string[];
tool?: string[];
steps: HowToStep[];
image?: string;
video?: {
name: string;
description: string;
thumbnailUrl: string;
uploadDate: string;
duration: string;
embedUrl: string;
};
}
export function HowToSchema({
title,
description,
totalTime,
estimatedCost,
supply,
tool,
steps,
image,
video
}: HowToSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "HowTo",
"name": title,
"description": description,
"image": image,
...(totalTime && { "totalTime": totalTime }),
...(estimatedCost && {
"estimatedCost": {
"@type": "MonetaryAmount",
"currency": estimatedCost.currency,
"value": estimatedCost.value
}
}),
...(supply && supply.length > 0 && {
"supply": supply.map(item => ({
"@type": "HowToSupply",
"name": item
}))
}),
...(tool && tool.length > 0 && {
"tool": tool.map(item => ({
"@type": "HowToTool",
"name": item
}))
}),
"step": steps.map((step, index) => ({
"@type": "HowToStep",
"name": step.name,
"text": step.text,
"position": index + 1,
...(step.image && { "image": step.image }),
...(step.url && { "url": step.url })
})),
...(video && {
"video": {
"@type": "VideoObject",
"name": video.name,
"description": video.description,
"thumbnailUrl": video.thumbnailUrl,
"uploadDate": video.uploadDate,
"duration": video.duration,
"embedUrl": video.embedUrl
}
})
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Example HowTo content for common queries
export const howToExamples = {
pythonAutomation: {
title: "Wie kann ich Python für Prozessautomatisierung einsetzen?",
description: "Schritt-für-Schritt Anleitung zur Automatisierung von Geschäftsprozessen mit Python",
totalTime: "PT30M",
tool: ["Python 3.8+", "pip", "Virtual Environment", "Code Editor (VS Code)"],
steps: [
{
name: "Python-Umgebung einrichten",
text: "Installieren Sie Python 3.8 oder höher und erstellen Sie eine virtuelle Umgebung mit 'python -m venv automation-env'"
},
{
name: "Notwendige Libraries installieren",
text: "Aktivieren Sie die virtuelle Umgebung und installieren Sie benötigte Pakete: pip install selenium pandas schedule requests"
},
{
name: "Automatisierungsskript erstellen",
text: "Erstellen Sie ein Python-Skript, das Ihre spezifischen Aufgaben automatisiert. Nutzen Sie Selenium für Web-Automation, Pandas für Datenverarbeitung"
},
{
name: "Zeitgesteuerte Ausführung einrichten",
text: "Verwenden Sie die Schedule-Library oder Cron-Jobs (Linux/Mac) bzw. Task Scheduler (Windows) für regelmäßige Ausführung"
},
{
name: "Monitoring und Logging implementieren",
text: "Fügen Sie Logging hinzu, um Fehler zu tracken und den Erfolg der Automatisierung zu überwachen"
}
]
},
ollamaSetup: {
title: "How to Set Up OLLAMA for Local AI Development",
description: "Complete guide to installing and using OLLAMA for privacy-focused AI applications",
totalTime: "PT45M",
tool: ["OLLAMA CLI", "Python 3.8+", "8GB+ RAM", "Terminal/Command Prompt"],
steps: [
{
name: "Install OLLAMA",
text: "Download and install OLLAMA from ollama.ai. For Mac/Linux: curl -fsSL https://ollama.ai/install.sh | sh"
},
{
name: "Download a Language Model",
text: "Pull a model like Llama 2: ollama pull llama2. This downloads the model locally to your machine"
},
{
name: "Test the Model",
text: "Run the model interactively: ollama run llama2. Ask a test question to verify it's working"
},
{
name: "Install Python Integration",
text: "Install the Python library: pip install ollama. This allows you to use OLLAMA in your Python applications"
},
{
name: "Create Your First Application",
text: "Write Python code to interact with OLLAMA: import ollama; response = ollama.chat(model='llama2', messages=[{'role': 'user', 'content': 'Hello'}])"
},
{
name: "Optimize for Production",
text: "Configure model parameters, implement caching, and set up proper error handling for production use"
}
]
}
};
@@ -1,158 +0,0 @@
import React from 'react';
export function LocalBusinessSchema() {
const localBusinessSchema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"@id": "https://damjan-savic.com/#localbusiness",
"name": "Damjan Savić - Senior Fullstack Developer & IT Consultant",
"alternateName": ["CoderConda", "Damjan Savic IT Services"],
"description": "Damjan Savić bietet professionelle IT-Dienstleistungen in Köln und Umgebung. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und digitale Transformation. Damjan Savić entwickelt maßgeschneiderte Lösungen für Ihr Unternehmen.",
"url": "https://damjan-savic.com",
"logo": {
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.png",
"width": "1000",
"height": "1000",
"caption": "Damjan Savić - Senior Fullstack Developer & IT Consultant Logo"
},
"image": "https://damjan-savic.com/logo.png",
"telephone": "+49-XXX-XXXXXXX",
"email": "info@damjan-savic.com",
"address": {
"@type": "PostalAddress",
"addressLocality": "Köln",
"addressRegion": "Nordrhein-Westfalen",
"postalCode": "50667",
"addressCountry": "DE",
"streetAddress": "Geschäftsadresse auf Anfrage"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "50.9375",
"longitude": "6.9603"
},
"priceRange": "€€€",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
}
],
"areaServed": [
{
"@type": "City",
"name": "Köln"
},
{
"@type": "State",
"name": "Nordrhein-Westfalen"
},
{
"@type": "Country",
"name": "Deutschland"
},
{
"@type": "Continent",
"name": "Europa"
}
],
"serviceArea": {
"@type": "GeoCircle",
"geoMidpoint": {
"@type": "GeoCoordinates",
"latitude": "50.9375",
"longitude": "6.9603"
},
"geoRadius": "500 km"
},
"makesOffer": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Enterprise Software Development",
"description": "Maßgeschneiderte Unternehmenssoftware von Damjan Savić"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "KI/AI Integration Services",
"description": "Integration von KI-Lösungen mit OLLAMA und LLMs durch Damjan Savić"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Cloud Architecture Consulting",
"description": "Cloud-native Lösungen und Migration von Damjan Savić"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Digital Transformation",
"description": "Digitale Transformation und Prozessoptimierung mit Damjan Savić"
}
}
],
"founder": {
"@id": "https://damjan-savic.com/#person"
},
"employee": {
"@id": "https://damjan-savic.com/#person"
},
"sameAs": [
"https://github.com/damjansavic",
"https://linkedin.com/in/damjansavic",
"https://twitter.com/damjansavic"
],
"knowsLanguage": ["de-DE", "en-US", "sr-RS"],
"paymentAccepted": ["Überweisung", "PayPal", "Rechnung"],
"currenciesAccepted": "EUR",
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "IT-Dienstleistungen von Damjan Savić",
"itemListElement": [
{
"@type": "Service",
"name": "Fullstack Web Development",
"description": "Entwicklung moderner Webanwendungen mit React, Python und TypeScript"
},
{
"@type": "Service",
"name": "API Development & Integration",
"description": "RESTful APIs, GraphQL und Microservices Architecture"
},
{
"@type": "Service",
"name": "DevOps & Cloud Infrastructure",
"description": "CI/CD, Docker, Kubernetes und Cloud-Deployment"
},
{
"@type": "Service",
"name": "E-Commerce Solutions",
"description": "Shopify, WooCommerce und Custom E-Commerce Plattformen"
},
{
"@type": "Service",
"name": "Business Process Automation",
"description": "Automatisierung von Geschäftsprozessen mit Python"
}
]
}
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessSchema) }}
/>
);
}
@@ -1,256 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export function LocalizedPersonSchema() {
const { i18n } = useTranslation();
const currentLanguage = i18n.language;
const personSchema = {
"@context": "https://schema.org",
"@type": "Person",
"@id": "https://damjan-savic.com/#person",
"name": currentLanguage === 'sr' ? "Дамјан Савић" : "Damjan Savić",
"alternateName": currentLanguage === 'sr' ? ["Damjan Savić", "Damjan Savic"] : "Damjan Savic",
"jobTitle": currentLanguage === 'de' ?
["AI & Automation Specialist", "Process Automation Specialist", "Fullstack Entwickler"] :
currentLanguage === 'sr' ?
["AI & Automation Specialist", "Specijalist za automatizaciju procesa", "Fullstack Developer"] :
["AI & Automation Specialist", "Process Automation Specialist", "Fullstack Developer"],
"description": currentLanguage === 'de' ?
"Damjan Savić ist AI & Automation Specialist. Spezialisiert auf KI-Agenten, Voice AI mit Vapi, Prozessautomatisierung mit n8n und Zapier. Entwickelt produktive Automatisierungslösungen und Voice-AI-Plattformen." :
currentLanguage === 'sr' ?
"Дамјан Савић је AI & Automation Specialist. Специјализован за AI агенте, Voice AI са Vapi, аутоматизацију процеса са n8n и Zapier. Развија продуктивна решења за аутоматизацију и Voice AI платформе." :
"Damjan Savić is an AI & Automation Specialist. Specialized in AI agents, Voice AI with Vapi, process automation with n8n and Zapier. Building production automation solutions and Voice AI platforms.",
"url": "https://damjan-savic.com",
"image": [
{
"@type": "ImageObject",
"url": "https://damjan-savic.com/portrait.jpg",
"caption": currentLanguage === 'sr' ? "Дамјан Савић портрет" : "Damjan Savić Portrait"
},
{
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.png",
"caption": currentLanguage === 'sr' ? "Дамјан Савић лого" : "Damjan Savić Logo"
}
],
"logo": "https://damjan-savic.com/logo.png",
"email": "info@damjan-savic.com",
"telephone": "+49-XXX-XXXXXXX",
"contactPoint": {
"@type": "ContactPoint",
"email": "info@damjan-savic.com",
"contactType": currentLanguage === 'de' ? "Geschäftsanfragen" :
currentLanguage === 'sr' ? "Пословни упити" :
"Business Inquiries",
"availableLanguage": ["de", "en", "sr"],
"areaServed": ["DE", "EU"],
"hoursAvailable": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
}
},
"sameAs": [
"https://github.com/damjansavic",
"https://linkedin.com/in/damjansavic",
"https://twitter.com/damjansavic",
"https://stackoverflow.com/users/damjansavic"
],
"knowsAbout": currentLanguage === 'de' ? [
"KI-Agenten Entwicklung",
"Voice AI (Vapi)",
"Prozessautomatisierung (n8n, Zapier)",
"GPT-4 & Claude API Integration",
"Python Entwicklung",
"TypeScript & React",
"Next.js",
"Web Scraping",
"WebSocket & Echtzeit-Anwendungen",
"Supabase & PostgreSQL",
"Backend Entwicklung",
"Frontend Entwicklung",
"Full Stack Entwicklung",
"Docker",
"Vercel Deployment"
] : currentLanguage === 'sr' ? [
"Развој AI агената",
"Voice AI (Vapi)",
"Аутоматизација процеса (n8n, Zapier)",
"GPT-4 & Claude API интеграција",
"Python развој",
"TypeScript & React",
"Next.js",
"Web Scraping",
"WebSocket & real-time апликације",
"Supabase & PostgreSQL",
"Backend развој",
"Frontend развој",
"Full Stack развој",
"Docker",
"Vercel Deployment"
] : [
"AI Agents Development",
"Voice AI (Vapi)",
"Process Automation (n8n, Zapier)",
"GPT-4 & Claude API Integration",
"Python Development",
"TypeScript & React",
"Next.js",
"Web Scraping",
"WebSocket & Real-time Applications",
"Supabase & PostgreSQL",
"Backend Development",
"Frontend Development",
"Full Stack Development",
"Docker",
"Vercel Deployment"
],
"address": {
"@type": "PostalAddress",
"addressLocality": currentLanguage === 'sr' ? "Келн" : currentLanguage === 'de' ? "Köln" : "Cologne",
"addressRegion": currentLanguage === 'sr' ? "Северна Рајна-Вестфалија" : currentLanguage === 'de' ? "Nordrhein-Westfalen" : "North Rhine-Westphalia",
"addressCountry": currentLanguage === 'sr' ? "Немачка" : currentLanguage === 'de' ? "DE" : "Germany"
},
"worksFor": {
"@type": "Organization",
"name": "Everlast Consulting GmbH",
"description": currentLanguage === 'de' ?
"Prozessautomatisierung und KI-Lösungen" :
currentLanguage === 'sr' ?
"Аутоматизација процеса и AI решења" :
"Process Automation and AI Solutions",
"url": "https://everlast-consulting.de"
},
"alumniOf": [
{
"@type": "EducationalOrganization",
"name": currentLanguage === 'sr' ? "Технички универзитет" : currentLanguage === 'de' ? "Technische Universität" : "Technical University",
"url": "https://www.tu.edu"
}
],
"award": currentLanguage === 'de' ? [
"Zertifizierter Cloud Architekt",
"Python Professional Zertifizierung",
"React Expert Zertifizierung"
] : currentLanguage === 'sr' ? [
"Сертификовани облак архитекта",
"Python професионална сертификација",
"React експерт сертификација"
] : [
"Certified Cloud Architect",
"Python Professional Certification",
"React Expert Certification"
],
"knowsLanguage": [
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Deutsch" : currentLanguage === 'sr' ? "Немачки" : "German",
"alternateName": "de"
},
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Englisch" : currentLanguage === 'sr' ? "Енглески" : "English",
"alternateName": "en"
},
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Serbisch" : currentLanguage === 'sr' ? "Српски" : "Serbian",
"alternateName": "sr"
},
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Französisch" : currentLanguage === 'sr' ? "Француски" : "French",
"alternateName": "fr"
},
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Spanisch" : currentLanguage === 'sr' ? "Шпански" : "Spanish",
"alternateName": "es"
},
{
"@type": "Language",
"name": currentLanguage === 'de' ? "Russisch" : currentLanguage === 'sr' ? "Руски" : "Russian",
"alternateName": "ru"
}
]
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personSchema) }}
/>
);
}
export function LocalizedWebsiteSchema() {
const { i18n } = useTranslation();
const currentLanguage = i18n.language;
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "https://damjan-savic.com/#website",
"url": "https://damjan-savic.com",
"name": currentLanguage === 'de' ?
"Damjan Savić - AI & Automation Specialist" :
currentLanguage === 'sr' ?
"Дамјан Савић - AI & Automation Specialist" :
"Damjan Savić - AI & Automation Specialist",
"alternateName": ["Damjan Savic Portfolio"],
"description": currentLanguage === 'de' ?
"Offizielle Website von Damjan Savić - AI & Automation Specialist. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und Fullstack Development." :
currentLanguage === 'sr' ?
"Званична веб страница Дамјана Савића - AI & Automation Specialist. Специјализован за AI агенте, Voice AI, аутоматизацију процеса са n8n и Fullstack развој." :
"Official website of Damjan Savić - AI & Automation Specialist. Specialized in AI agents, Voice AI, process automation with n8n, and Fullstack Development.",
"publisher": {
"@id": "https://damjan-savic.com/#person"
},
"potentialAction": {
"@type": "ContactAction",
"target": {
"@type": "EntryPoint",
"url": "https://damjan-savic.com/contact"
},
"name": currentLanguage === 'de' ?
"Damjan Savić kontaktieren" :
currentLanguage === 'sr' ?
"Контактирајте Дамјана Савића" :
"Contact Damjan Savić"
},
"keywords": currentLanguage === 'de' ?
"Damjan Savić, AI Automation Specialist, KI-Agenten, Voice AI, n8n, Prozessautomatisierung, TypeScript, Python, Next.js" :
currentLanguage === 'sr' ?
"Дамјан Савић, AI Automation Specialist, AI агенти, Voice AI, n8n, аутоматизација процеса, TypeScript, Python, Next.js" :
"Damjan Savić, AI Automation Specialist, AI Agents, Voice AI, n8n, Process Automation, TypeScript, Python, Next.js",
"dateCreated": "2020-01-01",
"dateModified": "2025-01-15",
"creator": {
"@id": "https://damjan-savic.com/#person"
},
"copyrightHolder": {
"@id": "https://damjan-savic.com/#person"
},
"copyrightYear": "2025",
"logo": {
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.png",
"width": "1000",
"height": "1000",
"caption": currentLanguage === 'sr' ? "Дамјан Савић лого" : "Damjan Savić Logo"
},
"image": "https://damjan-savic.com/logo.png",
"inLanguage": currentLanguage === 'de' ? ["de-DE"] :
currentLanguage === 'sr' ? ["sr-RS"] :
["en-US"]
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
);
}
@@ -1,175 +0,0 @@
import React from 'react';
export function PersonSchema() {
const personSchema = {
"@context": "https://schema.org",
"@type": "Person",
"@id": "https://damjan-savic.com/#person",
"name": "Damjan Savić",
"alternateName": "Damjan Savic",
"jobTitle": ["Senior Fullstack Developer", "Digital Solutions Consultant", "Software Architect", "KI/AI Spezialist"],
"description": "Damjan Savić ist ein Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Damjan Savić ist spezialisiert auf Python, JavaScript, React, Next.js, TypeScript und KI-Integration. Mit über 10 Jahren Erfahrung entwickelt Damjan Savić maßgeschneiderte Enterprise-Lösungen, moderne Web-Applikationen und innovative KI-gestützte Systeme für Unternehmen jeder Größe.",
"url": "https://damjan-savic.com",
"image": [
{
"@type": "ImageObject",
"url": "https://damjan-savic.com/portrait.jpg",
"caption": "Damjan Savić Portrait"
},
{
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.png",
"caption": "Damjan Savić Logo"
}
],
"logo": "https://damjan-savic.com/logo.png",
"email": "info@damjan-savic.com",
"telephone": "+49-XXX-XXXXXXX",
"contactPoint": {
"@type": "ContactPoint",
"email": "info@damjan-savic.com",
"contactType": "Business Inquiries",
"availableLanguage": ["de", "en", "sr"],
"areaServed": ["DE", "EU"],
"hoursAvailable": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
}
},
"sameAs": [
"https://github.com/damjansavic",
"https://linkedin.com/in/damjansavic",
"https://twitter.com/damjansavic",
"https://stackoverflow.com/users/damjansavic"
],
"knowsAbout": [
"Python Development",
"JavaScript Development",
"React.js",
"Next.js",
"TypeScript",
"Electron Desktop Applications",
"Künstliche Intelligenz (KI/AI)",
"OLLAMA AI/ML Integration",
"Machine Learning",
"Large Language Models (LLM)",
"ERP Systems Integration",
"SAP Integration",
"E-Commerce Development",
"Shopify Development",
"WooCommerce Integration",
"Process Automation",
"Workflow Optimization",
"Backend Development",
"Frontend Development",
"Full Stack Development",
"Cloud Architecture",
"AWS Services",
"Docker & Kubernetes",
"Microservices Architecture",
"DevOps & CI/CD",
"Agile Development",
"Software Architecture"
],
"hasSkill": [
{
"@type": "DefinedTerm",
"name": "Python Development",
"description": "Damjan Savić bietet Expert-level Python programming for automation, backend development, and AI/ML applications"
},
{
"@type": "DefinedTerm",
"name": "JavaScript/TypeScript Development",
"description": "Full-stack JavaScript development with React, Next.js, Node.js, and TypeScript"
},
{
"@type": "DefinedTerm",
"name": "AI/ML with OLLAMA",
"description": "Implementation of AI solutions using OLLAMA for local language models"
},
{
"@type": "DefinedTerm",
"name": "ERP & E-Commerce Integration",
"description": "Custom ERP system development and e-commerce platform integration"
},
{
"@type": "DefinedTerm",
"name": "Electron Desktop Development",
"description": "Cross-platform desktop application development with Electron and modern web technologies"
},
{
"@type": "DefinedTerm",
"name": "Process Automation",
"description": "Business process automation using Python, APIs, and custom integration solutions"
}
],
"address": {
"@type": "PostalAddress",
"addressLocality": "Köln",
"addressRegion": "NRW",
"addressCountry": "DE"
},
"worksFor": {
"@type": "Organization",
"name": "CoderConda",
"description": "Moderne Softwareentwicklung, KI-Integration und Prozessautomatisierung von Damjan Savić",
"url": "https://damjan-savic.com",
"founder": "Damjan Savić",
"foundingDate": "2020",
"slogan": "Innovative Lösungen für digitale Herausforderungen"
},
"alumniOf": [
{
"@type": "EducationalOrganization",
"name": "Technische Universität",
"url": "https://www.tu.edu"
}
],
"award": [
"Certified Cloud Architect",
"Python Professional Certification",
"React Expert Certification"
],
"knowsLanguage": [
{
"@type": "Language",
"name": "Deutsch",
"alternateName": "de"
},
{
"@type": "Language",
"name": "English",
"alternateName": "en"
},
{
"@type": "Language",
"name": "Srpski",
"alternateName": "sr"
},
{
"@type": "Language",
"name": "Français",
"alternateName": "fr"
},
{
"@type": "Language",
"name": "Español",
"alternateName": "es"
},
{
"@type": "Language",
"name": "Russian",
"alternateName": "ru"
}
]
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personSchema) }}
/>
);
}
@@ -1,58 +0,0 @@
import React from 'react';
interface Project {
title: string;
description: string;
category: string;
technologies: string[];
dateCreated: string;
url?: string;
liveUrl?: string;
githubUrl?: string;
screenshotUrl?: string;
features?: string[];
technicalChallenge?: string;
businessImpact?: {
roi?: string;
performanceGain?: string;
usersImpacted?: string;
};
}
interface ProjectSchemaProps {
project: Project;
}
export function ProjectSchema({ project }: ProjectSchemaProps) {
const schema = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": project.title,
"description": project.description,
"applicationCategory": project.category,
"operatingSystem": "Web-based",
"programmingLanguage": project.technologies,
"author": {
"@type": "Person",
"name": "Damjan Savić",
"@id": "https://damjan-savic.com/#person"
},
"dateCreated": project.dateCreated,
"url": project.url || project.liveUrl,
"screenshot": project.screenshotUrl,
"featureList": project.features || [],
"softwareRequirements": "Modern web browser with JavaScript enabled",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
}
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
@@ -1,138 +0,0 @@
import React from 'react';
export function ServiceSchema() {
const services = [
{
"@context": "https://schema.org",
"@type": "Service",
"@id": "https://damjan-savic.com/#service-fullstack",
"name": "Fullstack Web Development von Damjan Savić",
"description": "Professionelle Fullstack-Entwicklung durch Damjan Savić. Moderne Webanwendungen mit React, Next.js, Python, Django, FastAPI und TypeScript. Von der Konzeption bis zum Deployment entwickelt Damjan Savić skalierbare Lösungen.",
"provider": {
"@id": "https://damjan-savic.com/#person"
},
"serviceType": "Software Development",
"areaServed": ["Köln", "Deutschland", "Europa", "Weltweit"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Fullstack Development Services",
"itemListElement": [
"Frontend Development (React, Vue, Angular)",
"Backend Development (Python, Node.js)",
"Database Design (PostgreSQL, MongoDB)",
"API Development (REST, GraphQL)",
"Progressive Web Apps (PWA)",
"Single Page Applications (SPA)"
]
}
},
{
"@context": "https://schema.org",
"@type": "Service",
"@id": "https://damjan-savic.com/#service-ai",
"name": "KI/AI Integration Services von Damjan Savić",
"description": "Damjan Savić integriert künstliche Intelligenz in Ihre Geschäftsprozesse. Spezialisiert auf OLLAMA, Large Language Models (LLMs), Machine Learning und Computer Vision. Damjan Savić entwickelt maßgeschneiderte KI-Lösungen.",
"provider": {
"@id": "https://damjan-savic.com/#person"
},
"serviceType": "AI/ML Development",
"areaServed": ["Köln", "Deutschland", "Europa", "Weltweit"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "AI Integration Services",
"itemListElement": [
"OLLAMA Integration",
"Large Language Model Implementation",
"Natural Language Processing (NLP)",
"Computer Vision Solutions",
"Predictive Analytics",
"AI-powered Automation"
]
}
},
{
"@context": "https://schema.org",
"@type": "Service",
"@id": "https://damjan-savic.com/#service-cloud",
"name": "Cloud Architecture & DevOps von Damjan Savić",
"description": "Cloud-native Lösungen und DevOps-Expertise von Damjan Savić. AWS, Azure, Google Cloud Platform, Docker, Kubernetes und CI/CD Pipelines. Damjan Savić migriert und optimiert Ihre Cloud-Infrastruktur.",
"provider": {
"@id": "https://damjan-savic.com/#person"
},
"serviceType": "Cloud Consulting",
"areaServed": ["Köln", "Deutschland", "Europa", "Weltweit"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Cloud & DevOps Services",
"itemListElement": [
"AWS Architecture & Migration",
"Docker Containerization",
"Kubernetes Orchestration",
"CI/CD Pipeline Setup",
"Infrastructure as Code (IaC)",
"Cloud Cost Optimization"
]
}
},
{
"@context": "https://schema.org",
"@type": "Service",
"@id": "https://damjan-savic.com/#service-ecommerce",
"name": "E-Commerce Development von Damjan Savić",
"description": "Damjan Savić entwickelt moderne E-Commerce-Lösungen. Shopify, WooCommerce, Magento und Custom-Shops. Von der Produktverwaltung bis zur Payment-Integration bietet Damjan Savić vollständige E-Commerce-Services.",
"provider": {
"@id": "https://damjan-savic.com/#person"
},
"serviceType": "E-Commerce Development",
"areaServed": ["Köln", "Deutschland", "Europa", "Weltweit"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "E-Commerce Services",
"itemListElement": [
"Shopify Store Development",
"WooCommerce Integration",
"Payment Gateway Integration",
"Inventory Management Systems",
"Order Processing Automation",
"Multi-Channel Commerce"
]
}
},
{
"@context": "https://schema.org",
"@type": "Service",
"@id": "https://damjan-savic.com/#service-automation",
"name": "Process Automation von Damjan Savić",
"description": "Geschäftsprozess-Automatisierung durch Damjan Savić. Python-basierte Automatisierungslösungen, Workflow-Optimierung und Integration von Systemen. Damjan Savić steigert Ihre Effizienz durch intelligente Automatisierung.",
"provider": {
"@id": "https://damjan-savic.com/#person"
},
"serviceType": "Business Process Automation",
"areaServed": ["Köln", "Deutschland", "Europa", "Weltweit"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Automation Services",
"itemListElement": [
"Python Automation Scripts",
"Workflow Automation",
"Data Processing Pipelines",
"API Integration & Orchestration",
"Report Generation & Analytics",
"Task Scheduling & Monitoring"
]
}
}
];
return (
<>
{services.map((service, index) => (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(service) }}
/>
))}
</>
);
}
@@ -1,136 +0,0 @@
import React from 'react';
export function WebsiteSchema() {
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "https://damjan-savic.com/#website",
"url": "https://damjan-savic.com",
"name": "Damjan Savić - Senior Fullstack Developer & Digital Solutions Consultant",
"alternateName": ["Damjan Savic Portfolio", "CoderConda"],
"description": "Offizielle Website von Damjan Savić - Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und moderne Web-Technologien. Entdecken Sie innovative Lösungen von Damjan Savić.",
"publisher": {
"@id": "https://damjan-savic.com/#person"
},
"potentialAction": {
"@type": "ContactAction",
"target": {
"@type": "EntryPoint",
"url": "https://damjan-savic.com/contact"
},
"name": "Damjan Savić kontaktieren"
},
"keywords": "Damjan Savić, Senior Fullstack Developer, Digital Solutions Consultant, Software Architect Köln, Python Experte, React Spezialist, KI Integration",
"dateCreated": "2020-01-01",
"dateModified": "2025-01-15",
"creator": {
"@id": "https://damjan-savic.com/#person"
},
"copyrightHolder": {
"@id": "https://damjan-savic.com/#person"
},
"copyrightYear": "2025",
"logo": {
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.png",
"width": "1000",
"height": "1000",
"caption": "Damjan Savić Logo"
},
"image": "https://damjan-savic.com/logo.png",
"inLanguage": ["de-DE", "en-US", "sr-RS"]
};
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Damjan Savić",
"item": "https://damjan-savic.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Über Damjan Savić",
"item": "https://damjan-savic.com/about"
},
{
"@type": "ListItem",
"position": 3,
"name": "Portfolio von Damjan Savić",
"item": "https://damjan-savic.com/portfolio"
},
{
"@type": "ListItem",
"position": 4,
"name": "Blog von Damjan Savić",
"item": "https://damjan-savic.com/blog"
},
{
"@type": "ListItem",
"position": 5,
"name": "Kontakt Damjan Savić",
"item": "https://damjan-savic.com/contact"
}
]
};
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Wer ist Damjan Savić?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Damjan Savić ist ein Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln mit über 10 Jahren Erfahrung. Damjan Savić ist spezialisiert auf Enterprise Software Development, Cloud Architecture, KI-Integration mit OLLAMA und moderne Web-Technologien wie Python, React, TypeScript und Next.js. Als Software Architect entwickelt Damjan Savić skalierbare Lösungen für komplexe Geschäftsanforderungen."
}
},
{
"@type": "Question",
"name": "Welche Dienstleistungen bietet Damjan Savić an?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Damjan Savić bietet umfassende IT-Dienstleistungen: Enterprise Software Development, Cloud-native Lösungen (AWS, Azure), Microservices Architecture, KI/ML-Integration mit OLLAMA und LLMs, SAP/ERP-Systemintegration, E-Commerce-Plattformen (Shopify, WooCommerce), Business Process Automation, DevOps & CI/CD, Progressive Web Apps, Electron Desktop-Anwendungen und Digital Transformation Consulting. Damjan Savić arbeitet mit modernsten Technologien wie Python, React, TypeScript, Docker, Kubernetes und mehr."
}
},
{
"@type": "Question",
"name": "Wie kann ich Damjan Savić kontaktieren?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Sie können Damjan Savić auf verschiedenen Wegen erreichen: Über das Kontaktformular auf https://damjan-savic.com/contact, per E-Mail an info@damjan-savic.com, auf LinkedIn unter https://linkedin.com/in/damjansavic, auf GitHub unter https://github.com/damjansavic oder telefonisch während der Geschäftszeiten (Mo-Fr, 9-18 Uhr MEZ). Damjan Savić antwortet in der Regel innerhalb von 24 Stunden auf Anfragen."
}
},
{
"@type": "Question",
"name": "Wo ist Damjan Savić ansässig?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Damjan Savić ist in Köln, Nordrhein-Westfalen, Deutschland ansässig. Als erfahrener Remote-First Developer arbeitet Damjan Savić erfolgreich mit Kunden aus ganz Deutschland, Europa und weltweit zusammen. Damjan Savić bietet flexible Zusammenarbeitsmodelle: vor Ort in Köln und Umgebung, hybrid oder vollständig remote. Die Arbeitssprachen von Damjan Savić sind Deutsch, Englisch und Serbisch."
}
}
]
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
</>
);
}