diff --git a/src/components/ContactInfo.tsx b/src/components/ContactInfo.tsx index 0e2296a..284a9ee 100644 --- a/src/components/ContactInfo.tsx +++ b/src/components/ContactInfo.tsx @@ -1,15 +1,9 @@ import { Phone, Mail, ArrowRight } from "lucide-react" import { Link } from "react-router-dom" -import { motion } from "framer-motion" export function ContactInfo() { return ( - +

Kontakt @@ -19,7 +13,7 @@ export function ContactInfo() { href="tel:+1234567890" className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group" > - + +1 234 567 890 @@ -27,7 +21,7 @@ export function ContactInfo() { href="mailto:info@damjan-savic.com" className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group" > - + info@damjan-savic.com @@ -41,6 +35,6 @@ export function ContactInfo() {

- +
) -} \ No newline at end of file +} diff --git a/src/components/GlobalBackground.tsx b/src/components/GlobalBackground.tsx index 729d659..6ab973d 100644 --- a/src/components/GlobalBackground.tsx +++ b/src/components/GlobalBackground.tsx @@ -1,8 +1,11 @@ -import { useEffect, useState } from 'react'; -import { FloatingPaths } from './FloatingPaths'; +import { useEffect, useState, lazy, Suspense } from 'react'; + +// Lazy load FloatingPaths - it's just decoration and shouldn't block initial render +const FloatingPaths = lazy(() => import('./FloatingPaths').then(m => ({ default: m.FloatingPaths }))); const GlobalBackground = () => { const [scrollPosition, setScrollPosition] = useState(0); + const [showPaths, setShowPaths] = useState(false); useEffect(() => { const handleScroll = () => { @@ -13,12 +16,18 @@ const GlobalBackground = () => { return () => window.removeEventListener('scroll', handleScroll); }, []); + // Delay loading FloatingPaths until after initial render + useEffect(() => { + const timer = setTimeout(() => setShowPaths(true), 2000); + return () => clearTimeout(timer); + }, []); + return ( <> {/* Background layer - behind everything */} -
{ height: '100%' }} /> - + {/* Gradient overlay */} -
- - {/* FloatingPaths layer */} -
- -
- - {/* Test: Visible animated lines */} -
+ + + +
+ )} + + {/* Simple animated lines with CSS */} +
- - - - - - - - - - -
{/* Grid overlay */} -
{ ); }; -export default GlobalBackground; \ No newline at end of file +export default GlobalBackground; diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index f2bc23b..a6aadaa 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -1,8 +1,6 @@ -// C:\Development\Damjan Savic\Portfolio\src\components\LanguageSwitcher.tsx import React, { useRef, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Globe } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; const LanguageSwitcher: React.FC = () => { const { i18n } = useTranslation(); @@ -31,14 +29,13 @@ const LanguageSwitcher: React.FC = () => { }; useEffect(() => { - // Check if mobile on mount and resize const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; - + checkMobile(); window.addEventListener('resize', checkMobile); - + document.addEventListener('mousedown', handleClickOutside); return () => { window.removeEventListener('resize', checkMobile); @@ -48,12 +45,11 @@ const LanguageSwitcher: React.FC = () => { return (
- setIsOpen(!isOpen)} aria-expanded={isOpen} aria-haspopup="listbox" @@ -61,48 +57,42 @@ const LanguageSwitcher: React.FC = () => { > + - - {isOpen && ( - -
- {languages.map((lang) => ( - { - i18n.changeLanguage(lang.code); - setIsOpen(false); - }} - role="option" - aria-selected={i18n.language === lang.code} - > - {lang.name} - - ))} -
-
- )} -
+ {/* Dropdown with CSS transitions */} +
+
+ {languages.map((lang) => ( + + ))} +
+
); }; -export default LanguageSwitcher; \ No newline at end of file +export default LanguageSwitcher; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index cf1f077..23de25e 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,7 +1,6 @@ // Layout.tsx import { useEffect, useState, ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { motion, AnimatePresence } from "framer-motion"; import { Menu, X, @@ -143,7 +142,7 @@ const Layout = ({ children }: LayoutProps) => {
{/* Global Background */} - + {/* Navigation – oberste Ebene */}
- {/* Mobile Menu */} - - {isMenuOpen && ( - <> - {/* Backdrop */} - + {/* Mobile Menu - CSS-only Animationen */} + {/* Backdrop */} +
- {/* Sidebar */} - - {/* Sidebar Header mit Close-Button */} -
- Menu - + {/* Sidebar Header mit Close-Button */} +
+ Menu + +
+ + {/* Scrollbarer Sidebar-Inhalt */} +
+
+ {/* Contact Info */} + + + {/* Navigation Links */} +
+ {navigationLinks.map(({ path, label, icon }) => ( + setIsMenuOpen(false)} - aria-label="Close sidebar" - className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors" - whileTap={{ scale: 0.95 }} - > - - -
+ className="flex items-center w-full mb-1" + /> + ))} +
- {/* Scrollbarer Sidebar-Inhalt */} -
-
- {/* Contact Info */} - - - {/* Navigation Links */} -
- {navigationLinks.map(({ path, label, icon }) => ( - setIsMenuOpen(false)} - className="flex items-center w-full mb-1" - /> - ))} -
- - {/* Language Switcher */} -
-
- - Sprache ändern - - -
-
-
-
- - - )} - + {/* Language Switcher */} +
+
+ + Sprache ändern + + +
+
+
+
+
{/* Hauptinhalt */}
{children}
{/* Footer */}
- + {/* Performance Monitor */}
diff --git a/src/components/NavLink.tsx b/src/components/NavLink.tsx index 6b016d6..7228617 100644 --- a/src/components/NavLink.tsx +++ b/src/components/NavLink.tsx @@ -1,6 +1,5 @@ // components/NavLink.tsx import { Link, useLocation } from 'react-router-dom' -import { motion } from 'framer-motion' import React from 'react' interface NavLinkProps { @@ -22,26 +21,12 @@ export function NavLink({ to, icon, label, onClick, className }: NavLinkProps) { className={` relative flex items-center gap-2 rounded-full py-2 px-4 transition-all duration-200 ease-in-out - ${isActive ? 'text-white' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'} + ${isActive ? 'text-white bg-zinc-700/60' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'} ${className} `} > {icon} {label} - {isActive && ( - - )} ) -} \ No newline at end of file +} diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index 8915bf4..7f30c9a 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -1,154 +1,25 @@ "use client" -import React, { useEffect, useState, useRef } from "react" -import { motion, AnimatePresence } from "framer-motion" +import React, { useEffect, useRef } from "react" import { useLocation } from "react-router-dom" -import { useScrollContext } from './ScrollContext' interface PageTransitionProps { children: React.ReactNode } -const Logo = () => { - const [imageError, setImageError] = useState(false); - - return ( - - {!imageError ? ( - Logo setImageError(true)} - /> - ) : ( -
- DS -
- )} -
- ); -} - +// Simplified page transition - no loading delay for better LCP const PageTransition = ({ children }: PageTransitionProps) => { const location = useLocation() - const [isTransitioning, setIsTransitioning] = useState(true) // Start with transition on initial load - const [showContent, setShowContent] = useState(false) - const { setIsTransitioning: setGlobalTransitioning } = useScrollContext() const previousPathRef = useRef(null) - const isFirstMount = useRef(true) - - // Handle initial page load transition - useEffect(() => { - if (isFirstMount.current) { - setIsTransitioning(true); - setGlobalTransitioning(true); - - // Show content after transition animation - const timer = setTimeout(() => { - setIsTransitioning(false); - setGlobalTransitioning(false); - setShowContent(true); - isFirstMount.current = false; - }, 1200); - - return () => clearTimeout(timer); - } - }, [setGlobalTransitioning]); useEffect(() => { - // Skip on first mount - if (isFirstMount.current) { - previousPathRef.current = location.pathname; - return; + // Only scroll on path change, not on initial mount + if (previousPathRef.current && location.pathname !== previousPathRef.current) { + window.scrollTo(0, 0) } + previousPathRef.current = location.pathname + }, [location.pathname]) - // Only animate if path actually changed - if (location.pathname === previousPathRef.current) { - return; - } - - // Hide content immediately - setShowContent(false); - - // Small delay to ensure React has finished updating - const startTimer = setTimeout(() => { - // Update previous path - previousPathRef.current = location.pathname; - - // 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 unknown as Window & { __pageTransitionEndTimer?: NodeJS.Timeout }).__pageTransitionEndTimer = endTimer; - }, 10); - - return () => { - clearTimeout(startTimer); - const windowWithTimer = window as unknown as Window & { __pageTransitionEndTimer?: NodeJS.Timeout }; - if (windowWithTimer.__pageTransitionEndTimer) { - clearTimeout(windowWithTimer.__pageTransitionEndTimer); - } - setIsTransitioning(false); - setGlobalTransitioning(false); - }; - }, [location.pathname, setGlobalTransitioning]); - - return ( - <> - {/* Page transition overlay - always on top */} - - {isTransitioning && ( - - - - )} - - - {/* Content - only show when transition is complete */} -
- - {children} - -
- - ) + return <>{children} } -export default PageTransition \ No newline at end of file +export default PageTransition diff --git a/tailwind.config.js b/tailwind.config.js index fa01b18..6175f3c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,12 +15,17 @@ module.exports = { }, animation: { 'slide-up': 'slideUp 0.5s ease-out forwards', + 'fade-in': 'fadeIn 0.3s ease-out forwards', }, keyframes: { slideUp: { '0%': { transform: 'translateY(10px)' }, '100%': { transform: 'translateY(0)' }, }, + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, }, colors: { border: "rgb(var(--border))", diff --git a/vite.config.ts b/vite.config.ts index 64e6174..5ec6157 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -122,7 +122,8 @@ export default defineConfig({ 'react-vendor': ['react', 'react-dom', 'react-router-dom'], 'mdx-vendor': ['@mdx-js/react'], 'i18n-vendor': ['i18next', 'react-i18next'], - 'ui-vendor': ['framer-motion', 'lucide-react'] + 'icons': ['lucide-react'], + 'animations': ['framer-motion'] }, // Optimierte Asset-Dateinamen assetFileNames: (assetInfo) => {