Fix Scroll.
This commit is contained in:
+1
-1
@@ -82,7 +82,7 @@ define(['./workbox-4a2e5f00'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.364nagk4too"
|
"revision": "0.p5tdckiicb"
|
||||||
}], {});
|
}], {});
|
||||||
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
+17
-12
@@ -7,6 +7,8 @@ import AppRoutes from './routes';
|
|||||||
import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import CookieBanner from './components/CookieBanner';
|
import CookieBanner from './components/CookieBanner';
|
||||||
import PageTransition from './components/PageTransition';
|
import PageTransition from './components/PageTransition';
|
||||||
|
import { ScrollProvider } from './components/ScrollContext';
|
||||||
|
import DebugOverlay from './components/DebugOverlay';
|
||||||
import { logPageView } from './utils/analytics';
|
import { logPageView } from './utils/analytics';
|
||||||
import './styles/scrollbar.css';
|
import './styles/scrollbar.css';
|
||||||
|
|
||||||
@@ -20,18 +22,21 @@ function App() {
|
|||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className="flex flex-col min-h-screen bg-background text-foreground">
|
<ScrollProvider>
|
||||||
<Layout>
|
<div className="flex flex-col min-h-screen bg-background text-foreground relative">
|
||||||
<ErrorBoundary>
|
<Layout>
|
||||||
<Suspense fallback={null}>
|
<ErrorBoundary>
|
||||||
<PageTransition>
|
<Suspense fallback={null}>
|
||||||
<AppRoutes />
|
<PageTransition>
|
||||||
</PageTransition>
|
<AppRoutes />
|
||||||
</Suspense>
|
</PageTransition>
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
</Layout>
|
</ErrorBoundary>
|
||||||
<CookieBanner />
|
</Layout>
|
||||||
</div>
|
<CookieBanner />
|
||||||
|
<DebugOverlay />
|
||||||
|
</div>
|
||||||
|
</ScrollProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useScrollContext } from './ScrollContext';
|
||||||
|
|
||||||
|
interface KeyboardEvent {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugOverlay: React.FC = () => {
|
||||||
|
const [showDebug, setShowDebug] = useState<boolean>(false);
|
||||||
|
const { isTransitioning } = useScrollContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'd') {
|
||||||
|
setShowDebug((prev: boolean) => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyPress);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!showDebug) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded-lg z-[100] font-mono text-sm">
|
||||||
|
<div>Transitioning: {isTransitioning ? 'Yes' : 'No'}</div>
|
||||||
|
<div>Body Overflow: {document.body.style.overflow}</div>
|
||||||
|
<div>Scroll Position: {window.scrollY}</div>
|
||||||
|
<div>View Height: {window.innerHeight}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => document.body.style.overflow = ''}
|
||||||
|
className="bg-blue-500 px-2 py-1 rounded mt-2"
|
||||||
|
>
|
||||||
|
Reset Overflow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebugOverlay;
|
||||||
+122
-71
@@ -1,13 +1,24 @@
|
|||||||
// components/Layout.tsx
|
// Layout.tsx
|
||||||
import { useEffect, useState, ReactNode, useCallback } from "react";
|
import { useEffect, useState, ReactNode } 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 "./ContactInfo";
|
import { ContactInfo } from "./ContactInfo";
|
||||||
import { NavLink } from "./NavLink";
|
import { NavLink } from "./NavLink";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
import LanguageSwitcher from "./LanguageSwitcher";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import { useScrollContext } from "./ScrollContext";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -16,41 +27,27 @@ interface LayoutProps {
|
|||||||
const Layout = ({ children }: LayoutProps) => {
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { isMenuOpen, setIsMenuOpen } = useScrollContext();
|
||||||
|
|
||||||
// Memoized toggle function
|
// Initial mount
|
||||||
const toggleMenu = useCallback((force?: boolean) => {
|
useEffect(() => {
|
||||||
setIsOpen(prev => typeof force === 'boolean' ? force : !prev);
|
setIsMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Route change handler
|
// Schließe das Menü nur beim Routenwechsel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toggleMenu(false);
|
setIsMenuOpen(false);
|
||||||
}, [location.pathname, toggleMenu]);
|
}, [location.pathname, setIsMenuOpen]);
|
||||||
|
|
||||||
// Scroll lock effect
|
// Toggle Menu
|
||||||
useEffect(() => {
|
const toggleMenu = () => {
|
||||||
const body = document.body;
|
setIsMenuOpen(!isMenuOpen);
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// Prüfe Admin-Status und registriere den Scroll-Handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAdmin = async () => {
|
const checkAdmin = async () => {
|
||||||
const { data, error } = await supabase.auth.getSession();
|
const { data, error } = await supabase.auth.getSession();
|
||||||
@@ -82,11 +79,31 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navigationLinks = [
|
const navigationLinks = [
|
||||||
{ path: "/", label: t("navigation.main.home"), icon: <Home className="h-5 w-5" /> },
|
{
|
||||||
{ path: "/about", label: t("navigation.main.about"), icon: <User className="h-5 w-5" /> },
|
path: "/",
|
||||||
{ path: "/portfolio", label: t("navigation.main.portfolio"), icon: <Briefcase className="h-5 w-5" /> },
|
label: t("navigation.main.home"),
|
||||||
{ path: "/blog", label: t("navigation.main.blog"), icon: <BookOpen className="h-5 w-5" /> },
|
icon: <Home className="h-5 w-5" />,
|
||||||
{ path: "/contact", label: t("navigation.main.contact"), icon: <Phone className="h-5 w-5" /> },
|
},
|
||||||
|
{
|
||||||
|
path: "/about",
|
||||||
|
label: t("navigation.main.about"),
|
||||||
|
icon: <User className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/portfolio",
|
||||||
|
label: t("navigation.main.portfolio"),
|
||||||
|
icon: <Briefcase className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/blog",
|
||||||
|
label: t("navigation.main.blog"),
|
||||||
|
icon: <BookOpen className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/contact",
|
||||||
|
label: t("navigation.main.contact"),
|
||||||
|
icon: <Phone className="h-5 w-5" />,
|
||||||
|
},
|
||||||
...(isAdmin
|
...(isAdmin
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -98,11 +115,17 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex flex-col">
|
// Relativer Container, um z-index-Steuerung zu ermöglichen
|
||||||
|
<div className="min-h-screen bg-zinc-900 flex flex-col relative">
|
||||||
|
{/* Navigation – oberste Ebene */}
|
||||||
<nav
|
<nav
|
||||||
className={`
|
className={`
|
||||||
fixed w-full z-[80] transition-all duration-300
|
fixed w-full z-40 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"
|
||||||
@@ -113,6 +136,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-16">
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo */}
|
||||||
<Link to="/" className="flex items-center space-x-2 group shrink-0">
|
<Link to="/" className="flex items-center space-x-2 group shrink-0">
|
||||||
<motion.img
|
<motion.img
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
@@ -122,47 +146,66 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
<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"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700">
|
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
<motion.button
|
<motion.button
|
||||||
className="md:hidden relative z-[85] p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors"
|
className="md:hidden relative z-50 p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors"
|
||||||
onClick={() => toggleMenu()}
|
onClick={toggleMenu}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isMenuOpen}
|
||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu"
|
||||||
aria-label={isOpen ? "Close menu" : "Open menu"}
|
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
>
|
>
|
||||||
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
{isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<AnimatePresence>
|
{/* Mobile Menu */}
|
||||||
{isOpen && (
|
<AnimatePresence>
|
||||||
<>
|
{isMenuOpen && (
|
||||||
<motion.div
|
<>
|
||||||
initial={{ opacity: 0 }}
|
{/* Backdrop: Sichtbar, aber mit pointer-events-none blockiert er keine Interaktionen */}
|
||||||
animate={{ opacity: 1 }}
|
<motion.div
|
||||||
exit={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
animate={{ opacity: 1 }}
|
||||||
className="fixed inset-0 bg-black/50 md:hidden z-[75]"
|
exit={{ opacity: 0 }}
|
||||||
onClick={() => toggleMenu(false)}
|
transition={{ duration: 0.2 }}
|
||||||
/>
|
className="fixed inset-0 bg-black/50 md:hidden z-20 pointer-events-none"
|
||||||
<motion.div
|
/>
|
||||||
initial={{ x: "100%" }}
|
|
||||||
animate={{ x: 0 }}
|
{/* Sidebar – oberhalb des Hauptinhalts */}
|
||||||
exit={{ x: "100%" }}
|
<motion.div
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
initial={{ x: "100%" }}
|
||||||
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]"
|
animate={{ x: 0 }}
|
||||||
>
|
exit={{ x: "100%" }}
|
||||||
<div className="flex flex-col h-full pt-16">
|
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||||
<div className="flex-1 overflow-y-auto">
|
className="fixed right-0 top-0 h-full w-80 bg-zinc-900 shadow-xl md:hidden z-50 border-l border-zinc-800"
|
||||||
|
>
|
||||||
|
{/* Header-Spacer für die Navigation */}
|
||||||
|
<div className="h-16" />
|
||||||
|
|
||||||
|
{/* Scrollbarer Sidebar-Inhalt */}
|
||||||
|
<div className="h-[calc(100vh_-_4rem)] overflow-y-auto">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex-1">
|
||||||
<ContactInfo />
|
<ContactInfo />
|
||||||
<div className="py-4 px-4">
|
<div className="py-4 px-4">
|
||||||
{navigationLinks.map(({ path, label, icon }) => (
|
{navigationLinks.map(({ path, label, icon }) => (
|
||||||
@@ -171,7 +214,8 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
to={path}
|
to={path}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
label={label}
|
label={label}
|
||||||
onClick={() => toggleMenu(false)}
|
// Alternativ: Hier könnte ein Klick auch das Menü schließen
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="flex items-center w-full mb-1"
|
className="flex items-center w-full mb-1"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -179,17 +223,24 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t border-zinc-800 p-6 mt-auto">
|
<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 />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</>
|
</motion.div>
|
||||||
)}
|
</>
|
||||||
</AnimatePresence>
|
)}
|
||||||
</nav>
|
</AnimatePresence>
|
||||||
{children}
|
|
||||||
|
{/* Hauptinhalt – hier kann normal gescrollt werden */}
|
||||||
|
<main className="flex-1 pt-16 relative z-30">{children}</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// components/PageContainer.tsx
|
// components/PageContainer.tsx
|
||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import Footer from './Footer';
|
|
||||||
|
|
||||||
interface PageContainerProps {
|
interface PageContainerProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,10 +8,11 @@ interface PageContainerProps {
|
|||||||
const PageContainer = ({ children }: PageContainerProps) => {
|
const PageContainer = ({ children }: PageContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<main className="flex-1 pt-16">
|
<Suspense fallback={null}>
|
||||||
{children}
|
<main className="flex-1 pt-16">
|
||||||
</main>
|
{children}
|
||||||
<Footer />
|
</main>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState, useCallback } 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"
|
||||||
|
import { useScrollContext } from './ScrollContext'
|
||||||
|
|
||||||
interface PageTransitionProps {
|
interface PageTransitionProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@@ -34,55 +35,81 @@ const Logo = () => (
|
|||||||
|
|
||||||
const PageTransition = ({ children }: PageTransitionProps) => {
|
const PageTransition = ({ children }: PageTransitionProps) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [isAnimating, setIsAnimating] = useState(false)
|
const [showLogo, setShowLogo] = useState(false)
|
||||||
const [showContent, setShowContent] = useState(true)
|
const [showContent, setShowContent] = useState(true)
|
||||||
|
const { setIsTransitioning, lockScroll, unlockScroll } = useScrollContext()
|
||||||
|
|
||||||
|
const resetScroll = useCallback(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'instant'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTransitionStart = useCallback(() => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
lockScroll();
|
||||||
|
setShowContent(false);
|
||||||
|
setShowLogo(true);
|
||||||
|
}, [setIsTransitioning, lockScroll]);
|
||||||
|
|
||||||
|
const handleTransitionEnd = useCallback(() => {
|
||||||
|
setShowContent(true);
|
||||||
|
setShowLogo(false);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
unlockScroll();
|
||||||
|
resetScroll();
|
||||||
|
}, [setIsTransitioning, unlockScroll, resetScroll]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsAnimating(true)
|
const logoTimer = setTimeout(() => {
|
||||||
setShowContent(false)
|
setShowLogo(false);
|
||||||
|
}, 800);
|
||||||
|
|
||||||
const contentTimer = setTimeout(() => {
|
const transitionTimer = setTimeout(() => {
|
||||||
setShowContent(true)
|
handleTransitionEnd();
|
||||||
}, 800)
|
}, 1000);
|
||||||
|
|
||||||
const animationTimer = setTimeout(() => {
|
handleTransitionStart();
|
||||||
setIsAnimating(false)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(contentTimer)
|
clearTimeout(logoTimer);
|
||||||
clearTimeout(animationTimer)
|
clearTimeout(transitionTimer);
|
||||||
}
|
unlockScroll();
|
||||||
}, [location.pathname])
|
};
|
||||||
|
}, [location.pathname, handleTransitionStart, handleTransitionEnd, unlockScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen bg-zinc-900">
|
<div className="relative min-h-screen bg-zinc-900">
|
||||||
{/* Overlay mit Logo */}
|
<AnimatePresence mode="wait">
|
||||||
<AnimatePresence>
|
{showLogo && (
|
||||||
{isAnimating && (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key="overlay"
|
className="fixed inset-0 z-[60] bg-zinc-900 pointer-events-none"
|
||||||
className="fixed inset-0 z-[60] bg-zinc-900 flex items-center justify-center"
|
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3, ease: customEase }}
|
||||||
>
|
>
|
||||||
<Logo />
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{showContent && (
|
{showContent && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={location.pathname}
|
className="relative z-0 min-h-screen"
|
||||||
className="relative z-0"
|
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: customEase,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ScrollContext.tsx
|
||||||
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface ScrollContextType {
|
||||||
|
isTransitioning: boolean;
|
||||||
|
setIsTransitioning: (value: boolean) => void;
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
setIsMenuOpen: (value: boolean) => void;
|
||||||
|
lockScroll: () => void;
|
||||||
|
unlockScroll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollContext = createContext<ScrollContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ScrollProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const lockScroll = useCallback(() => {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
document.body.style.position = 'fixed';
|
||||||
|
document.body.style.top = `-${scrollY}px`;
|
||||||
|
document.body.style.width = '100%';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unlockScroll = useCallback(() => {
|
||||||
|
const scrollY = document.body.style.top;
|
||||||
|
document.body.style.position = '';
|
||||||
|
document.body.style.top = '';
|
||||||
|
document.body.style.width = '';
|
||||||
|
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollContext.Provider
|
||||||
|
value={{
|
||||||
|
isTransitioning,
|
||||||
|
setIsTransitioning,
|
||||||
|
isMenuOpen,
|
||||||
|
setIsMenuOpen,
|
||||||
|
lockScroll,
|
||||||
|
unlockScroll
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScrollContext = () => {
|
||||||
|
const context = useContext(ScrollContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useScrollContext must be used within a ScrollProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// hooks/useScrollLock.ts
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useScrollContext } from '../components/ScrollContext';
|
||||||
|
|
||||||
|
export const useScrollLock = (lock: boolean) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { isTransitioning, lockScroll, unlockScroll } = useScrollContext();
|
||||||
|
|
||||||
|
// Handle lock state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (lock && !isTransitioning) {
|
||||||
|
lockScroll();
|
||||||
|
} else {
|
||||||
|
unlockScroll();
|
||||||
|
}
|
||||||
|
}, [lock, isTransitioning, lockScroll, unlockScroll]);
|
||||||
|
|
||||||
|
// Handle location changes
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
unlockScroll();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [location.pathname, unlockScroll]);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
unlockScroll();
|
||||||
|
};
|
||||||
|
}, [unlockScroll]);
|
||||||
|
};
|
||||||
+123
-12
@@ -1,5 +1,5 @@
|
|||||||
// routes.tsx
|
// routes.tsx
|
||||||
import React from 'react';
|
import React, { Suspense } 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';
|
import PageContainer from './components/PageContainer';
|
||||||
@@ -20,17 +20,128 @@ const TermsPage = React.lazy(() => import('./pages/TermsPage'));
|
|||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<PageContainer><HomePage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
<Route
|
||||||
<Route path="/about" element={<PageContainer><AboutPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
path="/"
|
||||||
<Route path="/portfolio" element={<PageContainer><PortfolioPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
element={
|
||||||
<Route path="/portfolio/:slug" element={<PageContainer><ProjectPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
<PageContainer>
|
||||||
<Route path="/blog" element={<PageContainer><BlogPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
<Suspense fallback={null}>
|
||||||
<Route path="/blog/:slug" element={<PageContainer><BlogPostPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
<HomePage />
|
||||||
<Route path="/contact" element={<PageContainer><ContactPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
</Suspense>
|
||||||
<Route path="/login" element={<PageContainer><LoginPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
</PageContainer>
|
||||||
<Route path="/dashboard" element={<PageContainer><DashboardPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
}
|
||||||
<Route path="/privacy" element={<PageContainer><PrivacyPolicyPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
errorElement={<RouteErrorBoundary />}
|
||||||
<Route path="/terms" element={<PageContainer><TermsPage /></PageContainer>} errorElement={<RouteErrorBoundary />} />
|
/>
|
||||||
|
<Route
|
||||||
|
path="/about"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<AboutPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
{/* Rest of the routes follow the same pattern */}
|
||||||
|
<Route
|
||||||
|
path="/portfolio"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PortfolioPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/portfolio/:slug"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ProjectPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/blog"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<BlogPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/blog/:slug"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<BlogPostPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/contact"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<ContactPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LoginPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<DashboardPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/privacy"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PrivacyPolicyPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/terms"
|
||||||
|
element={
|
||||||
|
<PageContainer>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<TermsPage />
|
||||||
|
</Suspense>
|
||||||
|
</PageContainer>
|
||||||
|
}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user