mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +00:00
Refactor/ts sockets (#210)
* refactor(ts): create socket subscription service * refactor(ts-sockets): useSubscription hook * refactor(ts-sockets): extract colour selection to utility
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
+9
-10
@@ -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 (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<Tooltip label='Add ...' delay={tooltipDelayMid}>
|
||||
<Tooltip label='Add ...' openDelay={tooltipDelayMid}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
@@ -49,10 +55,3 @@ export default function ActionButtons(props) {
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
showAdd: PropTypes.bool,
|
||||
showDelay: PropTypes.bool,
|
||||
showBlock: PropTypes.bool,
|
||||
actionHandler: PropTypes.func,
|
||||
}
|
||||
+8
-9
@@ -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<HTMLButtonElement, MouseEvent>) => void;
|
||||
active: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default function PauseIconBtn(props: PauseIconBtnProps) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
_hover={!disabled && { bg: 'orange.400' }}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
disabled={disabled}
|
||||
aria-label='Pause playback'
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
PauseIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={`entry ${selectStyle}`} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className='entry-times'>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
{backstageEvent && <div className='backstage-indicator' />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
+18
-17
@@ -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<OntimeEvent[]>([]);
|
||||
const [pages, setPages] = useState<number>(0);
|
||||
const [selPage, setSelPage] = useState<number>(0);
|
||||
|
||||
// keep parent up to date
|
||||
useEffect(() => {
|
||||
@@ -70,7 +80,7 @@ export default function Paginator(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='paginator entries'>
|
||||
<div className={style.entries}>
|
||||
{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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
events: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
setPageNumber: PropTypes.func,
|
||||
setCurrentPage: PropTypes.func,
|
||||
};
|
||||
@@ -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 (
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
+26
-5
@@ -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<CursorContextState>({
|
||||
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 (
|
||||
<CursorContext.Provider
|
||||
@@ -56,6 +76,7 @@ export const CursorProvider = ({ children }) => {
|
||||
setCursor,
|
||||
moveCursorUp,
|
||||
moveCursorDown,
|
||||
moveCursorTo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -27,7 +27,7 @@ type LoggingProviderProps = {
|
||||
}
|
||||
|
||||
const notInitialised = () => {
|
||||
throw new Error("Not initialised");
|
||||
throw new Error('Not initialised');
|
||||
};
|
||||
|
||||
export const LoggingContext = createContext<LoggingProviderState>({
|
||||
@@ -35,7 +35,7 @@ export const LoggingContext = createContext<LoggingProviderState>({
|
||||
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 (
|
||||
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
|
||||
@@ -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<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const socketInstance = io(serverURL, { transports: ["websocket"] });
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useSocket } from './socketContext';
|
||||
|
||||
export default function useSubscription<T>(topic: string, initialState: T, requestString?: string) {
|
||||
const socket = useSocket();
|
||||
const [state, setState] = useState<T>(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;
|
||||
};
|
||||
@@ -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<string, any>) => {
|
||||
if (isElectron) {
|
||||
window?.ipcRenderer.send(channel, args);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
+29
-4
@@ -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 <limit> 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,
|
||||
};
|
||||
};
|
||||
@@ -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" };
|
||||
};
|
||||
+11
-3
@@ -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 {}
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<MenuBar isSettingsOpen={isOpen} onSettingsOpen={onOpenHandler}
|
||||
onSettingsClose={onCloseHandler} isUploadOpen={isOpen}
|
||||
onUploadOpen={onUploadOpenHandler} />
|
||||
</QueryClientProvider>,
|
||||
<MenuBar
|
||||
isSettingsOpen={isOpen}
|
||||
onSettingsOpen={onSettingOpenHandler}
|
||||
onSettingsClose={onSettingsCloseHandler}
|
||||
isUploadOpen={isOpen}
|
||||
onUploadOpen={onUploadOpenHandler}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<tr {...row.getRowProps()} className={selected ? style.selected : ''} id={row.original.id}>
|
||||
|
||||
@@ -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
|
||||
|
||||
const publicEvents = useMemo(() => {
|
||||
if (Array.isArray(eventsData)) {
|
||||
const pe = eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
|
||||
setPublicEvents(pe);
|
||||
|
||||
// everything goes backstage
|
||||
setBackstageEvents(eventsData);
|
||||
return eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [eventsData]);
|
||||
|
||||
// Set general data
|
||||
useEffect(() => {
|
||||
if (!genData) {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user