mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 03:49:11 +00:00
refactor: extract timeline data to hook
This commit is contained in:
committed by
Carlos Valente
parent
bea7e2bb2b
commit
c300fc18e1
@@ -78,6 +78,7 @@ export default function AppRouter() {
|
|||||||
path='timeline'
|
path='timeline'
|
||||||
element={
|
element={
|
||||||
<ViewLoader>
|
<ViewLoader>
|
||||||
|
<ViewNavigationMenu isLockable />
|
||||||
<Timeline />
|
<Timeline />
|
||||||
</ViewLoader>
|
</ViewLoader>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,3 +269,8 @@ export const useStudioTimersSocket = createSelector((state: RuntimeStore) => ({
|
|||||||
time: state.timer,
|
time: state.timer,
|
||||||
runtime: state.runtime,
|
runtime: state.runtime,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useTimelineSocket = createSelector((state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
offsetAbs: state.runtime.offsetAbs,
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,103 +1,86 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { MaybeString, OntimeEvent, OntimeView, ProjectData, Runtime, Settings } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
|
|
||||||
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
import { useClock, useSelectedEventId } from '../../common/hooks/useSocket';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import { formatDuration, formatTime, getDefaultFormat } from '../../common/utils/time';
|
|
||||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
import Loader from '../common/loader/Loader';
|
||||||
|
|
||||||
import Section from './timeline-section/TimelineSection';
|
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
import { getTimelineOptions } from './timeline.options';
|
import { getTimelineOptions } from './timeline.options';
|
||||||
import { getTimeToStart, getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
||||||
|
import TimelineSections from './TimelineSections';
|
||||||
|
import { TimelineData, useTimelineData } from './useTimelineData';
|
||||||
|
|
||||||
import './TimelinePage.scss';
|
import './TimelinePage.scss';
|
||||||
|
|
||||||
interface TimelinePageProps {
|
export default function TimelinePageLoader() {
|
||||||
events: OntimeEvent[];
|
const { data, status } = useTimelineData();
|
||||||
general: ProjectData;
|
|
||||||
runtime: Runtime;
|
|
||||||
selectedId: MaybeString;
|
|
||||||
settings: Settings | undefined;
|
|
||||||
time: ViewExtendedTimer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* since we inherit from viewPage
|
|
||||||
* which refreshes at least once a second
|
|
||||||
* There is little point splitting or memoising top level elements
|
|
||||||
*/
|
|
||||||
export default function TimelinePage({ events, general, runtime, selectedId, settings, time }: TimelinePageProps) {
|
|
||||||
// holds copy of the rundown with only relevant events
|
|
||||||
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedId);
|
|
||||||
const { getLocalizedString } = useTranslation();
|
|
||||||
const clock = formatTime(time.clock);
|
|
||||||
|
|
||||||
const { now, next, followedBy } = useMemo(() => {
|
|
||||||
return getUpcomingEvents(scopedRundown, selectedId);
|
|
||||||
}, [scopedRundown, selectedId]);
|
|
||||||
|
|
||||||
useWindowTitle('Timeline');
|
useWindowTitle('Timeline');
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
|
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TimelinePage {...data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimelinePage({ events, projectData, settings }: TimelineData) {
|
||||||
|
const { selectedEventId } = useSelectedEventId();
|
||||||
|
// holds copy of the rundown with only relevant events
|
||||||
|
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedEventId);
|
||||||
|
|
||||||
|
// gather card options
|
||||||
|
const { now, next, followedBy } = useMemo(() => {
|
||||||
|
return getUpcomingEvents(scopedRundown, selectedEventId);
|
||||||
|
}, [scopedRundown, selectedEventId]);
|
||||||
|
|
||||||
// populate options
|
// populate options
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]);
|
const progressOptions = useMemo(() => getTimelineOptions(defaultFormat), [defaultFormat]);
|
||||||
|
|
||||||
const titleNow = now?.title ?? '-';
|
|
||||||
const dueText = getLocalizedString('timeline.due').toUpperCase();
|
|
||||||
const nextText = next !== null ? next.title : '-';
|
|
||||||
const followedByText = followedBy !== null ? followedBy.title : '-';
|
|
||||||
let nextStatus: string | undefined;
|
|
||||||
let followedByStatus: string | undefined;
|
|
||||||
|
|
||||||
if (next !== null) {
|
|
||||||
const timeToStart = getTimeToStart(time.clock, next.timeStart, next?.delay ?? 0, runtime.offsetAbs);
|
|
||||||
if (timeToStart < 0) {
|
|
||||||
nextStatus = dueText;
|
|
||||||
} else {
|
|
||||||
nextStatus = `T - ${formatDuration(timeToStart)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (followedBy !== null) {
|
|
||||||
const timeToStart = getTimeToStart(time.clock, followedBy.timeStart, followedBy?.delay ?? 0, runtime.offsetAbs);
|
|
||||||
if (timeToStart < 0) {
|
|
||||||
followedByStatus = dueText;
|
|
||||||
} else {
|
|
||||||
followedByStatus = `T - ${formatDuration(timeToStart)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className='timeline' data-testid='timeline-view'>
|
<div className='timeline' data-testid='timeline-view'>
|
||||||
<ViewParamsEditor target={OntimeView.Timeline} viewOptions={progressOptions} />
|
<ViewParamsEditor target={OntimeView.Timeline} viewOptions={progressOptions} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
{projectData?.logo && <ViewLogo name={projectData.logo} className='logo' />}
|
||||||
<div className='title'>{general.title}</div>
|
<div className='title'>{projectData.title}</div>
|
||||||
<div className='clock-container'>
|
<TimelineClock />
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
|
||||||
<SuperscriptTime time={clock} className='time' />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='title-grid'>
|
<TimelineSections now={now} next={next} followedBy={followedBy} />
|
||||||
<Section title={getLocalizedString('timeline.live')} content={titleNow} category='now' />
|
|
||||||
<Section title={getLocalizedString('common.next')} status={nextStatus} content={nextText} category='next' />
|
|
||||||
<Section
|
|
||||||
title={getLocalizedString('timeline.followedby')}
|
|
||||||
status={followedByStatus}
|
|
||||||
content={followedByText}
|
|
||||||
category='next'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Timeline
|
<Timeline
|
||||||
firstStart={firstStart}
|
firstStart={firstStart}
|
||||||
rundown={scopedRundown}
|
rundown={scopedRundown}
|
||||||
selectedEventId={selectedId}
|
selectedEventId={selectedEventId}
|
||||||
totalDuration={totalDuration}
|
totalDuration={totalDuration}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TimelineClock() {
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
const { clock } = useClock();
|
||||||
|
|
||||||
|
// gather timer data
|
||||||
|
const formattedClock = formatTime(clock);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='clock-container'>
|
||||||
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
|
<SuperscriptTime time={formattedClock} className='time' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useTimelineSocket } from '../../common/hooks/useSocket';
|
||||||
|
import { formatDuration } from '../../common/utils/time';
|
||||||
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
|
import TimelineSection from './timeline-section/TimelineSection';
|
||||||
|
import { getTimeToStart } from './timeline.utils';
|
||||||
|
|
||||||
|
interface TimelineSectionsProps {
|
||||||
|
now: OntimeEvent | null;
|
||||||
|
next: OntimeEvent | null;
|
||||||
|
followedBy: OntimeEvent | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TimelineSections({ now, next, followedBy }: TimelineSectionsProps) {
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
const { clock, offsetAbs } = useTimelineSocket();
|
||||||
|
|
||||||
|
// gather card data
|
||||||
|
const titleNow = now?.title ?? '-';
|
||||||
|
const dueText = getLocalizedString('timeline.due').toUpperCase();
|
||||||
|
const nextText = next !== null ? next.title : '-';
|
||||||
|
const followedByText = followedBy !== null ? followedBy.title : '-';
|
||||||
|
let nextStatus: string | undefined;
|
||||||
|
let followedByStatus: string | undefined;
|
||||||
|
|
||||||
|
if (next !== null) {
|
||||||
|
const timeToStart = getTimeToStart(clock, next.timeStart, next?.delay ?? 0, offsetAbs);
|
||||||
|
if (timeToStart < 0) {
|
||||||
|
nextStatus = dueText;
|
||||||
|
} else {
|
||||||
|
nextStatus = `T - ${formatDuration(timeToStart)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followedBy !== null) {
|
||||||
|
const timeToStart = getTimeToStart(clock, followedBy.timeStart, followedBy?.delay ?? 0, offsetAbs);
|
||||||
|
if (timeToStart < 0) {
|
||||||
|
followedByStatus = dueText;
|
||||||
|
} else {
|
||||||
|
followedByStatus = `T - ${formatDuration(timeToStart)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='title-grid'>
|
||||||
|
<TimelineSection title={getLocalizedString('timeline.live')} content={titleNow} category='now' />
|
||||||
|
<TimelineSection
|
||||||
|
title={getLocalizedString('common.next')}
|
||||||
|
status={nextStatus}
|
||||||
|
content={nextText}
|
||||||
|
category='next'
|
||||||
|
/>
|
||||||
|
<TimelineSection
|
||||||
|
title={getLocalizedString('timeline.followedby')}
|
||||||
|
status={followedByStatus}
|
||||||
|
content={followedByText}
|
||||||
|
category='next'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ interface SectionProps {
|
|||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Section);
|
export default memo(TimelineSection);
|
||||||
|
|
||||||
function Section({ category, content, title, status }: SectionProps) {
|
function TimelineSection({ category, content, title, status }: SectionProps) {
|
||||||
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
const sectionClasses = cx(['section', category === 'now' && 'section--now']);
|
||||||
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
|
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { OntimeEntry, ProjectData, Settings } from 'ontime-types';
|
||||||
|
|
||||||
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
|
import { aggregateQueryStatus, ViewData } from '../utils/viewLoader.utils';
|
||||||
|
|
||||||
|
export interface TimelineData {
|
||||||
|
events: OntimeEntry[];
|
||||||
|
projectData: ProjectData;
|
||||||
|
settings: Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimelineData(): ViewData<TimelineData> {
|
||||||
|
// HTTP API data
|
||||||
|
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
||||||
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
events: rundownData,
|
||||||
|
projectData,
|
||||||
|
settings,
|
||||||
|
},
|
||||||
|
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus]),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user