Implementierung - SEO - 01.08.2025

This commit is contained in:
2025-08-02 02:02:51 +02:00
parent e69e68242c
commit 4a4388be64
42 changed files with 1098 additions and 321 deletions
+4
View File
@@ -10,11 +10,15 @@ import PageTransition from './components/PageTransition';
import { ScrollProvider } from './components/ScrollContext';
import DebugOverlay from './components/DebugOverlay';
import { logPageView, isCookieCategoryEnabled } from './utils/analytics';
import { useScrollTracking } from './hooks/useScrollTracking';
import './styles/scrollbar.css';
// RouteTracker Component für präzises Seiten-Tracking - aber nur mit Zustimmung
const RouteTracker = () => {
const location = useLocation();
// Scroll tracking aktivieren
useScrollTracking();
useEffect(() => {
// Nur tracken, wenn die Analytics-Cookies akzeptiert wurden
+21 -82
View File
@@ -1,87 +1,26 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface FAQItem {
question: string;
answer: string;
category?: string;
}
import { trackFAQInteraction } from '../services/gtm';
export function FAQSection() {
const { t, i18n } = useTranslation();
const [openItems, setOpenItems] = useState<number[]>([]);
const { t } = useTranslation();
const [openItem, setOpenItem] = useState<number | null>(null);
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 faqs = t('faq.questions', { returnObjects: true }) as Array<{
question: string;
answer: string;
category?: string;
}>;
const toggleItem = (index: number) => {
setOpenItems(prev =>
prev.includes(index)
? prev.filter(i => i !== index)
: [...prev, index]
);
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 = {
@@ -105,7 +44,7 @@ export function FAQSection() {
/>
<h2 className="text-3xl font-bold text-white mb-8 text-center">
{t('faq.title', 'Frequently Asked Questions')}
{t('faq.title')}
</h2>
<div className="space-y-4">
@@ -117,13 +56,13 @@ export function FAQSection() {
<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-expanded={openItem === index}
aria-controls={`faq-answer-${index}`}
>
<h3 className="text-lg font-medium text-white pr-4">
{faq.question}
</h3>
{openItems.includes(index) ? (
{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" />
@@ -133,7 +72,7 @@ export function FAQSection() {
<div
id={`faq-answer-${index}`}
className={`overflow-hidden transition-all duration-300 ${
openItems.includes(index) ? 'max-h-96' : 'max-h-0'
openItem === index ? 'max-h-96' : 'max-h-0'
}`}
>
<div className="px-6 py-4 text-zinc-300 border-t border-zinc-700">
@@ -146,12 +85,12 @@ export function FAQSection() {
<div className="mt-8 text-center">
<p className="text-zinc-400">
{t('faq.moreQuestions', 'Have more questions?')}{' '}
{t('faq.moreQuestions')}{' '}
<a
href="/contact"
className="text-white hover:text-zinc-300 underline transition-colors"
>
{t('faq.contactUs', 'Contact us')}
{t('faq.contactUs')}
</a>
</p>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ export function FloatingPaths({ position }: { position: number }) {
}))
return (
<div className="absolute inset-0 pointer-events-none floating-paths" style={{ isolation: 'isolate', zIndex: 0 }}>
<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) => (
+2 -2
View File
@@ -8,7 +8,7 @@ const Footer = () => {
const currentYear = new Date().getFullYear();
return (
<footer className="relative bg-zinc-900 pt-8 pb-4 px-4">
<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 */}
@@ -93,7 +93,7 @@ const Footer = () => {
</div>
{/* Legal Section */}
<div className="border-t border-zinc-800 pt-4">
<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-center">
&copy; {currentYear} Damjan Savić. {t('footer.legal.copyright')}
+146
View File
@@ -0,0 +1,146 @@
import { useEffect, useState } from 'react';
import { FloatingPaths } from './FloatingPaths';
const GlobalBackground = () => {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY * 0.001);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
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 */}
<div
className="fixed inset-0"
style={{
zIndex: -1,
pointerEvents: 'none'
}}
>
<FloatingPaths position={scrollPosition} />
</div>
{/* Test: Visible animated lines */}
<div
className="fixed inset-0 pointer-events-none"
style={{ zIndex: -1 }}
>
<svg
className="w-full h-full"
preserveAspectRatio="none"
style={{ opacity: 0.3 }}
>
<line
x1="0%"
y1="20%"
x2="100%"
y2="20%"
stroke="white"
strokeWidth="1"
opacity="0.2"
>
<animate
attributeName="y1"
values="20%;80%;20%"
dur="10s"
repeatCount="indefinite"
/>
<animate
attributeName="y2"
values="20%;80%;20%"
dur="10s"
repeatCount="indefinite"
/>
</line>
<line
x1="0%"
y1="50%"
x2="100%"
y2="50%"
stroke="white"
strokeWidth="1"
opacity="0.15"
>
<animate
attributeName="x1"
values="0%;100%;0%"
dur="15s"
repeatCount="indefinite"
/>
</line>
<line
x1="20%"
y1="0%"
x2="20%"
y2="100%"
stroke="white"
strokeWidth="1"
opacity="0.1"
>
<animate
attributeName="x1"
values="20%;80%;20%"
dur="12s"
repeatCount="indefinite"
/>
<animate
attributeName="x2"
values="20%;80%;20%"
dur="12s"
repeatCount="indefinite"
/>
</line>
</svg>
</div>
{/* Grid overlay */}
<div
className="fixed inset-0 pointer-events-none"
style={{
zIndex: -1,
backgroundImage: `
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
opacity: 0.5
}}
/>
</>
);
};
export default GlobalBackground;
+11 -6
View File
@@ -20,6 +20,7 @@ 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;
@@ -122,21 +123,25 @@ const Layout = ({ children }: LayoutProps) => {
// Render with opacity 0 on initial mount to prevent flicker
if (!isMounted) {
return (
<div className="min-h-screen bg-zinc-900">
<div className="min-h-screen bg-transparent">
<GlobalBackground />
{children}
</div>
);
}
return (
<div className="min-h-screen bg-zinc-900 flex flex-col relative">
<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-900/80 backdrop-blur-md shadow-lg shadow-zinc-900/50"
? "bg-zinc-800/70 backdrop-blur-md shadow-lg shadow-zinc-900/30"
: ""
}
`}
@@ -148,7 +153,7 @@ const Layout = ({ children }: LayoutProps) => {
{/* Logo */}
<Link to="/" className="flex items-center space-x-2 group shrink-0">
<motion.img
src="/logo.png"
src="/logo.svg"
alt=""
className="h-8 w-auto"
whileHover={{ scale: 1.05 }}
@@ -206,7 +211,7 @@ const Layout = ({ children }: LayoutProps) => {
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed right-0 top-0 h-full w-80 bg-zinc-900 shadow-xl md:hidden z-50 border-l border-zinc-800"
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"
>
{/* Sidebar Header mit Close-Button */}
<div className="flex items-center justify-between px-4 h-16 border-b border-zinc-800">
@@ -258,7 +263,7 @@ const Layout = ({ children }: LayoutProps) => {
</AnimatePresence>
{/* Hauptinhalt */}
<main className="flex-1 pt-2 relative z-30">{children}</main>
<main className="flex-1 pt-2 relative z-10 bg-transparent">{children}</main>
{/* Footer */}
<Footer />
+2 -2
View File
@@ -28,7 +28,7 @@ const Logo = () => {
>
{!imageError ? (
<img
src="/logo.png"
src="/logo.svg"
alt="Logo"
className="w-full h-full object-contain filter brightness-200"
onError={() => setImageError(true)}
@@ -123,7 +123,7 @@ const PageTransition = ({ children }: PageTransitionProps) => {
{isTransitioning && (
<motion.div
key="transition-overlay"
className="fixed inset-0 bg-zinc-900 flex items-center justify-center page-transition-active"
className="fixed inset-0 bg-zinc-900/95 backdrop-blur-sm flex items-center justify-center page-transition-active"
style={{ zIndex: 9999 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
+50
View File
@@ -0,0 +1,50 @@
import { useEffect, useRef } from 'react';
import { trackScrollDepth } from '../services/gtm';
/**
* Hook to track scroll depth for GTM
*/
export const useScrollTracking = () => {
const trackedDepths = useRef(new Set<number>());
useEffect(() => {
let scrollTimeout: NodeJS.Timeout;
const depths = [25, 50, 75, 90, 100];
const handleScroll = () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.scrollY;
const scrollPercentage = Math.round(
((scrollTop + windowHeight) / documentHeight) * 100
);
// Track each depth milestone only once
depths.forEach(depth => {
if (scrollPercentage >= depth && !trackedDepths.current.has(depth)) {
trackedDepths.current.add(depth);
trackScrollDepth(depth);
}
});
}, 100); // Debounce scroll events
};
window.addEventListener('scroll', handleScroll);
// Check initial scroll position
handleScroll();
return () => {
window.removeEventListener('scroll', handleScroll);
clearTimeout(scrollTimeout);
};
}, []);
// Reset tracked depths when route changes
useEffect(() => {
trackedDepths.current.clear();
}, [window.location.pathname]);
};
+37
View File
@@ -0,0 +1,37 @@
export const faq = {
title: 'Häufig gestellte Fragen',
moreQuestions: 'Haben Sie weitere Fragen?',
contactUs: 'Kontaktieren Sie uns',
questions: [
{
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"
}
]
};
+5
View File
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
// Footer translations
import footer from './footer';
// FAQ translations
import { faq } from './faq';
// Zusammenführung aller Übersetzungsobjekte
const translations = {
// Pages
@@ -77,6 +80,7 @@ const translations = {
},
meta,
navigation,
faq,
} as const;
// Default-Export: komplettes Übersetzungsobjekt
@@ -111,4 +115,5 @@ export {
common,
meta,
navigation,
faq,
};
+37
View File
@@ -0,0 +1,37 @@
export const faq = {
title: 'Frequently Asked Questions',
moreQuestions: 'Have more questions?',
contactUs: 'Contact us',
questions: [
{
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"
}
]
};
+5
View File
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
// Footer translations
import footer from './footer';
// FAQ translations
import { faq } from './faq';
// Merging all translation objects
const translations = {
// Pages
@@ -77,6 +80,7 @@ const translations = {
},
meta,
navigation,
faq,
} as const;
// Default export: complete translations object
@@ -111,4 +115,5 @@ export {
common,
meta,
navigation,
faq,
};
+37
View File
@@ -0,0 +1,37 @@
export const faq = {
title: 'Često postavljana pitanja',
moreQuestions: 'Imate još pitanja?',
contactUs: 'Kontaktirajte nas',
questions: [
{
question: "Kako mogu da koristim Python za automatizaciju procesa?",
answer: "Python je odličan za automatizaciju procesa kroz biblioteke kao što su Selenium za web automatizaciju, Pandas za obradu podataka i Schedule za vremenski zakazane zadatke. Razvijam prilagođena rešenja za automatizaciju vaših poslovnih procesa.",
category: "python"
},
{
question: "Šta čini OLLAMA idealnim za lokalni razvoj veštačke inteligencije?",
answer: "OLLAMA omogućava pokretanje velikih jezičkih modela lokalno, što garantuje privatnost podataka i smanjuje troškove API-ja. Specijalizovan sam za integraciju OLLAMA-e u aplikacije za prilagođena AI rešenja bez zavisnosti od oblaka.",
category: "ai"
},
{
question: "Koje su prednosti Electron desktop aplikacija?",
answer: "Electron omogućava razvoj desktop aplikacija za više platformi koristeći web tehnologije. Dobijate nativnu aplikaciju za Windows, Mac i Linux iz jedne baze koda. Idealno za alate koji zahtevaju kompleksan UI i offline funkcionalnost.",
category: "electron"
},
{
question: "Koliko dugo traje razvoj web aplikacije?",
answer: "Vreme razvoja zavisi od obima. MVP može biti razvijen za 4-8 nedelja, dok kompleksne enterprise aplikacije zahtevaju 3-6 meseci. Radim agilno i isporučujem upotrebljive rezultate u dvonedeljnim sprintovima.",
category: "general"
},
{
question: "Možete li konvertovati postojeće Python skripte u web aplikacije?",
answer: "Da, mogu transformisati vaše Python skripte u moderne web aplikacije sa FastAPI ili Django backend-om i React frontend-om. Ovo omogućava multi-korisnički pristup, bolji UI/UX i centralizovano upravljanje podacima.",
category: "python"
},
{
question: "Kako integrisati veštačku inteligenciju u postojeće poslovne procese?",
answer: "Analiziram vaše radne tokove i identifikujem potencijal za automatizaciju. Koristeći OLLAMA ili druge AI modele, razvijam rešenja za analizu teksta, obradu dokumenata, automatizaciju korisničke podrške ili ekstrakciju podataka - sve u skladu sa privatnošću podataka on-premise.",
category: "ai"
}
]
};
+5
View File
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
// Footer prevodi
import footer from './footer';
// FAQ prevodi
import { faq } from './faq';
// Objedinjavanje svih prevoda
const translations = {
// Stranice
@@ -77,6 +80,7 @@ const translations = {
},
meta,
navigation,
faq,
} as const;
// Default izvoz: kompletan objekat prevoda
@@ -111,4 +115,5 @@ export {
common,
meta,
navigation,
faq,
};
+36 -1
View File
@@ -19,8 +19,17 @@ html.fonts-fallback body {
font-family: Inter, system-ui, sans-serif;
}
html {
background-color: transparent !important;
}
body {
@apply bg-background text-foreground antialiased;
@apply text-foreground antialiased;
background-color: transparent !important;
}
#root {
background-color: transparent !important;
}
h1, h2, h3, h4, h5, h6 {
@@ -78,6 +87,32 @@ html.fonts-fallback body {
}
}
@keyframes slide-right {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes slide-down {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(100%);
}
}
.animate-slide-right {
animation: slide-right 8s linear infinite;
}
.animate-slide-down {
animation: slide-down 8s linear infinite;
}
/* Prevent hover flickering */
* {
-webkit-tap-highlight-color: transparent;
+1 -23
View File
@@ -1,5 +1,4 @@
import { motion } from 'framer-motion';
import { FloatingPaths } from '../../../components/FloatingPaths';
import { Terminal, Globe2, Code2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -18,28 +17,7 @@ const Hero = () => {
};
return (
<section className="relative min-h-screen bg-zinc-900 text-white overflow-hidden">
{/* Floating Paths Background */}
<div className="absolute inset-0">
<FloatingPaths position={1} />
<FloatingPaths position={-1} />
</div>
{/* Concentric Circles Background */}
<div className="absolute inset-0 flex items-center justify-center">
{[1, 2, 3, 4, 5].map((index) => (
<div
key={index}
className="absolute rounded-full border border-zinc-800"
style={{
width: `${index * 20}%`,
height: `${index * 20}%`,
opacity: 0.1
}}
/>
))}
</div>
<section className="relative min-h-screen text-white overflow-hidden">
{/* Main Content */}
<div className="flex flex-col items-center justify-center min-h-screen px-4 relative z-10">
{/* Title */}
+1 -1
View File
@@ -37,7 +37,7 @@ const Skills = () => {
const allSkills = skillGroups.flatMap(group => group.items);
return (
<section className="py-24 bg-zinc-900">
<section className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
+1 -1
View File
@@ -39,7 +39,7 @@ const Workflow = () => {
return (
<section
className="relative w-full bg-zinc-900 py-24 px-4 sm:px-6 lg:px-8"
className="relative w-full py-24 px-4 sm:px-6 lg:px-8"
aria-label={t('pages.about.workflow.aria.workflowSection')}
>
{/* Section Title and Description */}
+39 -3
View File
@@ -5,6 +5,7 @@ import { Calendar, ArrowLeft, Tag, Loader2, Folder } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import PageTransition from '../../../components/PageTransition';
import SEO from '../../../components/SEO';
import { trackBlogPostView } from '../../../services/gtm';
import { blog as blogDe } from '../../../i18n/locales/de/blog/';
import { blog as blogEn } from '../../../i18n/locales/en/blog';
@@ -120,6 +121,8 @@ const BlogPost: React.FC = () => {
...postData,
slug,
});
// Track blog post view
trackBlogPostView(postData.title, postData.category);
} else {
// Statt eine Exception zu werfen, setzen wir den Fehler direkt.
setError(new Error('Post not found'));
@@ -168,7 +171,7 @@ const BlogPost: React.FC = () => {
if (loading) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -183,7 +186,7 @@ const BlogPost: React.FC = () => {
if (error || !post) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -207,10 +210,43 @@ const BlogPost: React.FC = () => {
);
}
// Article Schema for SEO
const articleSchema = {
"@context": "https://schema.org",
"@type": "Article",
"headline": post.title,
"description": post.excerpt,
"datePublished": post.date,
"dateModified": post.date,
"author": {
"@type": "Person",
"name": "Damjan Savić",
"url": "https://damjan-savic.com"
},
"publisher": {
"@type": "Person",
"name": "Damjan Savić",
"logo": {
"@type": "ImageObject",
"url": "https://damjan-savic.com/logo.svg"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": `https://damjan-savic.com/blog/${post.slug}`
},
"articleSection": post.category ? blog.categories[post.category] : undefined,
"keywords": post.tags?.join(', ')
};
return (
<PageTransition>
<SEO title={`${post.title} - Blog`} description={post.excerpt} />
<main className="min-h-screen bg-zinc-900">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
<main className="min-h-screen">
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
<motion.div variants={containerVariants} initial="hidden" animate="visible">
{/* Back Navigation */}
+3 -3
View File
@@ -72,7 +72,7 @@ const BlogPage: React.FC = () => {
if (loading) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -87,7 +87,7 @@ const BlogPage: React.FC = () => {
if (error) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -110,7 +110,7 @@ const BlogPage: React.FC = () => {
description={metaDescription}
schema={schema}
/>
<main className="min-h-screen bg-zinc-900">
<main className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
{/* Header Section */}
<motion.div
@@ -14,6 +14,7 @@ import { Label } from '../../../components/Label';
import { Button } from '../../../components/Button';
import { Alert, AlertDescription } from '../../../components/Alert';
import { Loader2, Send, CheckCircle2, Mail } from 'lucide-react';
import { trackContactFormSubmit } from '../../../services/gtm';
interface FormData {
name: string;
@@ -72,6 +73,13 @@ const ContactForm = () => {
setSubmitStatus('idle');
try {
// Track form submission
trackContactFormSubmit({
name: formData.name,
email: formData.email,
subject: 'Contact Form'
});
// Simuliere einen API-Aufruf
await new Promise(resolve => setTimeout(resolve, 1500));
setSubmitStatus('success');
+1 -1
View File
@@ -9,7 +9,7 @@ const ContactPage = () => {
return (
<PageTransition>
<div className="min-h-screen bg-zinc-900">
<div className="min-h-screen">
{/* Hero Section */}
<div className="py-24 text-center">
<motion.h1
+86 -55
View File
@@ -1,88 +1,119 @@
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { MapPin, Briefcase, GraduationCap, Code, Award } from 'lucide-react';
const About = () => {
const { t } = useTranslation();
const highlights = [
{ icon: <MapPin className="w-4 h-4" />, label: "Köln, Deutschland" },
{ icon: <Briefcase className="w-4 h-4" />, label: "5+ Jahre Erfahrung" },
{ icon: <Code className="w-4 h-4" />, label: "Full-Stack Developer" },
{ icon: <Award className="w-4 h-4" />, label: "ERP Spezialist" }
];
return (
<section className="py-24 bg-zinc-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<section className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
>
{/* Image Section */}
<div className="relative">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="rounded-3xl overflow-hidden aspect-square"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
>
{/* Image Section - Simplified */}
<motion.div
className="relative"
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* Main Image Container */}
<div className="relative rounded-3xl overflow-hidden aspect-square bg-zinc-800/30 backdrop-blur-sm">
<img
src="/portrait.jpg"
alt={t('pages.home.about.image.alt')}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = 'data:image/svg+xml,%3Csvg width="400" height="400" xmlns="http://www.w3.org/2000/svg"%3E%3Crect width="400" height="400" fill="%2327272a"/%3E%3Ctext x="50%25" y="50%25" font-family="system-ui" font-size="24" fill="%2371717a" text-anchor="middle" dominant-baseline="middle"%3EDamjan Savić%3C/text%3E%3C/svg%3E';
}}
/>
</motion.div>
</div>
</div>
</motion.div>
{/* Content Section */}
<div className="space-y-6">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-3xl font-bold text-white"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
>
{t('pages.home.about.title')}
</motion.h2>
{/* Title */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="text-zinc-400 leading-relaxed"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
>
<div className="text-zinc-400 leading-relaxed text-justify">
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-2">
{t('pages.home.about.title')}
</h2>
<div className="h-1 w-20 bg-gradient-to-r from-zinc-600 to-zinc-800 rounded-full"></div>
</motion.div>
{/* Highlight Pills */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex flex-wrap gap-3"
>
{highlights.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.5 + index * 0.1 }}
whileHover={{ scale: 1.05, y: -2 }}
className="flex items-center gap-2 px-3 py-1.5 bg-zinc-800/30 backdrop-blur-sm rounded-full border border-zinc-700/50 hover:border-zinc-600 transition-colors"
>
<span className="text-zinc-400">{item.icon}</span>
<span className="text-zinc-300 text-sm">{item.label}</span>
</motion.div>
))}
</motion.div>
{/* Main Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="space-y-4"
>
<p className="text-zinc-300 leading-relaxed text-lg">
{t('pages.home.about.content')}
</div>
</p>
</motion.div>
{/* Call to Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
className="flex flex-wrap gap-4 pt-4"
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-zinc-800/50 backdrop-blur-sm hover:bg-zinc-700/50 text-white rounded-xl font-medium transition-colors duration-300 flex items-center gap-2"
>
<GraduationCap className="w-4 h-4" />
Mehr erfahren
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-transparent hover:bg-zinc-800/30 text-zinc-300 border border-zinc-700/50 rounded-xl font-medium transition-colors duration-300"
>
CV herunterladen
</motion.button>
</motion.div>
</div>
</motion.div>
{/* Copyright */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="text-center mt-24"
/>
{/* Background Dots Pattern */}
<div className="absolute top-12 right-12 grid grid-cols-7 gap-2 opacity-20">
{[...Array(28)].map((_, i) => (
<div key={i} className="w-1 h-1 rounded-full bg-zinc-600" />
))}
</div>
</div>
</section>
);
+64 -30
View File
@@ -53,12 +53,15 @@ const Experience = () => {
};
return (
<section className="py-12 md:py-24 bg-zinc-900 overflow-hidden">
<section className="py-12 md:py-24 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
transition={{
duration: 0.8,
ease: [0.25, 0.46, 0.45, 0.94]
}}
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
@@ -77,10 +80,13 @@ const Experience = () => {
<AnimatePresence mode="wait">
<motion.div
key={currentPage}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3 }}
initial={{ opacity: 0, x: 100, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: -100, scale: 0.95 }}
transition={{
duration: 0.5,
ease: [0.43, 0.13, 0.23, 0.96]
}}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
style={{
transform: 'translateZ(0)',
@@ -90,13 +96,22 @@ const Experience = () => {
{getCurrentPageItems().map((exp, index) => (
<motion.div
key={index}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-colors"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: index * 0.15,
ease: [0.25, 0.46, 0.45, 0.94]
}}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.3 }
}}
className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-all duration-300 hover:shadow-xl hover:shadow-zinc-900/50"
style={{
transform: 'translateZ(0)',
willChange: 'opacity'
willChange: 'opacity, transform'
}}
>
<h3 className="text-lg md:text-xl font-semibold text-white mb-1">
@@ -110,12 +125,20 @@ const Experience = () => {
</p>
<ul className="space-y-3 md:space-y-4">
{exp.highlights?.map((highlight: string, hIndex: number) => (
<li
<motion.li
key={hIndex}
className="text-zinc-400 text-sm leading-relaxed"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.4,
delay: 0.3 + (hIndex * 0.1),
ease: "easeOut"
}}
className="text-zinc-400 text-sm leading-relaxed flex items-start"
>
{highlight}
</li>
<span className="text-white/40 mr-2 mt-0.5"></span>
<span>{highlight}</span>
</motion.li>
))}
</ul>
</motion.div>
@@ -129,29 +152,40 @@ const Experience = () => {
<div className="flex justify-center items-center gap-4 mt-8">
<button
onClick={() => currentPage > 0 && setCurrentPage(prev => prev - 1)}
className="text-zinc-400 hover:text-white transition-colors p-2"
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === 0}
aria-label={t('pages.home.experience.navigation.prev')}
>
{t('pages.home.experience.navigation.prev')}
</button>
{[...Array(totalPages)].map((_, index) => (
<button
key={index}
onClick={() => setCurrentPage(index)}
className={`w-2 h-2 rounded-full transition-colors ${
index === currentPage ? 'bg-white' : 'bg-zinc-600 hover:bg-zinc-400'
}`}
aria-label={t('pages.home.experience.navigation.page', { page: index + 1 })}
/>
))}
<div className="flex items-center gap-2">
{[...Array(totalPages)].map((_, index) => (
<button
key={index}
onClick={() => setCurrentPage(index)}
className={`rounded-full transition-all duration-300 ${
index === currentPage
? 'bg-white'
: 'bg-zinc-600 hover:bg-zinc-400'
}`}
style={{
width: '5px',
height: '5px',
minWidth: '5px',
minHeight: '5px',
padding: '0'
}}
aria-label={t('pages.home.experience.navigation.page', { page: index + 1 })}
/>
))}
</div>
<button
onClick={() => currentPage < totalPages - 1 && setCurrentPage(prev => prev + 1)}
className="text-zinc-400 hover:text-white transition-colors p-2"
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === totalPages - 1}
aria-label={t('pages.home.experience.navigation.next')}
>
{t('pages.home.experience.navigation.next')}
</button>
</div>
)}
+14 -20
View File
@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Github, Linkedin, Mail } from 'lucide-react';
import { motion } from 'framer-motion';
import { FloatingPaths } from '../../../components/FloatingPaths';
const Hero = () => {
const { t } = useTranslation();
@@ -14,12 +13,7 @@ const Hero = () => {
};
return (
<section id="home" className="relative min-h-screen bg-zinc-900 text-white overflow-hidden">
{/* Floating Paths Background - nur eine Instanz für bessere Performance */}
<div className="absolute inset-0 pointer-events-none" style={{ zIndex: 1 }}>
<FloatingPaths position={1} />
</div>
<section id="home" className="relative min-h-screen text-white overflow-hidden">
{/* Concentric Circles Background - mit GPU-Beschleunigung */}
<div className="absolute inset-0 pointer-events-none flex items-center justify-center" style={{ zIndex: 2 }}>
{[1, 2, 3].map((index) => (
@@ -68,13 +62,13 @@ const Hero = () => {
</div>
{/* Main Content */}
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-16 relative" style={{ zIndex: 30 }}>
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-20 relative" style={{ zIndex: 30 }}>
{/* Profile Image */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="w-48 h-48 sm:w-64 sm:h-64 mb-6 rounded-full overflow-hidden border-2 border-zinc-800"
className="w-56 h-56 sm:w-72 sm:h-72 mb-8 rounded-full overflow-hidden border-2 border-zinc-800"
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
>
<img
@@ -91,7 +85,7 @@ const Hero = () => {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-white text-sm tracking-wider mb-2"
className="text-white text-base sm:text-lg tracking-wider mb-3"
>
{t('pages.home.hero.title')}
</motion.p>
@@ -99,7 +93,7 @@ const Hero = () => {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="text-4xl font-bold mb-8 flex items-center"
className="text-5xl sm:text-6xl font-bold mb-10 flex items-center"
>
{t('pages.home.hero.name')}<span className="animate-pulse ml-1">|</span>
</motion.h1>
@@ -109,35 +103,35 @@ const Hero = () => {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex flex-col sm:flex-row gap-3 sm:gap-6 text-white text-sm z-20"
className="flex flex-col sm:flex-row gap-4 sm:gap-8 text-white text-base sm:text-lg z-20"
>
<button
onClick={() => scrollToSection('experience')}
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
>
<span className="relative z-10">{t('pages.home.hero.navigation.experience')}</span>
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</button>
<button
onClick={() => scrollToSection('skills')}
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
>
<span className="relative z-10">{t('pages.home.hero.navigation.skills')}</span>
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</button>
<button
onClick={() => scrollToSection('projects')}
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
>
<span className="relative z-10">{t('pages.home.hero.navigation.projects')}</span>
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</button>
<button
onClick={() => scrollToSection('about')}
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
>
<span className="relative z-10">{t('pages.home.hero.navigation.about')}</span>
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</button>
</motion.div>
</div>
+2 -2
View File
@@ -41,7 +41,7 @@ const Projects = () => {
if (loading) {
return (
<section className="py-24 bg-zinc-900">
<section className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-center items-center min-h-[300px] gap-3">
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
@@ -55,7 +55,7 @@ const Projects = () => {
}
return (
<section className="py-24 bg-zinc-900">
<section className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
+74 -53
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { Database, Server, Store, Code2, CodepenIcon, Box } from 'lucide-react';
interface Skill {
@@ -50,7 +50,7 @@ const Skills = () => {
}
return (
<section className="py-24 bg-zinc-900">
<section className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -67,32 +67,20 @@ const Skills = () => {
<motion.div
key={skill.name}
className="space-y-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
style={{
transform: 'translateZ(0)',
willChange: 'opacity'
}}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: skills.indexOf(skill) * 0.1 }}
>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="text-white text-sm font-medium">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs">
{skill.description}
</span>
)}
</div>
<span className="text-white text-sm font-medium">
{skill.name}
</span>
<span className="text-white text-sm">
{skill.level}%
</span>
{skill.level}%
</span>
</div>
<div
className="h-2 bg-zinc-800 rounded-full overflow-hidden"
className="h-2 bg-zinc-800/50 rounded-full overflow-hidden"
role="progressbar"
aria-valuenow={skill.level}
aria-valuemin={0}
@@ -100,12 +88,14 @@ const Skills = () => {
aria-label={t('pages.home.skills.aria.skillLevel')}
>
<motion.div
className={`h-full rounded-full ${
selectedSkill === skill.name ? 'bg-zinc-500' : 'bg-zinc-600'
}`}
className="h-full rounded-full bg-gradient-to-r from-zinc-600 to-zinc-500"
initial={{ width: 0 }}
animate={{ width: `${skill.level}%` }}
transition={{ duration: 0.5 }}
animate={{
width: `${skill.level}%`
}}
transition={{
width: { duration: 1, delay: skills.indexOf(skill) * 0.1 }
}}
style={{
transform: 'translateZ(0)',
willChange: 'width'
@@ -121,32 +111,63 @@ const Skills = () => {
{skills.map((skill) => (
<motion.div
key={skill.name}
className={`p-6 rounded-xl bg-zinc-800 flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-zinc-700 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500' : ''
}`}
whileHover={{ scale: 1.05 }}
onClick={() =>
setSelectedSkill(skill.name === selectedSkill ? null : skill.name)
}
role="button"
aria-pressed={selectedSkill === skill.name}
aria-label={t('pages.home.skills.aria.selectSkill')}
style={{
transform: 'translateZ(0)',
willChange: 'transform'
}}
className="relative"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: skills.indexOf(skill) * 0.1 }}
>
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
{iconMap[skill.name]}
</div>
<span className="text-white text-sm text-center">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs text-center mt-1">
{skill.description}
</span>
)}
<motion.div
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
role="button"
aria-pressed={selectedSkill === skill.name}
aria-label={t('pages.home.skills.aria.selectSkill')}
style={{
transform: 'translateZ(0)',
willChange: 'transform',
position: 'relative',
zIndex: selectedSkill === skill.name ? 20 : 1
}}
>
<motion.div
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
animate={{
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
scale: selectedSkill === skill.name ? 1.1 : 1
}}
transition={{ duration: 0.4 }}
>
{iconMap[skill.name]}
</motion.div>
<span className="text-white text-sm text-center">
{skill.name}
</span>
<AnimatePresence>
{selectedSkill === skill.name && (
<motion.div
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
style={{
whiteSpace: 'nowrap',
transform: 'translate(-50%, calc(-100% - 12px))',
zIndex: 999
}}
>
{skill.description}
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800"></div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
))}
</div>
+3 -3
View File
@@ -58,13 +58,13 @@ const HomePage = () => {
'digital marketing automation. Based in Germany, working with clients worldwide.';
return (
<PageTransition>
<>
<SEO
title="Damjan Savić - Digital Solutions Consultant"
description={metaDescription}
schema={schema}
/>
<main className="min-h-screen bg-zinc-900">
<main className="min-h-screen">
{/* Hero Section */}
<div id="home">
<Hero />
@@ -95,7 +95,7 @@ const HomePage = () => {
<FAQSection />
</div>
</main>
</PageTransition>
</>
);
};
+3 -3
View File
@@ -114,7 +114,7 @@ const TranslatedProjectPage = () => {
if (loading) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -131,7 +131,7 @@ const TranslatedProjectPage = () => {
if (error || !project) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
<div className="min-h-screen flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -158,7 +158,7 @@ const TranslatedProjectPage = () => {
}
return (
<main className="min-h-screen bg-zinc-900">
<main className="min-h-screen">
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
{/* Back Navigation */}
<div className="mb-8">
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ExternalLink, Calendar, Tag } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { trackProjectClick } from '../../../services/gtm';
interface Project {
id: string;
@@ -33,9 +34,14 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
// Verwende technologies anstelle von tags wenn verfügbar
const displayTags = project.technologies || project.tags;
const handleClick = () => {
trackProjectClick(project.title, project.category);
};
return (
<Link
to={`/portfolio/${project.slug}`}
onClick={handleClick}
className="group relative block overflow-hidden bg-zinc-800/30 border border-zinc-800
rounded-lg hover:bg-zinc-800/50 hover:border-zinc-700
transition-all duration-300 focus:outline-none focus:ring-2
@@ -77,7 +77,7 @@ const PortfolioGrid = () => {
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] bg-zinc-900">
<div className="flex flex-col items-center justify-center min-h-[50vh]">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -94,7 +94,7 @@ const PortfolioGrid = () => {
if (projects.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] bg-zinc-900">
<div className="flex flex-col items-center justify-center min-h-[50vh]">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -118,7 +118,7 @@ const PortfolioGrid = () => {
}
return (
<section className="py-12 bg-zinc-900">
<section className="py-12">
<div className="max-w-6xl mx-auto px-4">
<motion.div
variants={containerVariants}
@@ -29,7 +29,7 @@ const ProjectLayout: React.FC<ProjectLayoutProps> = ({ children }) => {
variants={containerVariants}
initial="hidden"
animate="visible"
className="min-h-screen bg-zinc-900 relative overflow-x-hidden"
className="min-h-screen relative overflow-x-hidden"
>
{/* Header */}
<motion.div
+194
View File
@@ -0,0 +1,194 @@
/**
* Google Tag Manager Event Service
* Zentrale Verwaltung aller GTM Data Layer Events
*/
declare global {
interface Window {
dataLayer: any[];
}
}
// Initialisiere dataLayer falls nicht vorhanden
if (typeof window !== 'undefined' && !window.dataLayer) {
window.dataLayer = [];
}
/**
* Push event to GTM dataLayer
*/
export const pushToDataLayer = (data: Record<string, any>) => {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push(data);
}
};
/**
* Track page view
*/
export const trackPageView = (pagePath: string, pageTitle: string) => {
pushToDataLayer({
event: 'page_view',
page_path: pagePath,
page_title: pageTitle,
page_location: window.location.href,
});
};
/**
* Track contact form submission
*/
export const trackContactFormSubmit = (formData: {
name?: string;
email?: string;
subject?: string;
}) => {
pushToDataLayer({
event: 'contact_form_submit',
event_category: 'engagement',
event_label: 'contact_form',
form_name: formData.name,
form_subject: formData.subject,
});
};
/**
* Track portfolio project click
*/
export const trackProjectClick = (projectName: string, projectCategory?: string) => {
pushToDataLayer({
event: 'project_click',
event_category: 'portfolio',
event_label: projectName,
project_name: projectName,
project_category: projectCategory,
});
};
/**
* Track blog post view
*/
export const trackBlogPostView = (postTitle: string, postCategory?: string) => {
pushToDataLayer({
event: 'blog_post_view',
event_category: 'content',
event_label: postTitle,
post_title: postTitle,
post_category: postCategory,
});
};
/**
* Track social link click
*/
export const trackSocialClick = (platform: string) => {
pushToDataLayer({
event: 'social_click',
event_category: 'social',
event_label: platform,
social_platform: platform,
});
};
/**
* Track file download
*/
export const trackDownload = (fileName: string, fileType: string) => {
pushToDataLayer({
event: 'file_download',
event_category: 'download',
event_label: fileName,
file_name: fileName,
file_type: fileType,
});
};
/**
* Track navigation click
*/
export const trackNavigation = (navigationItem: string, navigationType: 'header' | 'footer' | 'menu') => {
pushToDataLayer({
event: 'navigation_click',
event_category: 'navigation',
event_label: navigationItem,
navigation_item: navigationItem,
navigation_type: navigationType,
});
};
/**
* Track scroll depth
*/
export const trackScrollDepth = (depth: number) => {
pushToDataLayer({
event: 'scroll_depth',
event_category: 'engagement',
event_label: `${depth}%`,
scroll_depth: depth,
});
};
/**
* Track time on page
*/
export const trackTimeOnPage = (seconds: number, pagePath: string) => {
pushToDataLayer({
event: 'time_on_page',
event_category: 'engagement',
event_label: pagePath,
time_seconds: seconds,
page_path: pagePath,
});
};
/**
* Track CTA button click
*/
export const trackCTAClick = (ctaText: string, ctaLocation: string) => {
pushToDataLayer({
event: 'cta_click',
event_category: 'conversion',
event_label: ctaText,
cta_text: ctaText,
cta_location: ctaLocation,
});
};
/**
* Track language change
*/
export const trackLanguageChange = (oldLang: string, newLang: string) => {
pushToDataLayer({
event: 'language_change',
event_category: 'preference',
event_label: `${oldLang}_to_${newLang}`,
old_language: oldLang,
new_language: newLang,
});
};
/**
* Track FAQ interaction
*/
export const trackFAQInteraction = (question: string, action: 'open' | 'close') => {
pushToDataLayer({
event: 'faq_interaction',
event_category: 'engagement',
event_label: question,
faq_question: question,
faq_action: action,
});
};
/**
* Track error
*/
export const trackError = (errorMessage: string, errorType: string) => {
pushToDataLayer({
event: 'error',
event_category: 'error',
event_label: errorType,
error_message: errorMessage,
error_type: errorType,
});
};
-1
View File
@@ -3,7 +3,6 @@
/* 1. Stabilize page transitions */
.page-transition-active {
z-index: 9999 !important;
background-color: rgb(24, 24, 27) !important; /* zinc-900 */
}
/* 2. Prevent floating paths from interfering */