From f4c5a2bf87d6fc1706b42a184421bd5d16d0058e Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Thu, 30 Jun 2022 20:26:01 +0200 Subject: [PATCH] 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 --- README.md | 1 + client/package.json | 2 +- .../{Countdown.jsx => TimerDisplay.jsx} | 8 +- ...n.module.scss => TimerDisplay.module.scss} | 0 .../components/nav/navigatorConstants.js | 1 + client/src/common/state/Empty.jsx | 5 +- client/src/common/state/Empty.module.scss | 30 +++-- .../common/utils/__tests__/dateConfig.test.js | 7 +- .../common/utils/__tests__/getDelayTo.test.js | 56 ++++++++ client/src/common/utils/dateConfig.js | 4 + client/src/common/utils/getDelayTo.js | 25 ++++ .../control/playback/PlaybackTimer.jsx | 4 +- client/src/features/viewers/ViewWrapper.jsx | 1 + .../features/viewers/countdown/Countdown.jsx | 127 +++++++++++++++++- .../viewers/countdown/Countdown.module.scss | 110 +++++++++++++++ .../countdown/__tests__/Countdown.test.js | 109 +++++++++++++++ .../viewers/countdown/countdown.helpers.js | 68 ++++++++++ client/src/features/viewers/timer/Timer.jsx | 6 +- server/cypress/integration/navigation.spec.js | 8 ++ server/package.json | 2 +- 20 files changed, 543 insertions(+), 31 deletions(-) rename client/src/common/components/countdown/{Countdown.jsx => TimerDisplay.jsx} (76%) rename client/src/common/components/countdown/{Countdown.module.scss => TimerDisplay.module.scss} (100%) create mode 100644 client/src/common/utils/__tests__/getDelayTo.test.js create mode 100644 client/src/common/utils/getDelayTo.js create mode 100644 client/src/features/viewers/countdown/Countdown.module.scss create mode 100644 client/src/features/viewers/countdown/__tests__/Countdown.test.js create mode 100644 client/src/features/viewers/countdown/countdown.helpers.js diff --git a/README.md b/README.md index 0af9486c3..bb0809b92 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/) same as app) - [x] Multi platform (available on Windows, MacOS and Linux) - [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 diff --git a/client/package.json b/client/package.json index ad2931b49..59eb2ede3 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "1.1.3", + "version": "1.2.1", "private": true, "dependencies": { "@chakra-ui/react": "^2.0.0-next.3", diff --git a/client/src/common/components/countdown/Countdown.jsx b/client/src/common/components/countdown/TimerDisplay.jsx similarity index 76% rename from client/src/common/components/countdown/Countdown.jsx rename to client/src/common/components/countdown/TimerDisplay.jsx index 792401d3d..b7bd25a6e 100644 --- a/client/src/common/components/countdown/Countdown.jsx +++ b/client/src/common/components/countdown/TimerDisplay.jsx @@ -2,9 +2,9 @@ import React, { memo } from 'react'; import { formatDisplay } from 'common/utils/dateConfig'; 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 const display = 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, small: PropTypes.bool, isNegative: PropTypes.bool, diff --git a/client/src/common/components/countdown/Countdown.module.scss b/client/src/common/components/countdown/TimerDisplay.module.scss similarity index 100% rename from client/src/common/components/countdown/Countdown.module.scss rename to client/src/common/components/countdown/TimerDisplay.module.scss diff --git a/client/src/common/components/nav/navigatorConstants.js b/client/src/common/components/nav/navigatorConstants.js index 4a4ead315..8ececfeba 100644 --- a/client/src/common/components/nav/navigatorConstants.js +++ b/client/src/common/components/nav/navigatorConstants.js @@ -6,6 +6,7 @@ const navigatorConstants = [ { url: '/lower', label: 'Lower Thirds' }, { url: '/pip', label: 'PiP' }, { url: '/studio', label: 'Studio Clock' }, + { url: '/countdown', label: 'Countdown' }, ]; export default navigatorConstants; diff --git a/client/src/common/state/Empty.jsx b/client/src/common/state/Empty.jsx index 01407d640..ab9ab9902 100644 --- a/client/src/common/state/Empty.jsx +++ b/client/src/common/state/Empty.jsx @@ -5,9 +5,9 @@ import PropTypes from 'prop-types'; import style from './Empty.module.scss'; export default function Empty(props) { - const { text, ...rest } = props; + const { text, dark, ...rest } = props; return ( -
+
{text}
@@ -16,4 +16,5 @@ export default function Empty(props) { Empty.propTypes = { text: PropTypes.string, + dark: PropTypes.bool, } \ No newline at end of file diff --git a/client/src/common/state/Empty.module.scss b/client/src/common/state/Empty.module.scss index d4b7b0100..dec63be38 100644 --- a/client/src/common/state/Empty.module.scss +++ b/client/src/common/state/Empty.module.scss @@ -2,16 +2,26 @@ .emptyContainer { width: 100%; + height: 100%; text-align: center; -} - -.empty { - width: 100%; - opacity: 0.3; -} - -.text { 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; + } + } } diff --git a/client/src/common/utils/__tests__/dateConfig.test.js b/client/src/common/utils/__tests__/dateConfig.test.js index eb7bb937f..031205036 100644 --- a/client/src/common/utils/__tests__/dateConfig.test.js +++ b/client/src/common/utils/__tests__/dateConfig.test.js @@ -14,6 +14,11 @@ describe('test string from formatDisplay function', () => { 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', () => { const t = { val: 3600, result: '01:00:00' }; 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', () => { 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); }); diff --git a/client/src/common/utils/__tests__/getDelayTo.test.js b/client/src/common/utils/__tests__/getDelayTo.test.js new file mode 100644 index 000000000..8ba459818 --- /dev/null +++ b/client/src/common/utils/__tests__/getDelayTo.test.js @@ -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); + }); +}); diff --git a/client/src/common/utils/dateConfig.js b/client/src/common/utils/dateConfig.js index 3bdfece95..1f3527cdf 100644 --- a/client/src/common/utils/dateConfig.js +++ b/client/src/common/utils/dateConfig.js @@ -13,6 +13,10 @@ const mth = 1000 * 60 * 60; // millis to hours * @returns {string} String representing absolute time 00:12:02 */ export function formatDisplay(seconds, hideZero = false) { + if (typeof seconds !== 'number') { + return '00:00:00'; + } + // add an extra 0 if necessary const format = (val) => `0${Math.floor(val)}`.slice(-2); diff --git a/client/src/common/utils/getDelayTo.js b/client/src/common/utils/getDelayTo.js new file mode 100644 index 000000000..a6802a0d4 --- /dev/null +++ b/client/src/common/utils/getDelayTo.js @@ -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; +} diff --git a/client/src/features/control/playback/PlaybackTimer.jsx b/client/src/features/control/playback/PlaybackTimer.jsx index e4c9b41d7..0a36b80a5 100644 --- a/client/src/features/control/playback/PlaybackTimer.jsx +++ b/client/src/features/control/playback/PlaybackTimer.jsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import { Button } from '@chakra-ui/button'; 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 { stringFromMillis } from '../../../common/utils/time'; @@ -45,7 +45,7 @@ const PlaybackTimer = (props) => {
- { ...timer, finished: playback === 'start' && timer.isNegative && timer.startedAt, clock: stringFromMillis(timer.clock), + clockMs: timer.clock, clockNoSeconds: stringFromMillis(timer.clock, false), playstate: playback, }; diff --git a/client/src/features/viewers/countdown/Countdown.jsx b/client/src/features/viewers/countdown/Countdown.jsx index ac893d1f4..f37618ab7 100644 --- a/client/src/features/viewers/countdown/Countdown.jsx +++ b/client/src/features/viewers/countdown/Countdown.jsx @@ -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) { + 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 ( -
- Countdown +
+ + {follow === null ? ( +
+ Select an event to follow +
    + {backstageEvents.length === 0 ? ( + + ) : ( + backstageEvents + .filter((e) => e.type === 'event') + .map((event, index) => ( +
  • + + {`${index + 1}. ${sanitiseTitle(event.title)}`} + +
  • + )) + )} +
+
+ ) : ( +
+
+
+
Time Now
+ {time.clock} +
+
+
Start Time
+ 0 ? style.delayed : ''}`}> + {stringFromMillis(follow.timeStart + delay)} + +
+
+
End Time
+ 0 ? style.delayed : ''}`}> + {stringFromMillis(follow.timeEnd + delay)} + +
+
+
{runningMessage}
+ + {formatDisplay( + time.running ? runningTimer : runningTimer + millisToSeconds(delay), + time.running || time.waiting + )} + +
{follow.title || 'Untitled Event'}
+
+ )}
- ) + ); } -Countdown.propTypes={ - -} \ No newline at end of file +Countdown.propTypes = { + backstageEvents: PropTypes.array, + time: PropTypes.object, + selectedId: PropTypes.string, +}; diff --git a/client/src/features/viewers/countdown/Countdown.module.scss b/client/src/features/viewers/countdown/Countdown.module.scss new file mode 100644 index 000000000..21ef47223 --- /dev/null +++ b/client/src/features/viewers/countdown/Countdown.module.scss @@ -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; + } + } + } +} diff --git a/client/src/features/viewers/countdown/__tests__/Countdown.test.js b/client/src/features/viewers/countdown/__tests__/Countdown.test.js new file mode 100644 index 000000000..90110fab5 --- /dev/null +++ b/client/src/features/viewers/countdown/__tests__/Countdown.test.js @@ -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)); + }); +}); diff --git a/client/src/features/viewers/countdown/countdown.helpers.js b/client/src/features/viewers/countdown/countdown.helpers.js new file mode 100644 index 000000000..885636c34 --- /dev/null +++ b/client/src/features/viewers/countdown/countdown.helpers.js @@ -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 }; +}; diff --git a/client/src/features/viewers/timer/Timer.jsx b/client/src/features/viewers/timer/Timer.jsx index e185bf64d..da631e60c 100644 --- a/client/src/features/viewers/timer/Timer.jsx +++ b/client/src/features/viewers/timer/Timer.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; 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 NavLogo from 'common/components/nav/NavLogo'; import TitleCard from 'common/components/views/TitleCard'; @@ -39,7 +39,7 @@ export default function Timer(props) { // show timer if end message is empty const endMessage = general.endMessage == null || general.endMessage === '' ? ( - {endMessage}
) : (
- +
)}
diff --git a/server/cypress/integration/navigation.spec.js b/server/cypress/integration/navigation.spec.js index 3652c6854..c37c85c94 100644 --- a/server/cypress/integration/navigation.spec.js +++ b/server/cypress/integration/navigation.spec.js @@ -37,6 +37,14 @@ describe('validate routes', () => { cy.visit('http://localhost:4001/studio'); 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', () => { diff --git a/server/package.json b/server/package.json index 6a3c1c599..87d0b4eb2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "1.2.1", + "version": "1.3.0", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime",