Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7272a17296 | ||
|
|
992c9d1a17 | ||
|
|
19d76d47ea | ||
|
|
fb99c91561 | ||
|
|
018921b6ea | ||
|
|
43593c7654 | ||
|
|
2b096416a0 | ||
|
|
77ceae5b9f | ||
|
|
c934eeaad8 |
@@ -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,18 +50,22 @@ const GlobalBackground = () => {
|
||||
strokeWidth="1"
|
||||
opacity="0.2"
|
||||
>
|
||||
<animate
|
||||
attributeName="y1"
|
||||
values="20%;80%;20%"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y2"
|
||||
values="20%;80%;20%"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
{shouldAnimate && (
|
||||
<>
|
||||
<animate
|
||||
attributeName="y1"
|
||||
values="20%;80%;20%"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y2"
|
||||
values="20%;80%;20%"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</line>
|
||||
|
||||
<line
|
||||
@@ -59,12 +77,14 @@ const GlobalBackground = () => {
|
||||
strokeWidth="1"
|
||||
opacity="0.15"
|
||||
>
|
||||
<animate
|
||||
attributeName="x1"
|
||||
values="0%;100%;0%"
|
||||
dur="15s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
{shouldAnimate && (
|
||||
<animate
|
||||
attributeName="x1"
|
||||
values="0%;100%;0%"
|
||||
dur="15s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
)}
|
||||
</line>
|
||||
|
||||
<line
|
||||
@@ -76,18 +96,22 @@ const GlobalBackground = () => {
|
||||
strokeWidth="1"
|
||||
opacity="0.1"
|
||||
>
|
||||
<animate
|
||||
attributeName="x1"
|
||||
values="20%;80%;20%"
|
||||
dur="12s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="x2"
|
||||
values="20%;80%;20%"
|
||||
dur="12s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
{shouldAnimate && (
|
||||
<>
|
||||
<animate
|
||||
attributeName="x1"
|
||||
values="20%;80%;20%"
|
||||
dur="12s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="x2"
|
||||
values="20%;80%;20%"
|
||||
dur="12s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</line>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user