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
+8 -7
View File
@@ -3,26 +3,26 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.7.1",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@chakra-ui/react": "^1.7.3",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0",
"framer-motion": "^4.1.6",
"jotai": "^0.16.5",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-icons": "^4.3.1",
"react-qr-code": "^2.0.2",
"react-qr-code": "2.0.3",
"react-query": "^3.34.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"socket.io-client": "^4.3.2",
"socket.io-client": "4.4.0",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1"
@@ -31,6 +31,7 @@
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
},
"browserslist": {
+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;
+232 -282
View File
@@ -1786,7 +1786,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
@@ -1800,7 +1800,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.16.3":
"@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
@@ -1877,24 +1877,24 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@chakra-ui/accordion@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.1.tgz#bd5373dcfd11572e5e2c535eca51aee4fc24c828"
integrity sha512-/E0FW5YHNVD6WwMGiuQuXpA70P2CKAV+MzcMITnSGPWsh9XD0mcXvMkIALVojfFk9tcCFdIGnxX/HWr41LzgIg==
"@chakra-ui/accordion@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.2.tgz#e851d5e0f748646e20efde909b5f70a93679283b"
integrity sha512-BAGMvcm2sFE5Ft7jwC9nF03/Yv7qztuhzwKBBy4iL0p1nCPh6kV54RBXUcoj3VWe+yrmNiAVYKRTdqQBTJFwOw==
dependencies:
"@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/alert@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.1.tgz#d091b36ee4a7e3684220cd6f86fd2e08ca71cb2a"
integrity sha512-BeR6l/1CLZarA3uAe+5Q3hioYf7SixYfy9rOte/29ck1lx9PLjjuPYYmuDPtZNbGibhUCh48z4U/uK2x8mbpKQ==
"@chakra-ui/alert@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.2.tgz#6c038b0bd58f3964e0a3af213c162c108176a8b0"
integrity sha512-+OMeVeGtydpj6nry0zH7qFDt36zEaxckRnufx1BGiCfWdUg6ahVwKXl8qX93Q8w82od7eAoBKMgGJz7IVL5NPw==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -1950,18 +1950,18 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/close-button@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.1.tgz#1109665e73cddcb643bd66e208b2aac75e8af40f"
integrity sha512-A/cuFtJPF8rp5p6tCIGlQdHB89gLCSOzxWssoTXAGJnmlwY2YunFHxgkYZXwPbDqFrM8ndya7Ys+AuL1JZsa3g==
"@chakra-ui/close-button@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.2.tgz#43e91b34d51f75dc08ff9a56cbfc7ed5ad158b8a"
integrity sha512-SqeLib0qgMjK3OsO1g5OnAHUmdCC8GMjToNEea7TeSrA44bH9EXVhFTkMMu2PnDVHbQmi4Ee1cuulNJt0UhQ3g==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/color-mode@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.1.tgz#08402d28a9c5b4dd3e1a64f69faad319d55dc9d6"
integrity sha512-UuuYMfYxaBE5fPtIR6dwIJn3snT0tVX67RN2RHv4/LQgUeiddq4VziXT3bd+tyIe9+zcsqrZhV7J7YP0h/Id/Q==
"@chakra-ui/color-mode@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.2.tgz#e87f9c17ae891c426fa8c9dbdd3c2ba80669fa48"
integrity sha512-/rWcbrzbaWCyyUnT07Qjz0xf/ltHS31CHOKtVCWr2uTgfn2gOQpdxsKRbjrLYPOYZGTMdINUHNiAsqQjLoAoTQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-env" "1.1.1"
@@ -2011,13 +2011,13 @@
"@chakra-ui/utils" "1.9.1"
react-focus-lock "2.5.2"
"@chakra-ui/form-control@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.1.tgz#235267088026690d6994fa406c9f2618fa0351ec"
integrity sha512-ASZYQFOs5mAoaNXAN/ZaesMy3XV07F0/Eba5PQ7Dejdn91aep6lqF889hmr8yqcR646xCOY7ISyYsskfh9QHrQ==
"@chakra-ui/form-control@1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.2.tgz#4ce34f03165cfafacecabdce3be82011b74308a7"
integrity sha512-uWv0/f+JEM0ZE5Hnj3TzCnJ09EB+A+DSs9QgyECOuxx9Ju6gnns2uaRki2BfxksQL9ZZomPCkMtXazY9Wa81ag==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2031,10 +2031,10 @@
compute-scroll-into-view "1.0.14"
copy-to-clipboard "3.3.1"
"@chakra-ui/icon@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-1.2.1.tgz#b47c0016531da8bef81061d2b96a2d5282b07ff8"
integrity sha512-uZxFsiY4Tld+LvGIX7cky0H6oMRac8udPMQRzIk/UQeNZcsWisGetatbQsew3y1lWV/iH/8+TlDuW13GWGyGGQ==
"@chakra-ui/icon@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-2.0.0.tgz#a2468736117139f94c6ed65eb86b9838e554c90e"
integrity sha512-/GuU+xIcOIy9uSUUUCu249ZJB/nLDbjWGkfpoSdBwqT4+ytJrKt+0Ckh3Ub14sz3BJD+Z6IiIt6ySOA9+7lbsA==
dependencies:
"@chakra-ui/utils" "1.9.1"
@@ -2046,21 +2046,21 @@
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/input@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.1.tgz#a407242239f7bb5fdf3b237ab90355c7d77aff9e"
integrity sha512-Z+LqkwVPMeUBuvB9dLDPKkBnWV52Q1PVl3KW9ouDIFg7SoemeYkBt3p4ttEKE+eIPsPlrcH1u2A/RGcCTZOe1g==
"@chakra-ui/input@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.2.tgz#59d49ab5c8645c51cd591d46ca23d5cb95e03392"
integrity sha512-VMxmQgFiQ2UnBlkgLX/336G0IfYfw8YWF2ZoEFj5WL9kDSrrL1FXSBgjFGxrper74G4W20tESBCfU1S891y6cg==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/layout@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.5.1.tgz#be33e8fb5fb187f13c5c0ef66fe94cd293094da6"
integrity sha512-nKiyZ5adjNTbBV3oFIUGIPijwutO1NGdev1jHtnZc3xo2urCIkBvKU8+mVjlX04IwZ7oLKoP3EiDDv0g7+o41Q==
"@chakra-ui/layout@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.6.0.tgz#fa45d72420f01f4f6082f5a5a0c202bf7b09a059"
integrity sha512-WUfQ104y1wNueU33/hPlZsMzYJGjO0dXMpVkQf5ZNhNX3IGDO+5+MO2x2xloP+j45yNPi3p8ti/HBnm3dXI+3Q==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2071,51 +2071,51 @@
dependencies:
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/media-query@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.1.tgz#c1df05cbe8bd909a410d134240f9bceee760ec09"
integrity sha512-Ho/qiPGTjNukFTE9WBdYV9FIXU7KFTJPqdRQPWANkz+j275n6sqSE1j5LRJllP+ett21KeuWLN4zL33pP0Ox+g==
"@chakra-ui/media-query@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.2.tgz#46db9059de1367a3aa85c1fb973a78f95fca6cc5"
integrity sha512-xSmDVleE1drWiGH/MX3RqyVm29x/8Vf6G0UGaI2kCpbNmon+Q1zHW/yDHvptIuctLrPHYO8LOBxuUjfnIXwC2g==
dependencies:
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/menu@1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.1.tgz#ad26e12843914747de1c36228f4f45c2be668a4d"
integrity sha512-fgzzFukBj4sQzTRf4q/+nHiVTKhrMtJdofnluqce/SCRJ1G+bbovUySblTzfI8iFlTSZt/eWc/Nju4JB1S+3Yg==
"@chakra-ui/menu@1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.2.tgz#849f51beef6220667eb47c831ddf2c65919ded0e"
integrity sha512-u2GfkwTqbWa8L/7i/kOFbU3JANiT2HStR+gsYKuiuOPiuBcUb8OlgfJfP70OtVKegNKmVEMjvzXtld3wCCo/1g==
dependencies:
"@chakra-ui/clickable" "1.2.1"
"@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/modal@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.1.tgz#849243b6f700845ef04e8247d3326e3e0324b789"
integrity sha512-cboC2ITm+5FjhrBc6yJ5cW4VXnfwlLhFa1EkPqF1k4kvYGyUHArvPN1q8AiPYOIrupHYu2Iu6YmQPg7TJwNImg==
"@chakra-ui/modal@1.10.2":
version "1.10.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.2.tgz#578c30812c63863bef25f4b5c279cacbb34528ec"
integrity sha512-ZlmYetPHwHW4CAM09j4/+Ui54dXR1nzU6mOwhWe4/IzLvEyoEU6fHJeKyGxVUpYTG/7wltG/wKFRJpYa77tiBg==
dependencies:
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/focus-lock" "1.2.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.4.1"
"@chakra-ui/number-input@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.1.tgz#1c46b2748240c780617955da4f2f20a5414edf5c"
integrity sha512-4vBRSShT5pedElgP9YGVC+9RHzQGmUVZqu3p0gZW0fLGVVQ9C1EGrO7djL+k3tgklyu8RvSwkRDJqEPvbQKDgQ==
"@chakra-ui/number-input@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.2.tgz#580035e56a13c30b04034ab02cb9c1132a1cf09c"
integrity sha512-7x7AoqwPXU1odyDcqIwjBwf0MJUwYMM2fa+6YZ52F941GKlvkDiiJOhK6xfhhBzkLUQD6DN8zgAmmGhaZ6UQXw==
dependencies:
"@chakra-ui/counter" "1.2.1"
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2129,12 +2129,12 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/popover@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.10.1.tgz#03a172201d46bde73eeff7e40e141d82876df493"
integrity sha512-/dMUQfd+h9j3GBtkA/nYaQ5xeu4vk0psUClFvLOAJRwXGN3aMrzn/mhrvHWQ/cJuwQrO1WzxH2+g6pwsFOm9ng==
"@chakra-ui/popover@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.11.0.tgz#3ad9faf26019c750a5f8c619ee80dd97bcc4ee3e"
integrity sha512-cCHXAfhIRir+M0ehlYIjDw3mHpiCxDTJ9WV0H1zHQV8nDYVIlZw3nEntaq8oJrv0wpIzq2WCW5ss+bBR7nLZ1A==
dependencies:
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1"
@@ -2165,24 +2165,24 @@
"@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/provider@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.1.tgz#d49c91abd91df8cd2824255f10353a7c17b8df9e"
integrity sha512-E2z56BAYytg+sA7arxUtg54AzhYPpR5QUcR4L679aahgiod+5y6H5TSZcTnxTFRLXVLZbBbP/eH63seIcRqCRQ==
"@chakra-ui/provider@1.7.3":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.3.tgz#625c0b7c1265515399bb501bc929d8851bba2958"
integrity sha512-D1SrQ7do4yzAv9/OTF3yj/BkLm7kFo5DdeuOCyvXGpVJumnvbtjltRmC7rFQH4R+y9qXPvfQP4LKMNBqSxPNng==
dependencies:
"@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/radio@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.1.tgz#62de496295f30a6b264cc3906dcfd17bc85a0e85"
integrity sha512-Rq7goauffMNqwGVuPh4zh0kX0AuvTll66PhOwPu+GYHzsYO5+L9X875Q9QBnJEFtDjNF/MVIrH9WrKWjdWLqWw==
"@chakra-ui/radio@1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.3.tgz#d87788fe688d0d58252e9efeb926d0417a70e158"
integrity sha512-TQdyfdUD3BLklOP67n82JN8ksQv1BYjvaYsK0m6WCa0LDJr9aCC+XtUPgVq/1L2t4HqHdiGOrGBooF4vvy/+BA==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2202,81 +2202,81 @@
dependencies:
"@chakra-ui/utils" "^1.9.1"
"@chakra-ui/react@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.1.tgz#f88417edb4d08766bf102a8d8cc294cbd38443de"
integrity sha512-mbU6M/lxD9BCuXXSEcscPpWqR4mWxXyTk0vkWcgLG59lMFAZ4/Ll+rG1xRHaCuRsAndWNws8h4+NcxY0eECfLQ==
"@chakra-ui/react@^1.7.3":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.3.tgz#358a3ff9847b78a798384db5c64cc222c9a48ae8"
integrity sha512-6mrfDUOa9MoQ44Xvi7xgdDq48jTTTjW9BupCGf2R3DI+z6RbUKIHzbcoDJZt2HGY6j9EarMVNRoQJzvzGUKpoQ==
dependencies:
"@chakra-ui/accordion" "1.4.1"
"@chakra-ui/alert" "1.3.1"
"@chakra-ui/accordion" "1.4.2"
"@chakra-ui/alert" "1.3.2"
"@chakra-ui/avatar" "1.3.1"
"@chakra-ui/breadcrumb" "1.3.1"
"@chakra-ui/button" "1.5.1"
"@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/control-box" "1.1.1"
"@chakra-ui/counter" "1.2.1"
"@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/editable" "1.3.1"
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/image" "1.1.1"
"@chakra-ui/input" "1.3.1"
"@chakra-ui/layout" "1.5.1"
"@chakra-ui/input" "1.3.2"
"@chakra-ui/layout" "1.6.0"
"@chakra-ui/live-region" "1.1.1"
"@chakra-ui/media-query" "1.2.1"
"@chakra-ui/menu" "1.8.1"
"@chakra-ui/modal" "1.10.1"
"@chakra-ui/number-input" "1.3.1"
"@chakra-ui/media-query" "1.2.2"
"@chakra-ui/menu" "1.8.2"
"@chakra-ui/modal" "1.10.2"
"@chakra-ui/number-input" "1.3.2"
"@chakra-ui/pin-input" "1.7.1"
"@chakra-ui/popover" "1.10.1"
"@chakra-ui/popover" "1.11.0"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/progress" "1.2.1"
"@chakra-ui/provider" "1.7.1"
"@chakra-ui/radio" "1.4.1"
"@chakra-ui/provider" "1.7.3"
"@chakra-ui/radio" "1.4.3"
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/select" "1.2.1"
"@chakra-ui/skeleton" "1.2.1"
"@chakra-ui/slider" "1.5.1"
"@chakra-ui/select" "1.2.2"
"@chakra-ui/skeleton" "1.2.3"
"@chakra-ui/slider" "1.5.2"
"@chakra-ui/spinner" "1.2.1"
"@chakra-ui/stat" "1.2.1"
"@chakra-ui/stat" "1.2.2"
"@chakra-ui/switch" "1.3.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/table" "1.3.1"
"@chakra-ui/tabs" "1.6.1"
"@chakra-ui/tag" "1.2.1"
"@chakra-ui/textarea" "1.2.1"
"@chakra-ui/theme" "1.12.1"
"@chakra-ui/toast" "1.4.1"
"@chakra-ui/tooltip" "1.4.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/tag" "1.2.2"
"@chakra-ui/textarea" "1.2.2"
"@chakra-ui/theme" "1.12.2"
"@chakra-ui/toast" "1.5.0"
"@chakra-ui/tooltip" "1.4.2"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/select@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.1.tgz#a014c44efdc992ced96a4a74364148a4846c3d1f"
integrity sha512-GqRmYGjVnw/Z/2RQiW7Ywuu9O5E0spmMUBjeE/v0rqjixBqrmdApjg5pmJ4YmUMvUI/WkGtR3FR5W9Y5PpvfKw==
"@chakra-ui/select@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.2.tgz#1c2956c21a012e7c9ea294db42d3cab0d05bed24"
integrity sha512-EchJW3St1DtSWHe//DHwKjGsQYL2zbKcNCLnJWQKGMPZsQhAD2wsm4xjowFrV8AkY7jbVM/U2v68puN7YTC3hg==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/skeleton@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.1.tgz#6083aadb3ad62e8d63eb64d3985b00a1fdcd9746"
integrity sha512-08yOmINorbxtv1xgZpHiTE7YCYdicZ5dCkc7nCVxvYQihH5efkEiLbP3EefZzFCTqa2KFN8BvgD5eA76TqleXA==
"@chakra-ui/skeleton@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.3.tgz#f7be28e3af214473fb6cdec45d0715a049f82575"
integrity sha512-u5ASkzPiBjfvKxKuBienUfmyYDTHziSWQ8Ny6k83LbwLv9IcmBNGsSkmsp7hesgi9cMHGBQ3hY2GTqG9ljndIg==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/media-query" "1.2.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/media-query" "1.2.2"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/slider@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.1.tgz#2bb3ef0321d81577132a67ef2d0aa1f594d2242a"
integrity sha512-3C0DnUapVdVq9hU1VWAsk0qc6ws1TesjQl/KTkxlP4yBJxKAja1HtyeN9c1x4jizl1a1Q7BJrYICupy70Ju76A==
"@chakra-ui/slider@1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.2.tgz#173218b55befe0f7200fe7f332234b3824d19b08"
integrity sha512-zP07TMew61GkJe47Nu7zEg/SUEwPHpN4alW6VUM6Y8UaVpQaDx7InarbWTc/bXdTP03SfE+hQ6WD9Oy7noe4hQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1"
@@ -2290,19 +2290,19 @@
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/stat@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.1.tgz#76be860aa5ae7ef66717ab7a7fcf5862268a05e7"
integrity sha512-BTZFeh/8VdgUX080taCQj1g/rS4wGc+y3GQnklqlZ9N/bEv0gyLqQga7TFC/NkVl3cvjRiMnCCPj6vRih9x+Og==
"@chakra-ui/stat@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.2.tgz#4a690b9a01986a838eb38ff20ee9cfaccdff0cdd"
integrity sha512-0StsPDC56MjzhdlBl0R8wU0uwj9L1tvhQzge/ELSDn4tQDI7VovrxpFzVH0qsj7EZDwZa0BRQaSrstzWvgmJ/Q==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/styled-system@1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.14.1.tgz#78f49de1842a3675c44b17c9f392d7172880209a"
integrity sha512-dgXFYZdJicsddUnPV1X7lQksgMD0z5EvwGaIh2JHJERqNRIvth/CBAnVLQQvy/xSJK5YaSEmeuVVU0veUOQcXg==
"@chakra-ui/styled-system@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.15.0.tgz#fe117ace12b452379e4df69f4779395ead55917f"
integrity sha512-LnsKeiYkUuJ+NMTwueiX0Mj8CW9XAMJrJxpQm/X3GY5L5PO7Hv6wW725Ovqdy4mhG3IK7S8444FthpsDv/luHw==
dependencies:
"@chakra-ui/utils" "1.9.1"
csstype "^3.0.9"
@@ -2315,14 +2315,14 @@
"@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/system@1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.1.tgz#ce6c5af67840e20f261894c0310a0547314b262c"
integrity sha512-CFHdAjXuEDIAvNBatTBo1Tfu/HJ1LfbmjGWI4uTRfA4sDIt+cCRc/VKSfl0IKQwy70yJOMoTehZBjS8g3RTymQ==
"@chakra-ui/system@1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.3.tgz#bad447f4fda427fc28e234470d9fcc5a9b524d62"
integrity sha512-6MaevsT7A2ifgOGQQCQsfvzPVd0kEXqFrX1Oxd842bawaqthmbFdo2bBTdaia/+Ivq/8Xot2uAQSbU+3NuRiUA==
dependencies:
"@chakra-ui/color-mode" "1.3.1"
"@chakra-ui/color-mode" "1.3.2"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/styled-system" "1.14.1"
"@chakra-ui/styled-system" "1.15.0"
"@chakra-ui/utils" "1.9.1"
react-fast-compare "3.2.0"
@@ -2344,20 +2344,20 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/tag@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.1.tgz#2e5b7bd9895ece35d495b83bb29c34a3142cfe01"
integrity sha512-O068n+qBc+CSyvpRBJ6Lwep6SydQ9UysRqw1ETF+4fJSp9dMrBp8vOcl2SVacKaCu13qdv8UdRMBxUiTz3lh7A==
"@chakra-ui/tag@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.2.tgz#25519544cdbc0a32b2f8f44c7a8bae3bda912cb1"
integrity sha512-H25y9nEyUAUdwQDND9P4mMXKf1wf9UH4A3DyP237qVKIyYBpa4aCH8eJU4dunh2yIzASB0DWcr7lsul/HAHxmg==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/textarea@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.1.tgz#cf1594ce6496817530106f70a5d787c1a9e40f59"
integrity sha512-3xDsL1qQ+eY5r4GcRL4bg90vtV/xxVlw0Z3PFehFP5JW7VwXNZIRjauR/+HlOA8eYq0cF6ch2boR1GPso6rQtw==
"@chakra-ui/textarea@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.2.tgz#3e62612e4b1bc0724ba084b0b08b247ac2bbdbab"
integrity sha512-DoLdKxHk0DyrQDnj1la9wjl2AW3/SK62nfWDYLAm0ouFsw1VKPw9nU+Yyj0dPruQTzI19nLaYF26i97rtnT27g==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/theme-tools@1.3.1", "@chakra-ui/theme-tools@^1.3.1":
@@ -2368,32 +2368,32 @@
"@chakra-ui/utils" "1.9.1"
"@ctrl/tinycolor" "^3.4.0"
"@chakra-ui/theme@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.1.tgz#48a61fe5b2b0f8f422255d3049e16bb0b2dd6e60"
integrity sha512-8yDril3rSzv42eKR0x7KdnrpN1ubY0m6q37CVUADgtboJqoJwWWX2/hqkv8CX6WJf8ZwPwFL5QIwS2FPSGgi+g==
"@chakra-ui/theme@1.12.2":
version "1.12.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.2.tgz#191e168d83e7a66bc3e65be802ad4e4e2a9bbeb5"
integrity sha512-LVjSf16yYHD40ILrsDEd3idVQRvJSY7JY8lvTGWo2p6v+JQESWF+zXlYi9Le+TXRpZuFvJuuQ1SEvoqVwdcJ8Q==
dependencies:
"@chakra-ui/anatomy" "1.2.1"
"@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/toast@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.4.1.tgz#3e3885dd039e19f8ce60446f4843e0d8ec074b24"
integrity sha512-vzQkYwnGq2nx0bOKIQ6XpJaGzUwnWKmUjcVrz9NzGwVI4g93PS7+13515R0m1NrDp30132OeDXQ+tmQwCRRe6w==
"@chakra-ui/toast@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.5.0.tgz#c6456c0a4f24669e16946de318ad568e24a5d497"
integrity sha512-rTsFx/Qos5oVPN6aZMbT/wTxwZlFNSXQqrTpJYaRcRFQGzxIDDxmGkKYfPnyJjRP9i6EqynJhXEIyhMA0xO0dw==
dependencies:
"@chakra-ui/alert" "1.3.1"
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/alert" "1.3.2"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/theme" "1.12.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/theme" "1.12.2"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@reach/alert" "0.13.2"
"@chakra-ui/tooltip@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.1.tgz#21e148b7107a92abbf90c19a3eead718b875a544"
integrity sha512-KvTuqSqIpIgE+YNUwN7ONDRkSGR6SK9+dgSx2PfKy0Sel7UgDPVtxByuZ6tfJ9O1VTRYEdF9k+s6Gf8eRFQbNA==
"@chakra-ui/tooltip@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.2.tgz#fce61c7ad4c3e6e5833487ddf75f03c774913fb9"
integrity sha512-+wyYXG8qenKkFy2YSFfOBf3rlWADnu6S9EUxP+3Rmm78unOWXDuTJWzqy2QlXs2BwoQoifaz1LVwzmMb7WLVgQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
@@ -2402,10 +2402,10 @@
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/transition@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.1.tgz#be31c02a285d182cc9c6dcd95548f8f35de64952"
integrity sha512-s/VFucc6grNdP1bxw0oQLzy167gjAgyl/GiGH9nt54nioDEiSsvn70qKg7sjajNTvpoot+urQUdr4Qh+fIUFZQ==
"@chakra-ui/transition@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.2.tgz#de59833aa5727c0dd328b828145e4b0dabd005b3"
integrity sha512-S+BNmpErHlntl//uaqv0sJegzMsQms0OnJapeZaRsvZL4s1SVYrR8kMrXigkdpeh4lAUqGsLpQHPKkzaKGbBOw==
dependencies:
"@chakra-ui/utils" "1.9.1"
@@ -2454,16 +2454,16 @@
source-map "^0.5.7"
stylis "^4.0.3"
"@emotion/cache@^11.5.0":
version "11.5.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.5.0.tgz#a5eb78cbef8163939ee345e3ddf0af217b845e62"
integrity sha512-mAZ5QRpLriBtaj/k2qyrXwck6yeoz1V5lMt/jfj6igWU35yYlNKs2LziXVgvH81gnJZ+9QQNGelSsnuoAy6uIw==
"@emotion/cache@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.0.3"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.10"
stylis "4.0.13"
"@emotion/hash@^0.8.0":
version "0.8.0"
@@ -2477,10 +2477,10 @@
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/is-prop-valid@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.0.tgz#29ef6be1e946fb4739f9707def860f316f668cde"
integrity sha512-9RkilvXAufQHsSsjQ3PIzSns+pxuX4EW8EbGeSPjZMHuMx6z/MOzb9LpqNieQX4F3mre3NWS2+X3JNRHTQztUQ==
"@emotion/is-prop-valid@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134"
integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==
dependencies:
"@emotion/memoize" "^0.7.4"
@@ -2494,15 +2494,15 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/react@^11.5.0":
version "11.5.0"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.5.0.tgz#19b5771bbfbda5e8517e948a2d9064810f0022bd"
integrity sha512-MYq/bzp3rYbee4EMBORCn4duPQfgpiEB5XzrZEBnUZAL80Qdfr7CEv/T80jwaTl/dnZmt9SnTa8NkTrwFNpLlw==
"@emotion/react@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07"
integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==
dependencies:
"@babel/runtime" "^7.13.10"
"@emotion/cache" "^11.5.0"
"@emotion/cache" "^11.7.1"
"@emotion/serialize" "^1.0.2"
"@emotion/sheet" "^1.0.3"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
@@ -2518,19 +2518,19 @@
"@emotion/utils" "^1.0.0"
csstype "^3.0.2"
"@emotion/sheet@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.3.tgz#00c326cd7985c5ccb8fe2c1b592886579dcfab8f"
integrity sha512-YoX5GyQ4db7LpbmXHMuc8kebtBGP6nZfRC5Z13OKJMixBEwdZrJ914D6yJv/P+ZH/YY3F5s89NYX2hlZAf3SRQ==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/styled@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.3.0.tgz#d63ee00537dfb6ff612e31b0e915c5cf9925a207"
integrity sha512-fUoLcN3BfMiLlRhJ8CuPUMEyKkLEoM+n+UyAbnqGEsCd5IzKQ7VQFLtzpJOaCD2/VR2+1hXQTnSZXVJeiTNltA==
"@emotion/styled@^11.6.0":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==
dependencies:
"@babel/runtime" "^7.13.10"
"@emotion/babel-plugin" "^11.3.0"
"@emotion/is-prop-valid" "^1.1.0"
"@emotion/is-prop-valid" "^1.1.1"
"@emotion/serialize" "^1.0.2"
"@emotion/utils" "^1.0.0"
@@ -5301,10 +5301,10 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
engine.io-client@~6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.0.2.tgz#ccfc059051e65ca63845e65929184757754cc34e"
integrity sha512-cAep9lhZV6Q8jMXx3TNSU5cydMzMed8/O7Tz5uzyqZvpNPtQ3WQXrLYGADxlsuaFmOLN7wZLmT7ImiFhUOku8g==
engine.io-client@~6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.1.1.tgz#800d4b9db5487d169686729e5bd887afa78d36b0"
integrity sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==
dependencies:
"@socket.io/component-emitter" "~3.0.0"
debug "~4.3.1"
@@ -6306,19 +6306,14 @@ hey-listen@^1.0.8:
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
history@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c"
integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
"@babel/runtime" "^7.7.6"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6853,11 +6848,6 @@ is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -7670,7 +7660,7 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -7832,14 +7822,6 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-create-react-context@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
dependencies:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-css-extract-plugin@^2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef"
@@ -8133,6 +8115,10 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
open@^8.0.9, open@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
@@ -8323,13 +8309,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -9220,15 +9199,15 @@ react-icons@^4.3.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-code@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8"
integrity sha512-73VGe81MgeE5FJNFgdY42ez/wPPJTHuooU3iE4CX+6F8M88O1Gg4zNA0L4bKEpoySQ0QjqreJgyXjFrG/QfsdA==
react-qr-code@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.3.tgz#cc80785e08f817d1ab066ca4035262f77d049648"
integrity sha512-6GDH0l53lksf2JgZwwcoS0D60a1OAal/GQRyNFkMBW19HjSqvtD5S20scmSQsKl+BgWM85Wd5DCcUYoHd+PZnQ==
dependencies:
prop-types "^15.7.2"
qr.js "0.0.0"
@@ -9278,34 +9257,20 @@ react-remove-scroll@2.4.1:
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
react-router-dom@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec"
integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
history "^5.2.0"
react-router "6.2.1"
react-router@5.2.0, react-router@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
react-router@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3"
integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
history "^5.2.0"
react-scripts@5.0.0:
version "5.0.0"
@@ -9577,11 +9542,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url-loader@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57"
@@ -9903,15 +9863,15 @@ slash@^4.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
socket.io-client@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.3.2.tgz#9cfdb8fecac8a24d5723daf8c8749e70c8fdeb25"
integrity sha512-2B9LqSunN60yV8F7S84CCEEcgbYNfrn7ejIInZtLZ7ppWtiX8rGZAjvdCvbnC8bqo/9RlCNOUsORLyskxSFP1g==
socket.io-client@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.4.0.tgz#d6568ebd79ac12e2d6b628e7e90e60f1d48d99ff"
integrity sha512-g7riSEJXi7qCFImPow98oT8X++MSsHz6MMFRXkWNJ6uEROSHOa3kxdrsYWMq85dO+09CFMkcqlpjvbVXQl4z6g==
dependencies:
"@socket.io/component-emitter" "~3.0.0"
backo2 "~1.0.2"
debug "~4.3.2"
engine.io-client "~6.0.1"
engine.io-client "~6.1.1"
parseuri "0.0.6"
socket.io-parser "~4.1.1"
@@ -10219,10 +10179,10 @@ stylehacks@^5.0.1:
browserslist "^4.16.0"
postcss-selector-parser "^6.0.4"
stylis@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
stylis@4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@^4.0.3:
version "4.0.9"
@@ -10409,16 +10369,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -10729,11 +10684,6 @@ v8-to-istanbul@^8.1.0:
convert-source-map "^1.6.0"
source-map "^0.7.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"