diff --git a/client/package.json b/client/package.json index 05700d8eff..7376de2733 100644 --- a/client/package.json +++ b/client/package.json @@ -54,6 +54,7 @@ "@testing-library/react": "^13.1.1", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.1.1", + "@types/color": "^3.0.3", "@types/node": "^18.7.16", "@types/react": "^18.0.19", "@types/react-dom": "^18.0.6", diff --git a/client/src/common/application-types/event.ts b/client/src/common/application-types/event.ts new file mode 100644 index 0000000000..c7a8bba232 --- /dev/null +++ b/client/src/common/application-types/event.ts @@ -0,0 +1,42 @@ +export interface OntimeBaseEvent { + type: 'block' | 'event' | 'delay'; + id: string; +} + +export interface OntimeDelay extends OntimeBaseEvent { + type: 'delay'; + duration: number; + revision: number; +} + +export interface OntimeBlock extends OntimeBaseEvent { + type: 'block'; +} + +export interface OntimeEvent extends OntimeBaseEvent { + type: 'event'; + title: string, + subtitle: string, + presenter: string, + note: string, + timeStart: number, + timeEnd: number, + timeType?: string, + duration: number, + isPublic: boolean, + skip: boolean, + colour: string, + user0: string, + user1: string, + user2: string, + user3: string, + user4: string, + user5: string, + user6: string, + user7: string, + user8: string, + user9: string, + revision: number, +} + +export type OntimeEventEntry = OntimeDelay | OntimeBlock | OntimeEvent; \ No newline at end of file diff --git a/client/src/common/atoms/LocalEventSettings.js b/client/src/common/atoms/LocalEventSettings.ts similarity index 100% rename from client/src/common/atoms/LocalEventSettings.js rename to client/src/common/atoms/LocalEventSettings.ts diff --git a/client/src/common/components/buttons/ActionButtons.jsx b/client/src/common/components/buttons/ActionButtons.tsx similarity index 84% rename from client/src/common/components/buttons/ActionButtons.jsx rename to client/src/common/components/buttons/ActionButtons.tsx index f462134d06..0e12c36898 100644 --- a/client/src/common/components/buttons/ActionButtons.jsx +++ b/client/src/common/components/buttons/ActionButtons.tsx @@ -4,11 +4,17 @@ import { Tooltip } from '@chakra-ui/tooltip'; import { FiClock } from '@react-icons/all-files/fi/FiClock'; import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; import { FiPlus } from '@react-icons/all-files/fi/FiPlus'; -import PropTypes from 'prop-types'; import { tooltipDelayMid } from '../../../ontimeConfig'; -export default function ActionButtons(props) { +interface ActionButtonProps { + showAdd?: boolean; + showDelay?: boolean; + showBlock?: boolean; + actionHandler: (action: string) => void; +} + +export default function ActionButtons(props: ActionButtonProps) { const { showAdd, showDelay, showBlock, actionHandler } = props; const menuStyle = { @@ -18,7 +24,7 @@ export default function ActionButtons(props) { return ( - + ); } - -ActionButtons.propTypes = { - showAdd: PropTypes.bool, - showDelay: PropTypes.bool, - showBlock: PropTypes.bool, - actionHandler: PropTypes.func, -} diff --git a/client/src/common/components/buttons/PauseIconBtn.jsx b/client/src/common/components/buttons/PauseIconBtn.tsx similarity index 71% rename from client/src/common/components/buttons/PauseIconBtn.jsx rename to client/src/common/components/buttons/PauseIconBtn.tsx index 7d80b431df..1b2737a4f3 100644 --- a/client/src/common/components/buttons/PauseIconBtn.jsx +++ b/client/src/common/components/buttons/PauseIconBtn.tsx @@ -1,30 +1,29 @@ import { IconButton } from '@chakra-ui/button'; import { Tooltip } from '@chakra-ui/tooltip'; import { IoPause } from '@react-icons/all-files/io5/IoPause'; -import PropTypes from 'prop-types'; import { tooltipDelayMid } from '../../../ontimeConfig'; -export default function PauseIconBtn(props) { +interface PauseIconBtnProps { + clickhandler: (event: React.MouseEvent) => void; + active: boolean; + disabled: boolean; +} + +export default function PauseIconBtn(props: PauseIconBtnProps) { const { clickhandler, active, disabled, ...rest } = props; return ( } colorScheme='orange' - _hover={!disabled && { bg: 'orange.400' }} variant={active ? 'solid' : 'outline'} onClick={clickhandler} width={120} disabled={disabled} + aria-label='Pause playback' {...rest} /> ); } - -PauseIconBtn.propTypes = { - clickhandler: PropTypes.func, - active: PropTypes.bool, - disabled: PropTypes.bool -} diff --git a/client/src/common/components/paginator/TodayItem.jsx b/client/src/common/components/paginator/TodayItem.jsx deleted file mode 100644 index 9200a6f525..0000000000 --- a/client/src/common/components/paginator/TodayItem.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; - -import { formatTime } from '../../utils/time'; - -import './Paginator.scss'; - -export default function TodayItem(props) { - const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props; - - // Format timers - const start = formatTime(timeStart, { format: 'hh:mm' }); - const end = formatTime(timeEnd, { format: 'hh:mm' }); - - // user colours - const userColour = colour !== '' ? colour : 'transparent'; - - // select styling - let selectStyle = 'entry--past'; - if (selected === 1) selectStyle = 'entry--now'; - else if (selected === 2) selectStyle = 'entry--future'; - return ( -
-
- {`${start} · ${end}`} -
-
{title}
- {backstageEvent &&
} -
- ); -} - -TodayItem.propTypes = { - selected: PropTypes.number, - timeStart: PropTypes.number, - timeEnd: PropTypes.number, - title: PropTypes.string, - backstageEvent: PropTypes.bool, - colour: PropTypes.string, -}; diff --git a/client/src/common/components/paginator/Paginator.jsx b/client/src/common/components/views/Paginator.tsx similarity index 76% rename from client/src/common/components/paginator/Paginator.jsx rename to client/src/common/components/views/Paginator.tsx index 21588a3b21..2e7ed592f1 100644 --- a/client/src/common/components/paginator/Paginator.jsx +++ b/client/src/common/components/views/Paginator.tsx @@ -1,14 +1,24 @@ import { useEffect, useState } from 'react'; import { useInterval } from 'common/hooks/useInterval'; -import PropTypes from 'prop-types'; +import { OntimeEvent } from '../../application-types/event'; import Empty from '../state/Empty'; import TodayItem from './TodayItem'; -import './Paginator.scss'; +import style from './Paginator.module.scss'; -export default function Paginator(props) { +interface PaginatorProps { + events: OntimeEvent[]; + selectedId: string; + limit?: number; + time?: number; + isBackstage: boolean; + setPageNumber: (page: number) => void; + setCurrentPage: (selectedPage: number) => void; +} + +export default function Paginator(props: PaginatorProps) { const { events, selectedId, @@ -21,9 +31,9 @@ export default function Paginator(props) { const LIMIT_PER_PAGE = limit; const SCROLL_TIME = time * 1000; const [numEvents, setNumEvents] = useState(0); - const [page, setPage] = useState([]); - const [pages, setPages] = useState(0); - const [selPage, setSelPage] = useState(0); + const [page, setPage] = useState([]); + const [pages, setPages] = useState(0); + const [selPage, setSelPage] = useState(0); // keep parent up to date useEffect(() => { @@ -70,7 +80,7 @@ export default function Paginator(props) { } return ( -
+
{page.map((e) => { if (e.id === selectedId) selectedState = 1; else if (selectedState === 1) selectedState = 2; @@ -83,19 +93,10 @@ export default function Paginator(props) { title={e.title} colour={isBackstage ? e.colour : ''} backstageEvent={!e.isPublic} + skip={e.skip} /> ); })}
); } - -Paginator.propTypes = { - events: PropTypes.array, - selectedId: PropTypes.string, - limit: PropTypes.number, - time: PropTypes.number, - isBackstage: PropTypes.bool, - setPageNumber: PropTypes.func, - setCurrentPage: PropTypes.func, -}; diff --git a/client/src/common/components/views/TodayItem.tsx b/client/src/common/components/views/TodayItem.tsx new file mode 100644 index 0000000000..5dd599d8eb --- /dev/null +++ b/client/src/common/components/views/TodayItem.tsx @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; + +import { formatTime } from '../../utils/time'; + +import style from './Paginator.module.scss'; + +interface TodayItemProps { + selected: number; + timeStart: number; + timeEnd: number; + title: string; + backstageEvent: boolean; + colour: string; + skip: boolean; +} + +// Todo: apply skip CSS and selector +export default function TodayItem(props: TodayItemProps) { + // @ts-ignore + const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props; + + // Format timers + const start = formatTime(timeStart, { format: 'hh:mm' }); + const end = formatTime(timeEnd, { format: 'hh:mm' }); + + // user colours + const userColour = colour !== '' ? colour : 'transparent'; + + // select styling + let selectStyle = style.entryPast; + if (selected === 1) selectStyle = style.entryNow; + else if (selected === 2) selectStyle = style.entryFuture; + return ( +
+
+ {`${start} · ${end}`} +
+
{title}
+ {backstageEvent &&
} +
+ ); +} + +TodayItem.propTypes = { + selected: PropTypes.number, + timeStart: PropTypes.number, + timeEnd: PropTypes.number, + title: PropTypes.string, + backstageEvent: PropTypes.bool, + colour: PropTypes.string, +}; diff --git a/client/src/common/context/CursorContext.jsx b/client/src/common/context/CursorContext.tsx similarity index 65% rename from client/src/common/context/CursorContext.jsx rename to client/src/common/context/CursorContext.tsx index 8f4fba8487..43a721c5fc 100644 --- a/client/src/common/context/CursorContext.jsx +++ b/client/src/common/context/CursorContext.tsx @@ -1,17 +1,32 @@ -import { createContext, useCallback, useMemo, useState } from 'react'; +import { createContext, ReactNode, useCallback, useMemo, useState } from 'react'; import { useLocalStorage } from '../hooks/useLocalStorage'; -export const CursorContext = createContext({ +interface CursorContextState { + cursor: number; + isCursorLocked: boolean; + toggleCursorLocked: (newValue?: boolean) => void; + setCursor: (index: number) => void; + moveCursorUp: () => void; + moveCursorDown: () => void; + moveCursorTo: (index: number) => void; +} + +export const CursorContext = createContext({ cursor: 0, isCursorLocked: false, - + toggleCursorLocked: () => undefined, setCursor: () => undefined, moveCursorUp: () => undefined, moveCursorDown: () => undefined, + moveCursorTo: () => undefined, }); -export const CursorProvider = ({ children }) => { +interface CursorProviderProps { + children: ReactNode +} + +export const CursorProvider = ({ children }: CursorProviderProps) => { const [cursor, setCursor] = useState(0); const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked'); const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]); @@ -31,7 +46,7 @@ export const CursorProvider = ({ children }) => { * @param {boolean | undefined} newValue */ const toggleCursorLocked = useCallback( - (newValue = undefined) => { + (newValue?: boolean) => { if (typeof newValue === 'undefined') { if (isCursorLocked) { cursorLockedOff(); @@ -46,6 +61,11 @@ export const CursorProvider = ({ children }) => { }, [cursorLockedOff, cursorLockedOn, isCursorLocked] ); + + // moves cursor to given index + const moveCursorTo = useCallback((index: number) => { + setCursor(index); + }, []); return ( { setCursor, moveCursorUp, moveCursorDown, + moveCursorTo, }} > {children} diff --git a/client/src/common/context/LoggingContext.tsx b/client/src/common/context/LoggingContext.tsx index e431cb56f7..025000c34f 100644 --- a/client/src/common/context/LoggingContext.tsx +++ b/client/src/common/context/LoggingContext.tsx @@ -27,7 +27,7 @@ type LoggingProviderProps = { } const notInitialised = () => { - throw new Error("Not initialised"); + throw new Error('Not initialised'); }; export const LoggingContext = createContext({ @@ -35,7 +35,7 @@ export const LoggingContext = createContext({ emitInfo: notInitialised, emitWarning: notInitialised, emitError: notInitialised, - clearLog: notInitialised + clearLog: notInitialised, }); export const LoggingProvider = ({ children }: LoggingProviderProps) => { @@ -52,7 +52,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { socket.emit('get-logger'); socket.on('logger', (data: Log) => { - setLogData((l) => [data, ...l]); + setLogData((currentLog) => [data, ...currentLog]); }); // Clear listener @@ -70,21 +70,21 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { const _send = useCallback( (text: string, level: LOG_LEVEL) => { if (socket != null) { - const m: Log = { + const newLogMessage: Log = { id: generateId(), origin, time: stringFromMillis(nowInMillis()), level, text, }; - setLogData((l) => [m, ...l]); - socket.emit('logger', m); + setLogData((currentLog) => [newLogMessage, ...currentLog]); + socket.emit('logger', newLogMessage); } if (logData.length > MAX_MESSAGES) { - setLogData((l) => l.slice(1)); + setLogData((currentLog) => currentLog.slice(1)); } }, - [logData, socket] + [logData.length, setLogData, socket], ); /** @@ -95,7 +95,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { (text: string) => { _send(text, 'INFO'); }, - [_send] + [_send], ); /** @@ -106,7 +106,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { (text: string) => { _send(text, 'WARN'); }, - [_send] + [_send], ); /** @@ -117,7 +117,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { (text: string) => { _send(text, 'ERROR'); }, - [_send] + [_send], ); /** @@ -125,7 +125,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { */ const clearLog = useCallback(() => { setLogData([]); - }, []); + }, [setLogData]); return ( diff --git a/client/src/common/context/socketContext.tsx b/client/src/common/context/socketContext.tsx index a291ae9757..330abd8603 100644 --- a/client/src/common/context/socketContext.tsx +++ b/client/src/common/context/socketContext.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; import { serverURL } from 'common/api/apiConstants'; import io, { Socket } from 'socket.io-client'; @@ -26,7 +25,7 @@ export const useSocket = () => { }; function SocketProvider({ children }: SocketProviderProps) { - const [socket, setSocket] = useState(null); + const [socket, setSocket] = useState(null); useEffect(() => { const socketInstance = io(serverURL, { transports: ["websocket"] }); diff --git a/client/src/common/context/useSubscription.tsx b/client/src/common/context/useSubscription.tsx new file mode 100644 index 0000000000..8c85c6dccc --- /dev/null +++ b/client/src/common/context/useSubscription.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +import { useSocket } from './socketContext'; + +export default function useSubscription(topic: string, initialState: T, requestString?: string) { + const socket = useSocket(); + const [state, setState] = useState(initialState); + + useEffect(() => { + if (!socket) { + return; + } + + if (requestString) { + socket.emit(requestString); + } else { + socket.emit(`get-${topic}`); + } + socket.on(topic, setState); + + return () => { + socket.off(topic); + }; + }, [requestString, socket, topic]); + + return [state, setState] as const; +}; diff --git a/client/src/common/hooks/useElectronEvent.ts b/client/src/common/hooks/useElectronEvent.ts index 504c76d603..065d1fa6ab 100644 --- a/client/src/common/hooks/useElectronEvent.ts +++ b/client/src/common/hooks/useElectronEvent.ts @@ -1,8 +1,7 @@ -// @ts-nocheck export default function useElectronEvent() { const isElectron = window?.process?.type === 'renderer'; - const sendToElectron = (channel: string, args: any) => { + const sendToElectron = (channel: string, args?: string | Record) => { if (isElectron) { window?.ipcRenderer.send(channel, args); } diff --git a/client/src/common/hooks/useFetch.js b/client/src/common/hooks/useFetch.js deleted file mode 100644 index 7cab3a8929..0000000000 --- a/client/src/common/hooks/useFetch.js +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -const refetchIntervalMs = 10000; - -/** - * @description utility hook to simplify query config - * @param namespace - * @param fn - */ -export const useFetch = (namespace, fn) => { - const { data, status, isError, refetch } = useQuery(namespace, fn, { - refetchInterval: refetchIntervalMs, - cacheTime: Infinity, - }); - - return { data, status, isError, refetch }; -}; diff --git a/client/src/common/hooks/useFetch.ts b/client/src/common/hooks/useFetch.ts new file mode 100644 index 0000000000..9c8e4bf85a --- /dev/null +++ b/client/src/common/hooks/useFetch.ts @@ -0,0 +1,18 @@ +import { QueryFunction, QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query'; + +interface UseFetchState { + data: unknown; + status: "loading" | "error" | "success"; + isError: boolean; + refetch: () => void; +} + +export const useFetch = ( key: QueryKey, fn: QueryFunction, options?: UseQueryOptions): UseFetchState => { + const { data, status, isError, refetch } = useQuery(key, fn, { + refetchInterval: 10000, + cacheTime: Infinity, + ...options + }); + + return { data, status, isError, refetch }; +}; diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.ts similarity index 71% rename from client/src/common/utils/eventsManager.js rename to client/src/common/utils/eventsManager.ts index 408c81c3cf..61571f8607 100644 --- a/client/src/common/utils/eventsManager.js +++ b/client/src/common/utils/eventsManager.ts @@ -1,3 +1,5 @@ +import { OntimeEvent, OntimeEventEntry } from '../application-types/event'; + import { formatTime } from './time'; /** @@ -6,7 +8,7 @@ import { formatTime } from './time'; * @returns {Object[]} Filtered events with calculated delays */ -export const getEventsWithDelay = (events) => { +export const getEventsWithDelay = (events: OntimeEventEntry[]) => { if (events == null) return []; const unfilteredEvents = [...events]; @@ -33,7 +35,7 @@ export const getEventsWithDelay = (events) => { * @param {number} limit - max number of events to return * @returns {Object[]} Event list with maximum objects */ -export const trimEventlist = (events, selectedId, limit) => { +export const trimEventlist = (events: OntimeEventEntry[], selectedId: string, limit: number) => { if (events == null) return []; const BEFORE = 2; @@ -53,6 +55,9 @@ export const trimEventlist = (events, selectedId, limit) => { return trimmedEvents; }; +type FormatEventListOptionsProp = { + showEnd?: boolean; +} /** * @description Returns list of events formatted to be displayed * @param {Object[]} events - given events @@ -62,7 +67,7 @@ export const trimEventlist = (events, selectedId, limit) => { * @param {boolean} [options.showEnd] - whether to show the end time * @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}] */ -export const formatEventList = (events, selectedId, nextId, options) => { +export const formatEventList = (events: OntimeEvent[], selectedId: string, nextId: string, options: FormatEventListOptionsProp) => { if (events == null) return []; const { showEnd = false } = options; @@ -71,7 +76,7 @@ export const formatEventList = (events, selectedId, nextId, options) => { // format list const formattedEvents = []; for (const event of givenEvents) { - const start = formatTime(event.timeStart) + const start = formatTime(event.timeStart); const end = formatTime(event.timeEnd); formattedEvents.push({ @@ -86,3 +91,23 @@ export const formatEventList = (events, selectedId, nextId, options) => { return formattedEvents; }; + +/** + * @description Creates a safe duplicate of an event + * @param {object} event + * @return {object} clean event + */ +export const duplicateEvent = (event: OntimeEvent) => { + return { + type: 'event', + title: event.title, + subtitle: event.subtitle, + presenter: event.presenter, + note: event.note, + timeStart: event.timeStart, + timeEnd: event.timeEnd, + isPublic: event.isPublic, + skip: event.skip, + colour: event.colour, + }; +}; diff --git a/client/src/common/utils/styleUtils.ts b/client/src/common/utils/styleUtils.ts new file mode 100644 index 0000000000..cc54b93423 --- /dev/null +++ b/client/src/common/utils/styleUtils.ts @@ -0,0 +1,23 @@ +import Color from 'color'; + +type ColourCombination = { + backgroundColor: string; + color: string; +} + +/** + * @description Selects text colour to maintain accessible contrast + * @param bgColour + * @return {{backgroundColor, color: string}} + */ +export const getAccessibleColour = (bgColour: string): ColourCombination => { + if (bgColour) { + try { + const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa'; + return { backgroundColor: bgColour, color: textColor }; + } catch (error) { + console.log(`Unable to parse colour: ${bgColour}`); + } + } + return { backgroundColor: '#000', color: "#fffffa" }; +}; diff --git a/client/src/declarations/declaration.d.ts b/client/src/declarations/declaration.d.ts index 77a73098ff..42ed86f0ff 100644 --- a/client/src/declarations/declaration.d.ts +++ b/client/src/declarations/declaration.d.ts @@ -3,8 +3,16 @@ declare module '*.scss' { export default content; } -declare namespace NodeJS { - export interface ProcessEnv { - type: string +declare global { + interface Window { + ipcRenderer: { + send: (channel: string, args?: string | object) => void; + }; + process: { + type: string; + } } } + +// eslint-disable-next-line import/no-anonymous-default-export +export default {} \ No newline at end of file diff --git a/client/src/features/editors/list/EventListWrapper.jsx b/client/src/features/editors/list/EventListWrapper.jsx index fea78f725e..24cf21f3f9 100644 --- a/client/src/features/editors/list/EventListWrapper.jsx +++ b/client/src/features/editors/list/EventListWrapper.jsx @@ -12,7 +12,7 @@ import { requestReorder, } from 'common/api/eventsApi.js'; import Empty from 'common/components/state/Empty'; -import { useFetch } from 'common/hooks/useFetch.js'; +import { useFetch } from 'common/hooks/useFetch.ts'; import EventListMenu from 'features/menu/EventListMenu.jsx'; import { CollapseContext } from '../../../common/context/CollapseContext'; diff --git a/client/src/features/menu/__tests__/MenuBar.test.tsx b/client/src/features/menu/__tests__/MenuBar.test.tsx index 5eba42f804..5741e6fd79 100644 --- a/client/src/features/menu/__tests__/MenuBar.test.tsx +++ b/client/src/features/menu/__tests__/MenuBar.test.tsx @@ -5,18 +5,22 @@ import { vi } from 'vitest'; import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; import MenuBar from '../MenuBar'; -const onOpenHandler = vi.fn(); -const onCloseHandler = vi.fn(); -const isOpen = false; +const onSettingOpenHandler = vi.fn(); +const onSettingsCloseHandler = vi.fn(); const onUploadOpenHandler = vi.fn(); +const isOpen = false; const renderInMock = () => { render( - - , + + ); }; diff --git a/client/src/features/table/tableRows/EventRow.jsx b/client/src/features/table/tableRows/EventRow.jsx index 075acba0d5..e25ea7c525 100644 --- a/client/src/features/table/tableRows/EventRow.jsx +++ b/client/src/features/table/tableRows/EventRow.jsx @@ -1,29 +1,13 @@ -import Color from 'color'; import PropTypes from 'prop-types'; -import style from '../Table.module.scss'; +import { getAccessibleColour } from '../../../common/utils/styleUtils'; -/** - * Selects text colour to maintain accessible contrast - * @param bgColour - * @return {{backgroundColor, color: string}} - */ -const selCol = (bgColour) => { - if (bgColour != null && bgColour !== '') { - try { - const textColor = Color(bgColour).isLight() ? 'black' : 'white'; - return { backgroundColor: bgColour, color: textColor }; - } catch (error) { - console.log(`Unable to parse colour: ${bgColour}`); - } - } -}; +import style from '../Table.module.scss'; export default function EventRow(props) { const { row, index, selectedId, delay } = props; const selected = row.original.id === selectedId; - const colours = selCol(row.original.colour); - + const colours = getAccessibleColour(row.original.colour); return ( diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 876ba50353..efd6e4da60 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -1,22 +1,25 @@ /* eslint-disable react/display-name */ -import { useEffect, useState } from 'react'; -import { EVENT_TABLE, EVENTS_TABLE, VIEW_SETTINGS } from 'common/api/apiConstants'; -import { fetchEvent } from 'common/api/eventApi'; -import { fetchAllEvents } from 'common/api/eventsApi'; -import { useSocket } from 'common/context/socketContext'; -import { useFetch } from 'common/hooks/useFetch'; +import { useEffect, useMemo, useState } from 'react'; +import { EVENT_TABLE, EVENTS_TABLE } from '../../common/api/apiConstants'; +import { fetchEvent } from '../../common/api/eventApi'; +import { fetchAllEvents } from '../../common/api/eventsApi'; +import { useSocket } from '../../common/context/socketContext'; +import { useFetch } from '../../common/hooks/useFetch'; import { getView } from '../../common/api/ontimeApi'; +import useSubscription from '../../common/context/useSubscription'; +import { eventPlaceholderSettings } from '../../common/api/ontimeApi'; const withSocket = (Component) => { return (props) => { - const { data: eventsData } = useFetch(EVENTS_TABLE, fetchAllEvents); - const { data: genData } = useFetch(EVENT_TABLE, fetchEvent); + const { data: eventsData } = useFetch(EVENTS_TABLE, fetchAllEvents, { + placeholderData: [], + }); + const { data: genData } = useFetch(EVENT_TABLE, fetchEvent, { + placeholderData: eventPlaceholderSettings, + }); const { data: viewSettings } = useFetch(VIEW_SETTINGS, getView); - const [publicEvents, setPublicEvents] = useState([]); - const [backstageEvents, setBackstageEvents] = useState([]); - const socket = useSocket(); const [pres, setPres] = useState({ text: '', @@ -30,14 +33,16 @@ const withSocket = (Component) => { text: '', visible: false, }); - const [timer, setTimer] = useState({ + const [publicSelectedId, setPublicSelectedId] = useState(null); + + const [timer] = useSubscription('timer', { clock: 0, running: 0, isNegative: false, startedAt: null, expectedFinish: null, }); - const [titles, setTitles] = useState({ + const [titles] = useSubscription('titles', { titleNow: '', subtitleNow: '', presenterNow: '', @@ -45,7 +50,7 @@ const withSocket = (Component) => { subtitleNext: '', presenterNext: '', }); - const [publicTitles, setPublicTitles] = useState({ + const [publicTitles] = useSubscription('publictitles', { titleNow: '', subtitleNow: '', presenterNow: '', @@ -53,18 +58,10 @@ const withSocket = (Component) => { subtitleNext: '', presenterNext: '', }); - const [selectedId, setSelectedId] = useState(null); - const [nextId, setNextId] = useState(null); - const [publicSelectedId, setPublicSelectedId] = useState(null); - const [general, setGeneral] = useState({ - title: '', - url: '', - publicInfo: '', - backstageInfo: '', - endMessage: '', - }); - const [playback, setPlayback] = useState(null); - const [onAir, setOnAir] = useState(false); + const [selectedId] = useSubscription('selected-id', null); + const [nextId] = useSubscription('next-id', null); + const [playback] = useSubscription('playstate', null); + const [onAir] = useSubscription('onAir', false); // Ask for update on load useEffect(() => { @@ -87,96 +84,29 @@ const withSocket = (Component) => { setLower({ ...data }); }); - // Handle timer - socket.on('timer', (data) => { - setTimer({ ...data }); - }); - - // Handle playstate - socket.on('playstate', (data) => { - setPlayback(data); - }); - socket.on('onAir', (data) => { - setOnAir(data); - }); - - // Handle titles - socket.on('titles', (data) => { - setTitles({ ...data }); - }); - socket.on('publictitles', (data) => { - setPublicTitles({ ...data }); - }); - - // Handle selected event - socket.on('selected-id', (data) => { - setSelectedId(data); - }); socket.on('publicselected-id', (data) => { setPublicSelectedId(data); }); - socket.on('next-id', (data) => { - setNextId(data); - }); // Ask for up to date data socket.emit('get-messages'); - // Ask for up to data - socket.emit('get-timer'); - - // ask for timer - socket.emit('get-timer'); - - // ask for playstate - socket.emit('get-playstate'); - socket.emit('get-onAir'); - - // Ask for up titles - socket.emit('get-titles'); - socket.emit('get-publictitles'); - - // Ask for up selected - socket.emit('get-selected-id'); - socket.emit('get-next-id'); - // Clear listeners return () => { socket.off('messages-public'); socket.off('messages-timer'); socket.off('messages-lower'); - socket.off('timer'); - socket.off('playstate'); - socket.off('onAir'); - socket.off('titles'); - socket.off('publictitles'); - socket.off('selected-id'); - socket.emit('next-id'); }; }, [socket]); - // Filter events only to pass down - useEffect(() => { - if (!eventsData) { - return; - } - // filter just events with title - if (Array.isArray(eventsData)) { - const pe = eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic); - setPublicEvents(pe); - - // everything goes backstage - setBackstageEvents(eventsData); - } - }, [eventsData]); - // Set general data - useEffect(() => { - if (!genData) { - return; + const publicEvents = useMemo(() => { + if (Array.isArray(eventsData)) { + return eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic); + } else { + return []; } - setGeneral(genData); - }, [genData]); + },[eventsData]) /********************************************/ /*** + titleManager ***/ @@ -245,12 +175,12 @@ const withSocket = (Component) => { publicTitle={publicTitleManager} time={timeManager} events={publicEvents} - backstageEvents={backstageEvents} + backstageEvents={eventsData} selectedId={selectedId} publicSelectedId={publicSelectedId} viewSettings={viewSettings} nextId={nextId} - general={general} + general={genData} onAir={onAir} /> ); diff --git a/client/src/features/viewers/backstage/Backstage.jsx b/client/src/features/viewers/backstage/Backstage.jsx index 915bcede86..72037afb5f 100644 --- a/client/src/features/viewers/backstage/Backstage.jsx +++ b/client/src/features/viewers/backstage/Backstage.jsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; import QRCode from 'react-qr-code'; import NavLogo from 'common/components/nav/NavLogo'; -import Paginator from 'common/components/paginator/Paginator'; import TitleSide from 'common/components/title-side/TitleSide'; import { formatDisplay } from 'common/utils/dateConfig'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; +import Paginator from '../../../common/components/views/Paginator'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { getEventsWithDelay } from '../../../common/utils/eventsManager'; import { formatTime } from '../../../common/utils/time'; diff --git a/client/src/features/viewers/picture-in-picture/Pip.jsx b/client/src/features/viewers/picture-in-picture/Pip.jsx index 8ea87fcd5a..c202ae7460 100644 --- a/client/src/features/viewers/picture-in-picture/Pip.jsx +++ b/client/src/features/viewers/picture-in-picture/Pip.jsx @@ -2,12 +2,12 @@ import { useEffect, useRef, useState } from 'react'; import QRCode from 'react-qr-code'; import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; import NavLogo from 'common/components/nav/NavLogo'; -import Paginator from 'common/components/paginator/Paginator'; import { formatDisplay } from 'common/utils/dateConfig'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; +import Paginator from '../../../common/components/views/Paginator'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { formatTime } from '../../../common/utils/time'; diff --git a/client/src/features/viewers/public/Public.jsx b/client/src/features/viewers/public/Public.jsx index 07f410da17..c4ce6fd3c3 100644 --- a/client/src/features/viewers/public/Public.jsx +++ b/client/src/features/viewers/public/Public.jsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; import QRCode from 'react-qr-code'; import NavLogo from 'common/components/nav/NavLogo'; -import Paginator from 'common/components/paginator/Paginator'; import TitleSide from 'common/components/title-side/TitleSide'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; +import Paginator from '../../../common/components/views/Paginator'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { formatTime } from '../../../common/utils/time'; import { titleVariants } from '../common/animation'; @@ -24,7 +24,6 @@ export default function Public(props) { const [pageNumber, setPageNumber] = useState(0); const [currentPage, setCurrentPage] = useState(0); - // Set window title useEffect(() => { document.title = 'ontime - Public Screen'; }, []); @@ -36,7 +35,6 @@ export default function Public(props) { // Format messages const showPubl = publ.text !== '' && publ.visible; - const clock = formatTime(time.clock, formatOptions); return ( @@ -136,7 +134,7 @@ Public.propTypes = { publ: PropTypes.object, publicTitle: PropTypes.object, time: PropTypes.object, - events: PropTypes.object, + events: PropTypes.array, publicSelectedId: PropTypes.string, general: PropTypes.object, viewSettings: PropTypes.object, diff --git a/client/yarn.lock b/client/yarn.lock index 47bfe28c49..b1c754478e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1583,6 +1583,25 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07" integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g== +"@types/color-convert@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" + integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/color@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.3.tgz#e6d8d72b7aaef4bb9fe80847c26c7c786191016d" + integrity sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA== + dependencies: + "@types/color-convert" "*" + "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"