Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad74da5dbc | ||
|
|
e182bf68b9 | ||
|
|
7272a17296 | ||
|
|
992c9d1a17 | ||
|
|
19d76d47ea | ||
|
|
ed27d2cc63 | ||
|
|
d53fba06a2 | ||
|
|
fb99c91561 | ||
|
|
370611cb01 | ||
|
|
018921b6ea | ||
|
|
75d4e006ff | ||
|
|
18f71c6730 | ||
|
|
43593c7654 | ||
|
|
2b096416a0 | ||
|
|
77ceae5b9f | ||
|
|
c934eeaad8 |
@@ -0,0 +1,45 @@
|
|||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
export function FloatingPaths({ position }: { position: number }) {
|
||||||
|
const paths = Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
d: `M-${380 - i * 15 * position} -${189 + i * 18}C-${
|
||||||
|
380 - i * 15 * position
|
||||||
|
} -${189 + i * 18} -${312 - i * 15 * position} ${216 - i * 18} ${
|
||||||
|
152 - i * 15 * position
|
||||||
|
} ${343 - i * 18}C${616 - i * 15 * position} ${470 - i * 18} ${
|
||||||
|
684 - i * 15 * position
|
||||||
|
} ${875 - i * 18} ${684 - i * 15 * position} ${875 - i * 18}`,
|
||||||
|
color: `rgba(var(--accent),${0.1 + i * 0.03})`,
|
||||||
|
width: 0.5 + i * 0.09,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 pointer-events-none floating-paths">
|
||||||
|
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none" style={{ transform: 'translateZ(0)' }}>
|
||||||
|
<title>Background Paths</title>
|
||||||
|
{paths.map((path) => (
|
||||||
|
<motion.path
|
||||||
|
key={path.id}
|
||||||
|
d={path.d}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={path.width}
|
||||||
|
strokeOpacity={0.1 + path.id * 0.03}
|
||||||
|
initial={{ pathLength: 0.3, opacity: 0.6 }}
|
||||||
|
animate={{
|
||||||
|
pathLength: 1,
|
||||||
|
opacity: [0.3, 0.6, 0.3],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 20 + (path.id % 3) * 5,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
style={{ willChange: 'opacity' }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePageVisibility } from '@/hooks/usePageVisibility';
|
||||||
|
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
|
||||||
|
|
||||||
const GlobalBackground = () => {
|
const GlobalBackground = () => {
|
||||||
|
const isPageVisible = usePageVisibility();
|
||||||
|
const { ref, isIntersecting } = useIntersectionObserver<HTMLDivElement>({
|
||||||
|
threshold: 0.1,
|
||||||
|
rootMargin: '0px'
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldAnimate = isPageVisible && isIntersecting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Background layer */}
|
{/* Background layer */}
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className="fixed inset-0 bg-zinc-900"
|
className="fixed inset-0 bg-zinc-900"
|
||||||
style={{ zIndex: -2 }}
|
style={{ zIndex: -2 }}
|
||||||
/>
|
/>
|
||||||
@@ -36,6 +50,8 @@ const GlobalBackground = () => {
|
|||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
opacity="0.2"
|
opacity="0.2"
|
||||||
>
|
>
|
||||||
|
{shouldAnimate && (
|
||||||
|
<>
|
||||||
<animate
|
<animate
|
||||||
attributeName="y1"
|
attributeName="y1"
|
||||||
values="20%;80%;20%"
|
values="20%;80%;20%"
|
||||||
@@ -48,6 +64,8 @@ const GlobalBackground = () => {
|
|||||||
dur="10s"
|
dur="10s"
|
||||||
repeatCount="indefinite"
|
repeatCount="indefinite"
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</line>
|
</line>
|
||||||
|
|
||||||
<line
|
<line
|
||||||
@@ -59,12 +77,14 @@ const GlobalBackground = () => {
|
|||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
opacity="0.15"
|
opacity="0.15"
|
||||||
>
|
>
|
||||||
|
{shouldAnimate && (
|
||||||
<animate
|
<animate
|
||||||
attributeName="x1"
|
attributeName="x1"
|
||||||
values="0%;100%;0%"
|
values="0%;100%;0%"
|
||||||
dur="15s"
|
dur="15s"
|
||||||
repeatCount="indefinite"
|
repeatCount="indefinite"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</line>
|
</line>
|
||||||
|
|
||||||
<line
|
<line
|
||||||
@@ -76,6 +96,8 @@ const GlobalBackground = () => {
|
|||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
opacity="0.1"
|
opacity="0.1"
|
||||||
>
|
>
|
||||||
|
{shouldAnimate && (
|
||||||
|
<>
|
||||||
<animate
|
<animate
|
||||||
attributeName="x1"
|
attributeName="x1"
|
||||||
values="20%;80%;20%"
|
values="20%;80%;20%"
|
||||||
@@ -88,6 +110,8 @@ const GlobalBackground = () => {
|
|||||||
dur="12s"
|
dur="12s"
|
||||||
repeatCount="indefinite"
|
repeatCount="indefinite"
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</line>
|
</line>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { usePathname } from 'next/navigation';
|
|||||||
import { NavLink } from './NavLink';
|
import { NavLink } from './NavLink';
|
||||||
import LanguageSwitcher from './LanguageSwitcher';
|
import LanguageSwitcher from './LanguageSwitcher';
|
||||||
import { ContactInfo } from './ContactInfo';
|
import { ContactInfo } from './ContactInfo';
|
||||||
|
import { throttle } from '@/utils/throttle';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const t = useTranslations('navigation');
|
const t = useTranslations('navigation');
|
||||||
@@ -33,9 +34,9 @@ const Header = () => {
|
|||||||
|
|
||||||
// Scroll handler
|
// Scroll handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = throttle(() => {
|
||||||
setScrolled(window.scrollY > 0);
|
setScrolled(window.scrollY > 0);
|
||||||
};
|
}, 100);
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('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