v2: views (#278)

* config(sentry): instrumentation only in production

* chore: upgrade deps

* refactor: simplify and convert to ts

* refactor: composition and styles

* chore: remove deprecated deps

* ux: close menu on click outside

* style: small tweaks and clarifications

* refactor: retire PiP view

* ux: small tweaks

* style: cleanup deprecated styles

* chore: remove unused

* refactor: remove usages of deprecated fields and improve null timer presentation

* refactor: improve composition of countdown

* style: cleanup overridable styles

* ux: show event overtime status

* style: correct logic in paused timers
This commit is contained in:
Carlos Valente
2022-12-30 11:41:03 +01:00
committed by GitHub
parent 110d3fb7a3
commit 832c060940
71 changed files with 3114 additions and 3657 deletions
@@ -5,25 +5,27 @@
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
background: var(--background-color-override, $viewer-background-color);
height: 100vh;
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
padding: 1vw;
/* =================== MAIN - SELECT ===================*/
.event-select {
display: flex;
margin-top: 8vh;
align-items: center;
justify-content: center;
font-size: max(1.3vw, 12px);
flex-direction: column;
&__title {
font-size: max(2vw, 14px);
font-size: clamp(24px, 2vw, 32px);
}
&__events {
font-size: clamp(16px, 1.5vw, 24px);
margin-top: 1em;
overflow-y: auto;
height: 70vh;
@@ -31,21 +33,88 @@
}
}
/* =================== MAIN - EVENT CONTAINER ===================*/
.countdown-container {
height: 100%;
width: 100%;
gap: min(2vh, 16px);
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
display: grid;
grid-template-rows: 1fr auto auto auto 1fr auto;
grid-template-rows: auto auto auto auto 1fr;
grid-template-columns: 100%;
grid-template-areas:
'title'
'header'
'status'
'clock'
'y'
'title'
'timers';
gap: 1vw;
justify-content: center;
text-align: center;
/* =================== HEADER + EXTRAS ===================*/
.clock-container {
grid-area: header;
margin-left: auto;
font-weight: 600;
.label {
font-size: clamp(16px, 1.5vw, 24px);
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
.time {
font-size: clamp(32px, 3.5vw, 50px);
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
}
}
.status {
grid-area: status;
color: var(--label-color-override, $viewer-label-color);
font-size: clamp(32px, 3.5vw, 50px);
font-weight: 600;
}
/* =================== TIMER + TITLE ===================*/
.timer {
grid-area: clock;
font-family: var(--font-family-override, $viewer-font-family);
color: var(--timer-color-override, $timer-color);
font-size: 15vw;
line-height: 0.9em;
text-align: center;
letter-spacing: 0.05em;
font-weight: 600;
opacity: 1;
&--paused {
opacity: $viewer-opacity-disabled;
}
&--finished {
color: $timer-finished-color;
}
}
.title {
grid-area: title;
background-color: var(--card-background-color-override, $viewer-card-bg-color);
padding: 16px 24px;
border-radius: 8px;
font-weight: 600;
font-size: clamp(40px, 4.5vw, 80px);
color: var(--accent-color-override, $accent-color);
line-height: 1.1em;
text-align: center;
}
/* =================== FOOTER TIMERS ===================*/
.timer-group {
grid-area: timers;
@@ -55,16 +124,17 @@
.aux-timers {
text-align: center;
font-size: clamp(24px, 1.75vw, 32px);
&__label {
font-size: max(1.3vw, 12px);
color: var(--accent-color-override, $accent-color);
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
&__value {
font-family: 'Open Sans', sans-serif;
font-size: max(2vw, 14px);
letter-spacing: 0.3px;
font-size: clamp(32px, 3.5vw, 50px);
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
&--delayed {
color: $delay-color;
@@ -72,42 +142,5 @@
}
}
}
.title {
grid-area: title;
font-size: max(4vw, 18px);
align-self: center;
color: var(--accent-color-override, $accent-color);
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 2vw;
border-radius: $element-border-radius;
}
.status {
grid-area: status;
padding-left: 5vw;
font-size: max(2.5vw, 14px);
justify-self: flex-start;
align-self: flex-end;
}
.timer {
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;
color: var(--timer-color-override, $viewer-color);
&--paused {
opacity: 0.6;
}
&--finished {
color: $timer-finished-color;
}
}
}
}
@@ -1,18 +1,19 @@
import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import Empty from '../../../common/components/state/Empty';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from '../../../common/models/EventTypes';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import getDelayTo from '../../../common/utils/getDelayTo';
import { formatTime } from '../../../common/utils/time';
import { fetchTimerData, sanitiseTitle, timerMessages } from './countdown.helpers';
import { fetchTimerData, TimerMessage } from './countdown.helpers';
import CountdownSelect from './CountdownSelect';
import './Countdown.scss';
@@ -21,15 +22,28 @@ const formatOptions = {
format: 'hh:mm:ss a',
};
const formatOptionsFinished = {
showSeconds: false,
format: 'hh:mm a',
};
Countdown.propTypes = {
backstageEvents: PropTypes.array,
time: PropTypes.object,
selectedId: PropTypes.string,
viewSettings: PropTypes.object,
};
// @ts-expect-error we are unable to type this just yet
export default function Countdown(props) {
const { backstageEvents, time, selectedId, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
const [follow, setFollow] = useState(null);
const [follow, setFollow] = useState<OntimeEvent | null>(null);
const [runningTimer, setRunningTimer] = useState(0);
const [runningMessage, setRunningMessage] = useState('');
const [runningMessage, setRunningMessage] = useState<TimerMessage>(TimerMessage.unhandled);
const [delay, setDelay] = useState(0);
useEffect(() => {
@@ -46,17 +60,17 @@ export default function Countdown(props) {
const eventId = searchParams.get('eventid');
const eventIndex = searchParams.get('event');
let followThis;
const events = [...backstageEvents].filter((event) => event.type === 'event');
let followThis: OntimeEvent | null = null;
const events: OntimeEvent[] = [...backstageEvents].filter((event) => event.type === SupportedEvent.Event);
if (eventId !== null) {
followThis = events.find((event) => event.id === eventId);
followThis = events.find((event) => event.id === eventId) || null;
} else if (eventIndex !== null) {
followThis = events?.[eventIndex - 1];
followThis = events?.[Number(eventIndex) - 1];
}
if (typeof followThis !== 'undefined') {
if (followThis !== null) {
setFollow(followThis);
const idx = backstageEvents.findIndex((event) => event.id === followThis.id);
const idx: number = backstageEvents.findIndex((event: OntimeRundownEntry) => event.id === followThis?.id);
const delayToEvent = getDelayTo(backstageEvents, idx);
setDelay(delayToEvent);
}
@@ -77,9 +91,9 @@ export default function Countdown(props) {
return null;
}
const standby = time.playback !== 'start' && selectedId === follow?.id;
const isRunningFinished = time.finished && runningMessage === timerMessages.running;
const isSelected = runningMessage === timerMessages.running;
const standby = time.playback !== 'play' && selectedId === follow?.id;
const isRunningFinished = time.finished && runningMessage === TimerMessage.running;
const isSelected = runningMessage === TimerMessage.running;
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
const clock = formatTime(time.clock, formatOptions);
@@ -91,36 +105,38 @@ export default function Countdown(props) {
follow === null
? '...'
: formatTime(follow.timeEnd + delay, formatOptions);
const formattedTimer = runningMessage === TimerMessage.ended
? formatTime(runningTimer, formatOptionsFinished)
: formatDisplay(
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
isSelected || time.waiting,
);
return (
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
<NavigationMenu />
{follow === null ? (
<div className='event-select' data-testid='countdown-select'>
<span className='event-select__title'>Select an event to follow</span>
<ul className='event-select__events'>
{backstageEvents.length === 0 ? (
<Empty 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>
<CountdownSelect events={backstageEvents} />
) : (
<div className='countdown-container' data-testid='countdown-event'>
<div className='clock-container'>
<div className='label'>Time Now</div>
<div className='time'>{clock}</div>
</div>
<div className='status'>{runningMessage}</div>
<span
className={`timer ${standby ? 'timer--paused' : ''} ${
isRunningFinished ? 'timer--finished' : ''
}`}
>
{formattedTimer}
</span>
<div className='title'>{follow?.title || 'Untitled Event'}</div>
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>Time Now</div>
<span className='aux-timers__value'>{clock}</span>
</div>
<div className='aux-timers'>
<div className='aux-timers__label'>Start Time</div>
<span className={`aux-timers__value ${delayedTimerStyles}`}>
@@ -134,27 +150,9 @@ export default function Countdown(props) {
</span>
</div>
</div>
<div className='status'>{runningMessage}</div>
<span
className={`timer ${standby ? 'timer--paused' : ''} ${
isRunningFinished ? 'timer--finished' : ''
}`}
>
{formatDisplay(
isSelected ? runningTimer : runningTimer + millisToSeconds(delay),
isSelected || time.waiting,
)}
</span>
<div className='title'>{follow?.title || 'Untitled Event'}</div>
</div>
)}
</div>
);
}
Countdown.propTypes = {
backstageEvents: PropTypes.array,
time: PropTypes.object,
selectedId: PropTypes.string,
viewSettings: PropTypes.object,
};
@@ -0,0 +1,46 @@
import { Link } from 'react-router-dom';
import Empty from '../../../common/components/state/Empty';
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from '../../../common/models/EventTypes';
import { formatTime } from '../../../common/utils/time';
import { sanitiseTitle } from './countdown.helpers';
import './Countdown.scss';
interface CountdownSelectProps {
events: OntimeRundownEntry[];
}
export default function CountdownSelect(props: CountdownSelectProps) {
const { events } = props;
const filteredEvents = events.filter((event: OntimeRundownEntry) => event.type === SupportedEvent.Event) as OntimeEvent[];
return (
<div className='event-select' data-testid='countdown-select'>
<span className='event-select__title'>Select an event to follow</span>
<ul className='event-select__events'>
{!events.length ? (
<Empty text='No events in database' />
) : (
filteredEvents.map((event: OntimeEvent, counter: number) => {
const index = counter + 1;
const title = sanitiseTitle(event.title);
const start = formatTime(event.timeStart, { format: 'hh:mm' });
const end = formatTime(event.timeEnd, { format: 'hh:mm' });
return (
<li key={event.id}>
<Link to={`/countdown?eventid=${event.id}`}>
{`${index}. ${start}${end} | ${title}`}
</Link>
</li>
);
},
)
)}
</ul>
</div>
);
}
@@ -1,68 +0,0 @@
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.playback === 'pause' ? timerMessages.waiting : timerMessages.running;
timer = time.running;
} else if (time.clock < follow.timeStart) {
// if it hasnt started, we count to start
message = timerMessages.toStart;
timer = millisToSeconds(follow.timeStart - time.clock);
} else if (follow.timeStart <= time.clock && time.clock <= 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.clock ) {
// if it hasnt started, we count to start
message = timerMessages.toStart;
timer = millisToSeconds(follow.timeStart - time.clock);
} else if (follow.timeStart <= time.clock) {
// 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 };
};
@@ -0,0 +1,69 @@
import { OntimeEvent } from '../../../common/models/EventTypes';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
export enum TimerMessage {
toStart = 'Time to start',
waiting = 'Waiting for event start',
running = 'Event running',
ended = 'Event ended at',
unhandled = '',
}
/**
* Parses string as a title
*/
export const sanitiseTitle = (title: string | null) =>
title ? title : '{no title}';
/**
* Returns a parsed timer and relevant status message
*/
export const fetchTimerData = (time: TimeManagerType, follow: OntimeEvent, selectedId: string): { message: TimerMessage, timer: number } => {
let message;
let timer;
if (selectedId === follow.id) {
// check that is not running
message = time.playback === 'pause' ? TimerMessage.waiting : TimerMessage.running;
timer = time.current ?? 0;
} else if (time.clock < follow.timeStart) {
// if it hasnt started, we count to start
message = TimerMessage.toStart;
timer = follow.timeStart - time.clock;
} else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) {
// if it has started, we show running timer
message = TimerMessage.waiting;
timer = time.current ?? 0;
} else {
// running timer timer is not the one we are following
if (follow.timeStart > follow.timeEnd) {
// ends day after
if (follow.timeStart > time.clock) {
// if it hasnt started, we count to start
message = TimerMessage.toStart;
timer = follow.timeStart - time.clock;
} else if (follow.timeStart <= time.clock) {
// if it has started, we show running timer
message = TimerMessage.waiting;
timer = time.current ?? 0;
} else {
// if it has ended, we show how long ago
message = TimerMessage.ended;
timer = follow.timeEnd;
}
} else {
// if it has ended, we show how long ago
message = TimerMessage.ended;
timer = follow.timeEnd;
}
}
return { message, timer };
};