mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
import { Day } from 'ontime-types';
|
|
import { CSSProperties, RefObject } from 'react';
|
|
|
|
import { useExpectedStartData, useTimer } from '../../common/hooks/useSocket';
|
|
import { getProgress } from '../../common/utils/getProgress';
|
|
import { alpha, cx } from '../../common/utils/styleUtils';
|
|
import { formatDuration, formatTime, getExpectedTimesFromExtendedEvent } from '../../common/utils/time';
|
|
import { useTranslation } from '../../translation/TranslationProvider';
|
|
import { getStatusLabel } from './timeline.utils';
|
|
|
|
import style from './Timeline.module.scss';
|
|
|
|
export type ProgressStatus = 'done' | 'live' | 'future';
|
|
|
|
interface TimelineEntryProps {
|
|
colour: string;
|
|
delay: number;
|
|
duration: number;
|
|
hasLink: boolean;
|
|
left: number;
|
|
status: ProgressStatus;
|
|
start: number;
|
|
dayOffset: Day;
|
|
totalGap: number;
|
|
isLinkedToLoaded: boolean;
|
|
title: string;
|
|
width: number;
|
|
cue: string;
|
|
ref?: RefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
const formatOptions = {
|
|
format12: 'h:mm a',
|
|
format24: 'HH:mm',
|
|
};
|
|
|
|
export function TimelineEntry({
|
|
colour,
|
|
delay,
|
|
duration,
|
|
hasLink,
|
|
left,
|
|
status,
|
|
start,
|
|
dayOffset,
|
|
totalGap,
|
|
isLinkedToLoaded,
|
|
title,
|
|
width,
|
|
cue,
|
|
ref,
|
|
}: TimelineEntryProps) {
|
|
const formattedStartTime = formatTime(start, formatOptions);
|
|
const formattedDuration = formatDuration(duration);
|
|
const delayedStart = start + delay;
|
|
const hasDelay = delay > 0;
|
|
|
|
const lighterColour = alpha(colour, 0.7);
|
|
const showTitle = width > 20;
|
|
const smallArea = width < 40;
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cx([style.column, smallArea && style.smallArea])}
|
|
style={
|
|
{
|
|
'--color': colour,
|
|
'--lighter': lighterColour ?? '',
|
|
left: `${left}px`,
|
|
width: `${width}px`,
|
|
} as CSSProperties
|
|
}
|
|
data-testid={cue}
|
|
>
|
|
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
|
|
<div
|
|
className={cx([style.content, width < 20 && style.hide, !hasLink && style.separeLeft])}
|
|
data-status={status}
|
|
style={
|
|
{
|
|
'--color': colour,
|
|
} as CSSProperties
|
|
}
|
|
>
|
|
<div className={style.maybeInline}>
|
|
<div className={cx([hasDelay && style.cross])}>{formattedStartTime}</div>
|
|
{hasDelay && <div className={style.delay}>{formatTime(delayedStart, formatOptions)}</div>}
|
|
{smallArea && (
|
|
<TimelineEntryStatus
|
|
delay={delay}
|
|
start={start}
|
|
dayOffset={dayOffset}
|
|
totalGap={totalGap}
|
|
isLinkedToLoaded={isLinkedToLoaded}
|
|
status={status}
|
|
/>
|
|
)}
|
|
{smallArea && <div className={style.title}>{title}</div>}
|
|
</div>
|
|
{showTitle && (
|
|
<>
|
|
{!smallArea && (
|
|
<TimelineEntryStatus
|
|
delay={delay}
|
|
start={start}
|
|
dayOffset={dayOffset}
|
|
totalGap={totalGap}
|
|
isLinkedToLoaded={isLinkedToLoaded}
|
|
status={status}
|
|
/>
|
|
)}
|
|
{!smallArea && <div className={style.title}>{title}</div>}
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className={style.timeOverview} data-status={status}>
|
|
{status !== 'done' && <div className={style.duration}>{formattedDuration}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface TimelineEntryStatusProps {
|
|
delay: number;
|
|
start: number;
|
|
dayOffset: Day;
|
|
totalGap: number;
|
|
isLinkedToLoaded: boolean;
|
|
status: ProgressStatus;
|
|
}
|
|
|
|
// extract component to isolate re-renders provoked by the clock changes
|
|
function TimelineEntryStatus({
|
|
delay,
|
|
start,
|
|
dayOffset,
|
|
totalGap,
|
|
isLinkedToLoaded,
|
|
status,
|
|
}: TimelineEntryStatusProps) {
|
|
const state = useExpectedStartData();
|
|
|
|
const { getLocalizedString } = useTranslation();
|
|
|
|
const { timeToStart } = getExpectedTimesFromExtendedEvent(
|
|
{ timeStart: start, delay, dayOffset, totalGap, isLinkedToLoaded, countToEnd: false, duration: 0 },
|
|
state,
|
|
);
|
|
|
|
let statusText = getStatusLabel(timeToStart, status);
|
|
if (statusText === 'live') {
|
|
statusText = getLocalizedString('timeline.live');
|
|
} else if (statusText === 'pending') {
|
|
statusText = getLocalizedString('timeline.due');
|
|
}
|
|
|
|
const isDue = status === 'future' && timeToStart <= 0;
|
|
|
|
return <div className={cx([style.status, isDue && style.due])}>{statusText}</div>;
|
|
}
|
|
|
|
/** Generates a block level progress bar */
|
|
function ActiveBlock() {
|
|
const { current, duration } = useTimer();
|
|
const progress = getProgress(current, duration);
|
|
return (
|
|
<div data-status='live' className={style.timelineBlock} style={{ '--progress': `${progress}%` } as CSSProperties} />
|
|
);
|
|
}
|