mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
Feat/120 (#164)
* feat/120: calculate delay to single event * feat/120 Countdown * feat/120 prevent parsing NaN * feat/120: version bump * feat/120: update README.md * feat/120: show delayed countdown timer
This commit is contained in:
@@ -80,6 +80,7 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
|||||||
same as app)
|
same as app)
|
||||||
- [x] Multi platform (available on Windows, MacOS and Linux)
|
- [x] Multi platform (available on Windows, MacOS and Linux)
|
||||||
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
|
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
|
||||||
|
- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): ability to have a countdown to any scheduled event
|
||||||
|
|
||||||
## Unopinionated
|
## Unopinionated
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "1.1.3",
|
"version": "1.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.0.0-next.3",
|
"@chakra-ui/react": "^2.0.0-next.3",
|
||||||
|
|||||||
+4
-4
@@ -2,9 +2,9 @@ import React, { memo } from 'react';
|
|||||||
import { formatDisplay } from 'common/utils/dateConfig';
|
import { formatDisplay } from 'common/utils/dateConfig';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import styles from './Countdown.module.scss';
|
import styles from './TimerDisplay.module.scss';
|
||||||
|
|
||||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
|
||||||
// prepare display string
|
// prepare display string
|
||||||
const display =
|
const display =
|
||||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||||
@@ -19,9 +19,9 @@ const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Countdown);
|
export default memo(TimerDisplay);
|
||||||
|
|
||||||
Countdown.propTypes = {
|
TimerDisplay.propTypes = {
|
||||||
time: PropTypes.number,
|
time: PropTypes.number,
|
||||||
small: PropTypes.bool,
|
small: PropTypes.bool,
|
||||||
isNegative: PropTypes.bool,
|
isNegative: PropTypes.bool,
|
||||||
@@ -6,6 +6,7 @@ const navigatorConstants = [
|
|||||||
{ url: '/lower', label: 'Lower Thirds' },
|
{ url: '/lower', label: 'Lower Thirds' },
|
||||||
{ url: '/pip', label: 'PiP' },
|
{ url: '/pip', label: 'PiP' },
|
||||||
{ url: '/studio', label: 'Studio Clock' },
|
{ url: '/studio', label: 'Studio Clock' },
|
||||||
|
{ url: '/countdown', label: 'Countdown' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default navigatorConstants;
|
export default navigatorConstants;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import PropTypes from 'prop-types';
|
|||||||
import style from './Empty.module.scss';
|
import style from './Empty.module.scss';
|
||||||
|
|
||||||
export default function Empty(props) {
|
export default function Empty(props) {
|
||||||
const { text, ...rest } = props;
|
const { text, dark, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<div className={style.emptyContainer} {...rest}>
|
<div className={`${style.emptyContainer} ${dark ? style.dark : ''}`} {...rest}>
|
||||||
<Emptyimage className={style.empty} />
|
<Emptyimage className={style.empty} />
|
||||||
<span className={style.text}>{text}</span>
|
<span className={style.text}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,4 +16,5 @@ export default function Empty(props) {
|
|||||||
|
|
||||||
Empty.propTypes = {
|
Empty.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
|
dark: PropTypes.bool,
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,26 @@
|
|||||||
|
|
||||||
.emptyContainer {
|
.emptyContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
color: $bg-black-300;
|
color: $bg-black-300;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 2em;
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background: $bg-black;
|
||||||
|
color: $bg-gray-500;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ describe('test string from formatDisplay function', () => {
|
|||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('test with not numbers', () => {
|
||||||
|
const t = { val: 'test', result: '00:00:00' };
|
||||||
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
|
});
|
||||||
|
|
||||||
it('test with valid millis', () => {
|
it('test with valid millis', () => {
|
||||||
const t = { val: 3600, result: '01:00:00' };
|
const t = { val: 3600, result: '01:00:00' };
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||||
@@ -59,7 +64,7 @@ describe('test formatDisplay handles partial secs', () => {
|
|||||||
|
|
||||||
describe('test string from formatDisplay function with hidezero', () => {
|
describe('test string from formatDisplay function with hidezero', () => {
|
||||||
it('test with null values', () => {
|
it('test with null values', () => {
|
||||||
const t = { val: null, result: '00:00' };
|
const t = { val: null, result: '00:00:00' };
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import getDelayTo from '../getDelayTo';
|
||||||
|
|
||||||
|
describe('getDelayTo function', () => {
|
||||||
|
it('handles list with delays', () => {
|
||||||
|
const delayDuration = 100;
|
||||||
|
const events = [
|
||||||
|
{ type: 'event' },
|
||||||
|
{ type: 'delay', duration: delayDuration },
|
||||||
|
{ type: 'event' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const notDelayed = getDelayTo(events, 0);
|
||||||
|
expect(notDelayed).toBe(0);
|
||||||
|
const delayedEvent = getDelayTo(events, 2);
|
||||||
|
expect(delayedEvent).toBe(delayDuration);
|
||||||
|
});
|
||||||
|
it('handles list without delays', () => {
|
||||||
|
const events = [{ type: 'event' }, { type: 'event' }];
|
||||||
|
const notDelayed = getDelayTo(events, 1);
|
||||||
|
expect(notDelayed).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles list with multiple delays', () => {
|
||||||
|
const delayDuration = 100;
|
||||||
|
const events = [
|
||||||
|
{ type: 'event' },
|
||||||
|
{ type: 'delay', duration: delayDuration },
|
||||||
|
{ type: 'event' },
|
||||||
|
{ type: 'delay', duration: delayDuration },
|
||||||
|
{ type: 'event' },
|
||||||
|
];
|
||||||
|
const doubleDelay = getDelayTo(events, 4);
|
||||||
|
expect(doubleDelay).toBe(delayDuration * 2);
|
||||||
|
});
|
||||||
|
it('handles list with blocks', () => {
|
||||||
|
const events = [
|
||||||
|
{ type: 'event' },
|
||||||
|
{ type: 'delay', duration: 100 },
|
||||||
|
{ type: 'event' },
|
||||||
|
{ type: 'block' },
|
||||||
|
{ type: 'event' },
|
||||||
|
];
|
||||||
|
const notDelayed = getDelayTo(events, 4);
|
||||||
|
expect(notDelayed).toBe(0);
|
||||||
|
});
|
||||||
|
it('handles index greater than list', () => {
|
||||||
|
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||||
|
const notDelayed = getDelayTo(events, 3);
|
||||||
|
expect(notDelayed).toBe(0);
|
||||||
|
});
|
||||||
|
it('handles negative index (not found)', () => {
|
||||||
|
const events = [{ type: 'event' }, { type: 'delay', duration: 100 }, { type: 'event' }];
|
||||||
|
const notDelayed = getDelayTo(events, -1);
|
||||||
|
expect(notDelayed).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,10 @@ const mth = 1000 * 60 * 60; // millis to hours
|
|||||||
* @returns {string} String representing absolute time 00:12:02
|
* @returns {string} String representing absolute time 00:12:02
|
||||||
*/
|
*/
|
||||||
export function formatDisplay(seconds, hideZero = false) {
|
export function formatDisplay(seconds, hideZero = false) {
|
||||||
|
if (typeof seconds !== 'number') {
|
||||||
|
return '00:00:00';
|
||||||
|
}
|
||||||
|
|
||||||
// add an extra 0 if necessary
|
// add an extra 0 if necessary
|
||||||
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @description calculates delay to a given event
|
||||||
|
* @param {array} events
|
||||||
|
* @param {number} eventIndex
|
||||||
|
* @return {number} - delay value of given event
|
||||||
|
*/
|
||||||
|
export default function getDelayTo(events, eventIndex) {
|
||||||
|
let delay = 0;
|
||||||
|
let index = 0;
|
||||||
|
if (eventIndex >= 0) {
|
||||||
|
for (const event of events) {
|
||||||
|
if (eventIndex === index) {
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'delay') {
|
||||||
|
delay += event.duration;
|
||||||
|
} else if (event.type === 'block') {
|
||||||
|
delay = 0;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { Button } from '@chakra-ui/button';
|
import { Button } from '@chakra-ui/button';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import Countdown from 'common/components/countdown/Countdown';
|
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { stringFromMillis } from '../../../common/utils/time';
|
import { stringFromMillis } from '../../../common/utils/time';
|
||||||
@@ -45,7 +45,7 @@ const PlaybackTimer = (props) => {
|
|||||||
<div className={style.indDelay} />
|
<div className={style.indDelay} />
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timer}>
|
<div className={style.timer}>
|
||||||
<Countdown
|
<TimerDisplay
|
||||||
time={isWaiting ? timer.secondary : timer.running}
|
time={isWaiting ? timer.secondary : timer.running}
|
||||||
isNegative={timer.isNegative}
|
isNegative={timer.isNegative}
|
||||||
small
|
small
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ const withSocket = (Component) => {
|
|||||||
...timer,
|
...timer,
|
||||||
finished: playback === 'start' && timer.isNegative && timer.startedAt,
|
finished: playback === 'start' && timer.isNegative && timer.startedAt,
|
||||||
clock: stringFromMillis(timer.clock),
|
clock: stringFromMillis(timer.clock),
|
||||||
|
clockMs: timer.clock,
|
||||||
clockNoSeconds: stringFromMillis(timer.clock, false),
|
clockNoSeconds: stringFromMillis(timer.clock, false),
|
||||||
playstate: playback,
|
playstate: playback,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,126 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||||
|
import Empty from '../../../common/state/Empty';
|
||||||
|
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||||
|
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||||
|
import { stringFromMillis } from '../../../common/utils/time';
|
||||||
|
|
||||||
|
import { fetchTimerData, sanitiseTitle } from './countdown.helpers';
|
||||||
|
|
||||||
|
import style from './Countdown.module.scss';
|
||||||
|
|
||||||
export default function Countdown(props) {
|
export default function Countdown(props) {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { backstageEvents, time, selectedId } = props;
|
||||||
|
const [follow, setFollow] = useState(null);
|
||||||
|
const [runningTimer, setRunningTimer] = useState(0);
|
||||||
|
const [runningMessage, setRunningMessage] = useState('');
|
||||||
|
const [delay, setDelay] = useState(0);
|
||||||
|
|
||||||
|
// Set window title
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'ontime - Countdown';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// eg. http://localhost:4001/countdown?eventId=ei0us
|
||||||
|
// Check for user options
|
||||||
|
useEffect(() => {
|
||||||
|
if (!backstageEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = searchParams.get('eventid');
|
||||||
|
const eventIndex = searchParams.get('event');
|
||||||
|
|
||||||
|
let followThis = undefined;
|
||||||
|
const events = [...backstageEvents].filter((event) => event.type === 'event');
|
||||||
|
|
||||||
|
if (eventId !== null) {
|
||||||
|
followThis = events.find((event) => event.id === eventId);
|
||||||
|
} else if (eventIndex !== null) {
|
||||||
|
followThis = events?.[eventIndex - 1];
|
||||||
|
}
|
||||||
|
if (typeof followThis !== 'undefined') {
|
||||||
|
setFollow(followThis);
|
||||||
|
const idx = backstageEvents.findIndex((event) => event.id === followThis.id);
|
||||||
|
const delay = getDelayTo(backstageEvents, idx);
|
||||||
|
setDelay(delay);
|
||||||
|
}
|
||||||
|
}, [backstageEvents, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!follow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, selectedId);
|
||||||
|
setRunningMessage(message);
|
||||||
|
setRunningTimer(timer);
|
||||||
|
}, [follow, selectedId, time]);
|
||||||
|
|
||||||
|
const standby = time.playstate !== 'start' && selectedId === follow?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={style.container}>
|
||||||
Countdown
|
<NavLogo />
|
||||||
|
{follow === null ? (
|
||||||
|
<div className={style.eventSelect}>
|
||||||
|
<span className={style.actionTitle}>Select an event to follow</span>
|
||||||
|
<ul className={style.events}>
|
||||||
|
{backstageEvents.length === 0 ? (
|
||||||
|
<Empty dark text='No events in database' />
|
||||||
|
) : (
|
||||||
|
backstageEvents
|
||||||
|
.filter((e) => e.type === 'event')
|
||||||
|
.map((event, index) => (
|
||||||
|
<li key={event.id}>
|
||||||
|
<Link to={`/countdown?eventid=${event.id}`}>
|
||||||
|
{`${index + 1}. ${sanitiseTitle(event.title)}`}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={style.countdownContainer}>
|
||||||
|
<div className={style.timers}>
|
||||||
|
<div className={style.timer}>
|
||||||
|
<div className={style.label}>Time Now</div>
|
||||||
|
<span className={style.value}>{time.clock}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style.timer}>
|
||||||
|
<div className={style.label}>Start Time</div>
|
||||||
|
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
|
||||||
|
{stringFromMillis(follow.timeStart + delay)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style.timer}>
|
||||||
|
<div className={style.label}>End Time</div>
|
||||||
|
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
|
||||||
|
{stringFromMillis(follow.timeEnd + delay)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.status}>{runningMessage}</div>
|
||||||
|
<span className={`${style.countdownClock} ${standby ? style.standby : ''}`}>
|
||||||
|
{formatDisplay(
|
||||||
|
time.running ? runningTimer : runningTimer + millisToSeconds(delay),
|
||||||
|
time.running || time.waiting
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className={style.title}>{follow.title || 'Untitled Event'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Countdown.propTypes={
|
Countdown.propTypes = {
|
||||||
|
backstageEvents: PropTypes.array,
|
||||||
}
|
time: PropTypes.object,
|
||||||
|
selectedId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
@use '../../../styles/main' as *;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box; /* reset */
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%; /* restrict the page width to viewport */
|
||||||
|
|
||||||
|
background: $bg-black;
|
||||||
|
height: 100vh;
|
||||||
|
color: $title-white;
|
||||||
|
padding: 1vw;
|
||||||
|
|
||||||
|
.eventSelect {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8vh;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: max(1.3vw, 12px);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.actionTitle {
|
||||||
|
font-size: max(2vw, 14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events {
|
||||||
|
margin-top: 1em;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 70vh;
|
||||||
|
width: 60vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdownContainer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto auto auto 1fr auto;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-areas:
|
||||||
|
'title'
|
||||||
|
'status'
|
||||||
|
'clock'
|
||||||
|
'y'
|
||||||
|
'timers';
|
||||||
|
gap: 1vw;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.timers {
|
||||||
|
grid-area: timers;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: max(1.3vw, 12px);
|
||||||
|
color: $ontime-pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: max(2vw, 14px);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: #ddd;
|
||||||
|
|
||||||
|
&.delayed {
|
||||||
|
color: $block-delay-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-area: title;
|
||||||
|
font-size: max(4vw, 18px);
|
||||||
|
align-self: center;
|
||||||
|
color: $ontime-pink;
|
||||||
|
background-color: $bg-gray-1000;
|
||||||
|
padding: 1vh 2vw;
|
||||||
|
border-radius: 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
grid-area: status;
|
||||||
|
padding-left: 5vw;
|
||||||
|
font-size: max(2.5vw, 14px);
|
||||||
|
justify-self: flex-start;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdownClock {
|
||||||
|
grid-area: clock;
|
||||||
|
line-height: 20vw;
|
||||||
|
font-size: 21vw;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.4vw;
|
||||||
|
align-self: flex-start;
|
||||||
|
opacity: 1.0;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
&.standby {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { DAY_TO_MS } from '../../../../../../server/src/classes/classUtils';
|
||||||
|
import { millisToSeconds } from '../../../../common/utils/dateConfig';
|
||||||
|
import { fetchTimerData, sanitiseTitle, timerMessages } from '../countdown.helpers';
|
||||||
|
|
||||||
|
describe('sanitiseTitle() function', () => {
|
||||||
|
test('should return a title when valid', () => {
|
||||||
|
const validTitles = ['Test', 'test', 'test000', '...', 'test0999', 'test%&'];
|
||||||
|
|
||||||
|
for (const title of validTitles) {
|
||||||
|
expect(sanitiseTitle(title)).toBe(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return {no title} when invalid', () => {
|
||||||
|
const invalidTitles = ['', undefined, null];
|
||||||
|
for (const title of invalidTitles) {
|
||||||
|
expect(sanitiseTitle(title)).toBe('{no title}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchTimerData() function', () => {
|
||||||
|
it('shows running timer if current is the one we follow', () => {
|
||||||
|
const followId = 'testId';
|
||||||
|
const runningMockValue = 13;
|
||||||
|
const follow = { id: followId };
|
||||||
|
const time = { running: runningMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, followId);
|
||||||
|
expect(message).toBe(timerMessages.running);
|
||||||
|
expect(timer).toBe(runningMockValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the countdown to an upcoming event', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const timeNow = 1000;
|
||||||
|
const follow = { id: 'anotherevent', timeStart: startMockValue };
|
||||||
|
const time = { clockMs: timeNow };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||||
|
expect(message).toBe(timerMessages.toStart);
|
||||||
|
expect(timer).toBe(millisToSeconds(startMockValue - timeNow));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the timer of a scheduled event that hasnt started', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const endMockValue = 20000;
|
||||||
|
const timeNow = 15000;
|
||||||
|
const followId = 'testId';
|
||||||
|
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||||
|
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||||
|
expect(message).toBe(timerMessages.waiting);
|
||||||
|
expect(timer).toBe(endMockValue - startMockValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the end time of a finished event', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const endMockValue = 20000;
|
||||||
|
const timeNow = 30000;
|
||||||
|
const followId = 'testId';
|
||||||
|
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||||
|
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||||
|
expect(message).toBe(timerMessages.ended);
|
||||||
|
expect(timer).toBe(millisToSeconds(endMockValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle an idle event that finishes after midnight', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const endMockValue = 1000;
|
||||||
|
const timeNow = 15000;
|
||||||
|
const followId = 'testId';
|
||||||
|
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||||
|
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||||
|
expect(message).toBe(timerMessages.waiting);
|
||||||
|
expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle an running event that finishes after midnight', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const endMockValue = 1000;
|
||||||
|
const timeNow = 15000;
|
||||||
|
const followId = 'testId';
|
||||||
|
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||||
|
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, followId);
|
||||||
|
expect(message).toBe(timerMessages.running);
|
||||||
|
expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle an event that finishes after midnight but hasnt started', () => {
|
||||||
|
const startMockValue = 10000;
|
||||||
|
const endMockValue = 1000;
|
||||||
|
const timeNow = 2000;
|
||||||
|
const followId = 'testId';
|
||||||
|
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||||
|
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||||
|
|
||||||
|
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||||
|
expect(message).toBe(timerMessages.toStart);
|
||||||
|
expect(timer).toBe(millisToSeconds(startMockValue - timeNow));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { millisToSeconds } from '../../../common/utils/dateConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description parses string as a title
|
||||||
|
* @param {string|null} title
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const sanitiseTitle = (title) =>
|
||||||
|
title === null || title === '' || typeof title === 'undefined' ? '{no title}' : title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description object with possible timer messages
|
||||||
|
* @type {{running: string, toStart: string, waiting: string, ended: string}}
|
||||||
|
*/
|
||||||
|
export const timerMessages = {
|
||||||
|
toStart: 'Time to start',
|
||||||
|
waiting: 'Waiting for event start',
|
||||||
|
running: 'Event running',
|
||||||
|
ended: 'Event ended at',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns a parsed timer and relevant status message
|
||||||
|
* @param {object} time
|
||||||
|
* @param {object} follow
|
||||||
|
* @param {string} selectedId
|
||||||
|
* @return {{timer: number, message: string}}
|
||||||
|
*/
|
||||||
|
export const fetchTimerData = (time, follow, selectedId) => {
|
||||||
|
let message = "";
|
||||||
|
let timer = 0;
|
||||||
|
|
||||||
|
if (selectedId === follow.id) {
|
||||||
|
// check that is not running
|
||||||
|
message = time.playstate === 'pause' ? timerMessages.waiting : timerMessages.running;
|
||||||
|
timer = time.running;
|
||||||
|
} else if (time.clockMs < follow.timeStart) {
|
||||||
|
// if it hasnt started, we count to start
|
||||||
|
message = timerMessages.toStart;
|
||||||
|
timer = millisToSeconds(follow.timeStart - time.clockMs);
|
||||||
|
} else if (follow.timeStart <= time.clockMs && time.clockMs <= follow.timeEnd) {
|
||||||
|
// if it has started, we show running timer
|
||||||
|
message = timerMessages.waiting;
|
||||||
|
timer = time.running;
|
||||||
|
} else {
|
||||||
|
if (follow.timeStart > follow.timeEnd) {
|
||||||
|
// ends day after
|
||||||
|
if (follow.timeStart > time.clockMs ) {
|
||||||
|
// if it hasnt started, we count to start
|
||||||
|
message = timerMessages.toStart;
|
||||||
|
timer = millisToSeconds(follow.timeStart - time.clockMs);
|
||||||
|
} else if (follow.timeStart <= time.clockMs) {
|
||||||
|
// if it has started, we show running timer
|
||||||
|
message = timerMessages.waiting;
|
||||||
|
timer = time.running;
|
||||||
|
} else {
|
||||||
|
// if it has ended, we show how long ago
|
||||||
|
message = timerMessages.ended;
|
||||||
|
timer = millisToSeconds(follow.timeEnd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if it has ended, we show how long ago
|
||||||
|
message = timerMessages.ended;
|
||||||
|
timer = millisToSeconds(follow.timeEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { message, timer };
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import Countdown from 'common/components/countdown/Countdown';
|
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||||
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||||
import NavLogo from 'common/components/nav/NavLogo';
|
import NavLogo from 'common/components/nav/NavLogo';
|
||||||
import TitleCard from 'common/components/views/TitleCard';
|
import TitleCard from 'common/components/views/TitleCard';
|
||||||
@@ -39,7 +39,7 @@ export default function Timer(props) {
|
|||||||
// show timer if end message is empty
|
// show timer if end message is empty
|
||||||
const endMessage =
|
const endMessage =
|
||||||
general.endMessage == null || general.endMessage === '' ? (
|
general.endMessage == null || general.endMessage === '' ? (
|
||||||
<Countdown
|
<TimerDisplay
|
||||||
time={time.running}
|
time={time.running}
|
||||||
isNegative={time.isNegative}
|
isNegative={time.isNegative}
|
||||||
hideZeroHours
|
hideZeroHours
|
||||||
@@ -90,7 +90,7 @@ export default function Timer(props) {
|
|||||||
<div className={style.finished}>{endMessage}</div>
|
<div className={style.finished}>{endMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
||||||
<Countdown time={normalisedTime} hideZeroHours />
|
<TimerDisplay time={normalisedTime} hideZeroHours />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ describe('validate routes', () => {
|
|||||||
|
|
||||||
cy.visit('http://localhost:4001/studio');
|
cy.visit('http://localhost:4001/studio');
|
||||||
cy.contains('ON AIR');
|
cy.contains('ON AIR');
|
||||||
|
|
||||||
|
cy.visit('http://localhost:4001/countdown');
|
||||||
|
cy.contains('Select an event to follow');
|
||||||
|
|
||||||
|
cy.visit('http://localhost:4001/countdown?event=1');
|
||||||
|
cy.contains('Time Now');
|
||||||
|
cy.contains('Start Time');
|
||||||
|
cy.contains('End Time');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('editor routes', () => {
|
it('editor routes', () => {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
Reference in New Issue
Block a user