+
{general.backstageUrl &&
}
{general.backstageInfo &&
{general.backstageInfo}
}
diff --git a/apps/client/src/views/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts
index ccf4f94e3..a0b2b414b 100644
--- a/apps/client/src/views/backstage/backstage.options.ts
+++ b/apps/client/src/views/backstage/backstage.options.ts
@@ -8,7 +8,7 @@ import {
OptionTitle,
} from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types';
-import { isStringBoolean } from '../../features/viewers/common/viewUtils';
+import { scheduleOptions } from '../common/schedule/schedule.options';
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
@@ -29,26 +29,7 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel
},
],
},
- {
- title: OptionTitle.Schedule,
- collapsible: true,
- options: [
- {
- id: 'stopCycle',
- title: 'Stop cycling through event pages',
- description: 'Schedule will not auto-cycle through events',
- type: 'boolean',
- defaultValue: false,
- },
- {
- id: 'cycleInterval',
- title: 'Cycle interval',
- description: 'How long (in seconds) should each schedule page be shown.',
- type: 'number',
- defaultValue: 10,
- },
- ],
- },
+ scheduleOptions,
];
};
@@ -75,24 +56,3 @@ export function useBackstageOptions(): BackstageOptions {
const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
return options;
}
-
-type ScheduleOptions = {
- cycleInterval: number;
- stopCycle: boolean;
-};
-
-function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions {
- return {
- cycleInterval: Number(searchParams.get('cycleInterval')) || 10,
- stopCycle: isStringBoolean(searchParams.get('stopCycle')),
- };
-}
-
-/**
- * Hook exposes the schedule component options
- */
-export function useScheduleOptions() {
- const [searchParams] = useSearchParams();
- const options = useMemo(() => getScheduleOptionsFromParams(searchParams), [searchParams]);
- return options;
-}
diff --git a/apps/client/src/views/backstage/backstage.utils.ts b/apps/client/src/views/backstage/backstage.utils.ts
index 03086b3c5..7e5e5992a 100644
--- a/apps/client/src/views/backstage/backstage.utils.ts
+++ b/apps/client/src/views/backstage/backstage.utils.ts
@@ -1,5 +1,6 @@
import { MaybeNumber, OntimeEvent, Playback, TimerPhase } from 'ontime-types';
+import { enDash } from '../../common/utils/styleUtils';
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
/**
@@ -45,16 +46,16 @@ export function getCardData(
}
// if we are loaded, we show the upcoming event as next
- const nowMain = getPropertyValue(eventNow, mainSource ?? 'title');
+ const nowMain = getPropertyValue(eventNow, mainSource ?? 'title') || enDash;
const nowSecondary = getPropertyValue(eventNow, secondarySource);
- const nextMain = getPropertyValue(eventNext, mainSource ?? 'title');
+ const nextMain = getPropertyValue(eventNext, mainSource ?? 'title') || enDash;
const nextSecondary = getPropertyValue(eventNext, secondarySource);
return {
- showNow: Boolean(nowMain) || Boolean(nowSecondary),
+ showNow: eventNow !== null,
nowMain,
nowSecondary,
- showNext: Boolean(nextMain) || Boolean(nextSecondary),
+ showNext: eventNext !== null,
nextMain,
nextSecondary,
};
diff --git a/apps/client/src/views/common/schedule/PublicSchedule.tsx b/apps/client/src/views/common/schedule/PublicSchedule.tsx
deleted file mode 100644
index b6c16d815..000000000
--- a/apps/client/src/views/common/schedule/PublicSchedule.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { memo } from 'react';
-import { MaybeString } from 'ontime-types';
-
-import Schedule from './Schedule';
-import { ScheduleProvider } from './ScheduleContext';
-import ScheduleNav from './ScheduleNav';
-
-interface PublicScheduleProps {
- selectedId: MaybeString;
-}
-
-export default memo(PublicSchedule);
-function PublicSchedule(props: PublicScheduleProps) {
- const { selectedId } = props;
- return (
-
-
-
-
- );
-}
diff --git a/apps/client/src/views/common/schedule/Schedule.scss b/apps/client/src/views/common/schedule/Schedule.scss
index ce83872f8..0984ddc8e 100644
--- a/apps/client/src/views/common/schedule/Schedule.scss
+++ b/apps/client/src/views/common/schedule/Schedule.scss
@@ -1,5 +1,8 @@
@use '../../../theme/viewerDefs' as *;
+$circle-size: clamp(8px, 0.75vw, 12px);
+$indeterminate-width: clamp(32px, 3vw, 48px);
+
.schedule {
width: 100%;
position: relative;
@@ -17,9 +20,9 @@
.entry-colour {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
- height: clamp(8px, 0.75vw, 12px);
- width: clamp(8px, 0.75vw, 12px);
- border-radius: 6px;
+ height: $circle-size;
+ width: $circle-size;
+ border-radius: $component-border-radius-full;
margin-right: 0.25rem;
}
@@ -49,7 +52,6 @@
}
.entry-title {
- font-size: clamp(16px, 1.5vw, 24px);
font-size: $base-font-size;
line-height: 1.2em;
}
@@ -62,10 +64,10 @@
background-color: var(--color-override, $viewer-color);
opacity: 0.2;
- height: clamp(8px, 0.75vw, 12px);
- width: clamp(8px, 0.75vw, 12px);
- border-radius: 6px;
- margin-right: 8px;
+ height: $circle-size;
+ width: $circle-size;
+ border-radius: $component-border-radius-full;
+ margin-right: 0.5em;
transition-property: opacity;
transition-duration: 1s;
@@ -77,7 +79,7 @@
}
&--indeterminate {
- width: clamp(32px, 3vw, 48px);
+ width: $indeterminate-width;
}
}
}
diff --git a/apps/client/src/views/common/schedule/ScheduleContext.tsx b/apps/client/src/views/common/schedule/ScheduleContext.tsx
index a690c3d76..1f57d7698 100644
--- a/apps/client/src/views/common/schedule/ScheduleContext.tsx
+++ b/apps/client/src/views/common/schedule/ScheduleContext.tsx
@@ -11,7 +11,8 @@ import {
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
-import { useScheduleOptions } from '../../backstage/backstage.options';
+
+import { useScheduleOptions } from './schedule.options';
interface ScheduleContextState {
events: OntimeEvent[];
@@ -51,6 +52,7 @@ export const ScheduleProvider = ({
const containerRef = useRef
(null);
+ // After the view is rendered, we paginate by hiding elements that dont fit
useLayoutEffect(() => {
if (!containerRef.current) return;
diff --git a/apps/client/src/views/common/schedule/BackstageSchedule.tsx b/apps/client/src/views/common/schedule/ScheduleExport.tsx
similarity index 59%
rename from apps/client/src/views/common/schedule/BackstageSchedule.tsx
rename to apps/client/src/views/common/schedule/ScheduleExport.tsx
index f29790b6b..2e39fd4d2 100644
--- a/apps/client/src/views/common/schedule/BackstageSchedule.tsx
+++ b/apps/client/src/views/common/schedule/ScheduleExport.tsx
@@ -5,17 +5,18 @@ import Schedule from './Schedule';
import { ScheduleProvider } from './ScheduleContext';
import ScheduleNav from './ScheduleNav';
-interface BackstageScheduleProps {
+interface ScheduleExportProps {
selectedId: MaybeString;
+ isBackstage?: boolean;
}
-export default memo(BackstageSchedule);
-function BackstageSchedule(props: BackstageScheduleProps) {
- const { selectedId } = props;
+export default memo(ScheduleExport);
+function ScheduleExport(props: ScheduleExportProps) {
+ const { selectedId, isBackstage } = props;
return (
-
+
);
}
diff --git a/apps/client/src/views/common/schedule/ScheduleItem.tsx b/apps/client/src/views/common/schedule/ScheduleItem.tsx
index b67c06a99..f2ffb8b2f 100644
--- a/apps/client/src/views/common/schedule/ScheduleItem.tsx
+++ b/apps/client/src/views/common/schedule/ScheduleItem.tsx
@@ -35,13 +35,13 @@ export default function ScheduleItem(props: ScheduleItemProps) {
- {' → '}
+ →
{backstageEvent && '*'}
- {' → '}
+ →
{backstageEvent && '*'}
@@ -56,7 +56,7 @@ export default function ScheduleItem(props: ScheduleItemProps) {
- {' → '}
+ →
{backstageEvent && '*'}
diff --git a/apps/client/src/views/common/schedule/schedule.options.ts b/apps/client/src/views/common/schedule/schedule.options.ts
new file mode 100644
index 000000000..b70fc15b9
--- /dev/null
+++ b/apps/client/src/views/common/schedule/schedule.options.ts
@@ -0,0 +1,48 @@
+import { useMemo } from 'react';
+import { useSearchParams } from 'react-router-dom';
+
+import { OptionTitle } from '../../../common/components/view-params-editor/constants';
+import { ViewOption } from '../../../common/components/view-params-editor/types';
+import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
+
+export const scheduleOptions: ViewOption = {
+ title: OptionTitle.Schedule,
+ collapsible: true,
+ options: [
+ {
+ id: 'stopCycle',
+ title: 'Stop cycling through event pages',
+ description: 'Schedule will not auto-cycle through events',
+ type: 'boolean',
+ defaultValue: false,
+ },
+ {
+ id: 'cycleInterval',
+ title: 'Cycle interval',
+ description: 'How long (in seconds) should each schedule page be shown.',
+ type: 'number',
+ defaultValue: 10,
+ },
+ ],
+};
+
+type ScheduleOptions = {
+ cycleInterval: number;
+ stopCycle: boolean;
+};
+
+function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions {
+ return {
+ cycleInterval: Number(searchParams.get('cycleInterval')) || 10,
+ stopCycle: isStringBoolean(searchParams.get('stopCycle')),
+ };
+}
+
+/**
+ * Hook exposes the schedule component options
+ */
+export function useScheduleOptions() {
+ const [searchParams] = useSearchParams();
+ const options = useMemo(() => getScheduleOptionsFromParams(searchParams), [searchParams]);
+ return options;
+}
diff --git a/apps/client/src/views/public/Public.scss b/apps/client/src/views/public/Public.scss
index e4c577677..7a020b7a6 100644
--- a/apps/client/src/views/public/Public.scss
+++ b/apps/client/src/views/public/Public.scss
@@ -10,23 +10,32 @@
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
- gap: min(2vh, 16px);
- padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
+ gap: $view-element-gap;
+ padding: $view-outer-padding;
+ font-size: $base-font-size;
display: grid;
- grid-template-columns: 1fr 1fr 40vw;
+ grid-template-columns: 1fr 40vw;
grid-template-rows: auto 12px 1fr auto;
grid-template-areas:
- ' header header header'
- ' progress progress schedule-nav'
- ' now now schedule'
- ' info info schedule';
+ 'header header'
+ 'now schedule-nav'
+ 'info schedule';
+
+ .empty-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin-top: 25vh;
+ color: var(--label-color-override, $viewer-label-color);
+ }
/* =================== HEADER + EXTRAS ===================*/
.project-header {
grid-area: header;
- font-size: clamp(32px, 4.5vw, 64px);
+ font-size: $header-font-size;
font-weight: 600;
display: flex;
gap: 1rem;
@@ -37,18 +46,22 @@
max-height: min(100px, 20vh);
}
+ .title {
+ line-height: 1.1em;
+ }
+
.clock-container {
margin-left: auto;
+ font-weight: 600;
.label {
- font-size: clamp(16px, 1.5vw, 24px);
- font-weight: 600;
+ font-size: $timer-label-size;
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
.time {
- font-size: clamp(32px, 3.5vw, 50px);
+ font-size: $timer-value-size;
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
line-height: 0.95em;
@@ -57,50 +70,136 @@
/* =================== MAIN - NOW ===================*/
- .now-container {
+ .card-container {
grid-area: now;
display: flex;
flex-direction: column;
- gap: min(2vh, 16px);
+ gap: $view-element-gap;
}
.event {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
- padding: 16px 24px;
- border-radius: 8px;
+ padding: $view-card-padding;
+ border-radius: $element-border-radius;
+ }
+
+ .timer-group {
+ border-top: 2px solid var(--background-color-override, $viewer-background-color);
+ margin-top: max(1vh, 16px);
+ padding-top: max(1vh, 16px);
+ display: flex;
+ row-gap: 0.5em;
+ }
+
+ .time-entry {
+ &__label {
+ font-size: $timer-label-size;
+ color: var(--label-color-override, $viewer-label-color);
+ font-weight: 600;
+ text-transform: uppercase;
+ }
+
+ &__value {
+ font-size: $base-font-size;
+ color: var(--secondary-color-override, $viewer-secondary-color);
+ letter-spacing: 0.05em;
+ line-height: 0.95em;
+ }
+
+ &--pending {
+ color: var(--timer-pending-color-override, $ontime-roll);
+ }
}
/* =================== MAIN - SCHEDULE ===================*/
+ $schedule-left-spacing: clamp(16px, 4vw, 64px);
.schedule-container {
grid-area: schedule;
overflow: hidden;
height: 100%;
- margin-left: clamp(16px, 5vw, 64px);
+ padding-left: $schedule-left-spacing;
}
.schedule-nav-container {
grid-area: schedule-nav;
- align-self: center;
+ padding-left: $schedule-left-spacing;
}
+ /* =================== MAIN - INFO ===================*/
+
.info {
grid-area: info;
display: flex;
gap: max(1vw, 16px);
+ align-self: flex-end;
overflow: hidden;
+ align-items: end;
&__message {
- font-size: clamp(16px, 1.5vw, 24px);
- line-height: 1.3em;
+ font-size: $base-font-size;
+ line-height: 1.2em;
white-space: pre-line;
overflow: hidden;
flex: 1;
}
.qr {
- padding: 4px;
- background-color: white;
+ padding: 0.5rem;
+ background-color: $ui-white;
+ border-radius: 2px;
+ }
+ }
+}
+
+/* =================== MOBILE ===================*/
+@media screen and (max-width: 768px) {
+ .public-screen {
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+
+ .project-header {
+ flex-direction: column;
+ position: relative;
+ gap: 0.5rem;
+ }
+
+ .logo {
+ height: min(50px, 10vh);
+ }
+
+ .timer-group {
+ flex-wrap: wrap;
+ }
+
+ .clock-container {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ .schedule-nav-container {
+ padding-left: 0;
+ justify-content: center;
+ }
+
+ .schedule-container {
+ padding-left: 0;
+ flex: 1;
+ }
+
+ .info {
+ width: 100%;
+ text-align: right;
+ }
+
+ .qr {
+ display: none;
+ }
+
+ .info--stretch {
+ flex: 1;
}
}
}
diff --git a/apps/client/src/views/public/Public.tsx b/apps/client/src/views/public/Public.tsx
index 3f174100a..aae5dd7b9 100644
--- a/apps/client/src/views/public/Public.tsx
+++ b/apps/client/src/views/public/Public.tsx
@@ -1,28 +1,28 @@
import QRCode from 'react-qr-code';
-import { useSearchParams } from 'react-router-dom';
-import { AnimatePresence, motion } from 'framer-motion';
+import { useViewportSize } from '@mantine/hooks';
import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types';
+import Empty from '../../common/components/state/Empty';
import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
+import { cx } from '../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
-import { getPropertyValue } from '../../features/viewers/common/viewUtils';
import { useTranslation } from '../../translation/TranslationProvider';
-import PublicSchedule from '../common/schedule/PublicSchedule';
-import { titleVariants } from '../timer/timer.animations';
+import { getIsPendingStart } from '../backstage/backstage.utils';
+import ScheduleExport from '../common/schedule/ScheduleExport';
-import { getPublicOptions } from './public.options';
+import { getPublicOptions, usePublicOptions } from './public.options';
+import { getCardData, getFirstStartTime } from './public.utils';
import './Public.scss';
-export const MotionTitleCard = motion(TitleCard);
-
interface BackstageProps {
customFields: CustomFields;
+ events: OntimeEvent[];
general: ProjectData;
isMirrored: boolean;
publicEventNow: OntimeEvent | null;
@@ -33,73 +33,86 @@ interface BackstageProps {
}
export default function Public(props: BackstageProps) {
- const { customFields, general, isMirrored, publicEventNow, publicEventNext, time, publicSelectedId, settings } =
- props;
+ const {
+ customFields,
+ events,
+ general,
+ isMirrored,
+ publicEventNow,
+ publicEventNext,
+ time,
+ publicSelectedId,
+ settings,
+ } = props;
const { getLocalizedString } = useTranslation();
- const [searchParams] = useSearchParams();
+ const { secondarySource } = usePublicOptions();
+ const { height: screenHeight } = useViewportSize();
useWindowTitle('Public Schedule');
- const clock = formatTime(time.clock);
- const qrSize = Math.max(window.innerWidth / 15, 128);
+ // gather card data
+ const hasEvents = events.length > 0;
+ const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
+ publicEventNow,
+ publicEventNext,
+ 'title',
+ secondarySource,
+ time.playback,
+ );
+ // gather timer data
+ const clock = formatTime(time.clock);
+ const isPendingStart = getIsPendingStart(time.playback, time.phase);
+ const scheduledStart = hasEvents && showNow ? '' : getFirstStartTime(events[0]);
+
+ // gather presentation styles
+ const qrSize = Math.max(window.innerWidth / 15, 72);
+ const showSchedule = hasEvents && screenHeight > 700; // in vertical screens we may not have space
+
+ // gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const publicOptions = getPublicOptions(defaultFormat, customFields);
- const secondarySource = searchParams.get('secondary-src');
- const secondaryTextNext = getPropertyValue(publicEventNext, secondarySource);
- const secondaryTextNow = getPropertyValue(publicEventNow, secondarySource);
-
return (
- {general?.projectLogo &&
}
- {general.title}
+ {general?.projectLogo ?
:
}
+
{general.title}
{getLocalizedString('common.time_now')}
-
-
- {publicEventNow && (
-
- )}
-
+ {!hasEvents &&
}
-
- {publicEventNext && (
-
- )}
-
+
+ {showNow && hasEvents && (
+
+ )}
+ {!showNow && scheduledStart && (
+
+
{getLocalizedString('countdown.waiting')}
+
+
+
+ {getLocalizedString('common.scheduled_start')}
+
+
+
+
+
+ )}
+ {showNext && hasEvents && (
+
+ )}
-
+ {showSchedule &&
}
-
+
{general.publicUrl &&
}
{general.publicInfo &&
{general.publicInfo}
}
diff --git a/apps/client/src/views/public/public.options.ts b/apps/client/src/views/public/public.options.ts
index 210abf12e..821de86c1 100644
--- a/apps/client/src/views/public/public.options.ts
+++ b/apps/client/src/views/public/public.options.ts
@@ -1,4 +1,6 @@
-import { CustomFields } from 'ontime-types';
+import { useMemo } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { CustomFields, OntimeEvent } from 'ontime-types';
import {
getTimeOption,
@@ -6,6 +8,7 @@ import {
OptionTitle,
} from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types';
+import { scheduleOptions } from '../common/schedule/schedule.options';
export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
@@ -26,32 +29,30 @@ export const getPublicOptions = (timeFormat: string, customFields: CustomFields)
},
],
},
- {
- title: OptionTitle.Schedule,
- collapsible: true,
- options: [
- {
- id: 'eventsPerPage',
- title: 'Events per page',
- description: 'Sets the number of events on the page, can cause overflow',
- type: 'number',
- placeholder: '8 (default)',
- },
- {
- id: 'hidePast',
- title: 'Hide past events',
- description: 'Scheduler will only show upcoming events',
- type: 'boolean',
- defaultValue: false,
- },
- {
- id: 'stopCycle',
- title: 'Stop cycling through event pages',
- description: 'Schedule will not auto-cycle through events',
- type: 'boolean',
- defaultValue: false,
- },
- ],
- },
+ scheduleOptions,
];
};
+
+type PublicOptions = {
+ secondarySource: keyof OntimeEvent | null;
+};
+
+/**
+ * Utility extract the view options from URL Params
+ * the names and fallback are manually matched with timerOptions
+ */
+function getOptionsFromParams(searchParams: URLSearchParams): PublicOptions {
+ // we manually make an object that matches the key above
+ return {
+ secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null,
+ };
+}
+
+/**
+ * Hook exposes the backstage view options
+ */
+export function usePublicOptions(): PublicOptions {
+ const [searchParams] = useSearchParams();
+ const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
+ return options;
+}
diff --git a/apps/client/src/views/public/public.utils.ts b/apps/client/src/views/public/public.utils.ts
new file mode 100644
index 000000000..a0b463b8b
--- /dev/null
+++ b/apps/client/src/views/public/public.utils.ts
@@ -0,0 +1,45 @@
+import { OntimeEvent, Playback } from 'ontime-types';
+
+import { enDash } from '../../common/utils/styleUtils';
+import { getPropertyValue } from '../../features/viewers/common/viewUtils';
+
+/**
+ * What should we be showing in the cards?
+ */
+export function getCardData(
+ eventNow: OntimeEvent | null,
+ eventNext: OntimeEvent | null,
+ mainSource: keyof OntimeEvent | null,
+ secondarySource: keyof OntimeEvent | null,
+ playback: Playback,
+) {
+ if (playback === Playback.Stop) {
+ return {
+ showNow: false,
+ nowMain: undefined,
+ nowSecondary: undefined,
+ showNext: false,
+ nextMain: undefined,
+ nextSecondary: undefined,
+ };
+ }
+
+ // if we are loaded, we show the upcoming event as next
+ const nowMain = getPropertyValue(eventNow, mainSource ?? 'title') || enDash;
+ const nowSecondary = getPropertyValue(eventNow, secondarySource);
+ const nextMain = getPropertyValue(eventNext, mainSource ?? 'title') || enDash;
+ const nextSecondary = getPropertyValue(eventNext, secondarySource);
+
+ return {
+ showNow: eventNow !== null,
+ nowMain,
+ nowSecondary,
+ showNext: eventNext !== null,
+ nextMain,
+ nextSecondary,
+ };
+}
+
+export function getFirstStartTime(firstPublicEvent: OntimeEvent | null): number | undefined {
+ return firstPublicEvent?.timeStart;
+}
diff --git a/e2e/tests/features/203-delay-block.spec.ts b/e2e/tests/features/203-delay-block.spec.ts
index 40af8d9b9..6b74df6f0 100644
--- a/e2e/tests/features/203-delay-block.spec.ts
+++ b/e2e/tests/features/203-delay-block.spec.ts
@@ -84,9 +84,9 @@ test('delays are show correctly', async ({ page }) => {
// delay is NOT shown in the public view
await page.goto('http://localhost:4001/public');
- await page.getByText('00:10 → 00:20').click();
+ await page.getByText('00:10→00:20').click();
// delay is shown in the backstage view
await page.goto('http://localhost:4001/backstage');
- await page.getByText('00:11 → 00:21').click();
+ await page.getByText('00:11→00:21').click();
});