Merge pull request #5 from damjan1996/auto-claude/026-pause-background-animations-when-not-visible

auto-claude: 026-pause-background-animations-when-not-visible
This commit is contained in:
Damjan Savic
2026-01-25 19:37:09 +01:00
committed by GitHub
5 changed files with 169 additions and 32 deletions
@@ -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<HTMLDivElement>({
threshold: 0.1,
rootMargin: '0px'
});
const shouldAnimate = isPageVisible && isIntersecting;
return (
<>
{/* Background layer */}
<div
ref={ref}
className="fixed inset-0 bg-zinc-900"
style={{ zIndex: -2 }}
/>
@@ -36,6 +50,8 @@ const GlobalBackground = () => {
strokeWidth="1"
opacity="0.2"
>
{shouldAnimate && (
<>
<animate
attributeName="y1"
values="20%;80%;20%"
@@ -48,6 +64,8 @@ const GlobalBackground = () => {
dur="10s"
repeatCount="indefinite"
/>
</>
)}
</line>
<line
@@ -59,12 +77,14 @@ const GlobalBackground = () => {
strokeWidth="1"
opacity="0.15"
>
{shouldAnimate && (
<animate
attributeName="x1"
values="0%;100%;0%"
dur="15s"
repeatCount="indefinite"
/>
)}
</line>
<line
@@ -76,6 +96,8 @@ const GlobalBackground = () => {
strokeWidth="1"
opacity="0.1"
>
{shouldAnimate && (
<>
<animate
attributeName="x1"
values="20%;80%;20%"
@@ -88,6 +110,8 @@ const GlobalBackground = () => {
dur="12s"
repeatCount="indefinite"
/>
</>
)}
</line>
</svg>
</div>
+3 -2
View File
@@ -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);
}, []);
+47
View File
@@ -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 = <T extends HTMLElement = HTMLDivElement>(
options: UseIntersectionObserverOptions = {}
): { ref: RefObject<T>; isIntersecting: boolean } => {
const { threshold = 0.5, root = null, rootMargin = '0px' } = options;
const [isIntersecting, setIsIntersecting] = useState<boolean>(false);
const ref = useRef<T>(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 };
};
+25
View File
@@ -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<boolean>(() =>
typeof document !== 'undefined' ? !document.hidden : true
);
useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return isVisible;
};
+40
View File
@@ -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<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let lastCall = 0;
let timeout: NodeJS.Timeout | null = null;
return function (...args: Parameters<T>) {
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);
}
};
}