Implementierung - SEO - 01.08.2025
This commit is contained in:
@@ -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
@@ -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
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
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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;
|
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>
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
// 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
@@ -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'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user