V2 rundown manager (#237)

* refactor: migrate eventlist > rundown
This commit is contained in:
Carlos Valente
2022-10-30 21:07:18 +01:00
committed by GitHub
parent a3f14182a4
commit c03ed07762
55 changed files with 487 additions and 489 deletions
@@ -0,0 +1,211 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
.eventBlock {
// layout
display: grid;
grid-template-areas:
"binder pb-actions times actions"
"binder pb-actions title title"
"binder pb-actions notes status"
"binder progb progb progb";
grid-template-columns: $binder-width auto 1fr auto;
grid-template-rows: 36px 36px 36px $element-spacing;
align-items: center;
margin:4px 2px;
padding-right: $clearance;
// style - general
border-radius: $block-border-radius;
gap: 2px;
// style - colour
background-color: $bg-container-l2;
border: $border-l3;
// variant
&.selected {
background-color: $bg-container-l3;
}
&.skip {
background-color: $bg-gray-1000;
border: 1px solid transparent;
.eventTimers > .delayNote,
.eventTitle,
.eventNote {
color: $text-gray-disabled;
}
.binder,
.eventTimers,
.eventStatus {
opacity: $opacity-disabled;
}
}
.binder {
// layout
grid-area: binder;
height: 100%;
display: grid;
place-content: center;
position: relative;
cursor: pointer;
// style - general
border-radius: 3px 0 0 3px;
border-right: 1px solid $bg-gray-900;
// style - colour
background-color: $bg-container-l3; // override inline
color: $text-white;
.drag {
@include drag-style;
position: absolute;
}
}
.playbackActions {
// layout
grid-area: pb-actions;
display: flex;
flex-direction: column;
margin: $element-spacing $clearance $element-spacing $element-spacing;
// style - general
gap: 6px;
}
.eventTimers {
grid-area: times;
display: flex;
align-items: center;
gap: $clearance;
height: 100%;
.delayNote {
font-size: 11px;
color: $text-delay;
}
}
.eventTitle {
grid-area: title;
display: block;
font-size: 18px;
.eventTitle__preview {
width: 100%;
}
&.noTitle {
span {
opacity: $opacity-disabled;
}
}
input {
border-radius: 2px;
border: 1px;
}
}
.eventNote {
grid-area: notes;
display: block;
font-size: 14px;
color: $notes-color;
line-height: 15px;
}
.eventActions {
grid-area: actions;
display: flex;
gap: $element-spacing;
justify-content: flex-end;
}
.eventStatus {
grid-area: status;
display: flex;
justify-content: flex-end;
gap: $element-spacing;
.statusIcon {
width: 24px;
height: 24px;
border-radius: 12px;
padding: 4px;
color: $text-gray-disabled;
}
.statusIcon.statusNext {
border: 1px solid transparent;
&.enabled {
color: $text-white;
background-color: $ontime-accent;
}
}
.statusIcon.statusDelay {
&.enabled {
color: $text-white;
background-color: $ontime-delay;
}
}
.statusIcon.statusPublic {
&.enabled {
color: $text-white;
background-color: $ontime-roll;
}
}
}
.eventOptions {
margin: $element-spacing 16px $element-spacing 0;
}
}
.progressBg {
grid-area: progb;
border-radius: 1px;
background-color: $bg-container-l1;
margin-bottom: 4px;
margin-left: 4px;
// layout
height: 100%;
.progressBar {
// layout
height: 100%;
width: 0;
border-radius: 1px 0 0 1px;
// animations
transition: 1s linear;
transition-property: width;
&.start {
background-color: $ontime-accent;
}
&.pause {
background-color: $ontime-paused;
}
&.roll {
background-color: $ontime-roll;
}
&.overtime {
background-color: red;
}
}
}
@@ -0,0 +1,250 @@
import { useCallback, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayBackOutline } from '@react-icons/all-files/io5/IoPlayBackOutline';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoReturnDownForward } from '@react-icons/all-files/io5/IoReturnDownForward';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
import { getAccessibleColour } from 'common/utils/styleUtils';
import { useAtom } from 'jotai';
import { useEventProvider } from '../../../common/hooks/useSocketProvider';
import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry';
import EventBlockActionMenu from './composite/EventBlockActionMenu';
import EventBlockTimers from './composite/EventBlockTimers';
import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
colorScheme: 'white',
variant: 'ghost',
fontSize: '20px',
};
const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockProps {
timeStart: number;
timeEnd: number;
duration: number;
index: number;
eventIndex: number;
eventId: string;
isPublic: boolean;
title: string;
note: string;
delay: number;
previousEnd: number;
colour: string;
next: boolean;
skip: boolean;
selected: boolean;
playback?: Playstate;
actionHandler: (action: EventItemActions, payload: any) => void;
}
export default function EventBlock(props: EventBlockProps) {
const {
timeStart,
timeEnd,
duration,
index,
eventIndex,
eventId,
isPublic = true,
title,
note,
delay,
previousEnd,
colour,
next,
skip = false,
selected,
playback,
actionHandler,
} = props;
const [openId, setOpenId] = useAtom(editorEventId);
const { setPlayback } = useEventProvider(eventId);
const [blockTitle, setBlockTitle] = useState<string>(title || '');
const binderColours = colour && getAccessibleColour(colour);
const hasDelay = delay !== 0 && delay !== null;
const handleTitle = useCallback(
(text: string) => {
if (text === title) {
return;
}
const cleanVal = text.trim();
setBlockTitle(cleanVal);
// Todo: no need for action handler
actionHandler('update', { field: 'title', value: cleanVal });
},
[actionHandler, title],
);
// Todo: data should come from socket
const progress = `${Math.random() * 100}%`;
const eventIsPlaying = selected && playback === 'start';
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' };
} else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = { };
}
return (
<Draggable key={eventId} draggableId={eventId} index={index}>
{(provided) => (
<div
className={`${style.eventBlock} ${skip ? style.skip : ''} ${
selected ? style.selected : ''
}`}
{...provided.draggableProps}
ref={provided.innerRef}
>
<div
className={style.binder}
style={{ ...binderColours }}
tabIndex={-1}
onClick={() => actionHandler('set-cursor', index)}
>
<span className={style.drag} {...provided.dragHandleProps}>
<IoReorderTwo />
</span>
{eventIndex}
</div>
<div className={selected ? style.progressBg : ''}>
<div
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
style={{ width: progress }}
/>
</div>
<div className={style.playbackActions}>
<TooltipActionBtn
aria-label='Skip event'
tooltip='Skip event'
openDelay={tooltipDelayMid}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...blockBtnStyle}
variant={skip ? 'solid' : 'ghost'}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1}
disabled={selected}
/>
<TooltipActionBtn
aria-label='Load event'
tooltip='Load event'
openDelay={tooltipDelayMid}
icon={selected ? <IoPlayBackOutline /> : <IoReload />}
disabled={skip}
{...blockBtnStyle}
clickHandler={() => setPlayback.loadEvent()}
tabIndex={-1}
/>
<TooltipActionBtn
aria-label='Start event'
tooltip='Start event'
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip}
{...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'ghost'}
clickHandler={() => setPlayback.startEvent()}
backgroundColor={eventIsPlaying ? '#58A151' : undefined}
tabIndex={-1}
/>
</div>
<EventBlockTimers
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
delay={delay}
actionHandler={actionHandler}
previousEnd={previousEnd}
/>
<Editable
value={blockTitle}
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`}
placeholder='Event title'
onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)}
>
<EditablePreview className={style.eventTitle__preview} />
<EditableInput />
</Editable>
<span className={style.eventNote}>{note}</span>
<div className={style.eventActions}>
<TooltipActionBtn
{...blockBtnStyle}
size='sm'
icon={<IoOptions />}
clickHandler={() => setOpenId((prev) => prev === eventId ? null : eventId)}
tooltip='Event options'
aria-label='Event options'
tabIndex={-1}
backgroundColor={openId === eventId ? '#ebedf0' : 'transparent'}
color={openId === eventId ? '#333' : '#ebedf0'}
_hover={{ bg: '#ebedf0', color: '#333' }}
/>
<EventBlockActionMenu
showAdd
showDelay
showBlock
showClone
actionHandler={actionHandler}
/>
</div>
<div className={style.eventStatus}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`}
>
<IoReturnDownForward />
</span>
</Tooltip>
<Tooltip label='Event has delay' isDisabled={!hasDelay} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusDelay} ${
hasDelay ? style.enabled : ''
}`}
>
<IoTimerOutline />
</span>
</Tooltip>
<Tooltip
label={`${isPublic ? 'Event is public' : 'Event is private'}`}
{...tooltipProps}
>
<span
className={`${style.statusIcon} ${style.statusPublic} ${
isPublic ? style.enabled : ''
}`}
>
<FiUsers />
</span>
</Tooltip>
</div>
</div>
)}
</Draggable>
);
}
@@ -0,0 +1,87 @@
import {
Divider,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../../ontimeConfig';
export default function EventBlockActionMenu(props) {
const { showAdd, showDelay, showBlock, showClone, actionHandler } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const blockBtnStyle = {
size: 'sm',
variant: 'outline',
colorScheme: 'whiteAlpha',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton as={IconButton} aria-label='Options' icon={<FiPlus />}
tabIndex={-1} {...blockBtnStyle} />
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem
icon={<IoTimerOutline />}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
Add Delay after
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
{showClone && (
<MenuItem
icon={<IoDuplicateOutline />}
onClick={() => actionHandler('clone')}
isDisabled={!showBlock}
>
Clone event
</MenuItem>
)}
<Divider />
<MenuItem
icon={<FiTrash2 />}
onClick={() => actionHandler('delete')}
isDisabled={!showBlock}
color='red.500'
>
Delete event
</MenuItem>
</MenuList>
</Menu>
);
}
EventBlockActionMenu.propTypes = {
showAdd: PropTypes.bool,
showDelay: PropTypes.bool,
showBlock: PropTypes.bool,
showClone: PropTypes.bool,
actionHandler: PropTypes.func,
};
@@ -0,0 +1,89 @@
import { useCallback, useContext } from 'react';
import TimeInput from 'common/components/input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext';
import { millisToMinutes } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import { validateEntry } from 'common/utils/timesManager';
import PropTypes from 'prop-types';
import style from '../EventBlock.module.scss';
export default function EventBlockTimers(props) {
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
const newTime = stringFromMillis(timeStart + delay);
/**
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidation = useCallback(
(field, value) => {
const valid = validateEntry(field, value, timeStart, timeEnd);
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
},
[emitWarning, timeEnd, timeStart]
);
const handleSubmit = useCallback(
(field, value) => {
actionHandler('update', { field, value });
},
[actionHandler]
);
return (
<div className={style.eventTimers}>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeStart}
delay={delay}
placeholder='Start'
previousEnd={previousEnd}
/>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeEnd}
delay={delay}
placeholder='End'
previousEnd={previousEnd}
/>
<TimeInput
name='duration'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={duration}
delay={delay}
placeholder='Duration'
previousEnd={previousEnd}
/>
{delay !== 0 && delay !== null && (
<div className={style.delayNote}>
{`${delayTime} minutes`}
<br />
{`New start: ${newTime}`}
</div>
)}
</div>
);
}
EventBlockTimers.propTypes = {
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
delay: PropTypes.number,
actionHandler: PropTypes.func,
previousEnd: PropTypes.number,
};