Feat/navigate (#81)

* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
This commit is contained in:
Carlos Valente
2022-01-04 22:12:59 +01:00
committed by GitHub
parent dd43d5e20a
commit 2fa397548a
74 changed files with 2592 additions and 1428 deletions
+49 -41
View File
@@ -1,18 +1,17 @@
import { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import { QueryClient, QueryClientProvider } from 'react-query';
import SocketProvider from 'app/context/socketContext';
import withSocket from 'features/viewers/ViewWrapper';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import ProtectRoute from './common/components/protectRoute/ProtectRoute';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/Editor'));
const PresenterView = lazy(() =>
import('features/viewers/presenter/PresenterView')
);
const PresenterSimple = lazy(() =>
import('features/viewers/presenter/PresenterSimple')
);
const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager')
);
@@ -21,18 +20,9 @@ const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper')
);
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const queryClient = new QueryClient();
// Seemed to cause issues
// broadcastQueryClient({
// queryClient,
// broadcastChannel: 'ontime',
// });
const SPresenter = withSocket(PresenterView);
const SPresenterSimple = withSocket(PresenterSimple);
const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
@@ -40,6 +30,10 @@ const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
// check if the alt key is pressed
@@ -65,33 +59,47 @@ function App() {
};
}, [handleKeyPress]);
// navigate if is alias route
useEffect(() => {
if (data == null) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return (
<SocketProvider>
<QueryClientProvider client={queryClient}>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<Switch>
<Route exact path='/' component={SPresenter} />
<Route exact path='/sm' component={SStageManager} />
<Route exact path='/speaker' component={SPresenter} />
<Route exact path='/presenter' component={SPresenter} />
<Route exact path='/stage' component={SPresenter} />
<Route exact path='/presentersimple' component={SPresenterSimple} />
<Route exact path='/editor' component={Editor} />
<Route exact path='/public' component={SPublic} />
<Route exact path='/pip' component={SPip} />
<Route exact path='/studio' component={SStudio} />
{/* Lower cannot have fallback */}
<Route exact path='/lower' component={SLowerThird} />
{/* Send to default if nothing found */}
<Route component={SPresenter} />
</Switch>
</Suspense>
</ErrorBoundary>
</div>
</QueryClientProvider>
</SocketProvider>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<SPresenter />} />
<Route path='/sm' element={<SStageManager />} />
<Route path='/speaker' element={<SPresenter />} />
<Route path='/presenter' element={<SPresenter />} />
<Route path='/stage' element={<SPresenter />} />
<Route path='/public' element={<SPublic />} />
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route
path='/editor'
element={
<ProtectRoute>
<Editor />
</ProtectRoute>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<SPresenter />} />
</Routes>
</Suspense>
</ErrorBoundary>
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
+2
View File
@@ -1,8 +1,10 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
+30
View File
@@ -9,6 +9,18 @@ export const ontimePlaceholderInfo = {
},
};
export const ontimePlaceholderSettings = {
pinCode: null,
};
export const eventPlaceholderSettings = {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
};
export const oscPlaceholderSettings = {
port: '',
portOut: '',
@@ -74,6 +86,15 @@ export const ontimeVars = [
},
];
export const getSettings = async () => {
const res = await axios.get(`${ontimeURL}/settings`);
return res.data;
};
export const postSettings = async (data) => {
return await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
@@ -83,6 +104,15 @@ export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`);
return res.data;
};
export const postAliases = async (data) => {
return await axios.post(`${ontimeURL}/aliases`, data);
};
export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
+14
View File
@@ -0,0 +1,14 @@
// Exported viewer links
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
export const viewerLinks = [
{ link: speakerLink, label: 'Speaker Screen' },
{ link: smLink, label: 'Backstage Screen' },
{ link: publicLink, label: 'Public Screen' },
{ link: pipLink, label: 'Picture in Picture' },
{ link: studioLink, label: 'Studio Clock' }
];
+37
View File
@@ -0,0 +1,37 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
export const AppContext = createContext({
auth: false,
data: {
pinCode: null
}
});
export const AppContextProvider = (props) => {
const [auth, setAuth] = useState(true);
const { data } = useFetch(APP_SETTINGS, getSettings);
useEffect(() => {
if (data == null) return;
if (data?.pinCode === null || data?.pinCode === '') {
setAuth(true);
} else {
setAuth(false);
}
},[data])
const validate = useCallback((pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
}, [data]);
return (
<AppContext.Provider value={{ auth, validate }}>
{props.children}
</AppContext.Provider>
);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-server/utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-server/utils/time';
import { generateId } from 'ontime-utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
export const LoggingContext = createContext({
logData: [],
-7
View File
@@ -1,7 +0,0 @@
export const userConfig = {
timerColorOnPause: '#555',
timerColorOnRunning: '#FFF',
timerColorOnMessage: '#CCC',
timerColorOnTimeOver: '#F00',
overTimeText: '',
}
-2
View File
@@ -1,2 +0,0 @@
export const clamp = (num, a, b) =>
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
@@ -0,0 +1,27 @@
import { validateAlias } from '../aliases';
describe('An alias fails if incorrect', () => {
const testsToFail = [
// no empty
'',
// no https, http or www
'https://www.test.com',
'http://www.test.com',
'www.test.com',
// no hostname
'localhost/test',
'127.0.0.1/test',
'0.0.0.0/test',
// no editor
'editor',
'editor?test'
];
testsToFail.forEach((t) => (
test(`${t}`, () => {
expect(validateAlias(t).status).toBeFalsy();
})
)
);
});
@@ -0,0 +1,21 @@
import { clamp } from '../math';
test('Clamps a set of numbers correctly', () => {
const testCases = [
{ num: 10, min: 0, max: 20, result: 10 },
{ num: 0, min: 0, max: 20, result: 0 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: -20, min: 0, max: 20, result: 0 },
{ num: -0, min: 0, max: 20, result: 0 },
{ num: -50, min: -30, max: -20, result: -30 },
{ num: -50, min: 0, max: 0, result: 0 },
{ num: 50.5, min: 0, max: 100, result: 50.5 },
{ num: 50, min: 0, max: 20.32, result: 20.32 },
{ num: 10, min: 20.32, max: 40, result: 20.32 }
];
testCases.forEach((t) => (
expect(clamp(t.num, t.min, t.max)).toBe(t.result)
));
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Validates an alias against defined parameters
* @param {string} alias
* @returns {{message: string, status: boolean}}
*/
export const validateAlias = (alias) => {
const valid = { status: true, message: 'ok' };
if (alias === '' || alias == null) {
// cannot be empty
valid.status = false;
valid.message = 'should not be empty';
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
// cannot contain http, https or www
valid.status = false;
valid.message = 'should not include http, https, www';
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
// aliases cannot contain hostname
valid.status = false;
valid.message = 'should not include hostname';
} else if (alias.includes('editor')) {
// no editor
valid.status = false;
valid.message = 'No aliases to editor page allowed';
}
return valid;
};
+9
View File
@@ -0,0 +1,9 @@
/**
* Clamps a value between a min and a max
* @param {number} num - Value to clamp
* @param {number} min - min value
* @param {number} max - max value
* @returns {number}
*/
export const clamp = (num, min, max) =>
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
@@ -1,5 +1,5 @@
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
@@ -1,4 +1,4 @@
import { clamp } from 'app/utils';
import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
export default function MyProgressBar(props) {
+1 -7
View File
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import { Link, Redirect } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';
@@ -57,7 +57,6 @@ export default function NavLogo(props) {
to='/presenter'
className={style.navItem}
tabIndex={1}
onKeyDownCapture={() => <Redirect push to='/presenter' />}
>
Presenter
</Link>
@@ -65,7 +64,6 @@ export default function NavLogo(props) {
to='/sm'
className={style.navItem}
tabIndex={2}
onKeyDownCapture={() => <Redirect push to='/sm' />}
>
Backstage
</Link>
@@ -73,7 +71,6 @@ export default function NavLogo(props) {
to='/public'
className={style.navItem}
tabIndex={3}
onKeyDownCapture={() => <Redirect push to='/public' />}
>
Public
</Link>
@@ -81,7 +78,6 @@ export default function NavLogo(props) {
to='/lower'
className={style.navItem}
tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/lower' />}
>
Lower Thirds
</Link>
@@ -89,7 +85,6 @@ export default function NavLogo(props) {
to='/pip'
className={style.navItem}
tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/pip' />}
>
PIP
</Link>
@@ -97,7 +92,6 @@ export default function NavLogo(props) {
to='/studio'
className={style.navItem}
tabIndex={5}
onKeyDownCapture={() => <Redirect push to='/studio' />}
>
Studio Clock
</Link>
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button';
import { FiCheck } from 'react-icons/fi';
import { AppContext } from '../../../app/context/AppContext';
import { useContext, useEffect, useState } from 'react';
export default function ProtectRoute(props) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
// Set window title
useEffect(() => {
document.title = 'ontime';
}, []);
const handleValidation = () => {
const r = validate(pin);
if (!r) {
setFailed(true);
}
}
return (
<>
{!isLocal && !auth ? (
<div className={style.container}>
ontime
<div className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
aria-label='Enter'
size='lg'
isRound
icon={<FiCheck />}
style={{ fontSize: '1.5em' }}
onClick={() => handleValidation()}
/>
</div>
</div>
) : (
props.children
)}
</>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired
};
@@ -0,0 +1,47 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
.container {
background: #222;
display: grid;
place-content: center;
height: 100vh;
padding-bottom: 30vh;
color: $ontime-pink;
font-family: 'Open Sans', sans-serif;
font-weight: 200;
text-align: center;
font-size: 3vw;
}
.pin,
.pin__failed {
display: flex;
gap: 10px;
padding: 20px;
input {
border-radius: 50%;
}
button {
margin-left: 20px;
}
}
.pin__failed {
input {
animation: colourFade 1.5s ease;
}
}
@keyframes colourFade {
from {
background: $ontime-pink;
}
to {
background: rgba($ontime-pink, 0);
}
}
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
+1 -1
View File
@@ -4,7 +4,7 @@ import {
isTimeString,
timeStringToMillis,
} from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext';
+1 -1
View File
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
/**
* @description From a list of events, returns only events of type event with calculated delays
+13
View File
@@ -0,0 +1,13 @@
/**
* Handles link to external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export default function handleLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
@@ -1,6 +1,6 @@
import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import {Tooltip} from '@chakra-ui/react';
import {Button} from '@chakra-ui/button';
import {memo} from 'react';
+4 -2
View File
@@ -24,12 +24,14 @@ export default function Editor() {
return (
<LoggingProvider>
<ModalManager isOpen={isOpen} onClose={onClose} />
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} />
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
+2 -1
View File
@@ -1,4 +1,5 @@
@use '../../main' as *;
@use '../../styles/main' as *;
@use '../../styles/mixins' as *;
@mixin container {
margin-top: 1em;
@@ -1,5 +1,6 @@
@use 'Info.module' as *;
@use '../../main' as *;
@use '../../styles/main' as *;
@use '../../styles/mixins' as *;
.container,
.container__expanded{
+1 -8
View File
@@ -4,6 +4,7 @@ import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import handleLink from '../../common/utils/handleLink';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -12,14 +13,6 @@ export default function InfoNif() {
const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001';
const handleLink = (url) => {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
};
return (
<div className={style.container}>
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
+21 -8
View File
@@ -6,14 +6,15 @@ import SettingsIconBtn from './buttons/SettingsIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.css';
import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
export default function MenuBar(props) {
const { onOpen } = props;
const { isOpen, onOpen } = props;
const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null);
const queryClient = useQueryClient();
@@ -33,6 +34,10 @@ export default function MenuBar(props) {
}
};
const buttonStyle = {
fontSize: '1.5em'
};
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
if (fileUploaded == null) return;
@@ -93,25 +98,27 @@ export default function MenuBar(props) {
<>
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
<MaxIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('max')}
/>
<MinIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('min')}
/>
<div className={style.gap} />
<HelpIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('help')}
/>
<SettingsIconBtn
style={{ fontSize: '1.5em' }}
style={{...buttonStyle}}
size='lg'
className={isOpen ? style.open : ''}
clickhandler={onOpen}
isRound
/>
<div className={style.gap} />
<input
@@ -122,15 +129,21 @@ export default function MenuBar(props) {
accept='.json, .xlsx'
/>
<UploadIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={handleClick}
/>
<DownloadIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={handleDownload}
/>
</>
);
}
MenuBar.propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
};
@@ -1,3 +0,0 @@
.gap {
height: 1em;
}
@@ -0,0 +1,9 @@
@use '../../styles/main' as *;
.gap {
height: 1em;
}
.open {
background: $light-bg;
}
+294 -147
View File
@@ -1,174 +1,321 @@
import { Button, IconButton } from '@chakra-ui/button';
import { FiPlus, FiMinus } from 'react-icons/fi';
import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { fetchEvent } from 'app/api/eventApi';
import { useState } from 'react';
import { getAliases, postAliases } from '../../app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
import { ALIASES } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { viewerLinks } from '../../app/appConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import { validateAlias } from '../../app/utils/aliases';
import { Tooltip } from '@chakra-ui/tooltip';
import SubmitContainer from './SubmitContainer';
import handleLink from '../../common/utils/handleLink';
export default function AliasesModal() {
const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent);
const { data, status, refetch } = useFetch(ALIASES, getAliases);
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [aliases, setAliases] = useState([]);
const host = window.location.host;
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setAliases([...data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
// NOTHING HERE YET
setSubmitting(true);
const validatedAliases = [...aliases];
let errors = false;
for (const alias of validatedAliases) {
// validate url
const isURLValid = validateAlias(alias.pathAndParams);
if (!isURLValid.status) {
alias.urlError = isURLValid.message;
errors = true;
} else {
alias.urlError = undefined;
}
// validate alias
const isAliasValid = validateAlias(alias.alias);
if (!isAliasValid.status) {
alias.aliasError = isAliasValid.message;
errors = true;
} else {
alias.aliasError = undefined;
}
}
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
}
setSubmitting(false);
};
// Hardcoded links for now
// it will need dynamic PORT assignment
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
/**
* Creates a new alias in state with a temporary id
*/
const addNew = () => {
if (aliases.length > 20) {
emitError('Maximum amount of aliases reacted (20)');
return;
}
const emptyAlias = {
id: Math.floor(Math.random() * 1000),
enabled: false,
alias: '',
pathAndParams: '',
};
setAliases((prevState) => [...prevState, emptyAlias]);
setChanged(true);
};
/**
* Deletes an alias by a given id
* @param {string} id - id of alias to delete
*/
const deleteAlias = (id) => {
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
setChanged(true);
};
/**
* Sets enabled flag to true / false
* @param {string} id - object id
* @param {boolean} isEnabled - whether to enable / disable flag
*/
const setEnabled = (id, isEnabled) => {
const aliasesState = [...aliases];
for (const a of aliasesState) {
if (a.id === id) {
if (isEnabled) {
if (a.alias === '' || a.pathAndParams === '') {
emitError('Alias incomplete');
break;
}
const isRepeated = aliases.some(
(r) => a.alias === r.alias && r.enabled
);
if (isRepeated) {
emitError('There is already an alias with this name');
break;
}
}
a.enabled = isEnabled;
break;
}
}
setChanged(true);
setAliases(aliasesState);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {number} index - index of item in array
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (index, field, value) => {
const temp = [...aliases];
temp[index][field] = value;
setAliases(temp);
setChanged(true);
};
return (
<>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Configure easy to use URL Aliases
<br />
🔥 Changes take effect on save 🔥
</p>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Configure easy to use URL Aliases
<div className={style.modalFields}>
<div className={style.hSeparator}>Default URLs</div>
<div className={style.blockNotes}>
{viewerLinks.map((l) => (
<a
href={l.link}
target='_blank'
rel='noreferrer'
className={style.flexNote}
key={l.link}
onClick={() => handleLink(`${host}/${l.link}`)}
>
{`${l.label} - ${l.link}`}
</a>
))}
</div>
<div className={style.hSeparator}>Custom Aliases</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<FiInfo color='#2b6cb0' fontSize={'2em'} />
URL aliases are useful in two main scenarios
</span>
<span className={style.labelNote}>Complicated URLs</span>
<br />
Feature is not yet implemented
</p>
<span>Default URLs</span>
<div className={style.highNotes}>
<p className={style.flexNote}>
Presenter Screen <br />
<a
href={speakerLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{speakerLink}
</a>
</p>
<p className={style.flexNote}>
Backstage / Stage Manager Screen <br />
<a
href={smLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{smLink}
</a>
</p>
<p className={style.flexNote}>
Public / Foyer Screen <br />
<a
href={publicLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{publicLink}
</a>
</p>
<p className={style.flexNote}>
Picture in Picture Screen <br />
<a
href={pipLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{pipLink}
</a>
</p>
<p className={style.flexNote}>
Studio Clock<br />
<a
href={studioLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{studioLink}
</a>
</p>
eg. a lower third url with some custom parameters
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
</tbody>
</table>
<br />
<span className={style.labelNote}>
URLs to be changed dynamically
</span>
<br />
eg. an unattended screen that you would need to change route from
the app
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
</tbody>
</table>
</div>
<div
className={style.inlineAliasPlaceholder}
style={{ padding: '0.5em 0' }}
>
<span className={style.labelNote}>Alias</span>
<span className={style.labelNote}>Page URL</span>
</div>
{aliases.map((alias, index) => (
<div key={alias.id}>
<div className={style.inlineAlias}>
<Input
size='sm'
variant='flushed'
name='Alias'
placeholder='URL Alias'
autoComplete='off'
value={alias.alias}
isInvalid={alias.aliasError}
onChange={(event) =>
handleChange(index, 'alias', event.target.value)
}
/>
<Input
size='sm'
fontSize={'0.75em'}
variant='flushed'
name='URL'
placeholder='URL (portion after ontime Port)'
autoComplete='off'
value={alias.pathAndParams}
isInvalid={alias.urlError}
onChange={(event) =>
handleChange(index, 'pathAndParams', event.target.value)
}
/>
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={500}>
<a
href='#!'
target='_blank'
rel='noreferrer'
onClick={(e) => {
e.preventDefault();
handleLink(`http://${host}/${alias.pathAndParams}`);
}}
/>
</Tooltip>
<Tooltip label='Enable alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiSun />}
colorScheme='blue'
variant={alias.enabled ? null : 'outline'}
onClick={() => setEnabled(alias.id, !alias.enabled)}
/>
</Tooltip>
<Tooltip label='Delete alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiMinus />}
colorScheme='red'
onClick={() => deleteAlias(alias.id)}
/>
</Tooltip>
</div>
{alias.aliasError ? (
<div
className={style.error}
>{`Alias error: ${alias.aliasError}`}</div>
) : null}
{alias.urlError ? (
<div
className={style.error}
>{`URL error: ${alias.urlError}`}</div>
) : null}
</div>
))}
<span>Manage custom aliases</span>
<div className={style.modalInline}>
<Input
size='sm'
name='URL'
placeholder='A long URL'
autoComplete='off'
value={'A long URL'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<Input
size='sm'
name='Alias'
placeholder='A nice alias'
autoComplete='off'
value={'A nice alias'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<IconButton
size='sm'
icon={<FiMinus />}
colorScheme='red'
disabled
/>
</div>
<div className={style.separator} />
<div className={style.modalInline}>
<Input
size='sm'
name='URL'
placeholder='URL'
autoComplete='off'
value={'URL'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<Input
size='sm'
name='Alias'
placeholder='Alias'
autoComplete='off'
value={'Alias'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<IconButton
size='sm'
icon={<FiPlus />}
colorScheme='blue'
disabled
/>
</div>
<div className={style.submitContainer}>
<div
className={style.inlineAliasPlaceholder}
style={{ padding: '0.5em 0' }}
>
<Button
size='xs'
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={true}
variant='outline'
onClick={() => addNew()}
>
Save
Add new
</Button>
</div>
</ModalBody>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+131 -144
View File
@@ -1,183 +1,170 @@
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import {
FormControl,
FormLabel,
Input,
PinInput,
PinInputField,
} from '@chakra-ui/react';
import {
getSettings,
ontimePlaceholderSettings,
postSettings,
} from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import { APP_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import { IconButton } from '@chakra-ui/button';
import { FiEye } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function AppSettingsModal() {
const { data, status } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
const { emitError, emitWarning } = useContext(LoggingContext);
const [formData, setFormData] = useState(ontimePlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
setFormData({ ...data });
}, [data]);
if (changed) return;
setFormData({
pinCode: data.pinCode,
});
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
let e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
if (f.pinCode === '' || f.pinCode == null) {
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.message += 'App pin code removed';
} else {
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
e.message += 'App pin code added';
}
// set fields with error
if (e.status) {
if (!e.status) {
emitError(`Invalid Input: ${e.message}`);
return;
} else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
}
// Post here
postOSC(formData);
setChanged(false);
setSubmitting(false);
};
return (
<>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}>
Options related to the application
<br />
🔥 Changes take effect after app restart 🔥
</p>
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
<FormControl id='serverPort'>
<FormLabel htmlFor='serverPort'>
Viewer Port
<span className={style.notes}>Port to access viewers</span>
</FormLabel>
<Input
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
const disableModal = status !== 'success';
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to the application
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>General App Settings</div>
<div className={style.modalInline}>
<FormControl id='serverPort'>
<FormLabel htmlFor='serverPort'>
Viewer Port
<span className={style.labelNote}>
<br />
Ontime is available at port
</span>
</FormLabel>
<Input
{...inputProps}
name='title'
value={4001}
disabled
style={{ width: '6em', textAlign: 'center' }}
/>
</FormControl>
<FormControl id='editorPin'>
<FormLabel htmlFor='editorPin'>
Editor Pincode
<span className={style.labelNote}>
<br />
Protect the editor with a Pincode
</span>
</FormLabel>
<div className={style.pin}>
<PinInput
{...inputProps}
type='alphanumeric'
defaultValue=''
value={formData.pinCode}
mask={hidePin}
isDisabled={disableModal}
onChange={(value) => handleChange('pinCode', value)}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
size='sm'
name='title'
placeholder='4001'
autoComplete='off'
value={4001}
readOnly
style={{ width: '6em', textAlign: 'center' }}
colorScheme='blue'
variant='ghost'
icon={<FiEye />}
aria-label='Editor pin code'
onMouseDown={() => setHidePin(false)}
onMouseUp={() => setHidePin(true)}
isDisabled={disableModal}
/>
<span className={style.notes}>(Read Only Value)</span>
</FormControl>
<FormControl id='port'>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.notes}>
<br />
App Control - Default 8888
</span>
</FormLabel>
<Input
size='sm'
name='port'
placeholder='8888'
autoComplete='off'
type='number'
value={formData.port}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
port: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'center' }}
/>
</FormControl>
<div className={style.modalInline}>
<FormControl id='targetIP' width='auto'>
<FormLabel htmlFor='targetIP'>
OSC Out Target IP
<span className={style.notes}>
<br />
App Feedback - Default 127.0.0.1
</span>
</FormLabel>
<Input
size='sm'
name='targetIP'
placeholder='127.0.0.1'
autoComplete='off'
value={formData.targetIP}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
targetIP: event.target.value,
});
}}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut' width='auto'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.notes}>
<br />
Default 9999
</span>
</FormLabel>
<Input
size='sm'
name='portOut'
placeholder='9999'
autoComplete='off'
type='number'
value={formData.portOut}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
portOut: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
>
Save
</Button>
</FormControl>
</div>
</ModalBody>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+133 -140
View File
@@ -1,31 +1,26 @@
import { ModalBody } from '@chakra-ui/modal';
import {
FormLabel,
FormControl,
Input,
Button,
Textarea,
} from '@chakra-ui/react';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { fetchEvent, postEvent } from 'app/api/eventApi';
import { useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { eventPlaceholderSettings } from '../../app/api/ontimeApi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function SettingsModal() {
const { data, status } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState({
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
});
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState(eventPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({
title: data.title,
@@ -34,8 +29,11 @@ export default function SettingsModal() {
backstageInfo: data.backstageInfo,
endMessage: data.endMessage,
});
}, [data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
@@ -46,133 +44,128 @@ export default function SettingsModal() {
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return (
<>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to the running event
<br />
Affects rendered views
</p>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}>
Options related to the running event
<br />
Affect rendered views
</p>
<FormControl id='title'>
<FormLabel htmlFor='title'>Event Title</FormLabel>
<Input
size='sm'
maxLength={35}
name='title'
placeholder='Event Title'
autoComplete='off'
value={formData.title}
onChange={(event) => {
setChanged(true);
setFormData({ ...formData, title: event.target.value });
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='url'>
<FormLabel htmlFor='url'>
Event URL
<span className={style.notes}>
(shown as a QR code in some views)
</span>
</FormLabel>
<Input
size='sm'
name='url'
placeholder='www.onsite.no'
autoComplete='off'
value={formData.url}
onChange={(event) => {
setChanged(true);
setFormData({ ...formData, url: event.target.value });
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='pubInfo'>
<FormLabel htmlFor='pubInfo'>Public Info</FormLabel>
<Textarea
size='sm'
name='pubInfo'
placeholder='Information to be shown on public screens'
autoComplete='off'
value={formData.publicInfo}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
publicInfo: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='backstageInfo'>
<FormLabel htmlFor='backstageInfo'>Backstage Info</FormLabel>
<Textarea
size='sm'
name='backstageInfo'
placeholder='Information to be shown on backstage screens'
autoComplete='off'
resize={false}
value={formData.backstageInfo}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
backstageInfo: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='endMessage'>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.notes}>
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
size='sm'
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
autoComplete='off'
value={formData.endMessage}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
endMessage: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
>
Save
</Button>
<div className={style.modalFields}>
<div className={style.hSeparator}>Event Data</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='title'>Event Title</FormLabel>
<Input
{...inputProps}
maxLength={35}
name='title'
placeholder='Event Title'
value={formData.title}
onChange={(event) => handleChange('title', event.target.value)}
/>
</div>
</ModalBody>
<div className={style.hSeparator}>Additional Screen Info</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='url'>
Event URL
<span className={style.labelNote}>
<br />
Shown as a QR code in some views
</span>
</FormLabel>
<Input
{...inputProps}
name='url'
placeholder='www.onsite.no'
value={formData.url}
onChange={(event) => handleChange('url', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='pubInfo'>
Public Info
<span className={style.labelNote}>
<br />
Information to be shown on public screens
</span>
</FormLabel>
<Textarea
{...inputProps}
name='pubInfo'
placeholder='Information to be shown on public screens'
value={formData.publicInfo}
onChange={(event) =>
handleChange('publicInfo', event.target.value)
}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='backstageInfo'>
Backstage Info
<span className={style.labelNote}>
<br />
Information to be shown on backstage screens
</span>
</FormLabel>
<Textarea
{...inputProps}
name='backstageInfo'
placeholder='Information to be shown on backstage screens'
resize={false}
value={formData.backstageInfo}
onChange={(event) =>
handleChange('backstageInfo', event.target.value)
}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) =>
handleChange('endMessage', event.target.value)
}
/>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
@@ -1,11 +1,5 @@
import { ModalBody } from '@chakra-ui/modal';
import {
FormLabel,
FormControl,
Input,
Button,
Switch,
} from '@chakra-ui/react';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import {
getInfo,
httpPlaceholder,
@@ -17,23 +11,23 @@ import { useFetch } from 'app/hooks/useFetch';
import { APP_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import { FiInfo } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function IntegrationSettingsModal() {
const { data, status } = useFetch(APP_TABLE, getInfo);
const { data, status, refetch } = useFetch(APP_TABLE, getInfo);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(httpPlaceholder);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const ready = status === 'success';
const integrationInputProps = {
size: 'sm',
autoComplete: 'off',
isDisabled: submitting || !ready,
};
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({
onLoad: data?.onLoad,
@@ -42,8 +36,11 @@ export default function IntegrationSettingsModal() {
onPause: data?.onPause,
onStop: data?.onStop,
});
}, [data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
@@ -54,283 +51,312 @@ export default function IntegrationSettingsModal() {
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
return;
} else {
await postInfo(f);
setChanged(false);
}
// Post here
postInfo(f);
setChanged(false);
setSubmitting(false);
};
return (
<>
<form onSubmit={submitHandler}>
<ModalBody
className={ready ? style.modalBody : style.modalBodyDisabled}
>
<>
<p className={style.notes}>
Integrate with third party over an HTTP API
<br />
🔥 Changes take effect after app restart 🔥
</p>
<div className={style.highNotes}>
<p>
Add HTTP messages that ontime will send during the app lifecycle
</p>
<p>
You can use the variables below to pass data directly from
ontime eg:
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=<b>$title</b>
&setSub=<b>$presenter</b>
</span>
</p>
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
<table>
// Todo: make change handler
// Todo: toggle between GET / POST
// Todo: add test button
// Todo: enabled should be button
// Todo: add friendly placeholder to input
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Integrate with third party over an HTTP API
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>Ontime event cycle</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<FiInfo color='#2b6cb0' fontSize={'2em'} />
Add HTTP messages that ontime will send during the event cycle
</span>
<span className={style.labelNote}>
You can use variables in the HTTP request URL to send data from
ontime
</span>
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=
<span className={style.labelNoteInline}>$title</span>
&setSub=<span className={style.labelNoteInline}>$presenter</span>
</span>
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Variable
</td>
<td className={style.labelNote}>Value</td>
</tr>
{ontimeVars.map((v) => (
<tr>
<td className={style.noteItem}>{v.name}</td>
<td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td>
</tr>
))}
</table>
</div>
<>
<FormLabel>
On Load
<span className={style.notes}>When a new event loads</span>
</FormLabel>
<FormControl id='onLoad' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onLoadURL'
value={formData?.onLoad?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onLoadEnable'
value={formData?.onLoad?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Start
<span className={style.notes}>
When an timer starts / resumes
</span>
</FormLabel>
<FormControl id='onStart' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onStartURL'
value={formData?.onStart?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStartEnable'
value={formData?.onStart?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Update
<span className={style.notes}>At every clock tick</span>
</FormLabel>
<FormControl id='onUpdate' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onUpdateURL'
value={formData?.onUpdate?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onUpdateEnable'
value={formData?.onUpdate?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Pause
<span className={style.notes}>When a timer pauses</span>
</FormLabel>
<FormControl id='onPause' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onPauseURL'
value={formData?.onPause?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onPauseEnable'
value={formData?.onPause?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Stop
<span className={style.notes}>When an event is unloaded</span>
</FormLabel>
<FormControl id='onStop' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onStopURL'
value={formData?.onStop?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStopEnable'
value={formData?.onStop?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Finish
<span className={style.notes}>When an event is finished</span>
</FormLabel>
<FormControl id='onFinish' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onFinishURL'
value={formData?.onFinish?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onFinish,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onFinishEnable'
value={formData?.onFinish?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
</>
</>
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || !ready}
>
Save
</Button>
</tbody>
</table>
</div>
</ModalBody>
<div className={style.hSeparator}>Send HTTP</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Load
<span className={style.labelNote}>
<br />
When a new event loads
</span>
</FormLabel>
<div className={style.modalInline}>
<Input
{...inputProps}
name='onLoadURL'
value={formData?.onLoad?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onLoadEnable'
value={formData?.onLoad?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
enabled: event.target.value,
},
});
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Start
<span className={style.labelNote}>
<br />
When an timer starts / resumes{' '}
</span>
</FormLabel>
<div className={style.modalInline}>
<Input
{...inputProps}
name='onStartURL'
value={formData?.onStart?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStartEnable'
value={formData?.onStart?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
enabled: event.target.value,
},
});
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Update
<span className={style.labelNote}>
<br />
At every clock tick
</span>
</FormLabel>
<FormControl id='onUpdate' className={style.modalInline}>
<Input
{...inputProps}
name='onUpdateURL'
value={formData?.onUpdate?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onUpdateEnable'
value={formData?.onUpdate?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Pause
<span className={style.labelNote}>
<br />
When a timer pauses
</span>
</FormLabel>
<FormControl id='onPause' className={style.modalInline}>
<Input
{...inputProps}
name='onPauseURL'
value={formData?.onPause?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onPauseEnable'
value={formData?.onPause?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Stop
<span className={style.labelNote}>
<br />
When an event is unloaded
</span>
</FormLabel>
<FormControl id='onStop' className={style.modalInline}>
<Input
{...inputProps}
name='onStopURL'
value={formData?.onStop?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStopEnable'
value={formData?.onStop?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Finish
<span className={style.labelNote}>
<br />
When an event is finished
</span>
</FormLabel>
<FormControl id='onFinish' className={style.modalInline}>
<Input
{...inputProps}
name='onFinishURL'
value={formData?.onFinish?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onFinish,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onFinishEnable'
value={formData?.onFinish?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+12 -6
View File
@@ -8,9 +8,10 @@ import {
} from '@chakra-ui/modal';
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
import EventSettingsModal from './EventSettingsModal';
import AppSettingsModal from './AppSettingsModal';
import OscSettingsModal from './OscSettingsModal';
import AliasesModal from './AliasesModal';
import IntegrationSettingsModal from './IntegrationSettingsModal';
import AppSettingsModal from './AppSettingsModal';
export default function ModalManager(props) {
const { isOpen, onClose } = props;
@@ -20,7 +21,8 @@ export default function ModalManager(props) {
onClose={onClose}
closeOnOverlayClick={false}
motionPreset={'slideInBottom'}
size='lg'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
@@ -29,21 +31,25 @@ export default function ModalManager(props) {
<Tabs size='sm' isLazy>
<TabList>
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
<Tab style={{ fontSize: '0.9em' }}>Application Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
</TabList>
<TabPanels>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel>
<AppSettingsModal />
</TabPanel>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel>
<AliasesModal />
</TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
{/*<TabPanel>*/}
{/* <IntegrationSettingsModal />*/}
{/*</TabPanel>*/}
+113 -36
View File
@@ -1,83 +1,155 @@
@use '../../styles/variables' as *;
@use '../../styles/main' as *;
//////////////////////////////////// main
.modalBody,
.modalBodyDisabled {
.modalBody {
font-weight: 400;
.notes {
font-weight: 400;
color: $light-bg;
display: grid;
place-items: center;
height: 4em;
}
.separator {
border: 1px solid $light-bg-transparent;
width: 50%;
margin: 0.5em auto;
.modalFields {
min-height: 45vh;
max-height: 45vh;
overflow-y: auto;
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
padding-right: 6px;
label {
//font-weight: 400;
font-size: 0.8em;
}
.inlineAlias,
.inlineAliasPlaceholder {
display: grid;
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
gap: 8px;
align-items: center;
}
.error {
font-size: 0.8em;
color: $error-red;
}
.inlineAliasPlaceholder {
grid-template-columns: 20% 1fr 4em;
.placeholder {
background: $light-text;
width: 100%;
height: 24px;
}
}
}
/* Track */
::-webkit-scrollbar-track {
background: rgba($light-bg, 0.15);
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba($light-bg, 0.35);
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba($light-bg, 0.45);
}
.modalInline {
display: flex;
gap: 2em;
align-items: center;
padding: 0 0.5em 0.5em 0.5em;
}
.spacedEntry {
padding: 0 0.5em 0.5em 0.5em;
}
.pin {
display: flex;
gap: 0.5em;
border-radius: 50%;
input {
border-radius: 50%;
}
}
.submitContainer {
margin-top: 2em;
display: flex;
flex-direction: row-reverse;
button {
margin-top: 1em;
}
justify-content: flex-end;
gap: 1em;
}
}
.modalBody > *,
.modalBodyDisabled > * {
.modalBody > * {
margin-top: 0.5em;
}
//////////////////////////////////// notes
p {
&.notes {
text-align: center;
border-color: $light-bg-transparent;
border-width: 0 2px;
font-size: 0.9em;
margin-bottom: 1em;
text-align: center;
border-color: $light-bg-transparent;
border-width: 0 2px;
font-size: 0.9em;
margin-bottom: 1em;
}
}
span {
&.notes {
font-size: 0.8em;
font-size: 0.9em;
padding-left: 0.4em;
}
}
.highNotes {
background-color: $light-text;
.blockNotes {
background-color: $bg-gray;
margin: 1em 0;
padding: 0.3em;
font-size: 0.9em;
padding: 0.5em;
font-size: 0.8em;
border-radius: 2px;
table {
background-color: $light-text;
background-color: #fff;
border-left: 4px solid lighten($ontime-pink, 5%);
width: 100%;
margin-top: 0.3em;
margin: 0.5em 0;
border-radius: 2px;
:first-child {
padding-left: 1em;
}
td {
user-select: text;
}
}
.noteItem {
user-select: text;
font-weight: 700;
font-weight: 600;
padding-right: 2em;
}
.flexNote {
user-select: text;
padding-bottom: 0.3em;
display: block;
}
.emNote {
@@ -87,13 +159,18 @@ span {
}
}
// Define style for a link
a {
&::after {
content: ' \2197';
color: $accent;
}
&:hover {
color: $accent;
}
.labelNote {
color: $light-bg;
padding-right: 1em;
}
.labelNoteInline {
color: $light-bg;
}
.inlineFlex {
display: flex;
gap: 1em;
align-items: center;
margin-bottom: 1em;
}
@@ -0,0 +1,168 @@
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import SubmitContainer from './SubmitContainer';
import { inputProps, portInputProps } from './modalHelper';
export default function OscSettingsModal() {
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({ ...data });
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
let e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
}
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
setChanged(false);
}
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {(string | number)} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to Open Sound Control
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>OSC Input (control)</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.labelNote}>
<br />
Open port for 3rd party control over OSC - Default 8888
</span>
</FormLabel>
<Input
{...portInputProps}
name='port'
placeholder='8888'
value={formData.port}
onChange={(event) =>
handleChange('port', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'center' }}
/>
</div>
<div className={style.hSeparator}>OSC Output (feedback)</div>
<div className={style.modalInline}>
<FormControl id='targetIP'>
<FormLabel htmlFor='targetIP'>
OSC Out Target IP
<span className={style.labelNote}>
<br />
Default 127.0.0.1
</span>
</FormLabel>
<Input
{...inputProps}
size='sm'
name='targetIP'
placeholder='127.0.0.1'
autoComplete='off'
value={formData.targetIP}
onChange={(event) =>
handleChange('targetIP', event.target.value)
}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.labelNote}>
<br />
Default 9999
</span>
</FormLabel>
<Input
{...portInputProps}
name='portOut'
placeholder='9999'
value={formData.portOut}
onChange={(event) =>
handleChange('portOut', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</ModalBody>
);
}
@@ -0,0 +1,35 @@
import style from './Modals.module.scss';
import { Button } from '@chakra-ui/button';
import PropTypes from 'prop-types';
export default function SubmitContainer(props) {
const { submitting, changed, revert, status } = props;
return (
<div className={style.submitContainer}>
<Button
type='submit'
isDisabled={submitting || !changed}
variant='ghosted'
onClick={() => revert()}
>
Revert
</Button>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || status !== 'success'}
>
Save
</Button>
</div>
);
}
SubmitContainer.propTypes = {
submitting: PropTypes.bool,
changed: PropTypes.bool,
status: PropTypes.string,
revert: PropTypes.func.isRequired,
};
+11
View File
@@ -0,0 +1,11 @@
export const inputProps = {
size: 'sm',
autoComplete: 'off',
};
export const portInputProps = {
...inputProps,
type: 'number',
min: '1024',
max: '65535',
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import { useFetch } from 'app/hooks/useFetch';
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
@@ -1,6 +1,7 @@
import { memo, useEffect, useState } from 'react';
import LowerClean from './LowerClean';
import LowerLines from './LowerLines';
import { useSearchParams } from 'react-router-dom';
const isEqual = require('react-fast-compare');
const areEqual = (prevProps, nextProps) => {
@@ -12,6 +13,7 @@ const areEqual = (prevProps, nextProps) => {
const Lower = (props) => {
const { title } = props;
const [searchParams,] = useSearchParams();
const [titles, setTitles] = useState({
titleNow: '',
titleNext: '',
@@ -61,61 +63,59 @@ const Lower = (props) => {
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
// Check for user options
useEffect(() => {
// get parameters
const params = new URLSearchParams(props.location.search);
// create aux
let options = {};
// preset: selector
// Should be a number 1-n
let p = parseInt(params.get('preset'));
let p = parseInt(searchParams.get('preset'));
if (!isNaN(p)) setPreset(p);
// size: multiplier
// Should be a number 0.0-n
let s = params.get('size');
let s = searchParams.get('size');
if (s) options.size = s;
// transitionIn: seconds
// Should be a number 0-n
let t = parseInt(params.get('transition'));
let t = parseInt(searchParams.get('transition'));
if (!isNaN(t)) options.transitionIn = t;
// textColour: string
// Should be a hex string '#ffffff'
let c = params.get('text');
let c = searchParams.get('text');
if (c) options.textColour = `#${c}`;
// bgColour: string
// Should be a hex string '#ffffff'
let b = params.get('bg');
let b = searchParams.get('bg');
if (b) options.bgColour = `#${b}`;
// key: string
// Should be a hex string '#00FF00' with key colour
let k = params.get('key');
let k = searchParams.get('key');
if (k) options.keyColour = `#${k}`;
// fadeOut: seconds
// Should be a number 0-n
let f = parseInt(params.get('fadeout'));
let f = parseInt(searchParams.get('fadeout'));
if (!isNaN(f)) options.fadeOut = f;
// x: pixels
// Should be a number 0-n
let x = parseInt(params.get('x'));
let x = parseInt(searchParams.get('x'));
if (!isNaN(x)) options.posX = x;
// y: pixels
// Should be a number 0-n
let y = parseInt(params.get('y'));
let y = parseInt(searchParams.get('y'));
if (!isNaN(y)) options.posY = y;
setLowerOptions({
...options,
set: true,
});
}, [props.location.search]);
}, [searchParams]);
// Defer rendering until we have data ready
if (!lowerOptions.set) return null;
+13 -3
View File
@@ -5,16 +5,26 @@ import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { AppContextProvider } from './app/context/AppContext';
import SocketProvider from './app/context/socketContext';
// Load Open Sans typeface
require('typeface-open-sans');
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<ChakraProvider resetCSS>
<BrowserRouter>
<App />
</BrowserRouter>
<SocketProvider>
<QueryClientProvider client={queryClient}>
<AppContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</SocketProvider>
</ChakraProvider>
</React.StrictMode>,
document.getElementById('root')
+44 -1
View File
@@ -1,4 +1,22 @@
//////////////////////////////////// general app style
//////////////////////////////////// general app colours
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
$bg-gray: #f4f4f8;
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$error-red: #E53E3E;
//////////////////////////////////// general app element overriders
// no decoration on lists
ul {
@@ -8,4 +26,29 @@ ul {
// no resizing on text areas
textarea {
resize: none !important;
}
// Define style for a link
a {
&::after {
content: ' \2197';
color: $ontime-pink;
}
&:hover {
color: $ontime-pink;
}
}
// horizontal separator
.hSeparator {
width: 100%;
border-bottom: 1px solid $light-text;
margin: 1em auto;
display: flex;
align-items: center;
}
// inline vertical separator
.vSpan {
margin: 0 0.5em;
}
+9
View File
@@ -0,0 +1,9 @@
//////////////////////////////////// general app elements
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
-4
View File
@@ -1,4 +0,0 @@
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$accent: #ff7597;