node server + separate client folder

This commit is contained in:
cv
2021-04-05 22:33:31 +02:00
parent d0c8567022
commit c39f3f2dde
56 changed files with 724 additions and 6 deletions
@@ -0,0 +1,77 @@
import { Button } from '@chakra-ui/button';
import style from './PlaybackControl.module.css';
import Countdown from '../../common/components/countdown/Countdown';
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
// BUTTON DEFINITION
const defProps = {
colorScheme: 'blackAlpha',
variant: 'outline',
};
const bigSize = 120;
export default function PlaybackControl(props) {
const { time, roll } = props;
return (
<div className={style.mainContainer}>
<div className={style.timeContainer}>
<div className={style.timer}>
<Countdown time={time} small />
</div>
</div>
<div className={style.playbackContainer}>
<Button
width={bigSize}
colorScheme='green'
className={style.start}
disabled={roll}
onClick={() => props.playbackControl('play')}
>
Start
</Button>
<Button
width={bigSize}
colorScheme='orange'
className={style.pause}
disabled={roll}
onClick={() => props.playbackControl('pause')}
>
Pause
</Button>
</div>
<div className={style.trackContainer}>
<Button
width={bigSize}
{...defProps}
leftIcon={<ArrowBackIcon />}
className={style.prev}
disabled={roll}
onClick={() => props.playbackControl('previous')}
>
Prev
</Button>
<Button
width={bigSize}
{...defProps}
rightIcon={<ArrowForwardIcon />}
className={style.next}
disabled={roll}
onClick={() => props.playbackControl('next')}
>
Next
</Button>
<Button
width={bigSize}
colorScheme='blue'
className={style.reset}
onClick={() => props.playbackControl('roll')}
disabled
>
Roll
</Button>
</div>
</div>
);
}
@@ -0,0 +1,43 @@
.mainContainer {
width: 90%;
display: flex;
margin: 0 auto;
flex-direction: column;
gap: 5px;
}
.timeContainer {
display: grid;
gap: 5px;
grid-template-areas:
'timer plus1 plus5'
'timer minu1 minu5';
grid-template-columns: auto 50px 50px;
}
.timer {
grid-area: timer;
}
.plus1 {
grid-area: plus1;
}
.plus5 {
grid-area: plus5;
}
.minu1 {
grid-area: minu1;
}
.minu5 {
grid-area: minu5;
}
.playbackContainer,
.trackContainer {
display: flex;
justify-content: space-evenly;
padding-top: 0.5em;
}
+389
View File
@@ -0,0 +1,389 @@
import { IconButton } from '@chakra-ui/button';
import { SettingsIcon } from '@chakra-ui/icons';
import { Grid, GridItem, Heading } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/layout';
import { addSeconds, getHours, getMinutes, getSeconds } from 'date-fns';
import { differenceInSeconds } from 'date-fns/esm';
import { useContext } from 'react';
import { useEffect, useState } from 'react';
import { EventContext } from '../../app/context/eventContext';
import { EventListContext } from '../../app/context/eventListContext';
import NumberedText from '../../common/components/text/NumberedText';
import PlaybackControl from '../control/PlaybackControl';
import MessageForm from '../form/MessageForm';
import PreviewContainer from '../viewers/PreviewContainer';
import styles from './Editor.module.css';
import EventList from './list/EventList';
import { io } from 'socket.io-client';
export default function Editor() {
const [formMode, setFormMode] = useState(null);
const [events] = useContext(EventListContext);
const [event, setEvent] = useContext(EventContext);
const [playback, setPlayback] = useState({
current: null,
next: null,
currentTimer: null,
numEvents: 0,
state: 'pause',
prevState: 'pause',
});
const [response, setResponse] = useState('');
// WEBSOCKETZ
useEffect(() => {
// TODO: add namespace?
const socket = io('http://localhost:4001', { transport: ['websocket'] });
console.log('websocket started');
socket.on('FromAPI', (data) => {
setResponse(data);
console.log('websocket stuff', data);
});
return () => socket.disconnect();
}, []);
// Timer stuff
const [timer, setTimer] = useState({
TIMER_UPDATE_INTERVAL: 250,
currentTime: null,
currentTimeSeconds: null,
playMode: 'stop',
isStarted: false,
isRunning: false,
startTime: null,
pauseTime: null,
lastRun: null,
elapsedTime: 0,
elapsedStartedTime: 0,
elapsedRunningTime: 0,
totalElapsedPausedTime: 0,
periodElapsedPausedTime: 0,
elapsedResumeTime: 0,
targetTime: null,
});
// update timer object
const updateTimer = (vals) => {
setTimer({ ...timer, ...vals });
};
// set timer target
const setTimerTargetinSeconds = (target) => {
const t = addSeconds(new Date(), target);
updateTimer({ currentTime: t, currentTimeSeconds: target, targetTime: t });
};
// timer playback control
const setTimerState = (state) => {
const now = new Date();
switch (state) {
case 'play': {
updateTimer({
playMode: state,
isStarted: true,
startTime: now,
lastRun: now,
});
break;
}
case 'pause': {
updateTimer({ playMode: state, pauseTime: now, isRunning: false });
break;
}
default:
break;
}
};
// update
const updateTime = () => {
// exit if we are not ready
if (timer.startTime == null) return;
// aux
const now = new Date();
// how long has the time been running
const elapsedTime = now - timer.startTime;
if (timer.playMode === 'play') {
// current time here
const currentTime = timer.targetTime - elapsedTime;
updateTimer({
currentTime: currentTime,
currentTimeSeconds: getSeconds(currentTime),
elapsedTime: elapsedTime,
lastRun: now,
});
}
if (timer.playMode === 'pause') {
const pausedTime = now - timer.pauseTime;
updateTimer({
totalElapsedPausedTime: timer.totalElapsedPausedTime + pausedTime,
periodElapsedPausedTime: timer.periodElapsedPausedTime + now,
elapsedTime: elapsedTime,
lastRun: now,
});
}
// call again
setTimeout(updateTime, timer.TIMER_UPDATE_INTERVAL);
};
// when playmode changes, we might schedule a timer
// is this enough for change or should i check prevstate?
useEffect(() => {
if (playback.state !== 'stop' || playback.state !== null) {
updateTime();
}
}, [playback.state]);
const updatePlayback = (vals) => {
setPlayback({ ...playback, ...vals });
};
// gets timer on current
// ?? I have already done this a few times,
// maybe loop once and get all data?
const getCurrentTime = (target) => {
if (events !== null) {
// loop through events to find target
const filteredEvents = events.filter((e) => e.type === 'event');
const curEvent = filteredEvents[target];
// set as event
setEvent(curEvent);
// extract time only from dates
let minStart = getHours(curEvent.timeStart) * 60;
minStart = minStart + getMinutes(curEvent.timeStart);
let minEnd = getHours(curEvent.timeEnd) * 60;
minEnd = minEnd + getMinutes(curEvent.timeEnd);
// return time in seconds
return (minEnd - minStart) * 60;
}
};
const playbackControl = (action, payload) => {
switch (action) {
case 'play': {
if (playback.state !== 'play') {
updatePlayback({ state: 'play', prevState: playback.state });
setTimerState('play');
}
break;
}
case 'pause': {
if (playback.state !== 'pause') {
updatePlayback({ state: 'pause', prevState: playback.state });
setTimerState('pause');
}
break;
}
case 'roll': {
if (playback.state === 'roll' && playback.prevState !== 'roll') {
updatePlayback({ state: playback.prevState, prevState: 'roll' });
} else {
updatePlayback({ state: 'roll', prevState: playback.state });
}
break;
}
case 'previous': {
if (playback.numEvents !== null) {
let cur = null,
nxt = null;
if (playback.current === null || playback.current === 0) {
cur = 0;
} else {
cur = playback.current - 1;
}
if (playback.numEvents > 1) nxt = cur + 1;
if (nxt > playback.numEvents) nxt = playback.numEvents;
// get time
let time = getCurrentTime(cur);
// update playback
updatePlayback({ current: cur, next: nxt, currentTimer: time });
// set timer
setTimerTargetinSeconds(time);
}
break;
}
case 'next': {
if (playback.numEvents !== null) {
let cur = null,
nxt = null;
if (playback.current === null) {
cur = 0;
} else {
cur = playback.next;
}
if (playback.numEvents > 1) nxt = cur + 1;
if (nxt >= playback.numEvents) nxt = playback.numEvents - 1;
// get time
let time = getCurrentTime(cur);
// update playback
updatePlayback({ current: cur, next: nxt, currentTimer: time });
// set timer
setTimerTargetinSeconds(time);
}
break;
}
default:
break;
}
};
console.log('playback here', playback);
console.log('timer here', timer);
return (
<Grid
templateRows='1fr 1fr 1fr'
templateColumns='1fr 25vw 25vw 5vw'
gap={5}
className={styles.mainContainer}
>
<GridItem rowSpan={3}>
<Box className={styles.editor} borderRadius='0.5em'>
<Heading size='lg' style={{ paddingBottom: '0.25em' }}>
Event List
</Heading>
<NumberedText number={1} text={'Manage and select event to run'} />
<div className={styles.content}>
<EventList
formMode={formMode}
setFormMode={setFormMode}
selected={playback.current}
updatePlayback={updatePlayback}
/>
</div>
</Box>
</GridItem>
<GridItem colStart={2} rowStart={2} rowSpan={2} colSpan={2}>
<Box className={styles.editor} borderRadius='0.5em' overflowX='auto'>
<Heading size='lg' style={{ paddingBottom: '0.25em' }}>
Preview Displays
</Heading>
<NumberedText number={4} text={'Realtime screen preview'} />
<div className={styles.content}>
<PreviewContainer />
</div>
</Box>
</GridItem>
<GridItem colStart={2} rowStart={1} rowSpan={1} colSpan={1}>
<Box className={styles.editor} borderRadius='0.5em'>
<Heading size='lg' style={{ paddingBottom: '0.25em' }}>
Display Messages
</Heading>
<NumberedText
number={2}
text={'Show realtime messages on separate screen types'}
/>
<div className={styles.content}>
<MessageForm />
</div>
</Box>
</GridItem>
<GridItem colStart={3} rowStart={1}>
<Box className={styles.editor} borderRadius='0.5em'>
<Heading size='lg' style={{ paddingBottom: '0.25em' }}>
Time Control
</Heading>
<NumberedText number={3} text={'Control Timer'} />
<div className={styles.content}>
<PlaybackControl
playback={playback}
playbackControl={playbackControl}
time={timer.currentTimeSeconds}
roll={playback.state === 'roll'}
/>
</div>
</Box>
</GridItem>
<GridItem colStart={4} rowSpan={3}>
<Box className={styles.editor} borderRadius='0.5em'>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2em',
paddingTop: '3em',
}}
>
<IconButton icon={<SettingsIcon />} isRound variant='outline' />
<div
style={{
width: 35,
height: 35,
backgroundColor: '#3b8cd8',
borderRadius: '50%',
}}
/>
<div
style={{
width: 35,
height: 35,
backgroundColor: '#3182ce',
borderRadius: '50%',
}}
/>
<div
style={{
width: 35,
height: 35,
backgroundColor: '#2778c4',
borderRadius: '50%',
}}
/>
<div
style={{
width: 35,
height: 35,
backgroundColor: '#1d6eba',
borderRadius: '50%',
}}
/>
<div
style={{
width: 35,
height: 35,
backgroundColor: '#1364b0',
borderRadius: '50%',
}}
/>
<div
style={{
width: 35,
height: 35,
backgroundColor: '#095aa6',
borderRadius: '50%',
}}
/>
</div>
</Box>
</GridItem>
</Grid>
);
}
@@ -0,0 +1,33 @@
.mainContainer {
width: 95%;
margin: auto;
height: 95vh;
}
.editor {
height: 100%;
margin-top: 1.5vh;
background-color: #fcfcfa;
/* 011627 */
padding: 1em 1.5em;
display: flex;
flex-direction: column;
}
.content {
padding-top: 1.5em;
}
.cornerButtonContainer {
position: relative;
top: -4.5em;
display: flex;
justify-content: flex-end;
}
.buttonContainer {
padding-bottom: 2em;
display: flex;
justify-content: flex-end;
}
@@ -0,0 +1,24 @@
import { IconButton } from '@chakra-ui/button';
import { AddIcon, MinusIcon } from '@chakra-ui/icons';
import style from './List.module.css';
export default function BlockBlock(props) {
return (
<div className={style.blockContainer}>
<div className={style.actionOverlay}>
<IconButton
size='xs'
icon={<MinusIcon />}
colorScheme='red'
onClick={() => props.deleteEvent(props.index)}
/>
<IconButton
size='xs'
icon={<AddIcon />}
colorScheme='blue'
onClick={() => props.createEvent(props.index)}
/>
</div>
</div>
);
}
@@ -0,0 +1,67 @@
import { IconButton } from '@chakra-ui/button';
import { AddIcon, MinusIcon } from '@chakra-ui/icons';
import { Box } from '@chakra-ui/layout';
import {
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
} from '@chakra-ui/slider';
import { useEffect, useState } from 'react';
import style from './List.module.css';
export default function DelayBlock(props) {
const [delay, setDelay] = useState(0);
const { data, ...rest } = props;
// update delay value in parent
const populateDelay = (value) => {
// check if value has changed
if (data.timerDuration !== value) {
// create object with new field
const newData = { ...data, timerDuration: delay };
// request update in parent
props.updateData(props.index, newData);
}
};
// update delay value in state
useEffect(() => {
setDelay(data.timerDuration);
}, [data]);
return (
<div className={style.delayContainer}>
<div className={style.delayValue}>{`${delay} min`}</div>
<Slider
defaultValue={data.timerDuration}
min={0}
max={60}
step={5}
onChange={(value) => setDelay(value)}
onChangeEnd={(value) => populateDelay(value)}
>
<SliderTrack bg='orange.100'>
<Box position='relative' right={10} />
<SliderFilledTrack bg='orange' />
</SliderTrack>
<SliderThumb boxSize={4} />
</Slider>
<div className={style.actionOverlay}>
<IconButton
size='xs'
icon={<MinusIcon />}
colorScheme='red'
onClick={() => props.deleteEvent(props.index)}
/>
<IconButton
size='xs'
icon={<AddIcon />}
colorScheme='blue'
onClick={() => props.createEvent(props.index)}
/>
</div>
</div>
);
}
@@ -0,0 +1,160 @@
import { useContext, useEffect } from 'react';
import { EventContext } from '../../../app/context/eventContext';
import { EventListContext } from '../../../app/context/eventListContext';
import EventListItem from './EventListItem';
import style from './List.module.css';
import DelayBlock from './DelayBlock';
import BlockBlock from './BlockBlock';
import EventListMenu from '../../menu/EventListMenu';
export default function EventList(props) {
const [events, setEvents] = useContext(EventListContext);
const [event] = useContext(EventContext);
// update number of events
useEffect(() => {
const f = events.filter((e) => e.type === 'event');
props.updatePlayback({ numEvents: f.length });
}, [events]);
const insertItemAt = (item, index) => {
// handle insert at beggining of array
if (index === -1) {
// move all items one element down, starting from new position
events.forEach((e) => {
e.order = e.order + 1;
});
return [item, ...events];
} else {
let before = events.slice(0, index + 1);
let after = events.slice(index + 1);
// move all items one element down, starting from new position
after.forEach((e) => {
e.order = e.order + 1;
});
return [...before, item, ...after];
}
};
const createEvent = (itemIndex = -1) => {
// make an event
// TODO: Replace this with global def somewhere
// TODO: handle random ids better
let newEvent = {
id: Math.random(),
order: itemIndex + 1,
title: '',
subtitle: '',
presenter: '',
timeStart: new Date(),
timeEnd: new Date(),
clockStarted: null,
timerDuration: 0,
type: 'event',
};
// set to state
setEvents(insertItemAt(newEvent, itemIndex));
};
const createDelay = (itemIndex = 0) => {
// make an event
// TODO: Replace this with global def somewhere
// TODO: handle random ids better
let newEvent = {
id: Math.random(),
order: itemIndex + 1,
timerDuration: 0,
type: 'delay',
};
// set to state
setEvents(insertItemAt(newEvent, itemIndex));
};
const createBlock = (itemIndex = 0) => {
// make an event
// TODO: Replace this with global def somewhere
// TODO: handle random ids better
let newEvent = {
id: Math.random(),
order: itemIndex + 1,
type: 'block',
};
// set to state
setEvents(insertItemAt(newEvent, itemIndex));
};
const deleteEvent = (index) => {
// TODO: This feels weird?
let e = events.splice(index, 1);
setEvents([...events]);
};
const replaceAt = (array, index, value) => {
const ret = array.slice(0);
ret[index] = value;
return ret;
};
const updateData = (itemIndex, data) => {
const newEvents = replaceAt(events, itemIndex, data);
setEvents(newEvents);
};
console.log('events in event list', events);
let cumulativeDelay = 0;
let eventCount = -1;
return (
<>
<EventListMenu createEvent={createEvent} />
<div className={style.eventContainer}>
{events.map((e, index) => {
if (e.type === 'event') {
eventCount = eventCount + 1;
return (
<EventListItem
key={e.id}
index={index}
data={e}
selected={props.selected === eventCount}
createEvent={createEvent}
deleteEvent={deleteEvent}
createDelay={createDelay}
createBlock={createBlock}
updateData={updateData}
delay={cumulativeDelay}
/>
);
} else if (e.type === 'block') {
cumulativeDelay = 0;
return (
<BlockBlock
key={e.id}
index={index}
createEvent={createEvent}
deleteEvent={deleteEvent}
/>
);
} else if (e.type === 'delay') {
cumulativeDelay = cumulativeDelay + e.timerDuration;
return (
<DelayBlock
key={e.id}
index={index}
data={e}
createEvent={createEvent}
deleteEvent={deleteEvent}
updateData={updateData}
/>
);
}
})}
</div>
</>
);
}
@@ -0,0 +1,175 @@
import {
AddIcon,
ChevronDownIcon,
ChevronUpIcon,
MinusIcon,
NotAllowedIcon,
TimeIcon,
} from '@chakra-ui/icons';
import {
IconButton,
Editable,
EditablePreview,
EditableInput,
} from '@chakra-ui/react';
import { addMinutes, format } from 'date-fns';
import { useEffect, useState } from 'react';
import { timeFormat, timeToDate } from '../../../common/dateConfig';
import style from './List.module.css';
// small shorthand for adding delay and formatting date
const addAndFormat = (time, delay) => {
return format(addMinutes(time, delay), timeFormat);
};
export default function EventListItem(props) {
const [more, setMore] = useState(false);
// const [timeStart, setTimeStart] = useState(
// addAndFormat(props.data.timeStart, props.delay)
// );
const [timeStart, setTimeStart] = useState(0);
const [timeEnd, setTimeEnd] = useState(
addAndFormat(props.data.timeEnd, props.delay)
);
const { data, selected, delay, ...rest } = props;
// prepare time fields
useEffect(() => {
setTimeStart(addAndFormat(props.data.timeStart, props.delay));
setTimeEnd(addAndFormat(props.data.timeEnd, props.delay));
}, [props.data, props.delay]);
const updateValues = (field, value) => {
// validate field
if (field in data) {
// create object with new field
const newData = { ...data, [field]: value };
// request update in parent
props.updateData(props.index, newData);
} else {
console.log('field error', field);
}
};
return (
<form>
<div className={selected ? style.eventRowActive : style.eventRow}>
<div className={style.time}>
<Editable
onSubmit={(v) => updateValues('timeStart', timeToDate(v))}
value={timeStart}
onChange={(val) => setTimeStart(val)}
placeholder='--:--'
style={{ textAlign: 'center' }}
className={delay > 0 && style.delayedEditable}
>
<EditablePreview />
<EditableInput type='time' min='00:00' max='23:59' />
</Editable>
</div>
<div className={style.time}>
<Editable
onSubmit={(v) => updateValues('timeEnd', timeToDate(v))}
value={timeEnd}
onChange={(val) => setTimeEnd(val)}
placeholder='--:--'
style={{ textAlign: 'center' }}
className={delay > 0 && style.delayedEditable}
>
<EditablePreview />
<EditableInput type='time' min='00:00' max='23:59' />
</Editable>
</div>
<div className={style.rowDetailed}>
{more ? (
<div className={style.detailedContainer}>
<div style={{ display: 'block' }}>
<span className={style.detailedTitleUnderlined}>Title</span>
<Editable
onSubmit={(v) => updateValues('title', v)}
defaultValue={data.title}
placeholder='Add title'
style={{ display: 'inline' }}
>
<EditablePreview />
<EditableInput style={{ width: '13em' }} />
</Editable>
</div>
<div style={{ display: 'block' }}>
<span className={style.detailedTitleUnderlined}>Subtitle</span>
<Editable
onSubmit={(v) => updateValues('subtitle', v)}
defaultValue={data.subtitle}
placeholder='Add subtitle'
style={{ display: 'inline' }}
>
<EditablePreview />
<EditableInput style={{ width: '13em', minWidth: '13em' }} />
</Editable>
</div>
<div style={{ display: 'block' }}>
<span className={style.detailedTitleUnderlined}>Presenter</span>
<Editable
onSubmit={(v) => updateValues('presenter', v)}
defaultValue={data.presenter}
placeholder='Add presenter name'
style={{ display: 'inline' }}
>
<EditablePreview style={{}} />
<EditableInput style={{ width: '13em' }} />
</Editable>
</div>
</div>
) : (
<div className={style.titleContainer}>
<div>
<span className={style.detailedTitle}>Title</span>
<Editable
onSubmit={(v) => updateValues('title', v)}
defaultValue={data.title}
placeholder='Add title'
style={{ display: 'inline' }}
id='title'
>
<EditablePreview />
<EditableInput style={{ width: '13em' }} />
</Editable>
</div>
</div>
)}
<div className={style.more} onClick={() => setMore(!more)}>
{more ? <ChevronUpIcon /> : <ChevronDownIcon />}
</div>
</div>
<div className={style.actionOverlay}>
<IconButton
size='xs'
icon={<MinusIcon />}
colorScheme='red'
onClick={() => props.deleteEvent(props.index)}
/>
<IconButton
size='xs'
icon={<AddIcon />}
colorScheme='blue'
onClick={() => props.createEvent(props.index)}
/>
<IconButton
size='xs'
icon={<TimeIcon />}
colorScheme='yellow'
onClick={() => props.createDelay(props.index)}
/>
<IconButton
size='xs'
icon={<NotAllowedIcon />}
colorScheme='purple'
onClick={() => props.createBlock(props.index)}
/>
</div>
</div>
</form>
);
}
@@ -0,0 +1,184 @@
/* ============= EVENT LIST ============= */
.eventContainer {
}
/* ============= EVENT ITEM ============= */
.eventRow,
.eventRowActive {
margin: 0.2em 0;
padding: 0.2em 0.5em;
position: relative;
top: 1em;
left: 0;
width: 100%;
display: grid;
grid-template-columns: auto 5em 1fr auto;
padding-left: 1em;
gap: 0.5em;
justify-content: center;
align-content: center;
align-items: baseline;
border-radius: 8px;
font-size: 15px;
}
.eventRow {
background-color: #0001;
border-top: 2px solid #0001;
border-bottom: 2px solid #0001;
}
.eventRowActive {
background-color: #b2f5eaaa;
border-top: 2px solid #b2f5ea;
border-bottom: 2px solid #b2f5ea;
}
.arm,
.armActive {
width: 18px;
height: 18px;
border-radius: 10px;
cursor: pointer;
}
.arm {
background-color: #888;
border: 1px solid #444;
}
.armActive {
background: radial-gradient(
circle,
rgba(22, 255, 0, 1) 0%,
rgba(50, 255, 31, 1) 49%,
rgba(0, 0, 0, 1) 65%,
rgba(0, 255, 156, 0.7083333333333333) 81%
);
border: 1px solid #256e62;
}
.time,
.titleContainer,
.detailedContainer {
background-color: #fff8;
border: 1px solid #0001;
border-radius: 3px;
}
.time {
width: 5em;
height: fit-content;
}
.rowDetailed {
display: inline-flex;
position: relative;
}
.titleContainer,
.detailedContainer {
width: 90%;
display: block;
}
.detailedContainer > div,
.titleContainer > div {
display: inline;
}
.detailedTitle,
.detailedTitleUnderlined {
padding-left: 1em;
font-size: 0.8em;
color: #888;
display: inline-block;
width: 6em;
cursor: default;
}
.detailedTitleUnderlined {
border-bottom: 1px solid #0001;
}
.more {
position: absolute;
right: 0.25em;
top: 0em;
}
.actionOverlay {
display: flex;
flex-direction: row;
gap: 0.5em;
align-content: center;
align-self: center;
opacity: 0.6;
transition: linear 0.1s;
}
.eventRow:hover > .actionOverlay {
opacity: 0.9;
transition: linear 0.1s;
}
.actionOverlay:hover {
opacity: 1;
}
/* ============= DELAY BLOCK ============= */
.delayContainer {
position: relative;
top: 1em;
margin: 0.2em 0;
padding: 0.2em 1em;
background-color: #ecc94baa;
border-radius: 8px;
color: white;
border: 1px solid #0001;
border-top: 4px solid #ecc94b;
box-sizing: border-box;
width: 100%;
display: flex;
margin: auto;
gap: 1em;
}
.delayValue {
color: #000;
background-color: #fff5;
border-radius: 4px;
padding: 0.2em;
text-align: center;
width: 6em;
border: 1px solid #0001;
}
.delayedEditable {
background-color: #ecc94b55;
}
/* ============= BLOCK BLOCK ============= */
.blockContainer {
position: relative;
top: 1em;
margin: 0.2em 0;
padding: 0.2em 1em;
width: 100%;
background-color: #805ad5aa;
border-radius: 8px;
color: white;
height: 3em;
border: 1px solid #0001;
border-bottom: 8px solid #805ad5;
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: row-reverse;
margin: auto;
gap: 1em;
}
@@ -0,0 +1,13 @@
export const sortByDate = (arr) => {
const sorter = (a, b) => {
return new Date(a.timeStart).getTime() - new Date(b.timeStart).getTime();
};
return arr.sort(sorter);
};
export const sortByOrderVal = (arr) => {
const sorter = (a, b) => {
return a.order - b.order;
};
return arr.sort(sorter);
};
+66
View File
@@ -0,0 +1,66 @@
import { IconButton } from '@chakra-ui/button';
import { FormControl, FormLabel } from '@chakra-ui/form-control';
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
import { Input } from '@chakra-ui/input';
import { useContext } from 'react';
import { useState } from 'react';
import { PresenterMessagesContext } from '../../app/context/presenterMessageContext';
export default function MessageForm() {
const [presenterShow, setPresenterShow] = useState(false);
const [publicShow, setPublicShow] = useState(false);
const [presMessage, setPresMessage] = useContext(PresenterMessagesContext);
const handleSetPresenter = () => {
setPresenterShow(!presenterShow);
setPresMessage((prev) => ({ ...prev, show: !presMessage.show }));
};
const handlePresenterChange = (val) => {
setPresMessage((prev) => ({ ...prev, text: val }));
};
const handleSetPublic = () => {
setPublicShow(!publicShow);
};
const handlePublicChange = (val) => {};
return (
<>
<form style={{ display: 'flex', gap: '1em', fontSize: '15px' }}>
<FormControl id='presenterMessage'>
<FormLabel>Presenter screen message</FormLabel>
<Input
placeholder='only the presenter screens see this'
onChange={(event) => handlePresenterChange(event.target.value)}
/>
</FormControl>
<IconButton
style={{ alignSelf: 'flex-end' }}
colorScheme='teal'
variant={presenterShow ? 'solid' : 'outline'}
onClick={handleSetPresenter}
icon={presenterShow ? <ViewOffIcon /> : <ViewIcon />}
/>
</form>
<form style={{ display: 'flex', gap: '1em', paddingTop: '1em' }}>
<FormControl id='generalMessage'>
<FormLabel>Public screen message</FormLabel>
<Input
placeholder='all screens will render this'
onChange={(event) => handlePublicChange(event.target.value)}
/>
</FormControl>
<IconButton
style={{ alignSelf: 'flex-end' }}
colorScheme='teal'
variant={publicShow ? 'solid' : 'outline'}
onClick={handleSetPublic}
icon={publicShow ? <ViewOffIcon /> : <ViewIcon />}
/>
</form>
</>
);
}
@@ -0,0 +1,54 @@
import { AddIcon, ChevronDownIcon } from '@chakra-ui/icons';
import {
Button,
ButtonGroup,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import style from './EventListMenu.module.css';
export default function EventListMenu(props) {
const buttonProps = {
size: 'sm',
variant: 'outline',
};
return (
<div className={style.headerButtons}>
<Menu>
<ButtonGroup isAttached>
<Button size='sm' variant='outline'>
Upload
</Button>
<MenuButton as={Button} {...buttonProps}>
<ChevronDownIcon />
</MenuButton>
</ButtonGroup>
<MenuList>
<MenuItem>Upload Excel</MenuItem>
<MenuItem>Upload CSV</MenuItem>
</MenuList>
</Menu>
<Menu>
<ButtonGroup isAttached>
<Button {...buttonProps}>Save</Button>
<MenuButton as={Button} {...buttonProps}>
<ChevronDownIcon />
</MenuButton>
</ButtonGroup>
<MenuList>
<MenuItem>Download Excel</MenuItem>
<MenuItem>Download CSV</MenuItem>
</MenuList>
</Menu>
<IconButton
size='sm'
icon={<AddIcon />}
colorScheme='blue'
onClick={() => props.createEvent()}
/>
</div>
);
}
@@ -0,0 +1,6 @@
.headerButtons {
display: flex;
gap: 0.5em;
align-content: center;
justify-content: flex-end;
}
@@ -0,0 +1,63 @@
import { differenceInSeconds, format, subMinutes } from 'date-fns';
import addMinutes from 'date-fns/addMinutes';
import { useContext, useEffect, useState } from 'react';
import { EventContext } from '../../app/context/eventContext';
import { PresenterMessagesContext } from '../../app/context/presenterMessageContext';
import Countdown from '../../common/components/countdown/Countdown';
import MyProgressBar from '../../common/components/myProgressBar/MyProgressBar';
import SmallTimer from '../../common/components/smallTimer/SmallTimer';
import './viewers.css';
export default function DefaultPresenter() {
const [event] = useContext(EventContext);
const [presMessage] = useContext(PresenterMessagesContext);
const [initialValues, setInitialValues] = useState(null);
const now = new Date();
let values = event ?? {
title: 'Presentation Title',
subtitle: 'Presentation Subtitle',
presenter: 'Presenter Name',
timerDuration: 10,
timeStart: now,
timeEnd: now,
};
useEffect(() => {
values = event;
}, [event]);
useEffect(() => {
setInitialValues(event);
}, [event]);
// NOTE: test only
const clockStarted = addMinutes(now, 6);
const timer = differenceInSeconds(
now,
subMinutes(clockStarted, values.timerDuration)
);
const timeStart = format(values.timeStart, 'HH:mm');
const currentTime = format(now, 'HH:mm');
const timeEnd = format(values.timeEnd, 'HH:mm');
const elapsed = timer / (values.timerDuration * 60);
return (
<div className='presenter'>
<div className='presentationTitle'>{values.title}</div>
<div className='presentationSub'>{values.subtitle}</div>
<Countdown time={timer} />
{!presMessage.show && <MyProgressBar normalisedComplete={elapsed} />}
<div className={presMessage.show ? 'userMessage' : 'userMessage hidden'}>
{presMessage.text}
</div>
<div className='extra'>
<SmallTimer label='Scheduled Start' time={timeStart} />
<SmallTimer label='Current Time' time={currentTime} />
<SmallTimer label='Scheduled End' time={timeEnd} />
</div>
</div>
);
}
@@ -0,0 +1,30 @@
import { AspectRatio } from '@chakra-ui/layout';
import { CircularProgress } from '@chakra-ui/progress';
import { useState } from 'react';
import style from './IFrameLoader.module.css';
export default function IFrameLoader(props) {
const [loading, setLoading] = useState(true);
const { title, src } = props;
return (
<AspectRatio maxW='300' ratio={16 / 9} className={style.iframeContainer}>
<>
{loading && (
<CircularProgress
className={style.loader}
isIndeterminate
color='orange.300'
trackColor='#FFF0'
/>
)}
<iframe
className={style.iframe}
title={title}
src={src}
onLoad={() => setLoading(false)}
/>
</>
</AspectRatio>
);
}
@@ -0,0 +1,13 @@
.iframeContainer {
margin: auto;
background-color: #0001;
border: 1px solid #0001;
}
.loader {
z-index: 100;
}
.iframe {
width: inherit;
}
@@ -0,0 +1,25 @@
import styles from './PreviewContainer.module.css';
import IFrameLoader from './IFrameLoader';
export default function PreviewContainer() {
return (
<div className={styles.previewContainer}>
<div className={styles.previewItem}>
<IFrameLoader title='Default Presenter' src='http://localhost:3000/' />
<div className={styles.label}>Default Presenter</div>
</div>
<div className={styles.previewItem}>
<IFrameLoader title='Audience' src='http://localhost:3000/' />
<div className={styles.label}>Audience</div>
</div>
<div className={styles.previewItem}>
<IFrameLoader title='Stage Manager' src='http://localhost:3000/' />
<div className={styles.label}>Stage Manager</div>
</div>
<div className={styles.previewItem}>
<IFrameLoader title='Lower third' src='http://localhost:3000/' />
<div className={styles.label}>Lower third</div>
</div>
</div>
);
}
@@ -0,0 +1,16 @@
.previewContainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 1em;
}
.previewItem {
width: 45%;
}
.label {
padding: 0.1em 4em;
}
+41
View File
@@ -0,0 +1,41 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap');
.presenter {
font-size: 1vh;
height: 100%;
color: #ffffff;
display: flex;
flex-direction: column;
padding-left: 4vh;
}
.presentationTitle {
font-size: 5vw;
text-align: left;
}
.presentationSub {
font-size: 3vw;
text-align: left;
}
.userMessage {
font-size: 10vw;
text-align: center;
}
.hidden {
display: none;
}
.extra {
width: 100%;
position: absolute;
bottom: 3vh;
left: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
}