cs-2022-class-profile/components/Timeline.tsx

166 lines
4.4 KiB
TypeScript

import {
Connector,
CircleSubject,
Annotation,
HtmlLabel,
} from "@visx/annotation";
import { Group } from "@visx/group";
import { LinePath } from "@visx/shape";
import React, { useState } from "react";
import styles from "./Timeline.module.css";
interface TimelineData {
time: string;
text: string;
}
interface TimelineProps {
data: TimelineData[];
/** Width of the entire timeline, in pixels. */
width: number;
/** Height of the entire timeline, in pixels. */
height: number;
/** Padding top and bottom, in pixels. */
padding: number;
/** Location of timeline width-wise, in pixels. Default is width / 2. */
timelinePosition?: number;
/** Distance between consecutive nodes on timeline, in pixels. Default is evenly spaced. padding is not calculated if user specifies this value. */
nodeGap?: number;
/** Whether the time is transformed to uppercase */
isTimeUppercase?: boolean;
/** Offset between the line and the time label, in pixels (negative for left offset, positive for right offset). */
timeXOffset?: number;
/** Width of time label, in pixels. */
timeWidth?: number;
/** Offset between the line and the text label, in pixels (negative for left offset, positive for right offset). */
textXOffset?: number;
/** Width of text label, in pixels. */
textWidth?: number;
/** Height of text label, in pixels. */
textHeight?: number;
className?: string;
}
export default function Timeline({
data,
width,
height,
padding = 20,
timelinePosition = width / 2,
nodeGap = data.length > 1
? (height - 2 * padding) / (data.length - 1)
: height - 2 * padding,
isTimeUppercase = true,
timeXOffset = -30,
timeWidth = 100,
textXOffset = 30,
textWidth = 100,
textHeight = 100,
className,
}: TimelineProps) {
const dataWithPositions = data.map((data, i) => ({
x: timelinePosition,
y: i * nodeGap + padding,
time: isTimeUppercase ? data.time.toUpperCase() : data.time,
text: data.text,
}));
return (
<svg className={className} width={width} height={height}>
<LinePath
data={dataWithPositions.map((data) => [data.x, data.y])}
stroke="#fff"
strokeWidth={10}
/>
{dataWithPositions.map((data, i) => (
<TimelineAnnotation
positionData={data}
timeXOffset={timeXOffset}
timeWidth={timeWidth}
textXOffset={textXOffset}
textWidth={textWidth}
textHeight={textHeight}
key={`${data.time}${i}`}
/>
))}
</svg>
);
}
interface TimelineAnnotationProps {
positionData: {
x: number;
y: number;
time: string;
text: string;
};
timeXOffset: number;
timeWidth: number;
textXOffset: number;
textWidth: number;
textHeight: number;
}
function TimelineAnnotation({
positionData,
timeXOffset,
timeWidth,
textXOffset,
textWidth,
textHeight,
}: TimelineAnnotationProps) {
const [hover, setHover] = useState(false);
function handleMouseEnter() {
setHover(true);
}
function handleMouseLeave() {
setHover(false);
}
console.log(hover);
return (
<Group>
<Annotation x={positionData.x} y={positionData.y} dx={timeXOffset} dy={0}>
<Connector />
<HtmlLabel showAnchorLine={false} verticalAnchor="start">
<h1
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`${styles.timebox} ${hover ? styles.timeboxHover : ""}`}
style={{ width: timeWidth }}
>
{positionData.time}
</h1>
</HtmlLabel>
</Annotation>
<Annotation x={positionData.x} y={positionData.y} dx={textXOffset} dy={0}>
<Connector />
<CircleSubject
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
radius={10}
fill="#000"
/>
<CircleSubject radius={4} fill="red" />
<HtmlLabel showAnchorLine={false} verticalAnchor="start">
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={styles.textbox}
style={{ width: textWidth, height: textHeight }}
>
<p
className={styles.textboxText}
style={{ maxHeight: textHeight - 10 }}
>
{positionData.text}
</p>
</div>
</HtmlLabel>
</Annotation>
</Group>
);
}