Implementierung - SEO - 01.08.2025
This commit is contained in:
@@ -3,7 +3,19 @@
|
||||
"allow": [
|
||||
"Bash(npm install:*)",
|
||||
"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": []
|
||||
}
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ if (!self.define) {
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict';
|
||||
define(['./workbox-be0f3d84'], (function (workbox) { 'use strict';
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
@@ -82,7 +82,7 @@ define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.umc7krbmrh"
|
||||
"revision": "0.8k8mhvbe2f8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
+1
-1
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
@@ -44,6 +44,16 @@
|
||||
background-color: #18181B !important;
|
||||
}
|
||||
|
||||
/* Prevent initial flicker */
|
||||
body {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
body.app-mounted {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
@@ -73,8 +83,24 @@
|
||||
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>
|
||||
|
||||
<!-- Preload Font -->
|
||||
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-N0PZBL7X18"></script>
|
||||
<script>
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"globals": "^15.9.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"postcss": "^8.4.35",
|
||||
"sharp": "^0.34.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
|
||||
Generated
+8550
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.');
|
||||
@@ -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
@@ -26,30 +26,17 @@ const RouteTracker = () => {
|
||||
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() {
|
||||
useEffect(() => {
|
||||
// Benutzerdefiniertes Scrollbar-Styling aktivieren
|
||||
document.documentElement.classList.add('custom-scrollbar');
|
||||
|
||||
// Blockiere Google Fonts beim ersten Laden, sofern nicht explizit zugestimmt wurde
|
||||
if (!isCookieCategoryEnabled('functional')) {
|
||||
blockExternalGoogleFonts();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<ScrollProvider>
|
||||
<Suspense fallback={null}>
|
||||
<PageTransition>
|
||||
|
||||
@@ -15,8 +15,8 @@ export function FloatingPaths({ position }: { position: number }) {
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="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" style={{ transform: 'translateZ(0)' }}>
|
||||
<title>Background Paths</title>
|
||||
{paths.map((path) => (
|
||||
<motion.path
|
||||
@@ -36,6 +36,7 @@ export function FloatingPaths({ position }: { position: number }) {
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "linear",
|
||||
}}
|
||||
style={{ willChange: 'auto' }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import LanguageSwitcher from "./LanguageSwitcher";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import Footer from "./Footer";
|
||||
import { useScrollContext } from "./ScrollContext";
|
||||
import PerformanceMonitor from "./PerformanceMonitor";
|
||||
import { PerformanceMonitor } from "./PerformanceMonitor";
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -33,9 +33,12 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
const location = useLocation();
|
||||
const { isMenuOpen, setIsMenuOpen } = useScrollContext();
|
||||
|
||||
// Initial mount
|
||||
// Initial mount with immediate execution
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
// Use requestAnimationFrame to ensure smooth initial render
|
||||
requestAnimationFrame(() => {
|
||||
setIsMounted(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 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) {
|
||||
return null;
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-900">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -126,9 +134,10 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<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 supports-[backdrop-filter]:bg-zinc-900/80"
|
||||
: "bg-transparent"
|
||||
? "bg-zinc-900/80 backdrop-blur-md shadow-lg shadow-zinc-900/50"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
role="navigation"
|
||||
|
||||
@@ -31,7 +31,7 @@ export function NavLink({ to, icon, label, onClick, className }: NavLinkProps) {
|
||||
{isActive && (
|
||||
<motion.div
|
||||
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 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import React, { useEffect, useState, useCallback } from "react"
|
||||
import { motion, AnimatePresence, cubicBezier } from "framer-motion"
|
||||
import React, { useEffect, useState, useRef } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { useScrollContext } from './ScrollContext'
|
||||
|
||||
@@ -8,126 +8,145 @@ interface PageTransitionProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const customEase = cubicBezier(0.25, 0.1, 0.25, 1)
|
||||
const Logo = () => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const Logo = () => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.4, ease: customEase },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 1.2,
|
||||
transition: { duration: 0.3, ease: customEase },
|
||||
}}
|
||||
className="w-16 h-16"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
className="w-full h-full object-contain filter brightness-200"
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.4 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 1.2,
|
||||
transition: { duration: 0.3 },
|
||||
}}
|
||||
className="w-16 h-16 relative"
|
||||
>
|
||||
{!imageError ? (
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
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 location = useLocation()
|
||||
const [showLogo, setShowLogo] = useState(false)
|
||||
const [showContent, setShowContent] = useState(true)
|
||||
const { setIsTransitioning } = useScrollContext()
|
||||
const [isAnimating, setIsAnimating] = useState(false)
|
||||
const [isTransitioning, setIsTransitioning] = useState(true) // Start with transition on initial load
|
||||
const [showContent, setShowContent] = useState(false)
|
||||
const { setIsTransitioning: setGlobalTransitioning } = useScrollContext()
|
||||
const previousPathRef = useRef<string | null>(null)
|
||||
const isFirstMount = useRef(true)
|
||||
|
||||
const resetScroll = useCallback(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'instant'
|
||||
})
|
||||
}, [])
|
||||
// Handle initial page load transition
|
||||
useEffect(() => {
|
||||
if (isFirstMount.current) {
|
||||
setIsTransitioning(true);
|
||||
setGlobalTransitioning(true);
|
||||
|
||||
const startTransition = useCallback(() => {
|
||||
setIsAnimating(true)
|
||||
setIsTransitioning(true)
|
||||
setShowContent(false)
|
||||
setShowLogo(true)
|
||||
}, [setIsTransitioning])
|
||||
// Show content after transition animation
|
||||
const timer = setTimeout(() => {
|
||||
setIsTransitioning(false);
|
||||
setGlobalTransitioning(false);
|
||||
setShowContent(true);
|
||||
isFirstMount.current = false;
|
||||
}, 1200);
|
||||
|
||||
const finishTransition = useCallback(() => {
|
||||
setShowContent(true)
|
||||
setShowLogo(false)
|
||||
setIsAnimating(false)
|
||||
setIsTransitioning(false)
|
||||
resetScroll()
|
||||
}, [setIsTransitioning, resetScroll])
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [setGlobalTransitioning]);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = () => {
|
||||
setShowLogo(false)
|
||||
setShowContent(true)
|
||||
setIsAnimating(false)
|
||||
setIsTransitioning(false)
|
||||
// Skip on first mount
|
||||
if (isFirstMount.current) {
|
||||
previousPathRef.current = location.pathname;
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname) {
|
||||
startTransition()
|
||||
// Only animate if path actually changed
|
||||
if (location.pathname === previousPathRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logoTimer = setTimeout(() => {
|
||||
setShowLogo(false)
|
||||
}, 800)
|
||||
// Hide content immediately
|
||||
setShowContent(false);
|
||||
|
||||
const contentTimer = setTimeout(() => {
|
||||
finishTransition()
|
||||
}, 1000)
|
||||
// Small delay to ensure React has finished updating
|
||||
const startTimer = setTimeout(() => {
|
||||
// Update previous path
|
||||
previousPathRef.current = location.pathname;
|
||||
|
||||
return () => {
|
||||
clearTimeout(logoTimer)
|
||||
clearTimeout(contentTimer)
|
||||
cleanup()
|
||||
// Reset scroll position
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
// Start transition
|
||||
setIsTransitioning(true);
|
||||
setGlobalTransitioning(true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanup
|
||||
}, [location.pathname, startTransition, finishTransition])
|
||||
setIsTransitioning(false);
|
||||
setGlobalTransitioning(false);
|
||||
};
|
||||
}, [location.pathname, setGlobalTransitioning]);
|
||||
|
||||
return (
|
||||
<div className={`relative min-h-screen bg-zinc-900 ${isAnimating ? 'pointer-events-none' : ''}`}>
|
||||
<AnimatePresence mode="wait">
|
||||
{showLogo && (
|
||||
<>
|
||||
{/* Page transition overlay - always on top */}
|
||||
<AnimatePresence>
|
||||
{isTransitioning && (
|
||||
<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 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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 />
|
||||
</div>
|
||||
<Logo />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{showContent && (
|
||||
<motion.div
|
||||
className="relative z-0 min-h-screen"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: customEase,
|
||||
}}
|
||||
>
|
||||
<div className={isAnimating ? 'pointer-events-none' : ''}>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{/* Content - only show when transition is complete */}
|
||||
<div style={{ opacity: showContent ? 1 : 0, transition: 'opacity 0.3s ease-in-out' }}>
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: showContent ? 1 : 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+30
-1
@@ -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';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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 {
|
||||
:root {
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
@@ -40,6 +50,14 @@
|
||||
.text-gradient {
|
||||
@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 {
|
||||
@@ -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 */
|
||||
@media (pointer: fine) {
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -4,9 +4,11 @@ import { HelmetProvider } from 'react-helmet-async';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import './styles/mobile-optimizations.css';
|
||||
import './styles/minimal-fixes.css';
|
||||
import './i18n';
|
||||
import { register as registerServiceWorker } from './utils/serviceWorkerRegistration';
|
||||
import { reportWebVitals } from './utils/webVitals';
|
||||
import { initializeFonts } from './utils/fontLoader';
|
||||
|
||||
// Typdefinition für Google Analytics
|
||||
declare global {
|
||||
@@ -17,6 +19,9 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize fonts as early as possible
|
||||
initializeFonts();
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) {
|
||||
@@ -29,6 +34,11 @@ createRoot(rootElement).render(
|
||||
</HelmetProvider>
|
||||
);
|
||||
|
||||
// Mark app as mounted to prevent flicker
|
||||
requestAnimationFrame(() => {
|
||||
document.body.classList.add('app-mounted');
|
||||
});
|
||||
|
||||
// Register service worker
|
||||
registerServiceWorker();
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ const About = () => {
|
||||
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">
|
||||
@@ -20,6 +24,10 @@ const About = () => {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="rounded-3xl overflow-hidden aspect-square"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity, transform'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/portrait.jpg"
|
||||
@@ -36,6 +44,10 @@ const About = () => {
|
||||
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>
|
||||
@@ -45,6 +57,10 @@ const About = () => {
|
||||
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">
|
||||
{t('pages.home.about.content')}
|
||||
|
||||
@@ -59,6 +59,10 @@ const Experience = () => {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity, transform'
|
||||
}}
|
||||
>
|
||||
<h2 className="text-3xl font-bold text-white text-center mb-12">
|
||||
{t('pages.home.experience.title')}
|
||||
@@ -78,6 +82,10 @@ const Experience = () => {
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity, transform'
|
||||
}}
|
||||
>
|
||||
{getCurrentPageItems().map((exp, index) => (
|
||||
<motion.div
|
||||
@@ -86,6 +94,10 @@ const Experience = () => {
|
||||
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"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity'
|
||||
}}
|
||||
>
|
||||
<h3 className="text-lg md:text-xl font-semibold text-white mb-1">
|
||||
{exp.role}
|
||||
|
||||
@@ -14,48 +14,48 @@ const Hero = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="home" className="relative min-h-screen bg-zinc-900 text-white">
|
||||
{/* Floating Paths Background */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<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} />
|
||||
<FloatingPaths position={-1} />
|
||||
</div>
|
||||
|
||||
{/* Concentric Circles Background */}
|
||||
<div className="absolute inset-0 pointer-events-none flex items-center justify-center">
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
{/* 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) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full border border-zinc-800"
|
||||
className="absolute rounded-full border border-zinc-800/30"
|
||||
style={{
|
||||
width: `${index * 20}%`,
|
||||
height: `${index * 20}%`,
|
||||
opacity: 0.1
|
||||
width: `${index * 30}%`,
|
||||
height: `${index * 30}%`,
|
||||
opacity: 0.1,
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<a
|
||||
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
|
||||
target="_blank"
|
||||
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')}
|
||||
>
|
||||
<Linkedin className="h-5 w-5" />
|
||||
<Linkedin className="h-5 w-5 pointer-events-none" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/damjan1996"
|
||||
target="_blank"
|
||||
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')}
|
||||
>
|
||||
<Github className="h-5 w-5" />
|
||||
<Github className="h-5 w-5 pointer-events-none" />
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
@@ -68,18 +68,21 @@ const Hero = () => {
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<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"
|
||||
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
|
||||
>
|
||||
<img
|
||||
src="/portrait.jpg"
|
||||
alt={t('pages.home.hero.image.alt')}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
style={{ transform: 'scale(1.01)' }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@@ -110,39 +113,43 @@ const Hero = () => {
|
||||
>
|
||||
<button
|
||||
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
|
||||
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
|
||||
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
|
||||
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>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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')}
|
||||
>
|
||||
<Mail className="h-6 w-6" />
|
||||
<Mail className="h-6 w-6 pointer-events-none" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -71,6 +71,10 @@ const Skills = () => {
|
||||
animate={{ opacity: 1 }}
|
||||
onHoverStart={() => setSelectedSkill(skill.name)}
|
||||
onHoverEnd={() => setSelectedSkill(null)}
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity'
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col">
|
||||
@@ -102,6 +106,10 @@ const Skills = () => {
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${skill.level}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'width'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -123,6 +131,10 @@ const Skills = () => {
|
||||
role="button"
|
||||
aria-pressed={selectedSkill === skill.name}
|
||||
aria-label={t('pages.home.skills.aria.selectSkill')}
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'transform'
|
||||
}}
|
||||
>
|
||||
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
|
||||
{iconMap[skill.name]}
|
||||
|
||||
@@ -45,6 +45,10 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
|
||||
variants={itemVariants}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity, transform'
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/9] w-full relative overflow-hidden">
|
||||
|
||||
@@ -27,7 +27,7 @@ const PortfolioGrid = () => {
|
||||
|
||||
try {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -33,7 +33,6 @@ a {
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Mobile navigation optimized for thumb reach */
|
||||
@@ -147,28 +146,6 @@ a {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Performance optimizations */
|
||||
.will-change-transform {
|
||||
will-change: transform;
|
||||
}
|
||||
/* Performance optimizations handled by interaction-fix.css */
|
||||
|
||||
.gpu-accelerated {
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
perspective: 1000;
|
||||
-webkit-perspective: 1000;
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
/* Reduced motion handled by interaction-fix.css */
|
||||
@@ -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);
|
||||
};
|
||||
@@ -39,10 +39,10 @@ function sendToAnalytics(metric: Metric) {
|
||||
}
|
||||
}));
|
||||
|
||||
// Also send to your analytics endpoint if needed
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon('/api/analytics', body);
|
||||
}
|
||||
// Comment out analytics endpoint for now - not implemented
|
||||
// if (navigator.sendBeacon) {
|
||||
// navigator.sendBeacon('/api/analytics', body);
|
||||
// }
|
||||
}
|
||||
|
||||
export function reportWebVitals() {
|
||||
|
||||
+8
-5
@@ -39,14 +39,16 @@ export default defineConfig({
|
||||
scope: '/',
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192x192.png',
|
||||
src: '/logo.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
},
|
||||
{
|
||||
src: '/icon-512x512.png',
|
||||
src: '/logo.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -81,7 +83,8 @@ export default defineConfig({
|
||||
cleanupOutdatedCaches: true
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
type: 'module'
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user