mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
node server + separate client folder
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.App {
|
||||
background-image: linear-gradient(rgb(20, 20, 20) 1.7%, rgb(48, 48, 48) 97%);
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Route } from 'react-router';
|
||||
import './App.css';
|
||||
import { EventProvider } from './app/context/eventContext';
|
||||
import { EventListProvider } from './app/context/eventListContext';
|
||||
import { PresenterMessageProvider } from './app/context/presenterMessageContext';
|
||||
import Editor from './features/editors/Editor';
|
||||
import DefaultPresenter from './features/viewers/DefaultPresenter';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PresenterMessageProvider>
|
||||
<EventProvider>
|
||||
<div className='App'>
|
||||
<Route path='/' exact component={DefaultPresenter} />
|
||||
<EventListProvider>
|
||||
<Route path='/editor' exact component={Editor} />
|
||||
</EventListProvider>
|
||||
</div>
|
||||
</EventProvider>
|
||||
</PresenterMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createContext, useState } from 'react';
|
||||
|
||||
export const EventContext = createContext([[], () => {}]);
|
||||
|
||||
export function EventProvider(props) {
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={[event, setEvent]}>
|
||||
{props.children}
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createContext, useState } from 'react';
|
||||
import { sampleData } from '../sampleData';
|
||||
|
||||
export const EventListContext = createContext([[], () => {}]);
|
||||
|
||||
export function EventListProvider(props) {
|
||||
const [events, setEvents] = useState(sampleData.events);
|
||||
|
||||
return (
|
||||
<EventListContext.Provider value={[events, setEvents]}>
|
||||
{props.children}
|
||||
</EventListContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createContext, useState } from 'react';
|
||||
|
||||
export const PresenterMessagesContext = createContext([[], () => {}]);
|
||||
|
||||
export function PresenterMessageProvider(props) {
|
||||
const [presMessage, setPresMessage] = useState({
|
||||
text: '',
|
||||
show: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<PresenterMessagesContext.Provider value={[presMessage, setPresMessage]}>
|
||||
{props.children}
|
||||
</PresenterMessagesContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
const dummy = new Date();
|
||||
|
||||
export const sampleData = {
|
||||
presenterMessage: {
|
||||
text: 'Only the presenter sees this',
|
||||
active: false,
|
||||
},
|
||||
publicMessage: {
|
||||
text: 'Everyone sees this',
|
||||
active: false,
|
||||
},
|
||||
events: [
|
||||
{
|
||||
id: '1',
|
||||
order: 1,
|
||||
title: 'Is the internet a fad?',
|
||||
subtitle: 'It is',
|
||||
presenter: 'Carlos Valente',
|
||||
timeStart: new Date('October 13, 2014 11:30:00'),
|
||||
timeEnd: new Date('October 13, 2014 11:50:00'),
|
||||
clockStarted: dummy,
|
||||
timerDuration: 60,
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: 0.4849093424577693,
|
||||
order: 2,
|
||||
timerDuration: 25,
|
||||
type: 'delay',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
order: 3,
|
||||
title: 'Is reddit a dictatorship?',
|
||||
subtitle: 'It is',
|
||||
presenter: 'Carlos Valente',
|
||||
timeStart: new Date('October 13, 2014 12:30:00'),
|
||||
timeEnd: new Date('October 13, 2014 12:50:00'),
|
||||
clockStarted: dummy,
|
||||
timerDuration: 60,
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
order: 4,
|
||||
title: 'Out of words',
|
||||
subtitle: '',
|
||||
presenter: 'Carlos Valente',
|
||||
timeStart: new Date('October 13, 2014 13:30:00'),
|
||||
timeEnd: new Date('October 13, 2014 13:50:00'),
|
||||
clockStarted: dummy,
|
||||
timerDuration: 60,
|
||||
type: 'event',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export const userConfig = {
|
||||
timerColorOnPause: '#555',
|
||||
timerColorOnRunning: '#FFF',
|
||||
timerColorOnMessage: '#CCC',
|
||||
timerColorOnTimeOver: '#F00',
|
||||
overTimeText: '',
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const clamp = (num, a, b) =>
|
||||
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
@@ -0,0 +1,18 @@
|
||||
import styles from './Countdown.module.css';
|
||||
|
||||
function display(seconds) {
|
||||
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
||||
const hours = seconds / 3600;
|
||||
const minutes = (seconds % 3600) / 60;
|
||||
|
||||
if (hours < 1) return [minutes, seconds % 60].map(format).join(':');
|
||||
else return [hours, minutes, seconds % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
export default function Countdown({ time, small }) {
|
||||
return (
|
||||
<div className={small ? styles.countdownClockSmall : styles.countdownClock}>
|
||||
{time === null ? '- -:- -' : display(time * 60)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap');
|
||||
|
||||
.countdownClock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 18vw;
|
||||
margin-top: 6vh;
|
||||
margin-bottom: 5vh;
|
||||
text-align: center;
|
||||
letter-spacing: 0.125em;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 6em;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 0.9;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { clamp } from '../../../app/utils';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
|
||||
export default function MyProgressBar({ normalisedComplete }) {
|
||||
const percentComplete = clamp(100 - normalisedComplete * 100, 0, 100);
|
||||
const completeWidth = `${percentComplete}%`;
|
||||
|
||||
return (
|
||||
<div className={styles.progress}>
|
||||
<div
|
||||
className={styles.progressBar}
|
||||
style={{ width: completeWidth }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.progress {
|
||||
padding: 1vh;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
height: 2vh;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(145, 255, 191, 1) 0%,
|
||||
rgba(145, 255, 191, 0.8) 20%,
|
||||
rgba(145, 255, 191, 0.5) 50%,
|
||||
rgba(145, 255, 191, 0.2) 100%
|
||||
);
|
||||
border-radius: 4px;
|
||||
transition: 0.4s linear;
|
||||
transition-property: width, background-color;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import styles from './SmallTimer.module.css';
|
||||
|
||||
export default function SmallTimer({ label, time }) {
|
||||
return (
|
||||
<div className={styles.SmallTimer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.timer}>{time}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap');
|
||||
|
||||
.smallTimer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label,
|
||||
.timer {
|
||||
font-size: 1vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.5vw;
|
||||
color: #888;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.125em;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Flex, Text } from '@chakra-ui/layout';
|
||||
import styles from './NumberedText.module.css';
|
||||
|
||||
export default function NumberedText({ number = 1, text = '' }) {
|
||||
return (
|
||||
<Flex>
|
||||
<div className={styles.stylednumber}>{number}</div>
|
||||
<Text>{text}</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.stylednumber {
|
||||
display: inline;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
|
||||
background-color: #319795;
|
||||
width: 1.5em;
|
||||
border-radius: 1em;
|
||||
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const timeFormat = 'HH:mm';
|
||||
|
||||
// make date with string
|
||||
export const timeToDate = (time) => {
|
||||
const today = new Date();
|
||||
return new Date(today.toDateString() + ' ' + time);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FormErrorMessage } from '@chakra-ui/form-control';
|
||||
import { FormLabel } from '@chakra-ui/form-control';
|
||||
import { FormControl } from '@chakra-ui/form-control';
|
||||
import { Input } from '@chakra-ui/input';
|
||||
import { Field } from 'formik';
|
||||
|
||||
export default function ChakraInput(props) {
|
||||
const { label, name, ...rest } = props;
|
||||
return (
|
||||
<Field name={name}>
|
||||
{({ field, form }) => {
|
||||
return (
|
||||
<FormControl isInvalid={form.errors[name] && form.touched[name]}>
|
||||
<FormLabel htmlFor={name}>{label}</FormLabel>
|
||||
<Input id={name} {...rest} {...field} />
|
||||
<FormErrorMessage>{form.errors[name]}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
// 1. import `ChakraProvider` component
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<ChakraProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ChakraProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user