mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
node server + separate client folder
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user