From 2fa397548a5d3323debd87f7cd5dcc473a92c19a Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 4 Jan 2022 22:12:59 +0100 Subject: [PATCH] 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 --- .github/workflows/ontime_cy.yml | 64 ++ .prettierrc | 7 + README.md | 13 +- client/package.json | 15 +- client/src/App.jsx | 90 +-- client/src/_main.scss | 15 - client/src/app/api/apiConstants.js | 2 + client/src/app/api/ontimeApi.js | 30 + client/src/app/appConstants.js | 14 + client/src/app/context/AppContext.js | 37 ++ client/src/app/context/LoggingContext.js | 4 +- client/src/app/userConfig.js | 7 - client/src/app/utils.js | 2 - .../src/app/utils/__tests__/aliases.test.js | 27 + client/src/app/utils/__tests__/math.test.js | 21 + client/src/app/utils/aliases.js | 29 + client/src/app/utils/math.js | 9 + .../eventTimes/EventTimesVertical.jsx | 2 +- .../myProgressBar/MyProgressBar.jsx | 2 +- client/src/common/components/nav/NavLogo.jsx | 8 +- .../components/protectRoute/ProtectRoute.jsx | 67 ++ .../protectRoute/ProtectRoute.module.scss | 47 ++ .../src/common/components/views/TodayItem.jsx | 2 +- client/src/common/input/EditableTimer.jsx | 2 +- client/src/common/utils/eventsManager.js | 2 +- client/src/common/utils/handleLink.js | 13 + client/src/features/control/PlaybackTimer.jsx | 2 +- client/src/features/editors/Editor.jsx | 6 +- client/src/features/info/Info.module.scss | 3 +- .../src/features/info/InfoLogger.module.scss | 3 +- client/src/features/info/InfoNif.jsx | 9 +- client/src/features/menu/MenuBar.jsx | 29 +- client/src/features/menu/MenuBar.module.css | 3 - client/src/features/menu/MenuBar.module.scss | 9 + client/src/features/modals/AliasesModal.jsx | 441 ++++++++----- .../src/features/modals/AppSettingsModal.jsx | 275 ++++---- .../features/modals/EventSettingsModal.jsx | 273 ++++---- .../modals/IntegrationSettingsModal.jsx | 588 +++++++++--------- client/src/features/modals/ModalManager.jsx | 18 +- client/src/features/modals/Modals.module.scss | 149 +++-- .../src/features/modals/OscSettingsModal.jsx | 168 +++++ .../src/features/modals/SubmitContainer.jsx | 35 ++ client/src/features/modals/modalHelper.js | 11 + client/src/features/viewers/ViewWrapper.jsx | 2 +- .../viewers/production/lower/LowerWrapper.jsx | 24 +- client/src/index.js | 16 +- client/src/styles/_main.scss | 45 +- client/src/styles/_mixins.scss | 9 + client/src/styles/_variables.scss | 4 - client/yarn.lock | 514 +++++++-------- server/data/db.json | 23 +- server/package.json | 18 +- server/src/app.js | 16 +- server/src/classes/EventTimer.js | 23 +- server/src/classes/Timer.js | 2 +- .../src/classes/__tests__/eventtimer.test.js | 43 +- server/src/controllers/eventsController.js | 2 +- server/src/controllers/ontimeController.js | 76 +++ server/src/models/aliasDefinition.js | 5 + server/src/models/dataModel.js | 10 +- server/src/package.json | 9 +- server/src/routes/ontimeRouter.js | 17 + .../src/utils/__tests__/generate_id.test.js | 20 - .../src/utils/__tests__/getRandomName.test.js | 16 +- server/src/utils/__tests__/parser.tests.js | 63 +- server/src/utils/parser.js | 338 ++++++---- server/src/yarn.lock | 50 +- server/utils/__tests__/generate_id.test.js | 28 + .../{src => }/utils/__tests__/time.tests.js | 4 +- server/{src => }/utils/generate_id.js | 0 server/utils/package.json | 11 + server/utils/time.js | 67 ++ server/utils/yarn.lock | 8 + server/yarn.lock | 4 +- 74 files changed, 2592 insertions(+), 1428 deletions(-) create mode 100644 .github/workflows/ontime_cy.yml create mode 100644 .prettierrc delete mode 100644 client/src/_main.scss create mode 100644 client/src/app/appConstants.js create mode 100644 client/src/app/context/AppContext.js delete mode 100644 client/src/app/userConfig.js delete mode 100644 client/src/app/utils.js create mode 100644 client/src/app/utils/__tests__/aliases.test.js create mode 100644 client/src/app/utils/__tests__/math.test.js create mode 100644 client/src/app/utils/aliases.js create mode 100644 client/src/app/utils/math.js create mode 100644 client/src/common/components/protectRoute/ProtectRoute.jsx create mode 100644 client/src/common/components/protectRoute/ProtectRoute.module.scss create mode 100644 client/src/common/utils/handleLink.js delete mode 100644 client/src/features/menu/MenuBar.module.css create mode 100644 client/src/features/menu/MenuBar.module.scss create mode 100644 client/src/features/modals/OscSettingsModal.jsx create mode 100644 client/src/features/modals/SubmitContainer.jsx create mode 100644 client/src/features/modals/modalHelper.js create mode 100644 client/src/styles/_mixins.scss delete mode 100644 client/src/styles/_variables.scss create mode 100644 server/src/models/aliasDefinition.js delete mode 100644 server/src/utils/__tests__/generate_id.test.js create mode 100644 server/utils/__tests__/generate_id.test.js rename server/{src => }/utils/__tests__/time.tests.js (95%) rename server/{src => }/utils/generate_id.js (100%) create mode 100644 server/utils/package.json create mode 100644 server/utils/time.js create mode 100644 server/utils/yarn.lock diff --git a/.github/workflows/ontime_cy.yml b/.github/workflows/ontime_cy.yml new file mode 100644 index 000000000..3eb4a7cf1 --- /dev/null +++ b/.github/workflows/ontime_cy.yml @@ -0,0 +1,64 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: ontime_test_CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + env: + CI: '' + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '14.x' + + # Utils server + - name: Utils - Install dependencies + run: yarn install + working-directory: ./server/utils + + - name: Utils - run link command + run: yarn link + working-directory: ./server/utils + + # React + - name: React - Link to utils + run: yarn link ontime-utils + working-directory: ./client + + - name: React - Install dependencies + run: yarn install + working-directory: ./client + + - name: React - Run tests + run: yarn test:pipeline + working-directory: ./client + + - name: React - Build project + run: yarn build + working-directory: ./client + + # Node server + - name: React - Link to utils + run: yarn link ontime-utils + working-directory: ./server/src + + - name: Server - Install dependencies + run: yarn install + working-directory: ./server/src + + # App + - name: Electron - Install dependencies + run: yarn install + working-directory: ./server + + - name: Electron - Run tests + run: yarn test + working-directory: ./server diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..7dbcef92e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true +} \ No newline at end of file diff --git a/README.md b/README.md index 1c5e7667f..06cb2c3b4 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,11 @@ From here, any device in the same network with a browser is able to render the v You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden). In case of unattended machines or automations, it is possible to use different URL to recall individual views +and extend with using the URL aliases feature ``` +For the presentation views... +------------------------------------------------------------- IP.ADDRESS:4001 > Web server default to presenter timer view IP.ADDRESS:4001/preseter > Presenter / Stage timer view IP.ADDRESS:4001/sm > Stage Manager / Backstage view @@ -30,6 +33,11 @@ IP.ADDRESS:4001/public > Public / Foyer view IP.ADDRESS:4001/pip > Picture in Picture view IP.ADDRESS:4001/lower > Lower Thirds IP.ADDRESS:4001/studio > Studio Clock + +...and for the editor (the control interface, same as the app) +------------------------------------------------------------- +IP.ADDRESS:4001/studio > Studio Clock + ``` More documentation available [here](https://cpvalente.gitbook.io/ontime/) @@ -47,6 +55,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/) - [x] Open Sound Control (OSC) Control and Feedback - [x] Roll mode: run independently using the system clock - [x] Import event list from Excel +- [x] URL Aliases (define configurable aliases to ease onsite setup) +- [x] Logging view +- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app) ## Unopinionated We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work. @@ -77,8 +88,6 @@ These will be implemented in a development friendly order unless there is user d - [ ] Companion module - [ ] Lower Third Manager - [ ] Note only event -- [ ] URL Aliases (define configurable aliases to ease onsite setup) -- [ ] Logging view - [ ] Reach Schedule: way to speedup timer to meet a deadline - [ ] vMix integration diff --git a/client/package.json b/client/package.json index 15eaa764b..e64adc4bc 100644 --- a/client/package.json +++ b/client/package.json @@ -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": { diff --git a/client/src/App.jsx b/client/src/App.jsx index fef3e7a31..ac14536c4 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -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 ( - - -
- - - - - - - - - - - - - - {/* Lower cannot have fallback */} - - {/* Send to default if nothing found */} - - - - -
-
-
+
+ + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/*/!* Lower cannot have fallback *!/*/} + } /> + {/*/!* Protected Routes *!/*/} + + + + } + /> + {/* Send to default if nothing found */} + } /> + + + +
); } diff --git a/client/src/_main.scss b/client/src/_main.scss deleted file mode 100644 index 83da3237e..000000000 --- a/client/src/_main.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/client/src/app/api/apiConstants.js b/client/src/app/api/apiConstants.js index 2fbdadb91..0ca3d29c4 100644 --- a/client/src/app/api/apiConstants.js +++ b/client/src/app/api/apiConstants.js @@ -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}/`); diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js index 91fdce1f5..26331631f 100644 --- a/client/src/app/api/ontimeApi.js +++ b/client/src/app/api/ontimeApi.js @@ -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; diff --git a/client/src/app/appConstants.js b/client/src/app/appConstants.js new file mode 100644 index 000000000..b74f6778a --- /dev/null +++ b/client/src/app/appConstants.js @@ -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' } +]; \ No newline at end of file diff --git a/client/src/app/context/AppContext.js b/client/src/app/context/AppContext.js new file mode 100644 index 000000000..a2d39bc96 --- /dev/null +++ b/client/src/app/context/AppContext.js @@ -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 ( + + {props.children} + + ); +}; \ No newline at end of file diff --git a/client/src/app/context/LoggingContext.js b/client/src/app/context/LoggingContext.js index 6f85ee39c..9e616ae27 100644 --- a/client/src/app/context/LoggingContext.js +++ b/client/src/app/context/LoggingContext.js @@ -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: [], diff --git a/client/src/app/userConfig.js b/client/src/app/userConfig.js deleted file mode 100644 index ed0daa965..000000000 --- a/client/src/app/userConfig.js +++ /dev/null @@ -1,7 +0,0 @@ -export const userConfig = { - timerColorOnPause: '#555', - timerColorOnRunning: '#FFF', - timerColorOnMessage: '#CCC', - timerColorOnTimeOver: '#F00', - overTimeText: '', -} \ No newline at end of file diff --git a/client/src/app/utils.js b/client/src/app/utils.js deleted file mode 100644 index 6eb270605..000000000 --- a/client/src/app/utils.js +++ /dev/null @@ -1,2 +0,0 @@ -export const clamp = (num, a, b) => - Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)); diff --git a/client/src/app/utils/__tests__/aliases.test.js b/client/src/app/utils/__tests__/aliases.test.js new file mode 100644 index 000000000..618aab7b1 --- /dev/null +++ b/client/src/app/utils/__tests__/aliases.test.js @@ -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(); + }) + ) + ); +}); \ No newline at end of file diff --git a/client/src/app/utils/__tests__/math.test.js b/client/src/app/utils/__tests__/math.test.js new file mode 100644 index 000000000..9b1ee536f --- /dev/null +++ b/client/src/app/utils/__tests__/math.test.js @@ -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) + )); +}); \ No newline at end of file diff --git a/client/src/app/utils/aliases.js b/client/src/app/utils/aliases.js new file mode 100644 index 000000000..a7b737291 --- /dev/null +++ b/client/src/app/utils/aliases.js @@ -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; +}; \ No newline at end of file diff --git a/client/src/app/utils/math.js b/client/src/app/utils/math.js new file mode 100644 index 000000000..c7a6bc5f8 --- /dev/null +++ b/client/src/app/utils/math.js @@ -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)); diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx index 7a796c294..5081b7aee 100644 --- a/client/src/common/components/eventTimes/EventTimesVertical.jsx +++ b/client/src/common/components/eventTimes/EventTimesVertical.jsx @@ -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'; diff --git a/client/src/common/components/myProgressBar/MyProgressBar.jsx b/client/src/common/components/myProgressBar/MyProgressBar.jsx index e7006cdaa..6a2422e02 100644 --- a/client/src/common/components/myProgressBar/MyProgressBar.jsx +++ b/client/src/common/components/myProgressBar/MyProgressBar.jsx @@ -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) { diff --git a/client/src/common/components/nav/NavLogo.jsx b/client/src/common/components/nav/NavLogo.jsx index 4497624ef..14264ee28 100644 --- a/client/src/common/components/nav/NavLogo.jsx +++ b/client/src/common/components/nav/NavLogo.jsx @@ -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={() => } > Presenter @@ -65,7 +64,6 @@ export default function NavLogo(props) { to='/sm' className={style.navItem} tabIndex={2} - onKeyDownCapture={() => } > Backstage @@ -73,7 +71,6 @@ export default function NavLogo(props) { to='/public' className={style.navItem} tabIndex={3} - onKeyDownCapture={() => } > Public @@ -81,7 +78,6 @@ export default function NavLogo(props) { to='/lower' className={style.navItem} tabIndex={4} - onKeyDownCapture={() => } > Lower Thirds @@ -89,7 +85,6 @@ export default function NavLogo(props) { to='/pip' className={style.navItem} tabIndex={4} - onKeyDownCapture={() => } > PIP @@ -97,7 +92,6 @@ export default function NavLogo(props) { to='/studio' className={style.navItem} tabIndex={5} - onKeyDownCapture={() => } > Studio Clock diff --git a/client/src/common/components/protectRoute/ProtectRoute.jsx b/client/src/common/components/protectRoute/ProtectRoute.jsx new file mode 100644 index 000000000..fcad4e854 --- /dev/null +++ b/client/src/common/components/protectRoute/ProtectRoute.jsx @@ -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 ? ( +
+ ontime +
+ { + setFailed(false); + setPin(value); + }} + > + + + + + + } + style={{ fontSize: '1.5em' }} + onClick={() => handleValidation()} + /> +
+
+ ) : ( + props.children + )} + + ); +} + +ProtectRoute.propTypes = { + children: PropTypes.node.isRequired +}; diff --git a/client/src/common/components/protectRoute/ProtectRoute.module.scss b/client/src/common/components/protectRoute/ProtectRoute.module.scss new file mode 100644 index 000000000..0b9118d31 --- /dev/null +++ b/client/src/common/components/protectRoute/ProtectRoute.module.scss @@ -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); + } +} diff --git a/client/src/common/components/views/TodayItem.jsx b/client/src/common/components/views/TodayItem.jsx index 5a7ca28df..c1a4d2413 100644 --- a/client/src/common/components/views/TodayItem.jsx +++ b/client/src/common/components/views/TodayItem.jsx @@ -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; diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx index c7f6c5c14..8de5dbbc7 100644 --- a/client/src/common/input/EditableTimer.jsx +++ b/client/src/common/input/EditableTimer.jsx @@ -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'; diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js index 5e861e9ff..b22c45f85 100644 --- a/client/src/common/utils/eventsManager.js +++ b/client/src/common/utils/eventsManager.js @@ -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 diff --git a/client/src/common/utils/handleLink.js b/client/src/common/utils/handleLink.js new file mode 100644 index 000000000..dcceb3e23 --- /dev/null +++ b/client/src/common/utils/handleLink.js @@ -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); + } +} \ No newline at end of file diff --git a/client/src/features/control/PlaybackTimer.jsx b/client/src/features/control/PlaybackTimer.jsx index 635646ecb..fe5118187 100644 --- a/client/src/features/control/PlaybackTimer.jsx +++ b/client/src/features/control/PlaybackTimer.jsx @@ -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'; diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx index 8ce7224f4..d2c20f0ea 100644 --- a/client/src/features/editors/Editor.jsx +++ b/client/src/features/editors/Editor.jsx @@ -24,12 +24,14 @@ export default function Editor() { return ( - + + +
- + diff --git a/client/src/features/info/Info.module.scss b/client/src/features/info/Info.module.scss index 6d5d2e3da..337c053f9 100644 --- a/client/src/features/info/Info.module.scss +++ b/client/src/features/info/Info.module.scss @@ -1,4 +1,5 @@ -@use '../../main' as *; +@use '../../styles/main' as *; +@use '../../styles/mixins' as *; @mixin container { margin-top: 1em; diff --git a/client/src/features/info/InfoLogger.module.scss b/client/src/features/info/InfoLogger.module.scss index 51087c484..0ec7ec283 100644 --- a/client/src/features/info/InfoLogger.module.scss +++ b/client/src/features/info/InfoLogger.module.scss @@ -1,5 +1,6 @@ @use 'Info.module' as *; -@use '../../main' as *; +@use '../../styles/main' as *; +@use '../../styles/mixins' as *; .container, .container__expanded{ diff --git a/client/src/features/info/InfoNif.jsx b/client/src/features/info/InfoNif.jsx index 647c35176..5548bcb1a 100644 --- a/client/src/features/info/InfoNif.jsx +++ b/client/src/features/info/InfoNif.jsx @@ -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 (
setCollapsed((c) => !c)}/> diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx index b902bebff..9027773fb 100644 --- a/client/src/features/menu/MenuBar.jsx +++ b/client/src/features/menu/MenuBar.jsx @@ -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) { <> handleIPC('shutdown')} /> handleIPC('max')} /> handleIPC('min')} />
handleIPC('help')} />
); } + +MenuBar.propTypes = { + isOpen: PropTypes.bool.isRequired, + onOpen: PropTypes.func.isRequired, +}; + diff --git a/client/src/features/menu/MenuBar.module.css b/client/src/features/menu/MenuBar.module.css deleted file mode 100644 index 00d82cfe7..000000000 --- a/client/src/features/menu/MenuBar.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.gap { - height: 1em; -} diff --git a/client/src/features/menu/MenuBar.module.scss b/client/src/features/menu/MenuBar.module.scss new file mode 100644 index 000000000..a11bf57e7 --- /dev/null +++ b/client/src/features/menu/MenuBar.module.scss @@ -0,0 +1,9 @@ +@use '../../styles/main' as *; + +.gap { + height: 1em; +} + +.open { + background: $light-bg; +} \ No newline at end of file diff --git a/client/src/features/modals/AliasesModal.jsx b/client/src/features/modals/AliasesModal.jsx index 8a9663ee1..9ee5d77c7 100644 --- a/client/src/features/modals/AliasesModal.jsx +++ b/client/src/features/modals/AliasesModal.jsx @@ -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 ( - <> + +

+ Configure easy to use URL Aliases +
+ 🔥 Changes take effect on save 🔥 +

- -

- Configure easy to use URL Aliases +

+
Default URLs
+ +
Custom Aliases
+
+ + + URL aliases are useful in two main scenarios + + Complicated URLs
- ❄️ Feature is not yet implemented ❄️ -

- - Default URLs - -
-

- Presenter Screen
- - {speakerLink} - -

-

- Backstage / Stage Manager Screen
- - {smLink} - -

-

- Public / Foyer Screen
- - {publicLink} - -

-

- Picture in Picture Screen
- - {pipLink} - -

-

- Studio Clock
- - {studioLink} - -

+ eg. a lower third url with some custom parameters + + + + + + + + + + + +
+ Alias + Page URL
mylowerlower?bg=ff2&text=f00&size=0.6&transition=5
+
+ + URLs to be changed dynamically + +
+ eg. an unattended screen that you would need to change route from + the app + + + + + + + + + + + +
+ Alias + Page URL
thirdfloorpublic
+
+ Alias + Page URL +
+ {aliases.map((alias, index) => ( + + ))} - Manage custom aliases -
- { - // Nothing here yet - }} - isDisabled={true} - /> - { - // Nothing here yet - }} - isDisabled={true} - /> - } - colorScheme='red' - disabled - /> -
-
-
- { - // Nothing here yet - }} - isDisabled={true} - /> - { - // Nothing here yet - }} - isDisabled={true} - /> - } - colorScheme='blue' - disabled - /> -
-
+
- +
+ - + ); } diff --git a/client/src/features/modals/AppSettingsModal.jsx b/client/src/features/modals/AppSettingsModal.jsx index 03ed5f975..44fd28bcf 100644 --- a/client/src/features/modals/AppSettingsModal.jsx +++ b/client/src/features/modals/AppSettingsModal.jsx @@ -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 ( - <> -
- - {status === 'success' && ( - <> -

- Options related to the application -
- 🔥 Changes take effect after app restart 🔥 -

+ /** + * Reverts local state equals to server state + */ + const revert = async () => { + setChanged(false); + await refetch(); + }; - - - Viewer Port - Port to access viewers - - { + const temp = { ...formData }; + temp[field] = value; + setFormData(temp); + setChanged(true); + }; + + const disableModal = status !== 'success'; + + return ( + +

+ Options related to the application +
+ 🔥 Changes take effect after app restart 🔥 +

+ +
+
General App Settings
+
+ + + Viewer Port + +
+ Ontime is available at port +
+
+ +
+ + + Editor Pincode + +
+ Protect the editor with a Pincode +
+
+
+ handleChange('pinCode', value)} + > + + + + + + } + aria-label='Editor pin code' + onMouseDown={() => setHidePin(false)} + onMouseUp={() => setHidePin(true)} + isDisabled={disableModal} /> - (Read Only Value) - - - - OSC In Port - -
- App Control - Default 8888 -
-
- { - setChanged(true); - setFormData({ - ...formData, - port: parseInt(event.target.value), - }); - }} - isDisabled={submitting} - style={{ width: '6em', textAlign: 'center' }} - /> -
-
- - - OSC Out Target IP - -
- App Feedback - Default 127.0.0.1 -
-
- { - setChanged(true); - setFormData({ - ...formData, - targetIP: event.target.value, - }); - }} - isDisabled={submitting} - style={{ width: '12em', textAlign: 'right' }} - /> -
- - - OSC Out Port - -
- Default 9999 -
-
- { - setChanged(true); - setFormData({ - ...formData, - portOut: parseInt(event.target.value), - }); - }} - isDisabled={submitting} - style={{ width: '6em', textAlign: 'left' }} - /> -
- - )} -
- +
- +
+ - + ); } diff --git a/client/src/features/modals/EventSettingsModal.jsx b/client/src/features/modals/EventSettingsModal.jsx index 472995b37..b96a19bb2 100644 --- a/client/src/features/modals/EventSettingsModal.jsx +++ b/client/src/features/modals/EventSettingsModal.jsx @@ -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 ( - <> + +

+ Options related to the running event +
+ Affects rendered views +

- - {status === 'success' && ( - <> -

- Options related to the running event -
- Affect rendered views -

- - - Event Title - { - setChanged(true); - setFormData({ ...formData, title: event.target.value }); - }} - isDisabled={submitting} - /> - - - - - Event URL - - (shown as a QR code in some views) - - - { - setChanged(true); - setFormData({ ...formData, url: event.target.value }); - }} - isDisabled={submitting} - /> - - - - Public Info -