diff --git a/src/components/layout/GlobalBackground.tsx b/src/components/layout/GlobalBackground.tsx index ca29edc..911be1d 100644 --- a/src/components/layout/GlobalBackground.tsx +++ b/src/components/layout/GlobalBackground.tsx @@ -1,8 +1,22 @@ +'use client'; + +import { usePageVisibility } from '@/hooks/usePageVisibility'; +import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; + const GlobalBackground = () => { + const isPageVisible = usePageVisibility(); + const { ref, isIntersecting } = useIntersectionObserver({ + threshold: 0.1, + rootMargin: '0px' + }); + + const shouldAnimate = isPageVisible && isIntersecting; + return ( <> {/* Background layer */}
@@ -36,18 +50,22 @@ const GlobalBackground = () => { strokeWidth="1" opacity="0.2" > - - + {shouldAnimate && ( + <> + + + + )} { strokeWidth="1" opacity="0.15" > - + {shouldAnimate && ( + + )} { strokeWidth="1" opacity="0.1" > - - + {shouldAnimate && ( + <> + + + + )}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 8af889e..599b18b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -18,6 +18,7 @@ import { usePathname } from 'next/navigation'; import { NavLink } from './NavLink'; import LanguageSwitcher from './LanguageSwitcher'; import { ContactInfo } from './ContactInfo'; +import { throttle } from '@/utils/throttle'; const Header = () => { const t = useTranslations('navigation'); @@ -33,9 +34,9 @@ const Header = () => { // Scroll handler useEffect(() => { - const handleScroll = () => { + const handleScroll = throttle(() => { setScrolled(window.scrollY > 0); - }; + }, 100); window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); diff --git a/src/hooks/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver.ts new file mode 100644 index 0000000..f2a1c6e --- /dev/null +++ b/src/hooks/useIntersectionObserver.ts @@ -0,0 +1,47 @@ +import { useState, useEffect, useRef, RefObject } from 'react'; + +interface UseIntersectionObserverOptions { + threshold?: number | number[]; + root?: Element | null; + rootMargin?: string; +} + +/** + * Hook to detect when an element is visible in the viewport using IntersectionObserver + * @param options - IntersectionObserver options (threshold, root, rootMargin) + * @returns Object containing ref to attach to element and isIntersecting state + */ +export const useIntersectionObserver = ( + options: UseIntersectionObserverOptions = {} +): { ref: RefObject; isIntersecting: boolean } => { + const { threshold = 0.5, root = null, rootMargin = '0px' } = options; + const [isIntersecting, setIsIntersecting] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + setIsIntersecting(entry.isIntersecting); + }, + { + threshold, + root, + rootMargin, + } + ); + + const element = ref.current; + if (element) { + observer.observe(element); + } + + return () => { + if (element) { + observer.unobserve(element); + } + observer.disconnect(); + }; + }, [threshold, root, rootMargin]); + + return { ref, isIntersecting }; +}; diff --git a/src/hooks/usePageVisibility.ts b/src/hooks/usePageVisibility.ts new file mode 100644 index 0000000..993e0d3 --- /dev/null +++ b/src/hooks/usePageVisibility.ts @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react'; + +/** + * Hook to detect tab visibility using the Page Visibility API + * @returns boolean - true when page is visible, false when hidden + */ +export const usePageVisibility = (): boolean => { + const [isVisible, setIsVisible] = useState(() => + typeof document !== 'undefined' ? !document.hidden : true + ); + + useEffect(() => { + const handleVisibilityChange = () => { + setIsVisible(!document.hidden); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, []); + + return isVisible; +}; diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts new file mode 100644 index 0000000..c28bf2e --- /dev/null +++ b/src/utils/throttle.ts @@ -0,0 +1,40 @@ +/** + * Throttles a function to execute at most once per specified delay period. + * The first call executes immediately, then subsequent calls are throttled. + * + * @param func - The function to throttle + * @param delay - Minimum time in milliseconds between function executions + * @returns Throttled version of the function + */ +export function throttle any>( + func: T, + delay: number +): (...args: Parameters) => void { + let lastCall = 0; + let timeout: NodeJS.Timeout | null = null; + + return function (...args: Parameters) { + const now = Date.now(); + const timeSinceLastCall = now - lastCall; + + const execute = () => { + lastCall = Date.now(); + func(...args); + }; + + if (timeSinceLastCall >= delay) { + // Enough time has passed, execute immediately + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + execute(); + } else if (!timeout) { + // Schedule execution after remaining delay + timeout = setTimeout(() => { + timeout = null; + execute(); + }, delay - timeSinceLastCall); + } + }; +}