Fix Page Transition and Mobile Version.

This commit is contained in:
2025-02-10 11:28:50 +01:00
parent bba0e331a8
commit 4b74640d87
9 changed files with 168 additions and 151 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.vc3cdld58co" "revision": "0.364nagk4too"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -1,3 +1,4 @@
// App.tsx
import { useEffect, Suspense } from 'react'; import { useEffect, Suspense } from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
@@ -12,8 +13,6 @@ import './styles/scrollbar.css';
function App() { function App() {
useEffect(() => { useEffect(() => {
logPageView(); logPageView();
// Stellt sicher, dass die Scrollbar von Anfang an konsistent ist
document.documentElement.classList.add('custom-scrollbar'); document.documentElement.classList.add('custom-scrollbar');
}, []); }, []);
+7 -6
View File
@@ -1,3 +1,4 @@
// C:\Development\Damjan Savic\Portfolio\src\components\LanguageSwitcher.tsx
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react'; import { Globe } from 'lucide-react';
@@ -36,13 +37,13 @@ const LanguageSwitcher: React.FC = () => {
}, []); }, []);
return ( return (
<div className="relative" ref={dropdownRef}> <div className="relative inline-block" ref={dropdownRef}>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="flex items-center space-x-2 text-zinc-400 hover:text-white className="flex items-center space-x-2 text-zinc-400 hover:text-white
px-4 py-2 rounded-full transition-colors duration-200 px-4 py-2 rounded-full transition-colors duration-200
hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800" hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-haspopup="listbox" aria-haspopup="listbox"
@@ -55,11 +56,11 @@ const LanguageSwitcher: React.FC = () => {
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
<motion.div <motion.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="absolute right-0 mt-2 w-48 rounded-lg overflow-hidden className="absolute bottom-full right-0 mb-2 w-48 rounded-lg overflow-hidden
border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm
shadow-lg" shadow-lg"
role="listbox" role="listbox"
+59 -29
View File
@@ -1,14 +1,13 @@
// components/Layout.tsx // components/Layout.tsx
import { useEffect, useState, ReactNode } from "react"; import { useEffect, useState, ReactNode, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Home, User, Briefcase, BookOpen, Phone, LayoutDashboard } from "lucide-react"; import { Menu, X, Home, User, Briefcase, BookOpen, Phone, LayoutDashboard } from "lucide-react";
import { supabase } from "../utils/supabaseClient"; import { supabase } from "../utils/supabaseClient";
import { ContactInfo } from "../components/ContactInfo"; import { ContactInfo } from "./ContactInfo";
import { NavLink } from "../components/NavLink"; import { NavLink } from "./NavLink";
import LanguageSwitcher from "./LanguageSwitcher"; import LanguageSwitcher from "./LanguageSwitcher";
import { Link } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import Footer from "./Footer";
interface LayoutProps { interface LayoutProps {
children: ReactNode; children: ReactNode;
@@ -19,6 +18,38 @@ const Layout = ({ children }: LayoutProps) => {
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const location = useLocation();
// Memoized toggle function
const toggleMenu = useCallback((force?: boolean) => {
setIsOpen(prev => typeof force === 'boolean' ? force : !prev);
}, []);
// Route change handler
useEffect(() => {
toggleMenu(false);
}, [location.pathname, toggleMenu]);
// Scroll lock effect
useEffect(() => {
const body = document.body;
if (isOpen) {
const scrollY = window.scrollY;
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.width = '100%';
body.style.overflow = 'hidden';
} else {
const scrollY = body.style.top;
body.style.position = '';
body.style.top = '';
body.style.width = '';
body.style.overflow = '';
if (scrollY) {
window.scrollTo(0, parseInt(scrollY || '0') * -1);
}
}
}, [isOpen]);
useEffect(() => { useEffect(() => {
const checkAdmin = async () => { const checkAdmin = async () => {
@@ -71,7 +102,7 @@ const Layout = ({ children }: LayoutProps) => {
<div className="min-h-screen bg-zinc-900 flex flex-col"> <div className="min-h-screen bg-zinc-900 flex flex-col">
<nav <nav
className={` className={`
fixed w-full z-50 transition-all duration-300 fixed w-full z-[80] transition-all duration-300
${scrolled ${scrolled
? "bg-zinc-900/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-900/80" ? "bg-zinc-900/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-900/80"
: "bg-transparent" : "bg-transparent"
@@ -91,7 +122,6 @@ const Layout = ({ children }: LayoutProps) => {
transition={{ type: "spring", stiffness: 400, damping: 10 }} transition={{ type: "spring", stiffness: 400, damping: 10 }}
/> />
</Link> </Link>
<div className="hidden md:flex items-center gap-2"> <div className="hidden md:flex items-center gap-2">
{navigationLinks.map(({ path, label, icon }) => ( {navigationLinks.map(({ path, label, icon }) => (
<NavLink key={path} to={path} icon={icon} label={label} className="text-sm" /> <NavLink key={path} to={path} icon={icon} label={label} className="text-sm" />
@@ -100,10 +130,9 @@ const Layout = ({ children }: LayoutProps) => {
<LanguageSwitcher /> <LanguageSwitcher />
</div> </div>
</div> </div>
<motion.button <motion.button
className="md:hidden p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors" className="md:hidden relative z-[85] p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors"
onClick={() => setIsOpen(!isOpen)} onClick={() => toggleMenu()}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-controls="mobile-menu" aria-controls="mobile-menu"
aria-label={isOpen ? "Close menu" : "Open menu"} aria-label={isOpen ? "Close menu" : "Open menu"}
@@ -122,31 +151,33 @@ const Layout = ({ children }: LayoutProps) => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 md:hidden" className="fixed inset-0 bg-black/50 md:hidden z-[75]"
onClick={() => setIsOpen(false)} onClick={() => toggleMenu(false)}
/> />
<motion.div <motion.div
initial={{ x: "100%" }} initial={{ x: "100%" }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: "100%" }} exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }} transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed right-0 top-16 h-[calc(100vh-4rem)] w-80 bg-zinc-900 shadow-xl md:hidden overflow-y-auto border-l border-zinc-800" className="fixed right-0 top-0 h-full w-80 bg-zinc-900 shadow-xl md:hidden overflow-y-auto border-l border-zinc-800 z-[75]"
> >
<div className="flex flex-col h-full"> <div className="flex flex-col h-full pt-16">
<ContactInfo /> <div className="flex-1 overflow-y-auto">
<div className="flex-1 py-4 px-4"> <ContactInfo />
{navigationLinks.map(({ path, label, icon }) => ( <div className="py-4 px-4">
<NavLink {navigationLinks.map(({ path, label, icon }) => (
key={path} <NavLink
to={path} key={path}
icon={icon} to={path}
label={label} icon={icon}
onClick={() => setIsOpen(false)} label={label}
className="flex items-center w-full mb-1" onClick={() => toggleMenu(false)}
/> className="flex items-center w-full mb-1"
))} />
))}
</div>
</div> </div>
<div className="border-t border-zinc-800 p-6"> <div className="border-t border-zinc-800 p-6 mt-auto">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-zinc-500">Sprache ändern</span> <span className="text-sm text-zinc-500">Sprache ändern</span>
<LanguageSwitcher /> <LanguageSwitcher />
@@ -158,8 +189,7 @@ const Layout = ({ children }: LayoutProps) => {
)} )}
</AnimatePresence> </AnimatePresence>
</nav> </nav>
<main className="flex-1 pt-16">{children}</main> {children}
<Footer />
</div> </div>
); );
}; };
+20
View File
@@ -0,0 +1,20 @@
// components/PageContainer.tsx
import React from 'react';
import Footer from './Footer';
interface PageContainerProps {
children: React.ReactNode;
}
const PageContainer = ({ children }: PageContainerProps) => {
return (
<div className="flex flex-col min-h-screen">
<main className="flex-1 pt-16">
{children}
</main>
<Footer />
</div>
);
};
export default PageContainer;
+62 -93
View File
@@ -1,11 +1,10 @@
"use client" "use client"
import type React from "react" import React, { useEffect, useState } from "react"
import { useEffect, useState } from "react"
import { motion, AnimatePresence, cubicBezier } from "framer-motion" import { motion, AnimatePresence, cubicBezier } from "framer-motion"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
interface PageTransitionProps { interface PageTransitionProps {
children: React.ReactNode children: React.ReactNode
} }
const customEase = cubicBezier(0.25, 0.1, 0.25, 1) const customEase = cubicBezier(0.25, 0.1, 0.25, 1)
@@ -14,113 +13,83 @@ const Logo = () => (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ animate={{
opacity: 1, opacity: 1,
scale: 1, scale: 1,
transition: { duration: 0.3, ease: customEase }, transition: { duration: 0.4, ease: customEase },
}} }}
exit={{ exit={{
opacity: 0, opacity: 0,
scale: 1.2, scale: 1.2,
transition: { duration: 0.3, ease: customEase }, transition: { duration: 0.3, ease: customEase },
}} }}
className="w-16 h-16" className="w-16 h-16"
> >
<img <img
src="/logo.png" src="/logo.png"
alt="Logo" alt="Logo"
className="w-full h-full object-contain filter brightness-200" className="w-full h-full object-contain filter brightness-200"
/> />
</motion.div> </motion.div>
) )
const PageTransition = ({ children }: PageTransitionProps) => { const PageTransition = ({ children }: PageTransitionProps) => {
const location = useLocation() const location = useLocation()
const [isTransitioning, setIsTransitioning] = useState(true) const [isAnimating, setIsAnimating] = useState(false)
const [isOverlayActive, setIsOverlayActive] = useState(true) const [showContent, setShowContent] = useState(true)
useEffect(() => { useEffect(() => {
setIsTransitioning(true) setIsAnimating(true)
setIsOverlayActive(true) setShowContent(false)
const overlayTimer = setTimeout(() => { const contentTimer = setTimeout(() => {
setIsOverlayActive(false) setShowContent(true)
}, 1200) // Overlay animation duration }, 800)
const transitionTimer = setTimeout(() => { const animationTimer = setTimeout(() => {
setIsTransitioning(false) setIsAnimating(false)
}, 800) }, 1000)
return () => { return () => {
clearTimeout(overlayTimer) clearTimeout(contentTimer)
clearTimeout(transitionTimer) clearTimeout(animationTimer)
} }
}, [location]) }, [location.pathname])
return ( return (
<AnimatePresence mode="wait"> <div className="relative min-h-screen bg-zinc-900">
<motion.div key={location.pathname} className="relative"> {/* Overlay mit Logo */}
{/* Transition Container */}
<div className={`fixed inset-0 pointer-events-none ${isOverlayActive ? 'z-50' : '-z-10'}`}>
{/* Overlay Animation */}
<motion.div
className="absolute inset-0 bg-zinc-900"
initial={{ opacity: 0 }}
animate={{
opacity: [0, 1, 1, 0],
transition: {
duration: 1.2,
times: [0, 0.3, 0.7, 1],
ease: customEase,
},
}}
/>
{/* Logo */}
<AnimatePresence> <AnimatePresence>
{isTransitioning && ( {isAnimating && (
<motion.div <motion.div
className="absolute inset-0 flex items-center justify-center" key="overlay"
initial={{ opacity: 0 }} className="fixed inset-0 z-[60] bg-zinc-900 flex items-center justify-center"
animate={{ initial={{ opacity: 0 }}
opacity: 1, animate={{ opacity: 1 }}
transition: { exit={{ opacity: 0 }}
delay: 0.3, transition={{ duration: 0.3 }}
duration: 0.3, >
}, <Logo />
}} </motion.div>
exit={{ )}
opacity: 0,
transition: {
duration: 0.3,
},
}}
>
<Logo />
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
</div>
{/* Page Content */} {/* Content */}
<motion.div <AnimatePresence mode="wait">
className="relative" {showContent && (
initial={{ opacity: 0, y: 20 }} <motion.div
animate={{ key={location.pathname}
opacity: 1, className="relative z-0"
y: 0, initial={{ opacity: 0 }}
transition: { animate={{ opacity: 1 }}
duration: 0.4, exit={{ opacity: 0 }}
delay: 0.8, transition={{ duration: 0.3 }}
ease: customEase, >
}, {children}
}} </motion.div>
exit={{ opacity: 0, y: -20, transition: { duration: 0.3 } }} )}
> </AnimatePresence>
{children} </div>
</motion.div> )
</motion.div>
</AnimatePresence>
)
} }
export default PageTransition export default PageTransition
+4 -7
View File
@@ -1,5 +1,4 @@
// Umbenennen zu main.tsx // main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import App from './App'; import App from './App';
@@ -14,11 +13,9 @@ if (!rootElement) {
} }
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <HelmetProvider>
<HelmetProvider> <App />
<App /> </HelmetProvider>
</HelmetProvider>
</StrictMode>
); );
// Register service worker // Register service worker
+12 -11
View File
@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import RouteErrorBoundary from './components/RouteErrorBoundary'; import RouteErrorBoundary from './components/RouteErrorBoundary';
import PageContainer from './components/PageContainer';
// Lazy load components // Lazy load components
const HomePage = React.lazy(() => import('./pages/home')); const HomePage = React.lazy(() => import('./pages/home'));
@@ -19,17 +20,17 @@ const TermsPage = React.lazy(() => import('./pages/TermsPage'));
const AppRoutes = () => { const AppRoutes = () => {
return ( return (
<Routes> <Routes>
<Route path="/" element={<HomePage />} errorElement={<RouteErrorBoundary />} /> <Route path="/" element={<PageContainer><HomePage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/about" element={<AboutPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/about" element={<PageContainer><AboutPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/portfolio" element={<PortfolioPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/portfolio" element={<PageContainer><PortfolioPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/portfolio/:slug" element={<ProjectPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/portfolio/:slug" element={<PageContainer><ProjectPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/blog" element={<BlogPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/blog" element={<PageContainer><BlogPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/blog/:slug" element={<BlogPostPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/blog/:slug" element={<PageContainer><BlogPostPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/contact" element={<ContactPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/contact" element={<PageContainer><ContactPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/login" element={<LoginPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/login" element={<PageContainer><LoginPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/dashboard" element={<DashboardPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/dashboard" element={<PageContainer><DashboardPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/privacy" element={<PrivacyPolicyPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/privacy" element={<PageContainer><PrivacyPolicyPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
<Route path="/terms" element={<TermsPage />} errorElement={<RouteErrorBoundary />} /> <Route path="/terms" element={<PageContainer><TermsPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
</Routes> </Routes>
); );
}; };