diff --git a/client/.eslintrc b/client/.eslintrc index c1e08bcb1..f95ffe524 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -11,9 +11,10 @@ "extends": [ "eslint:recommended", "plugin:react/recommended", + "plugin:react-hooks/recommended", "plugin:@typescript-eslint/recommended", "eslint-config-prettier", - "plugin:@tanstack/eslint-plugin-query/recommended" + "plugin:@tanstack/eslint-plugin-query/recommended" ], "plugins": [ "react", diff --git a/client/package.json b/client/package.json index 79aa3f8c8..d0fb760bd 100644 --- a/client/package.json +++ b/client/package.json @@ -68,6 +68,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.0.4", "eslint-plugin-react": "^7.31.8", + "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-testing-library": "^5.6.4", "jsdom": "^20.0.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index ff3df35ea..4ece9ea3c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, useCallback, useEffect } from 'react'; +import { Suspense, useEffect } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { ChakraProvider } from '@chakra-ui/react'; import { QueryClientProvider } from '@tanstack/react-query'; @@ -19,7 +19,7 @@ import('typeface-open-sans'); function App() { const { isElectron, sendToElectron } = useElectronEvent(); - const handleKeyPress = useCallback((event:KeyboardEvent) => { + const handleKeyPress = (event:KeyboardEvent) => { // handle held key if (event.repeat) return; // check if the alt key is pressed @@ -29,7 +29,7 @@ function App() { sendToElectron('set-window', 'show-dev'); } } - },[]); + }; useEffect(() => { if (isElectron) { @@ -40,7 +40,7 @@ function App() { document.removeEventListener('keydown', handleKeyPress); } }; - }, [handleKeyPress]); + }, []); return ( diff --git a/client/src/common/components/input/delay-input/DelayInput.module.scss b/client/src/common/components/input/delay-input/DelayInput.module.scss index 131a17b2f..edad5c2d9 100644 --- a/client/src/common/components/input/delay-input/DelayInput.module.scss +++ b/client/src/common/components/input/delay-input/DelayInput.module.scss @@ -1,7 +1,11 @@ +@use '../../../../theme/v2Styles' as *; + .delayInput { display: flex; - gap: 8px; + gap: $element-spacing; align-items: center; + color: $ontime-delay-text; + font-size: $text-body-size; } .inputField { diff --git a/client/src/common/components/input/delay-input/DelayInput.tsx b/client/src/common/components/input/delay-input/DelayInput.tsx index 6e1286bfb..745639191 100644 --- a/client/src/common/components/input/delay-input/DelayInput.tsx +++ b/client/src/common/components/input/delay-input/DelayInput.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Input } from '@chakra-ui/react'; import { clamp } from 'common/utils/math'; -import PropTypes from 'prop-types'; import style from './DelayInput.module.scss'; @@ -81,8 +80,3 @@ export default function DelayInput(props: DelayInputProps) { ); } - -DelayInput.propTypes = { - submitHandler: PropTypes.func, - value: PropTypes.number, -}; diff --git a/client/src/common/components/input/text-input/TextInput.tsx b/client/src/common/components/input/text-input/TextInput.tsx index 4de5bf428..def0f858c 100644 --- a/client/src/common/components/input/text-input/TextInput.tsx +++ b/client/src/common/components/input/text-input/TextInput.tsx @@ -1,6 +1,7 @@ import { useCallback, useRef } from 'react'; import { Input, Textarea } from '@chakra-ui/react'; +import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor'; import { Size } from '../../../models/UtilTypes'; import useReactiveTextInput from './useReactiveTextInput'; @@ -9,9 +10,9 @@ interface TextInputProps { isTextArea?: boolean; isFullHeight?: boolean; size?: Size; - field: string; + field: EventEditorSubmitActions; initialText?: string; - submitHandler: (field: string, newValue: string) => void; + submitHandler: (field: EventEditorSubmitActions, newValue: string) => void; } export default function TextInput(props: TextInputProps) { @@ -20,7 +21,7 @@ export default function TextInput(props: TextInputProps) { const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue) - , [field]); + ,[field, submitHandler]); const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true }); const textAreaProps = useReactiveTextInput(initialText, submitCallback); diff --git a/client/src/common/components/input/text-input/useReactiveTextInput.tsx b/client/src/common/components/input/text-input/useReactiveTextInput.tsx index 24f5f189e..45dc54b87 100644 --- a/client/src/common/components/input/text-input/useReactiveTextInput.tsx +++ b/client/src/common/components/input/text-input/useReactiveTextInput.tsx @@ -76,7 +76,7 @@ export default function useReactiveTextInput( break; } }, - [initialText, handleSubmit, text], + [initialText, options?.submitOnEnter, handleSubmit, text], ); return { diff --git a/client/src/common/context/LoggingContext.tsx b/client/src/common/context/LoggingContext.tsx index 92da245df..4ae7ad01c 100644 --- a/client/src/common/context/LoggingContext.tsx +++ b/client/src/common/context/LoggingContext.tsx @@ -61,7 +61,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { return () => { socket.off('logger'); }; - }, [socket]); + }, []); /** * Utility function sends message over socket @@ -86,7 +86,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { setLogData((currentLog) => currentLog.slice(1)); } }, - [logData.length, setLogData, socket], + [logData.length, setLogData], ); /** diff --git a/client/src/common/hooks/useEventAction.js b/client/src/common/hooks/useEventAction.js index b4b19ac24..ed2e37a6a 100644 --- a/client/src/common/hooks/useEventAction.js +++ b/client/src/common/hooks/useEventAction.js @@ -60,7 +60,7 @@ export const useEventAction = () => { emitError(`Error fetching data: ${error.message}`); } }, - [_addEventMutation, emitError], + [_addEventMutation, emitError, queryClient], ); /** diff --git a/client/src/common/hooks/useKeyDown.ts b/client/src/common/hooks/useKeyDown.ts index 4fbee6d36..2b93bbcf7 100644 --- a/client/src/common/hooks/useKeyDown.ts +++ b/client/src/common/hooks/useKeyDown.ts @@ -14,5 +14,5 @@ export const useKeyDown = (callback: () => void, targetKey: string) => { return () => { document.removeEventListener('keydown', onKeyDown); }; - }, [onKeyDown]); + }, []); }; diff --git a/client/src/common/hooks/useSubscription.tsx b/client/src/common/hooks/useSubscription.tsx index 634aa38f7..cda26fa18 100644 --- a/client/src/common/hooks/useSubscription.tsx +++ b/client/src/common/hooks/useSubscription.tsx @@ -16,7 +16,7 @@ export default function useSubscription(topic: string, initialState: T, reque return () => { socket.off(topic); }; - }, [requestString, socket, topic]); + }, [requestString, topic]); return [state, setState] as const; }; diff --git a/client/src/common/utils/timesManager.ts b/client/src/common/utils/timesManager.ts index 2eb0d2c6f..c95988055 100644 --- a/client/src/common/utils/timesManager.ts +++ b/client/src/common/utils/timesManager.ts @@ -1,3 +1,5 @@ +export type TimeEntryField = 'timeStart' |'timeEnd' | 'durationOverride'; + /** * @description Milliseconds in a day */ @@ -12,7 +14,7 @@ export const calculateDuration = (start: number, end: number): number => /** * @description Checks which field the value relates to */ -export const handleTimeEntry = (field: string, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => { +export const handleTimeEntry = (field: TimeEntryField, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => { let start = timeStart; let end = timeEnd; let durationOverride = false; @@ -30,7 +32,7 @@ export const handleTimeEntry = (field: string, val: number, timeStart: number, t /** * @description Validates time entry */ -export const validateEntry = (field: string, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => { +export const validateEntry = (field: TimeEntryField, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => { const validate = { value: true, catch: '' }; // 1. if one of times is not entered, anything goes diff --git a/client/src/features/editors/Editor.module.scss b/client/src/features/editors/Editor.module.scss index 756323b3d..3a7995b44 100644 --- a/client/src/features/editors/Editor.module.scss +++ b/client/src/features/editors/Editor.module.scss @@ -1,12 +1,16 @@ @use '../../theme/ontimeColours' as *; @use '../../theme/v2Styles' as *; +$menu-width: 48px; +$rundown-width: 46em; +$playback-width: 450px; + @mixin absolute-top-right($distance) { position: absolute; top: $distance; right: $distance; cursor: pointer; - color: #f2f2f2; + color: $ui-white; } .corner { @@ -26,7 +30,7 @@ display: grid; grid-template-rows: auto 1fr; - grid-template-columns: 48px 46em 450px auto; + grid-template-columns: $menu-width $rundown-width $playback-width auto; grid-template-areas: 'sett even play info' 'sett even mess info'; @@ -50,7 +54,7 @@ .mainContainer { height: 100%; grid-template-rows: auto 1fr; - grid-template-columns: 48px 48em auto; + grid-template-columns: $menu-width $rundown-width auto; .info { visibility: hidden; @@ -63,7 +67,7 @@ .mainContainer { height: 100%; grid-template-rows: 100%; - grid-template-columns: 48px 48em; + grid-template-columns: $menu-width $rundown-width; grid-template-areas: 'sett even'; diff --git a/client/src/features/event-editor/EventEditor.module.scss b/client/src/features/event-editor/EventEditor.module.scss index c159dcc3b..ca5d3a6f8 100644 --- a/client/src/features/event-editor/EventEditor.module.scss +++ b/client/src/features/event-editor/EventEditor.module.scss @@ -45,7 +45,6 @@ display: flex; flex-direction: column; gap: 8px; - margin-top: 4px; } .left { diff --git a/client/src/features/event-editor/EventEditor.jsx b/client/src/features/event-editor/EventEditor.tsx similarity index 85% rename from client/src/features/event-editor/EventEditor.jsx rename to client/src/features/event-editor/EventEditor.tsx index 1eff125ef..911cc1a8f 100644 --- a/client/src/features/event-editor/EventEditor.jsx +++ b/client/src/features/event-editor/EventEditor.tsx @@ -10,20 +10,23 @@ import { useEventAction } from 'common/hooks/useEventAction'; import { millisToMinutes } from 'common/utils/dateConfig'; import getDelayTo from 'common/utils/getDelayTo'; import { stringFromMillis } from 'common/utils/time'; -import { calculateDuration, validateEntry } from 'common/utils/timesManager'; +import { calculateDuration, TimeEntryField, validateEntry } from 'common/utils/timesManager'; import { useAtom } from 'jotai'; import CopyTag from '../../common/components/copy-tag/CopyTag'; import useRundown from '../../common/hooks-query/useRundown'; +import { OntimeEvent } from '../../common/models/EventTypes'; import style from './EventEditor.module.scss'; +export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride'; + export default function EventEditor() { const [openId] = useAtom(editorEventId); const { data } = useRundown(); const { emitWarning, emitError } = useContext(LoggingContext); const { updateEvent } = useEventAction(); - const [event, setEvent] = useState(null); + const [event, setEvent] = useState(null); const [delay, setDelay] = useState(0); useEffect(() => { @@ -33,28 +36,34 @@ export default function EventEditor() { const eventIndex = data.findIndex((event) => event.id === openId); if (eventIndex > -1) { - setDelay(getDelayTo(data, eventIndex)); - setEvent(data[eventIndex]); + const event = data[eventIndex]; + if (event.type === 'event') { + setDelay(getDelayTo(data, eventIndex)); + setEvent(data[eventIndex] as OntimeEvent); + } } }, [data, event, openId]); const handleSubmit = useCallback( - (field, value) => { - const newEventData = { id: event.id }; + (field: EventEditorSubmitActions, value: any) => { + if (event === null) { + return; + } + const newEventData: Partial = { id: event.id }; switch (field) { case 'durationOverride': { // duration defines timeEnd - newEventData.timeEnd = event.timeStart += value; + newEventData.timeEnd = event.timeStart += value as number; break; } case 'timeStart': { - newEventData.duration = calculateDuration(value, event.timeEnd); - newEventData.timeStart = value; + newEventData.duration = calculateDuration(value as number, event.timeEnd); + newEventData.timeStart = value as number; break; } case 'timeEnd': { - newEventData.duration = calculateDuration(event.timeStart, value); - newEventData.timeEnd = value; + newEventData.duration = calculateDuration(event.timeStart, value as number); + newEventData.timeEnd = value as number; break; } default: { @@ -73,22 +82,26 @@ export default function EventEditor() { [emitError, event, updateEvent], ); - const timerValidationHandler = useCallback( - (entry, val) => { + const timerValidationHandler = useCallback((entry: TimeEntryField, val: number) => { + if (!event) { + return; + } const valid = validateEntry(entry, val, event.timeStart, event.timeEnd); if (!valid.value) { emitWarning(`Time Input Warning: ${valid.catch}`); } return valid.value; }, - [emitWarning, event?.timeStart, event?.timeEnd], + [event, emitWarning], ); - const togglePublic = useCallback( - (currentValue) => { + const togglePublic = useCallback((currentValue: boolean) => { + if (!event) { + return; + } updateEvent({ id: event.id, isPublic: !currentValue }); }, - [event?.id, updateEvent], + [event, updateEvent], ); if (!event) { @@ -197,7 +210,7 @@ export default function EventEditor() { handleChange={(value) => handleSubmit('colour', value)} />