Implementierung - SEO - 01.08.2025

This commit is contained in:
2025-08-01 20:33:47 +02:00
parent f176743885
commit e69e68242c
32 changed files with 12822 additions and 192 deletions
+13 -1
View File
@@ -3,7 +3,19 @@
"allow": [ "allow": [
"Bash(npm install:*)", "Bash(npm install:*)",
"Bash(mkdir:*)", "Bash(mkdir:*)",
"Bash(node:*)" "Bash(node:*)",
"Bash(timeout 10 pnpm run dev)",
"Bash(pnpm run:*)",
"Bash(pnpm exec:*)",
"Bash(npx tsc:*)",
"Bash(npm run dev:*)",
"Bash(npm run build:*)",
"Bash(git add:*)",
"Bash(rm:*)",
"Bash(ls:*)",
"Bash(pnpm add:*)",
"Bash(pnpm approve-builds:*)",
"Bash(curl:*)"
], ],
"deny": [] "deny": []
} }
+2 -2
View File
@@ -67,7 +67,7 @@ if (!self.define) {
}); });
}; };
} }
define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict'; define(['./workbox-be0f3d84'], (function (workbox) { 'use strict';
self.skipWaiting(); self.skipWaiting();
workbox.clientsClaim(); workbox.clientsClaim();
@@ -82,7 +82,7 @@ define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.umc7krbmrh" "revision": "0.8k8mhvbe2f8"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+26
View File
@@ -43,6 +43,16 @@
body { body {
background-color: #18181B !important; background-color: #18181B !important;
} }
/* Prevent initial flicker */
body {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
body.app-mounted {
opacity: 1;
}
html { html {
overflow-y: scroll; overflow-y: scroll;
@@ -73,8 +83,24 @@
background-color: #18181B; background-color: #18181B;
} }
} }
/* Font-Loading Optimierung */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/inter-var.woff2') format('woff2-variations');
}
body {
font-family: Inter, system-ui, sans-serif;
}
</style> </style>
<!-- Preload Font -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<!-- Google Analytics --> <!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-N0PZBL7X18"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-N0PZBL7X18"></script>
<script> <script>
+1
View File
@@ -69,6 +69,7 @@
"globals": "^15.9.0", "globals": "^15.9.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"sharp": "^0.34.3",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "^8.3.0", "typescript-eslint": "^8.3.0",
+8550
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const FONTS_DIR = path.join(__dirname, '..', 'public', 'fonts');
// Ensure fonts directory exists
if (!fs.existsSync(FONTS_DIR)) {
fs.mkdirSync(FONTS_DIR, { recursive: true });
}
// Download Inter font
const INTER_URL = 'https://rsms.me/inter/font-files/InterVariable.woff2';
const INTER_PATH = path.join(FONTS_DIR, 'inter-var.woff2');
console.log('Downloading Inter font...');
const file = fs.createWriteStream(INTER_PATH);
https.get(INTER_URL, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('Inter font downloaded successfully!');
});
}).on('error', (err) => {
fs.unlink(INTER_PATH, () => {});
console.error('Error downloading font:', err.message);
});
+32
View File
@@ -0,0 +1,32 @@
import https from 'https';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const FONTS_DIR = path.join(__dirname, '..', 'public', 'fonts');
// Ensure fonts directory exists
if (!fs.existsSync(FONTS_DIR)) {
fs.mkdirSync(FONTS_DIR, { recursive: true });
}
// Download Inter font
const INTER_URL = 'https://rsms.me/inter/font-files/InterVariable.woff2';
const INTER_PATH = path.join(FONTS_DIR, 'inter-var.woff2');
console.log('Downloading Inter font...');
const file = fs.createWriteStream(INTER_PATH);
https.get(INTER_URL, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('Inter font downloaded successfully!');
});
}).on('error', (err) => {
fs.unlink(INTER_PATH, () => {});
console.error('Error downloading font:', err.message);
});
+26
View File
@@ -0,0 +1,26 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
// Create simple placeholder SVG icons
const svg192 = `<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="192" fill="#18181b"/>
<text x="50%" y="50%" font-family="Arial, sans-serif" font-size="80" fill="white" text-anchor="middle" dominant-baseline="middle">DS</text>
</svg>`;
const svg512 = `<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#18181b"/>
<text x="50%" y="50%" font-family="Arial, sans-serif" font-size="200" fill="white" text-anchor="middle" dominant-baseline="middle">DS</text>
</svg>`;
// Convert SVG to PNG using Canvas API (simple approach)
// For now, we'll just save as SVG files
fs.writeFileSync(path.join(PUBLIC_DIR, 'icon-192x192.svg'), svg192);
fs.writeFileSync(path.join(PUBLIC_DIR, 'icon-512x512.svg'), svg512);
console.log('Placeholder SVG icons created. Note: These are SVG files, not PNG. Update manifest accordingly.');
+59
View File
@@ -0,0 +1,59 @@
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const SOURCE_IMAGE = path.join(PUBLIC_DIR, 'logo.png');
async function generateIcons() {
try {
// Check if source image exists
if (!fs.existsSync(SOURCE_IMAGE)) {
console.error('Source logo.png not found in public directory');
// Create placeholder icons
const svg = `
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#18181b"/>
<text x="50%" y="50%" font-family="Arial, sans-serif" font-size="200" fill="white" text-anchor="middle" dominant-baseline="middle">DS</text>
</svg>
`;
// Generate 192x192 icon
await sharp(Buffer.from(svg))
.resize(192, 192)
.png()
.toFile(path.join(PUBLIC_DIR, 'icon-192x192.png'));
// Generate 512x512 icon
await sharp(Buffer.from(svg))
.resize(512, 512)
.png()
.toFile(path.join(PUBLIC_DIR, 'icon-512x512.png'));
console.log('Placeholder icons created successfully');
return;
}
// Generate icons from source image
await sharp(SOURCE_IMAGE)
.resize(192, 192)
.png()
.toFile(path.join(PUBLIC_DIR, 'icon-192x192.png'));
await sharp(SOURCE_IMAGE)
.resize(512, 512)
.png()
.toFile(path.join(PUBLIC_DIR, 'icon-512x512.png'));
console.log('Icons generated successfully');
} catch (error) {
console.error('Error generating icons:', error);
}
}
generateIcons();
+1 -14
View File
@@ -26,30 +26,17 @@ const RouteTracker = () => {
return null; return null;
}; };
// Funktion zum Blockieren von externen Google Fonts
const blockExternalGoogleFonts = () => {
// Suche nach allen Google Fonts Link-Elementen und entferne sie
const linkElements = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
linkElements.forEach(link => {
link.parentNode?.removeChild(link);
});
};
function App() { function App() {
useEffect(() => { useEffect(() => {
// Benutzerdefiniertes Scrollbar-Styling aktivieren // Benutzerdefiniertes Scrollbar-Styling aktivieren
document.documentElement.classList.add('custom-scrollbar'); document.documentElement.classList.add('custom-scrollbar');
// Blockiere Google Fonts beim ersten Laden, sofern nicht explizit zugestimmt wurde
if (!isCookieCategoryEnabled('functional')) {
blockExternalGoogleFonts();
}
}, []); }, []);
return ( return (
<HelmetProvider> <HelmetProvider>
<ErrorBoundary> <ErrorBoundary>
<BrowserRouter> <BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ScrollProvider> <ScrollProvider>
<Suspense fallback={null}> <Suspense fallback={null}>
<PageTransition> <PageTransition>
+3 -2
View File
@@ -15,8 +15,8 @@ export function FloatingPaths({ position }: { position: number }) {
})) }))
return ( return (
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none floating-paths" style={{ isolation: 'isolate', zIndex: 0 }}>
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none"> <svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none" style={{ transform: 'translateZ(0)' }}>
<title>Background Paths</title> <title>Background Paths</title>
{paths.map((path) => ( {paths.map((path) => (
<motion.path <motion.path
@@ -36,6 +36,7 @@ export function FloatingPaths({ position }: { position: number }) {
repeat: Number.POSITIVE_INFINITY, repeat: Number.POSITIVE_INFINITY,
ease: "linear", ease: "linear",
}} }}
style={{ willChange: 'auto' }}
/> />
))} ))}
</svg> </svg>
-1
View File
@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'react-router-dom';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
+15 -6
View File
@@ -19,7 +19,7 @@ import LanguageSwitcher from "./LanguageSwitcher";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import Footer from "./Footer"; import Footer from "./Footer";
import { useScrollContext } from "./ScrollContext"; import { useScrollContext } from "./ScrollContext";
import PerformanceMonitor from "./PerformanceMonitor"; import { PerformanceMonitor } from "./PerformanceMonitor";
interface LayoutProps { interface LayoutProps {
children: ReactNode; children: ReactNode;
@@ -33,9 +33,12 @@ const Layout = ({ children }: LayoutProps) => {
const location = useLocation(); const location = useLocation();
const { isMenuOpen, setIsMenuOpen } = useScrollContext(); const { isMenuOpen, setIsMenuOpen } = useScrollContext();
// Initial mount // Initial mount with immediate execution
useEffect(() => { useEffect(() => {
setIsMounted(true); // Use requestAnimationFrame to ensure smooth initial render
requestAnimationFrame(() => {
setIsMounted(true);
});
}, []); }, []);
// Schließe das Menü beim Routenwechsel // Schließe das Menü beim Routenwechsel
@@ -116,8 +119,13 @@ const Layout = ({ children }: LayoutProps) => {
: []), : []),
]; ];
// Render with opacity 0 on initial mount to prevent flicker
if (!isMounted) { if (!isMounted) {
return null; return (
<div className="min-h-screen bg-zinc-900">
{children}
</div>
);
} }
return ( return (
@@ -126,9 +134,10 @@ const Layout = ({ children }: LayoutProps) => {
<nav <nav
className={` className={`
fixed w-full z-40 transition-all duration-300 fixed w-full z-40 transition-all duration-300
bg-zinc-800/50 backdrop-blur-sm border-b border-zinc-700/30
${scrolled ${scrolled
? "bg-zinc-900/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-900/80" ? "bg-zinc-900/80 backdrop-blur-md shadow-lg shadow-zinc-900/50"
: "bg-transparent" : ""
} }
`} `}
role="navigation" role="navigation"
+1 -1
View File
@@ -31,7 +31,7 @@ export function NavLink({ to, icon, label, onClick, className }: NavLinkProps) {
{isActive && ( {isActive && (
<motion.div <motion.div
layoutId="activeIndicator" layoutId="activeIndicator"
className="absolute inset-0 rounded-full bg-zinc-800/50 -z-10" className="absolute inset-0 rounded-full bg-zinc-700/60 -z-10"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
+119 -100
View File
@@ -1,6 +1,6 @@
"use client" "use client"
import React, { useEffect, useState, useCallback } from "react" import React, { useEffect, useState, useRef } from "react"
import { motion, AnimatePresence, cubicBezier } from "framer-motion" import { motion, AnimatePresence } from "framer-motion"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { useScrollContext } from './ScrollContext' import { useScrollContext } from './ScrollContext'
@@ -8,126 +8,145 @@ interface PageTransitionProps {
children: React.ReactNode children: React.ReactNode
} }
const customEase = cubicBezier(0.25, 0.1, 0.25, 1) const Logo = () => {
const [imageError, setImageError] = useState(false);
const Logo = () => (
<motion.div return (
initial={{ opacity: 0, scale: 0.8 }} <motion.div
animate={{ initial={{ opacity: 0, scale: 0.8 }}
opacity: 1, animate={{
scale: 1, opacity: 1,
transition: { duration: 0.4, ease: customEase }, scale: 1,
}} transition: { duration: 0.4 },
exit={{ }}
opacity: 0, exit={{
scale: 1.2, opacity: 0,
transition: { duration: 0.3, ease: customEase }, scale: 1.2,
}} transition: { duration: 0.3 },
className="w-16 h-16" }}
> className="w-16 h-16 relative"
<img >
src="/logo.png" {!imageError ? (
alt="Logo" <img
className="w-full h-full object-contain filter brightness-200" src="/logo.png"
/> alt="Logo"
</motion.div> className="w-full h-full object-contain filter brightness-200"
) onError={() => setImageError(true)}
/>
) : (
<div className="w-full h-full flex items-center justify-center text-white font-bold text-2xl bg-zinc-800 rounded-full">
DS
</div>
)}
</motion.div>
);
}
const PageTransition = ({ children }: PageTransitionProps) => { const PageTransition = ({ children }: PageTransitionProps) => {
const location = useLocation() const location = useLocation()
const [showLogo, setShowLogo] = useState(false) const [isTransitioning, setIsTransitioning] = useState(true) // Start with transition on initial load
const [showContent, setShowContent] = useState(true) const [showContent, setShowContent] = useState(false)
const { setIsTransitioning } = useScrollContext() const { setIsTransitioning: setGlobalTransitioning } = useScrollContext()
const [isAnimating, setIsAnimating] = useState(false) const previousPathRef = useRef<string | null>(null)
const isFirstMount = useRef(true)
const resetScroll = useCallback(() => { // Handle initial page load transition
window.scrollTo({ useEffect(() => {
top: 0, if (isFirstMount.current) {
behavior: 'instant' setIsTransitioning(true);
}) setGlobalTransitioning(true);
}, [])
// Show content after transition animation
const startTransition = useCallback(() => { const timer = setTimeout(() => {
setIsAnimating(true) setIsTransitioning(false);
setIsTransitioning(true) setGlobalTransitioning(false);
setShowContent(false) setShowContent(true);
setShowLogo(true) isFirstMount.current = false;
}, [setIsTransitioning]) }, 1200);
const finishTransition = useCallback(() => { return () => clearTimeout(timer);
setShowContent(true) }
setShowLogo(false) }, [setGlobalTransitioning]);
setIsAnimating(false)
setIsTransitioning(false)
resetScroll()
}, [setIsTransitioning, resetScroll])
useEffect(() => { useEffect(() => {
const cleanup = () => { // Skip on first mount
setShowLogo(false) if (isFirstMount.current) {
setShowContent(true) previousPathRef.current = location.pathname;
setIsAnimating(false) return;
setIsTransitioning(false)
} }
if (location.pathname) { // Only animate if path actually changed
startTransition() if (location.pathname === previousPathRef.current) {
return;
}
const logoTimer = setTimeout(() => { // Hide content immediately
setShowLogo(false) setShowContent(false);
}, 800)
// Small delay to ensure React has finished updating
const startTimer = setTimeout(() => {
// Update previous path
previousPathRef.current = location.pathname;
const contentTimer = setTimeout(() => { // Reset scroll position
finishTransition() window.scrollTo(0, 0);
}, 1000)
return () => { // Start transition
clearTimeout(logoTimer) setIsTransitioning(true);
clearTimeout(contentTimer) setGlobalTransitioning(true);
cleanup()
// End transition after animation and show new content
const endTimer = setTimeout(() => {
setIsTransitioning(false);
setGlobalTransitioning(false);
setShowContent(true);
}, 1200);
// Store the end timer for cleanup
(window as any).__pageTransitionEndTimer = endTimer;
}, 10);
return () => {
clearTimeout(startTimer);
if ((window as any).__pageTransitionEndTimer) {
clearTimeout((window as any).__pageTransitionEndTimer);
} }
} setIsTransitioning(false);
setGlobalTransitioning(false);
return cleanup };
}, [location.pathname, startTransition, finishTransition]) }, [location.pathname, setGlobalTransitioning]);
return ( return (
<div className={`relative min-h-screen bg-zinc-900 ${isAnimating ? 'pointer-events-none' : ''}`}> <>
<AnimatePresence mode="wait"> {/* Page transition overlay - always on top */}
{showLogo && ( <AnimatePresence>
{isTransitioning && (
<motion.div <motion.div
className="fixed inset-0 z-[60] bg-zinc-900 pointer-events-none" key="transition-overlay"
className="fixed inset-0 bg-zinc-900 flex items-center justify-center page-transition-active"
style={{ zIndex: 9999 }}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: customEase }} transition={{ duration: 0.4, ease: "easeInOut" }}
> >
<div className="absolute inset-0 flex items-center justify-center"> <Logo />
<Logo />
</div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence mode="wait"> {/* Content - only show when transition is complete */}
{showContent && ( <div style={{ opacity: showContent ? 1 : 0, transition: 'opacity 0.3s ease-in-out' }}>
<motion.div <motion.div
className="relative z-0 min-h-screen" key={location.pathname}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: showContent ? 1 : 0 }}
exit={{ opacity: 0 }} transition={{ duration: 0.3 }}
transition={{ >
duration: 0.3, {children}
ease: customEase, </motion.div>
}} </div>
> </>
<div className={isAnimating ? 'pointer-events-none' : ''}>
{children}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) )
} }
+30 -1
View File
@@ -1,9 +1,19 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import './colors.css'; @import './colors.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Font loading states */
html.fonts-loading body {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
html.fonts-loaded body,
html.fonts-fallback body {
opacity: 1;
}
@layer base { @layer base {
:root { :root {
font-family: Inter, system-ui, sans-serif; font-family: Inter, system-ui, sans-serif;
@@ -40,6 +50,14 @@
.text-gradient { .text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary via-accent to-secondary; @apply bg-clip-text text-transparent bg-gradient-to-r from-primary via-accent to-secondary;
} }
/* GPU acceleration utilities - simplified */
.gpu-accelerated {
transform: translateZ(0);
-webkit-transform: translateZ(0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
} }
@keyframes float { @keyframes float {
@@ -60,6 +78,17 @@
} }
} }
/* Prevent hover flickering */
* {
-webkit-tap-highlight-color: transparent;
}
a, button {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform: translateZ(0);
}
/* Scrollbar Styling */ /* Scrollbar Styling */
@media (pointer: fine) { @media (pointer: fine) {
::-webkit-scrollbar { ::-webkit-scrollbar {
+10
View File
@@ -4,9 +4,11 @@ import { HelmetProvider } from 'react-helmet-async';
import App from './App'; import App from './App';
import './index.css'; import './index.css';
import './styles/mobile-optimizations.css'; import './styles/mobile-optimizations.css';
import './styles/minimal-fixes.css';
import './i18n'; import './i18n';
import { register as registerServiceWorker } from './utils/serviceWorkerRegistration'; import { register as registerServiceWorker } from './utils/serviceWorkerRegistration';
import { reportWebVitals } from './utils/webVitals'; import { reportWebVitals } from './utils/webVitals';
import { initializeFonts } from './utils/fontLoader';
// Typdefinition für Google Analytics // Typdefinition für Google Analytics
declare global { declare global {
@@ -17,6 +19,9 @@ declare global {
} }
} }
// Initialize fonts as early as possible
initializeFonts();
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
if (!rootElement) { if (!rootElement) {
@@ -29,6 +34,11 @@ createRoot(rootElement).render(
</HelmetProvider> </HelmetProvider>
); );
// Mark app as mounted to prevent flicker
requestAnimationFrame(() => {
document.body.classList.add('app-mounted');
});
// Register service worker // Register service worker
registerServiceWorker(); registerServiceWorker();
+16
View File
@@ -12,6 +12,10 @@ const About = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center" className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
{/* Image Section */} {/* Image Section */}
<div className="relative"> <div className="relative">
@@ -20,6 +24,10 @@ const About = () => {
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="rounded-3xl overflow-hidden aspect-square" className="rounded-3xl overflow-hidden aspect-square"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
<img <img
src="/portrait.jpg" src="/portrait.jpg"
@@ -36,6 +44,10 @@ const About = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }} transition={{ duration: 0.5, delay: 0.2 }}
className="text-3xl font-bold text-white" className="text-3xl font-bold text-white"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
{t('pages.home.about.title')} {t('pages.home.about.title')}
</motion.h2> </motion.h2>
@@ -45,6 +57,10 @@ const About = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }} transition={{ duration: 0.5, delay: 0.3 }}
className="text-zinc-400 leading-relaxed" className="text-zinc-400 leading-relaxed"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
<div className="text-zinc-400 leading-relaxed text-justify"> <div className="text-zinc-400 leading-relaxed text-justify">
{t('pages.home.about.content')} {t('pages.home.about.content')}
+12
View File
@@ -59,6 +59,10 @@ const Experience = () => {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
<h2 className="text-3xl font-bold text-white text-center mb-12"> <h2 className="text-3xl font-bold text-white text-center mb-12">
{t('pages.home.experience.title')} {t('pages.home.experience.title')}
@@ -78,6 +82,10 @@ const Experience = () => {
exit={{ opacity: 0, x: -50 }} exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="grid grid-cols-1 md:grid-cols-2 gap-6" className="grid grid-cols-1 md:grid-cols-2 gap-6"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
{getCurrentPageItems().map((exp, index) => ( {getCurrentPageItems().map((exp, index) => (
<motion.div <motion.div
@@ -86,6 +94,10 @@ const Experience = () => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.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" className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-colors"
style={{
transform: 'translateZ(0)',
willChange: 'opacity'
}}
> >
<h3 className="text-lg md:text-xl font-semibold text-white mb-1"> <h3 className="text-lg md:text-xl font-semibold text-white mb-1">
{exp.role} {exp.role}
+35 -28
View File
@@ -14,48 +14,48 @@ const Hero = () => {
}; };
return ( return (
<section id="home" className="relative min-h-screen bg-zinc-900 text-white"> <section id="home" className="relative min-h-screen bg-zinc-900 text-white overflow-hidden">
{/* Floating Paths Background */} {/* Floating Paths Background - nur eine Instanz für bessere Performance */}
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none" style={{ zIndex: 1 }}>
<FloatingPaths position={1} /> <FloatingPaths position={1} />
<FloatingPaths position={-1} />
</div> </div>
{/* Concentric Circles Background */} {/* Concentric Circles Background - mit GPU-Beschleunigung */}
<div className="absolute inset-0 pointer-events-none flex items-center justify-center"> <div className="absolute inset-0 pointer-events-none flex items-center justify-center" style={{ zIndex: 2 }}>
{[1, 2, 3, 4, 5].map((index) => ( {[1, 2, 3].map((index) => (
<div <div
key={index} key={index}
className="absolute rounded-full border border-zinc-800" className="absolute rounded-full border border-zinc-800/30"
style={{ style={{
width: `${index * 20}%`, width: `${index * 30}%`,
height: `${index * 20}%`, height: `${index * 30}%`,
opacity: 0.1 opacity: 0.1,
transform: 'translateZ(0)'
}} }}
/> />
))} ))}
</div> </div>
{/* Top Navigation */} {/* Top Navigation */}
<div className="absolute top-4 w-full px-8 flex justify-between items-center z-20"> <div className="absolute top-4 w-full px-8 flex justify-between items-center" style={{ zIndex: 40 }}>
<div className="flex gap-4"> <div className="flex gap-4">
<a <a
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/" href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-white hover:text-zinc-300 transition-colors" className="inline-flex items-center justify-center w-9 h-9 text-white hover:text-zinc-300 transition-colors"
aria-label={t('pages.home.hero.social.linkedin.title')} aria-label={t('pages.home.hero.social.linkedin.title')}
> >
<Linkedin className="h-5 w-5" /> <Linkedin className="h-5 w-5 pointer-events-none" />
</a> </a>
<a <a
href="https://github.com/damjan1996" href="https://github.com/damjan1996"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-white hover:text-zinc-300 transition-colors" className="inline-flex items-center justify-center w-9 h-9 text-white hover:text-zinc-300 transition-colors"
aria-label={t('pages.home.hero.social.github.title')} aria-label={t('pages.home.hero.social.github.title')}
> >
<Github className="h-5 w-5" /> <Github className="h-5 w-5 pointer-events-none" />
</a> </a>
</div> </div>
<a <a
@@ -68,18 +68,21 @@ const Hero = () => {
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-16 relative z-10"> <div className="flex flex-col items-center justify-start min-h-screen px-4 pt-16 relative" style={{ zIndex: 30 }}>
{/* Profile Image */} {/* Profile Image */}
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }} 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-48 h-48 sm:w-64 sm:h-64 mb-6 rounded-full overflow-hidden border-2 border-zinc-800"
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
> >
<img <img
src="/portrait.jpg" src="/portrait.jpg"
alt={t('pages.home.hero.image.alt')} alt={t('pages.home.hero.image.alt')}
className="w-full h-full object-cover" className="w-full h-full object-cover"
loading="eager"
style={{ transform: 'scale(1.01)' }}
/> />
</motion.div> </motion.div>
@@ -110,39 +113,43 @@ const Hero = () => {
> >
<button <button
onClick={() => scrollToSection('experience')} onClick={() => scrollToSection('experience')}
className="w-full sm:w-auto px-6 py-2 hover:bg-zinc-800 rounded-full transition-all duration-300 uppercase cursor-pointer" className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
> >
{t('pages.home.hero.navigation.experience')} <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" />
</button> </button>
<button <button
onClick={() => scrollToSection('skills')} onClick={() => scrollToSection('skills')}
className="w-full sm:w-auto px-6 py-2 hover:bg-zinc-800 rounded-full transition-all duration-300 uppercase cursor-pointer" className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
> >
{t('pages.home.hero.navigation.skills')} <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" />
</button> </button>
<button <button
onClick={() => scrollToSection('projects')} onClick={() => scrollToSection('projects')}
className="w-full sm:w-auto px-6 py-2 hover:bg-zinc-800 rounded-full transition-all duration-300 uppercase cursor-pointer" className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
> >
{t('pages.home.hero.navigation.projects')} <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" />
</button> </button>
<button <button
onClick={() => scrollToSection('about')} onClick={() => scrollToSection('about')}
className="w-full sm:w-auto px-6 py-2 hover:bg-zinc-800 rounded-full transition-all duration-300 uppercase cursor-pointer" className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
> >
{t('pages.home.hero.navigation.about')} <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" />
</button> </button>
</motion.div> </motion.div>
</div> </div>
{/* Contact Button Bottom */} {/* Contact Button Bottom */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-20"> <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2" style={{ zIndex: 40 }}>
<a <a
href="mailto:info@damjan-savic.com" href="mailto:info@damjan-savic.com"
className="text-white hover:text-zinc-300 transition-colors" className="inline-flex items-center justify-center w-10 h-10 text-white hover:text-zinc-300 transition-colors"
aria-label={t('pages.home.hero.social.getInTouch')} aria-label={t('pages.home.hero.social.getInTouch')}
> >
<Mail className="h-6 w-6" /> <Mail className="h-6 w-6 pointer-events-none" />
</a> </a>
</div> </div>
</section> </section>
+12
View File
@@ -71,6 +71,10 @@ const Skills = () => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
onHoverStart={() => setSelectedSkill(skill.name)} onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)} onHoverEnd={() => setSelectedSkill(null)}
style={{
transform: 'translateZ(0)',
willChange: 'opacity'
}}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex flex-col"> <div className="flex flex-col">
@@ -102,6 +106,10 @@ const Skills = () => {
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${skill.level}%` }} animate={{ width: `${skill.level}%` }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
style={{
transform: 'translateZ(0)',
willChange: 'width'
}}
/> />
</div> </div>
</motion.div> </motion.div>
@@ -123,6 +131,10 @@ const Skills = () => {
role="button" role="button"
aria-pressed={selectedSkill === skill.name} aria-pressed={selectedSkill === skill.name}
aria-label={t('pages.home.skills.aria.selectSkill')} aria-label={t('pages.home.skills.aria.selectSkill')}
style={{
transform: 'translateZ(0)',
willChange: 'transform'
}}
> >
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}> <div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
{iconMap[skill.name]} {iconMap[skill.name]}
@@ -45,6 +45,10 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
variants={itemVariants} variants={itemVariants}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="relative" className="relative"
style={{
transform: 'translateZ(0)',
willChange: 'opacity, transform'
}}
> >
{/* Image Container */} {/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden"> <div className="aspect-[16/9] w-full relative overflow-hidden">
@@ -27,7 +27,7 @@ const PortfolioGrid = () => {
try { try {
const localeModule = await import( const localeModule = await import(
`../../../i18n/locales/${i18n.language}/portfolio` `../../../i18n/locales/${i18n.language}/portfolio/index.ts`
); );
// Mische die Projekte mit lokalisierten Inhalten, sofern verfügbar // Mische die Projekte mit lokalisierten Inhalten, sofern verfügbar
+31
View File
@@ -0,0 +1,31 @@
/* Minimal fixes for animation issues */
/* 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 */
.floating-paths {
z-index: -1 !important;
pointer-events: none !important;
}
/* 3. Ensure smooth font loading */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 4. Fix for Safari/iOS */
@supports (-webkit-touch-callout: none) {
body {
-webkit-transform: translateZ(0);
}
}
/* 5. Prevent animation conflicts during route changes */
.page-transition-active * {
animation-play-state: paused !important;
}
+2 -25
View File
@@ -33,7 +33,6 @@ a {
justify-content: center; justify-content: center;
padding: 12px 24px; padding: 12px 24px;
touch-action: manipulation; touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
} }
/* Mobile navigation optimized for thumb reach */ /* Mobile navigation optimized for thumb reach */
@@ -147,28 +146,6 @@ a {
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
/* Performance optimizations */ /* Performance optimizations handled by interaction-fix.css */
.will-change-transform {
will-change: transform;
}
.gpu-accelerated { /* Reduced motion handled by interaction-fix.css */
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;
}
}
+30
View File
@@ -0,0 +1,30 @@
// Font loading utility to ensure fonts are loaded before showing content
export const preloadFonts = async () => {
if ('fonts' in document) {
try {
await document.fonts.load('1em Inter');
document.documentElement.classList.add('fonts-loaded');
} catch (err) {
console.warn('Font loading failed:', err);
// Fallback to system fonts
document.documentElement.classList.add('fonts-fallback');
}
}
};
// Call this function as early as possible
export const initializeFonts = () => {
// Add loading class immediately
document.documentElement.classList.add('fonts-loading');
// Start font loading
preloadFonts();
// Remove loading class after a timeout to prevent indefinite loading state
setTimeout(() => {
document.documentElement.classList.remove('fonts-loading');
if (!document.documentElement.classList.contains('fonts-loaded')) {
document.documentElement.classList.add('fonts-fallback');
}
}, 3000);
};
+4 -4
View File
@@ -39,10 +39,10 @@ function sendToAnalytics(metric: Metric) {
} }
})); }));
// Also send to your analytics endpoint if needed // Comment out analytics endpoint for now - not implemented
if (navigator.sendBeacon) { // if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', body); // navigator.sendBeacon('/api/analytics', body);
} // }
} }
export function reportWebVitals() { export function reportWebVitals() {
+8 -5
View File
@@ -39,14 +39,16 @@ export default defineConfig({
scope: '/', scope: '/',
icons: [ icons: [
{ {
src: '/icon-192x192.png', src: '/logo.png',
sizes: '192x192', sizes: '192x192',
type: 'image/png' type: 'image/png',
purpose: 'any maskable'
}, },
{ {
src: '/icon-512x512.png', src: '/logo.png',
sizes: '512x512', sizes: '512x512',
type: 'image/png' type: 'image/png',
purpose: 'any maskable'
} }
] ]
}, },
@@ -81,7 +83,8 @@ export default defineConfig({
cleanupOutdatedCaches: true cleanupOutdatedCaches: true
}, },
devOptions: { devOptions: {
enabled: true enabled: true,
type: 'module'
} }
}) })
], ],