Fertig
This commit is contained in:
133
components/process-timeline.tsx
Normal file
133
components/process-timeline.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ProcessStep } from "@/data/site-content";
|
||||
|
||||
type ProcessTimelineProps = {
|
||||
steps: ProcessStep[];
|
||||
};
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function ProcessTimeline({ steps }: ProcessTimelineProps) {
|
||||
const sectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const stepRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const markers = useMemo(() => steps.map((step) => step.step), [steps]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateTimeline() {
|
||||
const section = sectionRef.current;
|
||||
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionRect = section.getBoundingClientRect();
|
||||
const viewportAnchor = window.innerHeight * 0.42;
|
||||
const rawProgress =
|
||||
(viewportAnchor - sectionRect.top) /
|
||||
Math.max(sectionRect.height - window.innerHeight * 0.3, 1);
|
||||
|
||||
setProgress(clamp(rawProgress, 0, 1));
|
||||
|
||||
let closestIndex = 0;
|
||||
let closestDistance = Number.POSITIVE_INFINITY;
|
||||
|
||||
stepRefs.current.forEach((stepNode, index) => {
|
||||
if (!stepNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = stepNode.getBoundingClientRect();
|
||||
const center = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(center - viewportAnchor);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
setActiveIndex(closestIndex);
|
||||
}
|
||||
|
||||
updateTimeline();
|
||||
|
||||
let frame = 0;
|
||||
|
||||
function onScrollOrResize() {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = window.requestAnimationFrame(updateTimeline);
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScrollOrResize, { passive: true });
|
||||
window.addEventListener("resize", onScrollOrResize);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("scroll", onScrollOrResize);
|
||||
window.removeEventListener("resize", onScrollOrResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sectionRef}
|
||||
className="process-timeline"
|
||||
style={
|
||||
{
|
||||
"--timeline-progress": `${progress}`,
|
||||
"--timeline-step-count": `${steps.length}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="process-rail" aria-hidden="true">
|
||||
<div className="process-rail-track" />
|
||||
<div className="process-rail-fill" />
|
||||
<div className="process-rail-markers">
|
||||
{markers.map((marker, index) => (
|
||||
<span
|
||||
key={marker}
|
||||
className={`process-rail-marker ${
|
||||
index <= activeIndex ? "is-active" : ""
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="process-rows">
|
||||
{steps.map((step, index) => {
|
||||
const sideClass = index % 2 === 0 ? "is-right" : "is-left";
|
||||
const isActive = index <= activeIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.step}
|
||||
ref={(node) => {
|
||||
stepRefs.current[index] = node;
|
||||
}}
|
||||
className={`process-row ${sideClass} ${
|
||||
isActive ? "is-active" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="process-row-spacer" aria-hidden="true" />
|
||||
<div className="process-row-pin" aria-hidden="true" />
|
||||
<article className="process-step-card">
|
||||
<span className="process-step-number">{step.step}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.description}</p>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user