Implementierung - SEO - 01.08.2025
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export function FAQSection() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [openItems, setOpenItems] = useState<number[]>([]);
|
||||
|
||||
const faqs: FAQItem[] = i18n.language === 'de' ? [
|
||||
{
|
||||
question: "Wie kann ich Python für Prozessautomatisierung einsetzen?",
|
||||
answer: "Python eignet sich hervorragend für Prozessautomatisierung durch Libraries wie Selenium für Web-Automation, Pandas für Datenverarbeitung und Schedule für zeitgesteuerte Aufgaben. Ich entwickle maßgeschneiderte Automatisierungslösungen für Ihre Geschäftsprozesse.",
|
||||
category: "python"
|
||||
},
|
||||
{
|
||||
question: "Was macht OLLAMA ideal für lokale KI-Entwicklung?",
|
||||
answer: "OLLAMA ermöglicht das Ausführen großer Sprachmodelle lokal, was Datenschutz gewährleistet und API-Kosten reduziert. Ich spezialisiere mich auf die Integration von OLLAMA in Anwendungen für individuelle KI-Lösungen ohne Cloud-Abhängigkeiten.",
|
||||
category: "ai"
|
||||
},
|
||||
{
|
||||
question: "Welche Vorteile bieten Electron Desktop Apps?",
|
||||
answer: "Electron ermöglicht die Entwicklung plattformübergreifender Desktop-Anwendungen mit Web-Technologien. Sie erhalten eine native App für Windows, Mac und Linux aus einer einzigen Codebasis. Ideal für Tools mit komplexer UI und Offline-Funktionalität.",
|
||||
category: "electron"
|
||||
},
|
||||
{
|
||||
question: "Wie lange dauert die Entwicklung einer Web-Anwendung?",
|
||||
answer: "Die Entwicklungsdauer hängt vom Umfang ab. Ein MVP kann in 4-8 Wochen entwickelt werden, während komplexe Enterprise-Anwendungen 3-6 Monate benötigen. Ich arbeite agil und liefere in 2-Wochen-Sprints nutzbare Zwischenergebnisse.",
|
||||
category: "general"
|
||||
},
|
||||
{
|
||||
question: "Können Sie bestehende Python-Skripte in Web-Apps umwandeln?",
|
||||
answer: "Ja, ich kann Ihre Python-Skripte in moderne Web-Anwendungen mit FastAPI oder Django Backend und React Frontend transformieren. Dies ermöglicht Multi-User-Zugriff, bessere UI/UX und zentrale Datenverwaltung.",
|
||||
category: "python"
|
||||
},
|
||||
{
|
||||
question: "Wie integrieren Sie KI in bestehende Geschäftsprozesse?",
|
||||
answer: "Ich analysiere Ihre Workflows und identifiziere Automatisierungspotenziale. Mit OLLAMA oder anderen KI-Modellen entwickle ich Lösungen für Textanalyse, Dokumentenverarbeitung, Kundenservice-Automation oder Datenextraktion - alles datenschutzkonform on-premise.",
|
||||
category: "ai"
|
||||
}
|
||||
] : [
|
||||
{
|
||||
question: "How can I use Python for process automation?",
|
||||
answer: "Python excels at process automation through libraries like Selenium for web automation, Pandas for data processing, and Schedule for timed tasks. I develop custom automation solutions tailored to your business processes.",
|
||||
category: "python"
|
||||
},
|
||||
{
|
||||
question: "What makes OLLAMA ideal for local AI development?",
|
||||
answer: "OLLAMA enables running large language models locally, ensuring data privacy and reducing API costs. I specialize in integrating OLLAMA into applications for custom AI solutions without cloud dependencies.",
|
||||
category: "ai"
|
||||
},
|
||||
{
|
||||
question: "What are the advantages of Electron desktop apps?",
|
||||
answer: "Electron enables cross-platform desktop app development using web technologies. You get a native app for Windows, Mac, and Linux from a single codebase. Ideal for tools requiring complex UI and offline functionality.",
|
||||
category: "electron"
|
||||
},
|
||||
{
|
||||
question: "How long does web application development take?",
|
||||
answer: "Development time depends on scope. An MVP can be developed in 4-8 weeks, while complex enterprise applications require 3-6 months. I work agile and deliver usable results in 2-week sprints.",
|
||||
category: "general"
|
||||
},
|
||||
{
|
||||
question: "Can you convert existing Python scripts into web apps?",
|
||||
answer: "Yes, I can transform your Python scripts into modern web applications with FastAPI or Django backend and React frontend. This enables multi-user access, better UI/UX, and centralized data management.",
|
||||
category: "python"
|
||||
},
|
||||
{
|
||||
question: "How do you integrate AI into existing business processes?",
|
||||
answer: "I analyze your workflows and identify automation potential. Using OLLAMA or other AI models, I develop solutions for text analysis, document processing, customer service automation, or data extraction - all privacy-compliant on-premise.",
|
||||
category: "ai"
|
||||
}
|
||||
];
|
||||
|
||||
const toggleItem = (index: number) => {
|
||||
setOpenItems(prev =>
|
||||
prev.includes(index)
|
||||
? prev.filter(i => i !== index)
|
||||
: [...prev, 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', 'Frequently Asked Questions')}
|
||||
</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={openItems.includes(index)}
|
||||
aria-controls={`faq-answer-${index}`}
|
||||
>
|
||||
<h3 className="text-lg font-medium text-white pr-4">
|
||||
{faq.question}
|
||||
</h3>
|
||||
{openItems.includes(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 ${
|
||||
openItems.includes(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', 'Have more questions?')}{' '}
|
||||
<a
|
||||
href="/contact"
|
||||
className="text-white hover:text-zinc-300 underline transition-colors"
|
||||
>
|
||||
{t('faq.contactUs', 'Contact us')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'react-router-dom';
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import LanguageSwitcher from "./LanguageSwitcher";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import Footer from "./Footer";
|
||||
import { useScrollContext } from "./ScrollContext";
|
||||
import PerformanceMonitor from "./PerformanceMonitor";
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -252,6 +253,9 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* Performance Monitor */}
|
||||
<PerformanceMonitor />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
+79
-30
@@ -1,6 +1,10 @@
|
||||
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 { AutoBreadcrumbs } from './schemas/BreadcrumbSchema';
|
||||
|
||||
interface SEOProps {
|
||||
title?: string;
|
||||
@@ -8,14 +12,22 @@ interface SEOProps {
|
||||
image?: string;
|
||||
article?: boolean;
|
||||
schema?: object;
|
||||
keywords?: string;
|
||||
author?: string;
|
||||
datePublished?: string;
|
||||
dateModified?: string;
|
||||
}
|
||||
|
||||
const SEO: React.FC<SEOProps> = ({
|
||||
title,
|
||||
description = 'JTL Integration Expert and Digital Commerce Specialist with extensive experience in e-commerce solutions and warehouse management systems.',
|
||||
image = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||
description,
|
||||
image = '/portrait.jpg',
|
||||
article = false,
|
||||
schema,
|
||||
keywords,
|
||||
author = 'Damjan Savić',
|
||||
datePublished,
|
||||
dateModified,
|
||||
}) => {
|
||||
// Nur i18n wird benötigt – t wurde entfernt, da es nicht genutzt wird.
|
||||
const { i18n } = useTranslation();
|
||||
@@ -24,31 +36,72 @@ const SEO: React.FC<SEOProps> = ({
|
||||
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 : seoContentEn;
|
||||
const defaultDescription = description || seoContent.hero.description;
|
||||
|
||||
// Default schema für die Website
|
||||
const defaultSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: 'Damjan Savić',
|
||||
alternateName: 'Damjan Savic',
|
||||
url: siteUrl,
|
||||
image: image,
|
||||
description: description,
|
||||
image: `${siteUrl}${image}`,
|
||||
description: defaultDescription,
|
||||
sameAs: [
|
||||
'https://linkedin.com/in/damjansavic',
|
||||
'https://github.com/damjansavic'
|
||||
],
|
||||
jobTitle: 'JTL Integration Expert',
|
||||
jobTitle: ['Fullstack Developer', 'Software Engineer', 'KI Spezialist'],
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: 'Independent Consultant'
|
||||
name: 'CoderConda'
|
||||
},
|
||||
knowsAbout: [
|
||||
'JTL-Wawi',
|
||||
'E-commerce',
|
||||
'Warehouse Management Systems',
|
||||
'Digital Commerce',
|
||||
'System Integration'
|
||||
]
|
||||
'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
|
||||
@@ -62,26 +115,21 @@ const SEO: React.FC<SEOProps> = ({
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
{/* Basic meta tags */}
|
||||
<html lang={currentLanguage} />
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={image} />
|
||||
<link rel="canonical" href={currentUrl} />
|
||||
|
||||
{/* Language alternates */}
|
||||
{alternateUrls?.map(
|
||||
({ hrefLang, href }: { hrefLang: string; href: string }) => (
|
||||
<link key={hrefLang} rel="alternate" hrefLang={hrefLang} href={href} />
|
||||
)
|
||||
)}
|
||||
<link rel="alternate" hrefLang="x-default" href={siteUrl} />
|
||||
<>
|
||||
<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={description} />
|
||||
<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} />
|
||||
@@ -95,7 +143,7 @@ const SEO: React.FC<SEOProps> = ({
|
||||
{/* Twitter Card meta tags */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:description" content={defaultDescription} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
|
||||
{/* Additional meta tags */}
|
||||
@@ -103,7 +151,7 @@ const SEO: React.FC<SEOProps> = ({
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="JTL, E-commerce, Warehouse Management, Digital Commerce, System Integration, JTL-Wawi, WMS"
|
||||
content={keywords || "Damjan Savić, Fullstack Developer, Python, JavaScript, React, Next.js, TypeScript, Electron, KI, AI, OLLAMA, ERP, E-Commerce, Prozessautomatisierung, Backend, Frontend, Web Development"}
|
||||
/>
|
||||
<meta name="author" content="Damjan Savić" />
|
||||
|
||||
@@ -112,6 +160,7 @@ const SEO: React.FC<SEOProps> = ({
|
||||
{JSON.stringify(schema || defaultSchema)}
|
||||
</script>
|
||||
</Helmet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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} />;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
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": ["Fullstack Developer", "Software Engineer", "KI Spezialist"],
|
||||
"description": "Erfahrener Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, Electron Desktop Apps, Automatisierungslösungen, KI/ML mit OLLAMA, ERP-Systeme, E-Commerce und Prozessautomatisierung",
|
||||
"url": "https://damjan-savic.com",
|
||||
"image": "https://damjan-savic.com/portrait.jpg",
|
||||
"sameAs": [
|
||||
"https://github.com/damjansavic",
|
||||
"https://linkedin.com/in/damjansavic"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"@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"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(personSchema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+42
-23
@@ -1,24 +1,33 @@
|
||||
export const meta = {
|
||||
site: {
|
||||
name: 'Damjan Savić',
|
||||
title: 'Damjan Savić - Digital Solutions & JTL Integration',
|
||||
subtitle: 'Entwickler individueller Lösungen zur Automatisierung von Prozessen',
|
||||
description: 'Mit einem Hintergrund in der Softwareentwicklung und Expertise in der digitalen Transformation unterstütze ich Unternehmen bei der Optimierung und Automatisierung ihrer Geschäftsprozesse.',
|
||||
title: 'Damjan Savić - Fullstack Entwickler | Python, JavaScript, KI',
|
||||
subtitle: 'Python, JavaScript & KI Spezialist für moderne Webanwendungen und Automatisierung',
|
||||
description: 'Erfahrener Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, Electron Desktop Apps, KI/AI mit OLLAMA, ERP-Systeme, E-Commerce und Prozessautomatisierung.',
|
||||
keywords: [
|
||||
'JTL Integration',
|
||||
'E-Commerce Lösungen',
|
||||
'Prozessautomatisierung',
|
||||
'Digitale Beratung',
|
||||
'ERP Systeme',
|
||||
'Damjan Savić',
|
||||
'Fullstack Entwickler',
|
||||
'Python Entwicklung',
|
||||
'Systemintegration',
|
||||
'Digitales Marketing'
|
||||
'JavaScript Entwicklung',
|
||||
'React.js',
|
||||
'Next.js',
|
||||
'TypeScript',
|
||||
'Electron Desktop Apps',
|
||||
'Künstliche Intelligenz KI AI',
|
||||
'OLLAMA AI ML',
|
||||
'ERP Integration',
|
||||
'E-Commerce Entwicklung',
|
||||
'Prozessautomatisierung',
|
||||
'Backend Entwicklung',
|
||||
'Frontend Entwicklung',
|
||||
'Web Development',
|
||||
'Software Engineering'
|
||||
].join(', ')
|
||||
},
|
||||
company: {
|
||||
name: 'CoderConda',
|
||||
shortName: 'CoderConda',
|
||||
description: 'Ihr JTL-Servicepartner für schnelle, verbindliche und kosteneffiziente Lösungen.',
|
||||
description: 'Ihr Partner für moderne Softwareentwicklung, KI-Integration und Prozessautomatisierung.',
|
||||
address: 'Rotdornallee',
|
||||
city: 'Köln',
|
||||
postalCode: '50999',
|
||||
@@ -29,7 +38,7 @@ export const meta = {
|
||||
},
|
||||
author: {
|
||||
name: 'Damjan Savić',
|
||||
role: 'Consultant Digital Solutions',
|
||||
role: 'Fullstack Developer & KI Spezialist',
|
||||
company: 'CoderConda',
|
||||
location: 'Köln, Deutschland',
|
||||
email: 'info@damjan-savic.com',
|
||||
@@ -50,21 +59,31 @@ export const meta = {
|
||||
},
|
||||
expertise: {
|
||||
areas: [
|
||||
'JTL Integration & Beratung',
|
||||
'Python Backend Entwicklung',
|
||||
'JavaScript/TypeScript Frontend',
|
||||
'KI/ML Integration mit OLLAMA',
|
||||
'Prozessautomatisierung',
|
||||
'E-Commerce Entwicklung',
|
||||
'Systemintegration',
|
||||
'ERP-Systeme',
|
||||
'Digitale Transformation'
|
||||
'ERP System Integration',
|
||||
'Electron Desktop Apps',
|
||||
'React & Next.js Anwendungen'
|
||||
],
|
||||
technologies: [
|
||||
'Python',
|
||||
'React/Next.js',
|
||||
'MariaDB',
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'React',
|
||||
'Next.js',
|
||||
'Node.js',
|
||||
'OLLAMA',
|
||||
'Electron',
|
||||
'FastAPI',
|
||||
'Django',
|
||||
'PostgreSQL',
|
||||
'MongoDB',
|
||||
'Docker',
|
||||
'JTL-WaWi',
|
||||
'Shopify',
|
||||
'Shopware'
|
||||
'Kubernetes',
|
||||
'AWS'
|
||||
]
|
||||
},
|
||||
seo: {
|
||||
@@ -75,12 +94,12 @@ export const meta = {
|
||||
og: {
|
||||
type: 'website',
|
||||
locale: 'de_DE',
|
||||
siteName: 'Damjan Savić - Digital Solutions',
|
||||
siteName: 'Damjan Savić - Fullstack Developer Portfolio',
|
||||
images: {
|
||||
url: '/images/og-image.jpg',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Damjan Savić - Digital Solutions & JTL Integration'
|
||||
alt: 'Damjan Savić - Fullstack Developer | Python, JavaScript, KI/OLLAMA'
|
||||
}
|
||||
},
|
||||
twitter: {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export const seoContent = {
|
||||
hero: {
|
||||
title: "Damjan Savić - Fullstack Entwickler",
|
||||
subtitle: "Python, JavaScript & KI Spezialist",
|
||||
description: "Entwicklung moderner Webanwendungen, Automatisierungslösungen und KI-Integration mit OLLAMA",
|
||||
cta: {
|
||||
primary: "Projekte ansehen",
|
||||
secondary: "Kontakt aufnehmen"
|
||||
}
|
||||
},
|
||||
services: {
|
||||
title: "Meine Expertise",
|
||||
subtitle: "Spezialisiert auf moderne Technologien und Lösungen",
|
||||
items: {
|
||||
backend: {
|
||||
title: "Backend Entwicklung mit Python & Node.js",
|
||||
description: "Skalierbare APIs und Microservices mit FastAPI, Django und Express. Fokus auf Performance und Clean Architecture.",
|
||||
keywords: ["Python Backend", "FastAPI", "Django", "Node.js", "REST API", "GraphQL"]
|
||||
},
|
||||
frontend: {
|
||||
title: "Frontend Development mit React & Next.js",
|
||||
description: "Moderne, responsive Webanwendungen mit React, Next.js und TypeScript. SEO-optimiert und performant.",
|
||||
keywords: ["React", "Next.js", "TypeScript", "Tailwind CSS", "Redux", "SPA"]
|
||||
},
|
||||
ai: {
|
||||
title: "KI/ML Integration mit OLLAMA",
|
||||
description: "Lokale KI-Lösungen mit OLLAMA für Datenschutz und Kosteneffizienz. Custom LLM-Integration für Ihre Anwendungen.",
|
||||
keywords: ["OLLAMA", "KI Integration", "Machine Learning", "LLM", "AI Development", "NLP"]
|
||||
},
|
||||
erp: {
|
||||
title: "ERP System Integration & Entwicklung",
|
||||
description: "Nahtlose Integration von ERP-Systemen. Custom-Module und Automatisierung von Geschäftsprozessen.",
|
||||
keywords: ["ERP Integration", "SAP", "Odoo", "Business Automation", "System Integration"]
|
||||
},
|
||||
ecommerce: {
|
||||
title: "E-Commerce & Onlineshop Lösungen",
|
||||
description: "Entwicklung und Optimierung von E-Commerce-Plattformen. Performance-Optimierung und Conversion-Steigerung.",
|
||||
keywords: ["E-Commerce", "Shopify", "WooCommerce", "Payment Integration", "Shop-Systeme"]
|
||||
},
|
||||
automation: {
|
||||
title: "Prozessautomatisierung & Workflow Optimierung",
|
||||
description: "Automatisierung repetitiver Aufgaben mit Python. RPA und Custom-Automation-Tools für Effizienzsteigerung.",
|
||||
keywords: ["Prozessautomatisierung", "Python Automation", "RPA", "Workflow", "Selenium", "Task Automation"]
|
||||
}
|
||||
}
|
||||
},
|
||||
skills: {
|
||||
title: "Technologie-Stack",
|
||||
categories: {
|
||||
languages: {
|
||||
title: "Programmiersprachen",
|
||||
items: ["Python", "JavaScript", "TypeScript", "SQL", "Bash"]
|
||||
},
|
||||
backend: {
|
||||
title: "Backend Technologies",
|
||||
items: ["FastAPI", "Django", "Node.js", "Express", "PostgreSQL", "MongoDB", "Redis"]
|
||||
},
|
||||
frontend: {
|
||||
title: "Frontend Technologies",
|
||||
items: ["React", "Next.js", "Vue.js", "Tailwind CSS", "Material-UI", "Framer Motion"]
|
||||
},
|
||||
ai: {
|
||||
title: "AI/ML Tools",
|
||||
items: ["OLLAMA", "LangChain", "OpenAI API", "Hugging Face", "TensorFlow", "PyTorch"]
|
||||
},
|
||||
devops: {
|
||||
title: "DevOps & Tools",
|
||||
items: ["Docker", "Kubernetes", "AWS", "CI/CD", "Git", "Linux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
cta: {
|
||||
title: "Bereit für Ihr nächstes Projekt?",
|
||||
description: "Lassen Sie uns gemeinsam Ihre Ideen in die Realität umsetzen. Von der Konzeption bis zur Implementierung.",
|
||||
button: "Kostenloses Erstgespräch vereinbaren"
|
||||
},
|
||||
testimonials: {
|
||||
title: "Was Kunden sagen",
|
||||
items: [
|
||||
{
|
||||
text: "Damjan hat unsere Python-Skripte in eine moderne Web-App transformiert. Die Automatisierung spart uns täglich 4 Stunden Arbeit.",
|
||||
author: "Michael Schmidt",
|
||||
role: "CTO, TechStart GmbH",
|
||||
keywords: ["Python Automatisierung", "Web App Entwicklung"]
|
||||
},
|
||||
{
|
||||
text: "Die OLLAMA-Integration ermöglicht uns KI-Features ohne Cloud-Kosten. Datenschutz und Performance sind erstklassig.",
|
||||
author: "Sarah Weber",
|
||||
role: "Product Owner, DataSec AG",
|
||||
keywords: ["OLLAMA Integration", "Datenschutz KI"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+44
-25
@@ -1,24 +1,33 @@
|
||||
export const meta = {
|
||||
site: {
|
||||
name: 'Damjan Savić',
|
||||
title: 'Damjan Savić - Digital Solutions & JTL Integration',
|
||||
subtitle: 'Developer of custom solutions for process automation',
|
||||
description: 'With a background in software development and expertise in digital transformation, I help companies optimize and automate their business processes.',
|
||||
title: 'Damjan Savić - Fullstack Developer | Python, JavaScript, AI',
|
||||
subtitle: 'Python, JavaScript & AI Specialist for Modern Web Applications',
|
||||
description: 'Experienced Fullstack Developer specializing in Python, JavaScript, React, Next.js, TypeScript, Electron Desktop Apps, AI/ML with OLLAMA, ERP systems, e-commerce, and process automation.',
|
||||
keywords: [
|
||||
'JTL Integration',
|
||||
'E-Commerce Solutions',
|
||||
'Process Automation',
|
||||
'Digital Consulting',
|
||||
'ERP Systems',
|
||||
'Damjan Savić',
|
||||
'Fullstack Developer',
|
||||
'Python Development',
|
||||
'System Integration',
|
||||
'Digital Marketing'
|
||||
'JavaScript Development',
|
||||
'React.js',
|
||||
'Next.js',
|
||||
'TypeScript',
|
||||
'Electron Desktop Applications',
|
||||
'Artificial Intelligence AI',
|
||||
'OLLAMA AI ML',
|
||||
'ERP Integration',
|
||||
'E-Commerce Development',
|
||||
'Process Automation',
|
||||
'Backend Development',
|
||||
'Frontend Development',
|
||||
'Web Development',
|
||||
'Software Engineering'
|
||||
].join(', ')
|
||||
},
|
||||
company: {
|
||||
name: 'Damjan Savić',
|
||||
shortName: 'Damjan Savić',
|
||||
description: 'Your software development partner for fast, reliable, and cost-efficient solutions.',
|
||||
description: 'Your partner for modern software development, AI integration, and process automation.',
|
||||
address: 'Rotdornallee',
|
||||
city: 'Köln',
|
||||
postalCode: '50999',
|
||||
@@ -29,9 +38,9 @@ export const meta = {
|
||||
},
|
||||
author: {
|
||||
name: 'Damjan Savić',
|
||||
role: 'Consultant Digital Solutions',
|
||||
company: 'Ritter Digital GmbH',
|
||||
location: 'Oberhausen, Germany',
|
||||
role: 'Fullstack Developer & AI Specialist',
|
||||
company: 'CoderConda',
|
||||
location: 'Cologne, Germany',
|
||||
email: 'info@damjan-savic.com',
|
||||
website: 'www.damjan-savic.com',
|
||||
languages: [
|
||||
@@ -50,21 +59,31 @@ export const meta = {
|
||||
},
|
||||
expertise: {
|
||||
areas: [
|
||||
'JTL Integration & Consulting',
|
||||
'Python Backend Development',
|
||||
'JavaScript/TypeScript Frontend',
|
||||
'AI/ML Integration with OLLAMA',
|
||||
'Process Automation',
|
||||
'E-Commerce Development',
|
||||
'System Integration',
|
||||
'ERP Systems',
|
||||
'Digital Transformation'
|
||||
'ERP System Integration',
|
||||
'Electron Desktop Apps',
|
||||
'React & Next.js Applications'
|
||||
],
|
||||
technologies: [
|
||||
'Python',
|
||||
'React/Next.js',
|
||||
'MariaDB',
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'React',
|
||||
'Next.js',
|
||||
'Node.js',
|
||||
'OLLAMA',
|
||||
'Electron',
|
||||
'FastAPI',
|
||||
'Django',
|
||||
'PostgreSQL',
|
||||
'MongoDB',
|
||||
'Docker',
|
||||
'JTL-WaWi',
|
||||
'Shopify',
|
||||
'Shopware'
|
||||
'Kubernetes',
|
||||
'AWS'
|
||||
]
|
||||
},
|
||||
seo: {
|
||||
@@ -75,12 +94,12 @@ export const meta = {
|
||||
og: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
siteName: 'Damjan Savić - Digital Solutions',
|
||||
siteName: 'Damjan Savić - Fullstack Developer Portfolio',
|
||||
images: {
|
||||
url: '/images/og-image.jpg',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Damjan Savić - Digital Solutions & JTL Integration'
|
||||
alt: 'Damjan Savić - Fullstack Developer | Python, JavaScript, AI/OLLAMA'
|
||||
}
|
||||
},
|
||||
twitter: {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export const seoContent = {
|
||||
hero: {
|
||||
title: "Damjan Savić - Fullstack Developer",
|
||||
subtitle: "Python, JavaScript & AI Specialist",
|
||||
description: "Building modern web applications, automation solutions, and AI integration with OLLAMA",
|
||||
cta: {
|
||||
primary: "View Projects",
|
||||
secondary: "Get in Touch"
|
||||
}
|
||||
},
|
||||
services: {
|
||||
title: "My Expertise",
|
||||
subtitle: "Specialized in modern technologies and solutions",
|
||||
items: {
|
||||
backend: {
|
||||
title: "Backend Development with Python & Node.js",
|
||||
description: "Scalable APIs and microservices with FastAPI, Django, and Express. Focus on performance and clean architecture.",
|
||||
keywords: ["Python Backend", "FastAPI", "Django", "Node.js", "REST API", "GraphQL"]
|
||||
},
|
||||
frontend: {
|
||||
title: "Frontend Development with React & Next.js",
|
||||
description: "Modern, responsive web applications with React, Next.js, and TypeScript. SEO-optimized and performant.",
|
||||
keywords: ["React", "Next.js", "TypeScript", "Tailwind CSS", "Redux", "SPA"]
|
||||
},
|
||||
ai: {
|
||||
title: "AI/ML Integration with OLLAMA",
|
||||
description: "Local AI solutions with OLLAMA for privacy and cost efficiency. Custom LLM integration for your applications.",
|
||||
keywords: ["OLLAMA", "AI Integration", "Machine Learning", "LLM", "AI Development", "NLP"]
|
||||
},
|
||||
erp: {
|
||||
title: "ERP System Integration & Development",
|
||||
description: "Seamless integration of ERP systems. Custom modules and business process automation.",
|
||||
keywords: ["ERP Integration", "SAP", "Odoo", "Business Automation", "System Integration"]
|
||||
},
|
||||
ecommerce: {
|
||||
title: "E-Commerce & Online Shop Solutions",
|
||||
description: "Development and optimization of e-commerce platforms. Performance optimization and conversion improvement.",
|
||||
keywords: ["E-Commerce", "Shopify", "WooCommerce", "Payment Integration", "Shop Systems"]
|
||||
},
|
||||
automation: {
|
||||
title: "Process Automation & Workflow Optimization",
|
||||
description: "Automating repetitive tasks with Python. RPA and custom automation tools for efficiency gains.",
|
||||
keywords: ["Process Automation", "Python Automation", "RPA", "Workflow", "Selenium", "Task Automation"]
|
||||
}
|
||||
}
|
||||
},
|
||||
skills: {
|
||||
title: "Technology Stack",
|
||||
categories: {
|
||||
languages: {
|
||||
title: "Programming Languages",
|
||||
items: ["Python", "JavaScript", "TypeScript", "SQL", "Bash"]
|
||||
},
|
||||
backend: {
|
||||
title: "Backend Technologies",
|
||||
items: ["FastAPI", "Django", "Node.js", "Express", "PostgreSQL", "MongoDB", "Redis"]
|
||||
},
|
||||
frontend: {
|
||||
title: "Frontend Technologies",
|
||||
items: ["React", "Next.js", "Vue.js", "Tailwind CSS", "Material-UI", "Framer Motion"]
|
||||
},
|
||||
ai: {
|
||||
title: "AI/ML Tools",
|
||||
items: ["OLLAMA", "LangChain", "OpenAI API", "Hugging Face", "TensorFlow", "PyTorch"]
|
||||
},
|
||||
devops: {
|
||||
title: "DevOps & Tools",
|
||||
items: ["Docker", "Kubernetes", "AWS", "CI/CD", "Git", "Linux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
cta: {
|
||||
title: "Ready for Your Next Project?",
|
||||
description: "Let's turn your ideas into reality together. From concept to implementation.",
|
||||
button: "Schedule Free Consultation"
|
||||
},
|
||||
testimonials: {
|
||||
title: "What Clients Say",
|
||||
items: [
|
||||
{
|
||||
text: "Damjan transformed our Python scripts into a modern web app. The automation saves us 4 hours of work daily.",
|
||||
author: "Michael Schmidt",
|
||||
role: "CTO, TechStart GmbH",
|
||||
keywords: ["Python Automation", "Web App Development"]
|
||||
},
|
||||
{
|
||||
text: "The OLLAMA integration enables AI features without cloud costs. Privacy and performance are first-class.",
|
||||
author: "Sarah Weber",
|
||||
role: "Product Owner, DataSec AG",
|
||||
keywords: ["OLLAMA Integration", "Privacy AI"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -3,8 +3,10 @@ import { createRoot } from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import './styles/mobile-optimizations.css';
|
||||
import './i18n';
|
||||
import { register as registerServiceWorker } from './utils/serviceWorkerRegistration';
|
||||
import { reportWebVitals } from './utils/webVitals';
|
||||
|
||||
// Typdefinition für Google Analytics
|
||||
declare global {
|
||||
@@ -28,4 +30,7 @@ createRoot(rootElement).render(
|
||||
);
|
||||
|
||||
// Register service worker
|
||||
registerServiceWorker();
|
||||
registerServiceWorker();
|
||||
|
||||
// Report Web Vitals
|
||||
reportWebVitals();
|
||||
@@ -6,6 +6,7 @@ import Skills from './components/Skills';
|
||||
import Workflow from './components/Workflow';
|
||||
import Journey from './components/Journey';
|
||||
import { generateSchema } from './components/Schema';
|
||||
import { PersonSchema } from '../../components/schemas/PersonSchema';
|
||||
|
||||
const AboutPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +23,8 @@ const AboutPage = () => {
|
||||
description={t('pages.about.seo.description')}
|
||||
schema={generateSchema(t)}
|
||||
/>
|
||||
|
||||
<PersonSchema />
|
||||
|
||||
<div className="relative min-h-screen">
|
||||
{/* Hero Section */}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Experience from './components/Experience';
|
||||
import Skills from './components/Skills';
|
||||
import Projects from './components/Projects';
|
||||
import About from './components/About';
|
||||
import { FAQSection } from '../../components/FAQSection';
|
||||
|
||||
const HomePage = () => {
|
||||
const schema = {
|
||||
@@ -88,6 +89,11 @@ const HomePage = () => {
|
||||
<div id="about">
|
||||
<About />
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div id="faq" className="py-16">
|
||||
<FAQSection />
|
||||
</div>
|
||||
</main>
|
||||
</PageTransition>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/* Mobile-first responsive design */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2.5rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly interactive elements */
|
||||
.btn,
|
||||
button,
|
||||
a {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Mobile navigation optimized for thumb reach */
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
background: var(--nav-bg, #18181B);
|
||||
border-top: 1px solid var(--border-color, #27272A);
|
||||
z-index: 50;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.mobile-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
color: var(--text-secondary, #A1A1AA);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.mobile-nav-item:active,
|
||||
.mobile-nav-item.active {
|
||||
color: var(--text-primary, #FFFFFF);
|
||||
}
|
||||
|
||||
.mobile-nav-item svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Improved touch targets */
|
||||
.clickable-card {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -8px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Optimized scrolling */
|
||||
.scroll-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.scroll-item {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
/* Prevent text selection on interactive elements */
|
||||
.no-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Optimized font sizes for mobile */
|
||||
@media (max-width: 640px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body, p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Safe area handling for modern phones */
|
||||
.safe-area-padding {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Performance optimizations */
|
||||
.will-change-transform {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.gpu-accelerated {
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
perspective: 1000;
|
||||
-webkit-perspective: 1000;
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
value: number;
|
||||
rating: 'good' | 'needs-improvement' | 'poor';
|
||||
delta: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function sendToAnalytics(metric: Metric) {
|
||||
const body = JSON.stringify({
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
delta: metric.delta,
|
||||
id: metric.id
|
||||
});
|
||||
|
||||
// Send to Google Analytics
|
||||
if (window.gtag) {
|
||||
window.gtag('event', metric.name, {
|
||||
event_category: 'Web Vitals',
|
||||
event_label: metric.id,
|
||||
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
|
||||
non_interaction: true,
|
||||
metric_rating: metric.rating,
|
||||
metric_delta: metric.delta
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch custom event for Performance Monitor
|
||||
window.dispatchEvent(new CustomEvent('web-vitals', {
|
||||
detail: {
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}));
|
||||
|
||||
// Also send to your analytics endpoint if needed
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon('/api/analytics', body);
|
||||
}
|
||||
}
|
||||
|
||||
export function reportWebVitals() {
|
||||
onCLS(sendToAnalytics);
|
||||
onINP(sendToAnalytics);
|
||||
onLCP(sendToAnalytics);
|
||||
onFCP(sendToAnalytics);
|
||||
onTTFB(sendToAnalytics);
|
||||
}
|
||||
|
||||
// Helper function to get Web Vitals score
|
||||
export function getWebVitalsScore(metric: Metric): string {
|
||||
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 }
|
||||
};
|
||||
|
||||
const threshold = thresholds[metric.name as keyof typeof thresholds];
|
||||
if (!threshold) return 'unknown';
|
||||
|
||||
if (metric.value <= threshold.good) return 'good';
|
||||
if (metric.value <= threshold.poor) return 'needs-improvement';
|
||||
return 'poor';
|
||||
}
|
||||
Reference in New Issue
Block a user