Initial commit gov-llm-v2
This commit is contained in:
92
components/Timeline.tsx
Executable file
92
components/Timeline.tsx
Executable file
@@ -0,0 +1,92 @@
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user