93 lines
3.2 KiB
TypeScript
Executable File
93 lines
3.2 KiB
TypeScript
Executable File
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
const sections = [
|
|
{ id: 'hero', label: 'Старт' },
|
|
{ id: 'infra', label: 'ЦОД' },
|
|
{ id: 'science', label: 'Наука' },
|
|
{ id: 'social', label: 'Социум' },
|
|
{ id: 'leadership', label: 'Рейтинг' },
|
|
{ id: 'metrics', label: 'Цифры' },
|
|
{ id: 'scaling', label: 'Масштаб' },
|
|
{ id: 'team', label: 'Команда' },
|
|
];
|
|
|
|
const Timeline: React.FC = () => {
|
|
const [activeId, setActiveId] = useState<string>('hero');
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
const visibilities = useRef<Map<string, number>>(new Map());
|
|
|
|
useEffect(() => {
|
|
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
|
|
entries.forEach((entry) => {
|
|
// Calculate approximate visible height in pixels
|
|
// This creates a fair comparison between scrolling (tall) sections and snapping (short) sections
|
|
const visibleHeight = entry.intersectionRect.height;
|
|
visibilities.current.set(entry.target.id, visibleHeight);
|
|
});
|
|
|
|
let maxVisibleHeight = 0;
|
|
let maxId = '';
|
|
|
|
// The active section is the one occupying the most vertical space in the viewport
|
|
for (const [id, height] of visibilities.current.entries()) {
|
|
if (height > maxVisibleHeight) {
|
|
maxVisibleHeight = height;
|
|
maxId = id;
|
|
}
|
|
}
|
|
|
|
if (maxId && maxVisibleHeight > 0) {
|
|
setActiveId(maxId);
|
|
}
|
|
};
|
|
|
|
const options = {
|
|
root: null, // viewport
|
|
rootMargin: '0px',
|
|
threshold: Array.from({ length: 11 }, (_, i) => i * 0.1),
|
|
};
|
|
|
|
observerRef.current = new IntersectionObserver(handleIntersection, options);
|
|
|
|
// Observe all sections
|
|
sections.forEach(({ id }) => {
|
|
const element = document.getElementById(id);
|
|
if (element) observerRef.current?.observe(element);
|
|
});
|
|
|
|
return () => observerRef.current?.disconnect();
|
|
}, []);
|
|
|
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
e.preventDefault();
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed right-8 top-1/2 -translate-y-1/2 z-[60] hidden md:flex flex-col gap-3 mix-blend-difference items-end">
|
|
{sections.map((item) => {
|
|
const isActive = activeId === item.id;
|
|
return (
|
|
<a
|
|
key={item.id}
|
|
href={`#${item.id}`}
|
|
onClick={(e) => handleClick(e, item.id)}
|
|
className="group flex flex-row-reverse items-center justify-end gap-3"
|
|
>
|
|
<div className={`w-1.5 h-1.5 transition-all duration-300 ${isActive ? 'bg-[#ff4b4b] scale-150' : 'bg-white/30 group-hover:bg-white'}`} />
|
|
<span className={`text-[10px] font-mono transition-all duration-300 uppercase tracking-widest text-white ${isActive ? 'opacity-100' : 'opacity-0 translate-x-2 group-hover:opacity-50 group-hover:translate-x-0'}`}>
|
|
{item.label}
|
|
</span>
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Timeline;
|