* feat(time): add optional time format
* feat(time): show 12 hour time in stage timer
* feat(time): override locally
* feat(time): show timer in selected format
* feat(time): show 12 hour time in table header
* refactor: extract time formatting into utility
* refactor: migrate datetime library
* refactor(formatTime): centralise time formatting for viewers
* feature(12hour): version bump
This commit is contained in:
Carlos Valente
2022-08-30 19:04:02 +02:00
committed by GitHub
parent a8ce6f3fc1
commit 0651777055
37 changed files with 459 additions and 378 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "1.3.0",
"version": "1.6.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.0.0-next.3",
@@ -14,6 +14,8 @@
"axios": "^0.25.0",
"color": "^4.2.3",
"framer-motion": "^6.3.3",
"jotai": "^1.7.8",
"luxon": "^3.0.1",
"react": "^18.1.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.1.0",
+2 -2
View File
@@ -11,7 +11,7 @@ import AppRouter from './AppRouter';
// Load Open Sans typeface
require('typeface-open-sans');
const queryClient = new QueryClient();
export const ontimeQueryClient = new QueryClient();
function App() {
@@ -46,7 +46,7 @@ function App() {
return (
<ChakraProvider resetCSS theme={theme}>
<SocketProvider>
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter>
<div className='App'>
+8 -4
View File
@@ -19,7 +19,12 @@ export const ontimePlaceholderInfo = {
* @type {{pinCode: null}}
*/
export const ontimePlaceholderSettings = {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
};
/**
@@ -223,9 +228,7 @@ export const downloadEvents = async () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(
new Blob([response.data], { type: 'application/json' })
);
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
@@ -252,4 +255,5 @@ export const uploadEvents = async (file) => {
* @description HTTP request to upload events
* @return {Promise}
*/
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
export const uploadEventsWithPath = async (filepath) =>
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
@@ -0,0 +1,20 @@
import { atomWithStorage, selectAtom } from 'jotai/utils';
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
showQuickEntry: false,
startTimeIsLastEnd: false,
defaultPublic: false,
});
export const showQuickEntryAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.showQuickEntry
);
export const startTimeIsLastEndAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.startTimeIsLastEnd
);
export const defaultPublicAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.defaultPublic
);
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { stringFromMillis } from '../../utils/time';
import { formatTime } from '../../utils/time';
import style from './Paginator.module.scss';
@@ -9,8 +9,8 @@ export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = stringFromMillis(timeStart, false) || '';
const end = stringFromMillis(timeEnd, false) || '';
const start = formatTime(timeStart, { format: 'hh:mm' });
const end = formatTime(timeEnd, { format: 'hh:mm' });
// user colours
const userColour = colour !== '' ? colour : 'transparent';
@@ -1,32 +0,0 @@
import React, { createContext, useState } from 'react';
export const LocalEventSettingsContext = createContext({
showQuickEntry: false,
starTimeIsLastEnd: true,
defaultPublic: true,
setShowQuickEntry: () => undefined,
setStarTimeIsLastEnd: () => undefined,
setDefaultPublic: () => undefined,
});
export const LocalEventSettingsProvider = ({ children }) => {
const [showQuickEntry, setShowQuickEntry] = useState(false);
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
const [defaultPublic, setDefaultPublic] = useState(false);
return (
<LocalEventSettingsContext.Provider
value={{
showQuickEntry,
setShowQuickEntry,
starTimeIsLastEnd,
setStarTimeIsLastEnd,
defaultPublic,
setDefaultPublic,
}}
>
{children}
</LocalEventSettingsContext.Provider>
);
};
@@ -373,10 +373,9 @@ describe('test formatEvents function', () => {
isNext: false,
colour: "",
},
]
const parsed = formatEventList(testEvent, selectedId, nextId, true);
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
expect(parsed).toStrictEqual(expected);
});
@@ -403,7 +402,7 @@ describe('test formatEvents function', () => {
]
const parsed = formatEventList(testEvent, selectedId, nextId, true);
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
expect(parsed).toStrictEqual(expected);
});
@@ -431,7 +430,7 @@ describe('test formatEvents function', () => {
]
const parsed = formatEventList(testEvent, selectedId, nextId, true);
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
expect(parsed).toStrictEqual(expected);
});
})
});
@@ -0,0 +1,29 @@
import { formatTime } from '../time';
describe('formatTime()', () => {
test('parses 24h strings', () => {
const ms = 13 * 60 * 60 * 1000;
const options = {
showSeconds: true,
format: 'irrelevant',
};
const time = formatTime(ms, options, () => '24');
expect(time).toStrictEqual('13:00:00');
});
test('parses same string in 12h strings', () => {
const ms = 13 * 60 * 60 * 1000;
const options = {
showSeconds: true,
format: 'hh:mm:ss a',
};
const time = formatTime(ms, options, () => '12');
expect(time).toStrictEqual('01:00:00 PM');
});
test('handles null times', () => {
const ms = null;
const time = formatTime(ms);
expect(time).toStrictEqual('...');
});
});
+13 -11
View File
@@ -1,4 +1,4 @@
import { stringFromMillis } from './time';
import { formatTime } from './time';
/**
* @description From a list of events, returns only events of type event with calculated delays
@@ -58,27 +58,29 @@ export const trimEventlist = (events, selectedId, limit) => {
* @param {Object[]} events - given events
* @param {string} selectedId - id of currently selected event
* @param {string} nextId - id of next event
* @param {boolean} [showEnd] - whether to show the end time
* @param {object} [options]
* @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, showEnd = false) => {
export const formatEventList = (events, selectedId, nextId, options) => {
if (events == null) return [];
const { showEnd = false } = options;
const givenEvents = [...events];
// format list
const formattedEvents = [];
for (const g of givenEvents) {
const start = stringFromMillis(g.timeStart, false);
const end = stringFromMillis(g.timeEnd, false);
for (const event of givenEvents) {
const start = formatTime(event.timeStart)
const end = formatTime(event.timeEnd);
formattedEvents.push({
id: g.id,
id: event.id,
time: showEnd ? `${start} - ${end}` : start,
title: g.title,
isNow: g.id === selectedId,
isNext: g.id === nextId,
colour: g.colour,
title: event.title,
isNow: event.id === selectedId,
isNext: event.id === nextId,
colour: event.colour,
});
}
+34 -18
View File
@@ -1,3 +1,8 @@
import { DateTime } from 'luxon';
import { ontimeQueryClient } from '../../App';
import { APP_SETTINGS } from '../api/apiConstants';
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
@@ -26,12 +31,7 @@ export const nowInMillis = () => {
* @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02
*/
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms);
@@ -54,18 +54,34 @@ export const stringFromMillis = (
};
/**
* @description Converts an excel date to milliseconds
* @argument {string} excelDate - excel string date
* @returns {number} - time in milliseconds
* @description Resolves format from url and store
* @return {string|undefined}
*/
export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
export const resolveTimeFormat = () => {
const params = new URL(document.location).searchParams;
const urlOptions = params.get('format');
const settings = ontimeQueryClient.getQueryData(APP_SETTINGS);
return h * mth + m * mtm + s * mts;
}
return 0;
return urlOptions || settings?.timeFormat;
};
/**
/**
* @description utility function to format a date in 12 or 24 hour format
* @param {number} milliseconds
* @param {object} [options]
* @param {boolean} [options.showSeconds]
* @param {string} [options.format]
* @param {function} resolver
* @return {string}
*/
export const formatTime = (milliseconds, options, resolver = resolveTimeFormat) => {
if (milliseconds === null) {
return '...';
}
const timeFormat = resolver();
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
return timeFormat === '12'
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
: stringFromMillis(milliseconds, showSeconds);
};
+16 -21
View File
@@ -1,10 +1,9 @@
import React, { lazy, useEffect } from 'react';
import React, { lazy } from 'react';
import { useDisclosure } from '@chakra-ui/hooks';
import { Box } from '@chakra-ui/layout';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import ModalManager from 'features/modals/ModalManager';
import { LocalEventSettingsProvider } from '../../common/context/LocalEventSettingsContext';
import { LoggingProvider } from '../../common/context/LoggingContext';
import MenuBar from '../menu/MenuBar';
@@ -19,28 +18,24 @@ export default function Editor() {
const { isOpen, onOpen, onClose } = useDisclosure();
// Set window title
useEffect(() => {
document.title = 'ontime - Editor';
}, []);
document.title = 'ontime - Editor';
return (
<LoggingProvider>
<LocalEventSettingsProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
</Box>
<EventList onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
<MessageControl />
<TimerControl />
<Info />
</div>
</LocalEventSettingsProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
</Box>
<EventList onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
<MessageControl />
<TimerControl />
<Info />
</div>
</LoggingProvider>
);
}
@@ -1,9 +1,13 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Checkbox } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
import {
defaultPublicAtom,
startTimeIsLastEndAtom,
} from '../../../common/atoms/LocalEventSettings';
import style from './EntryBlock.module.scss';
@@ -16,13 +20,14 @@ export default function EntryBlock(props) {
disableAddDelay = true,
disableAddBlock,
} = props;
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const [doStartTime, setStartTime] = useState(startTimeIsLastEnd);
const [doPublic, setPublic] = useState(defaultPublic);
useEffect(() => {
setStartTime(starTimeIsLastEnd);
}, [starTimeIsLastEnd]);
setStartTime(startTimeIsLastEnd);
}, [startTimeIsLastEnd]);
useEffect(() => {
setPublic(defaultPublic);
@@ -36,7 +41,7 @@ export default function EntryBlock(props) {
onClick={() =>
eventsHandler(
'add',
{ type: 'event', after: previousId, isPublic: doPublic },
{ type: 'event', after: previousId, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? previousId : undefined }
)
}
@@ -68,7 +73,9 @@ export default function EntryBlock(props) {
size='sm'
colorScheme='blue'
isChecked={doStartTime}
onChange={(e) => setStartTime(e.target.checked)}
onChange={(e) => {
setStartTime(e.target.checked);
}}
>
Start time is last end
</Checkbox>
@@ -93,4 +100,3 @@ EntryBlock.propTypes = {
disableAddDelay: PropTypes.bool,
disableAddBlock: PropTypes.bool,
};
+21 -17
View File
@@ -2,9 +2,10 @@ import React, { createRef, useCallback, useContext, useEffect, useState } from '
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import Empty from 'common/components/state/Empty';
import { useSocket } from 'common/context/socketContext';
import { useAtomValue } from 'jotai';
import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings';
import { CursorContext } from '../../../common/context/CursorContext';
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
import EntryBlock from '../EntryBlock/EntryBlock';
import EventListItem from './EventListItem';
@@ -19,21 +20,24 @@ export default function EventList(props) {
const [selectedId, setSelectedId] = useState(null);
const [nextId, setNextId] = useState(null);
const cursorRef = createRef();
const { showQuickEntry } = useContext(LocalEventSettingsContext);
const showQuickEntry = useAtomValue(showQuickEntryAtom);
const insertAtCursor = useCallback((type, cursor) => {
if (cursor === -1) {
eventsHandler('add', { type: type });
} else {
const previousEvent = events[cursor];
const nextEvent = events[cursor + 1];
if (type === 'event') {
eventsHandler('add', { type: type, after: previousEvent.id });
} else if (previousEvent?.type !== type && nextEvent?.type !== type) {
eventsHandler('add', { type: type, after: previousEvent.id });
const insertAtCursor = useCallback(
(type, cursor) => {
if (cursor === -1) {
eventsHandler('add', { type: type });
} else {
const previousEvent = events[cursor];
const nextEvent = events[cursor + 1];
if (type === 'event') {
eventsHandler('add', { type: type, after: previousEvent.id });
} else if (previousEvent?.type !== type && nextEvent?.type !== type) {
eventsHandler('add', { type: type, after: previousEvent.id });
}
}
}
},[events, eventsHandler])
},
[events, eventsHandler]
);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
@@ -54,19 +58,19 @@ export default function EventList(props) {
if (e.key === 'e' || e.key === 'E') {
e.preventDefault();
if (cursor == null) return;
insertAtCursor('event', cursor)
insertAtCursor('event', cursor);
}
// D
if (e.key === 'd' || e.key === 'D') {
e.preventDefault();
if (cursor == null) return;
insertAtCursor('delay', cursor)
insertAtCursor('delay', cursor);
}
// B
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
if (cursor == null) return;
insertAtCursor('block', cursor)
insertAtCursor('block', cursor);
}
}
},
@@ -1,7 +1,11 @@
import React, { memo, useCallback, useContext } from 'react';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
import {
defaultPublicAtom,
startTimeIsLastEndAtom,
} from '../../../common/atoms/LocalEventSettings';
import { LoggingContext } from '../../../common/context/LoggingContext';
import BlockBlock from '../BlockBlock/BlockBlock';
import DelayBlock from '../DelayBlock/DelayBlock';
@@ -19,19 +23,11 @@ const areEqual = (prevProps, nextProps) => {
};
const EventListItem = (props) => {
const {
type,
index,
eventIndex,
data,
selected,
next,
eventsHandler,
delay,
previousEnd,
} = props;
const { type, index, eventIndex, data, selected, next, eventsHandler, delay, previousEnd } =
props;
const { emitError } = useContext(LoggingContext);
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
/**
* @description calculates duration from given options
@@ -56,7 +52,7 @@ const EventListItem = (props) => {
after: data.id,
isPublic: defaultPublic,
},
{ startIsLastEnd: starTimeIsLastEnd ? data.id : undefined }
{ startIsLastEnd: startTimeIsLastEnd ? data.id : undefined }
);
break;
case 'delay':
@@ -103,7 +99,7 @@ const EventListItem = (props) => {
break;
}
},
[calculateDuration, data, defaultPublic, emitError, eventsHandler, starTimeIsLastEnd]
[calculateDuration, data, defaultPublic, emitError, eventsHandler, startTimeIsLastEnd]
);
switch (type) {
@@ -147,5 +143,5 @@ EventListItem.propTypes = {
next: PropTypes.bool,
eventsHandler: PropTypes.func,
delay: PropTypes.number,
previousEnd: PropTypes.number
}
previousEnd: PropTypes.number,
};
+91 -114
View File
@@ -1,22 +1,32 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { ModalBody } from '@chakra-ui/modal';
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
import {
Checkbox,
FormControl,
FormLabel,
Input,
PinInput,
PinInputField,
Select,
} from '@chakra-ui/react';
import { FiEye } from '@react-icons/all-files/fi/FiEye';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { APP_SETTINGS } from 'common/api/apiConstants';
import { getSettings, ontimePlaceholderSettings, postSettings } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch';
import { useAtom } from 'jotai';
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import { LocalEventSettingsContext } from '../../common/context/LocalEventSettingsContext';
import { LoggingContext } from '../../common/context/LoggingContext';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
const version = require('../../../package.json').version
const version = require('../../../package.json').version;
export default function AppSettingsModal() {
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
@@ -26,18 +36,8 @@ export default function AppSettingsModal() {
const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true);
const {
showQuickEntry,
setShowQuickEntry,
starTimeIsLastEnd,
setStarTimeIsLastEnd,
defaultPublic,
setDefaultPublic,
} = useContext(LocalEventSettingsContext);
const [doShowQuickEntry, setDoShowQuickEntry] = useState(showQuickEntry);
const [doStarTimeIsLastEnd, setDoStarTimeIsLastEnd] = useState(starTimeIsLastEnd);
const [doDefaultPublic, setDoDefaultPublic] = useState(defaultPublic);
const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom);
const [formSettings, setFormSettings] = useState(eventSettings);
/**
* Set formdata from server state
@@ -47,117 +47,72 @@ export default function AppSettingsModal() {
if (changed) return;
setFormData({
pinCode: data.pinCode,
timeFormat: data.timeFormat,
});
}, [changed, data]);
/**
* Set formdata from context
*/
useEffect(() => {
if (showQuickEntry == null) return;
setDoShowQuickEntry(showQuickEntry);
}, [showQuickEntry]);
useEffect(() => {
if (starTimeIsLastEnd == null) return;
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
}, [starTimeIsLastEnd]);
useEffect(() => {
if (defaultPublic == null) return;
setDoDefaultPublic(defaultPublic);
}, [defaultPublic]);
/**
* Validate and submit data
*/
const submitHandler = useCallback(
async (event) => {
event.preventDefault();
setSubmitting(true);
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
// set context
setShowQuickEntry(doShowQuickEntry);
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
setDefaultPublic(doDefaultPublic);
// set context
saveEventSettings(formSettings);
const validation = { isValid: false };
const f = formData;
// we might not have changed this
if (f.pinCode !== data.pinCode) {
const e = { status: false, message: '' };
// Validate fields
if (f.pinCode === '' || f.pinCode == null) {
e.status = true;
e.message += 'App pin code removed';
} else {
e.status = true;
e.message += 'App pin code added';
}
// set fields with error
if (!e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
}
// we might not have changed this
if (formData.pinCode !== data.pinCode) {
// Validate fields
if (formData.pinCode === '' || formData.pinCode == null) {
validation.isValid = true;
validation.message += 'App pin code removed';
} else {
validation.isValid = true;
validation.message += 'App pin code added';
}
setSubmitting(false);
setChanged(false);
},
[
data.pinCode,
doDefaultPublic,
doShowQuickEntry,
doStarTimeIsLastEnd,
emitError,
emitWarning,
formData,
refetch,
setDefaultPublic,
setShowQuickEntry,
setStarTimeIsLastEnd,
]
);
}
if (formData.timeFormat !== data.timeFormat) {
if (formData.timeFormat === '12' || formData.timeFormat === '24') {
validation.isValid = true;
}
}
// set fields with error
if (!validation.isValid) {
emitError(`Invalid Input: ${validation.message}`);
} else {
await postSettings(formData);
await refetch();
validation?.message && emitWarning(validation.message);
}
setSubmitting(false);
setChanged(false);
};
/**
* Reverts local state equals to server state
*/
const revert = useCallback(async () => {
const revert = async () => {
setChanged(false);
await refetch();
// set from context
setDoShowQuickEntry(showQuickEntry);
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
setDoDefaultPublic(defaultPublic);
}, [defaultPublic, refetch, showQuickEntry, starTimeIsLastEnd]);
setFormSettings(eventSettings);
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 = useCallback(
(field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
},
[formData]
);
/**
* Sets changed flag to true
*/
const handleContextChange = useCallback(() => {
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
}, []);
};
const disableModal = status !== 'success';
@@ -201,6 +156,7 @@ export default function AppSettingsModal() {
<PinInput
{...inputProps}
type='alphanumeric'
name='pinCode'
defaultValue=''
value={formData.pinCode}
mask={hidePin}
@@ -235,31 +191,52 @@ export default function AppSettingsModal() {
</div>
</FormControl>
</div>
<div className={style.modalColumn}>
<FormControl id='timeFormat'>
<FormLabel htmlFor='timeFormat'>
Time format
<span className={style.labelNote}>
<br />
12 / 24 hour format (viewers only for now)
</span>
</FormLabel>
<Select
size='sm'
name='timeFormat'
value={formData.timeFormat}
isDisabled={disableModal}
onChange={(event) => handleChange('timeFormat', event.target.value)}
>
<option value='12'>12 hours eg. 11:00:10 PM</option>
<option value='24'>24 hours eg. 23:00:10</option>
</Select>
</FormControl>
</div>
<div className={style.hSeparator}>Create Event Default Settings</div>
<div className={style.modalColumn}>
<Checkbox
isChecked={doShowQuickEntry}
isChecked={formSettings.showQuickEntry}
onChange={(e) => {
setDoShowQuickEntry(e.target.checked);
handleContextChange();
setFormSettings((prev) => ({ ...prev, showQuickEntry: e.target.checked }));
setChanged(true);
}}
>
Show quick entry on hover
</Checkbox>
<Checkbox
isChecked={doStarTimeIsLastEnd}
isChecked={formSettings.startTimeIsLastEnd}
onChange={(e) => {
setDoStarTimeIsLastEnd(e.target.checked);
handleContextChange();
setFormSettings((prev) => ({ ...prev, startTimeIsLastEnd: e.target.checked }));
setChanged(true);
}}
>
Start time is last end
</Checkbox>
<Checkbox
isChecked={doDefaultPublic}
isChecked={formSettings.defaultPublic}
onChange={(e) => {
setDoDefaultPublic(e.target.checked);
handleContextChange();
setFormSettings((prev) => ({ ...prev, defaultPublic: e.target.checked }));
setChanged(true);
}}
>
Event default public
+1 -1
View File
@@ -32,7 +32,7 @@
grid-template-areas:
'name playback running time actions'
'now playback running time actions';
grid-template-columns: 1fr auto 10em 10em auto;
grid-template-columns: 1fr auto 10em 12.5em auto;
align-items: center;
padding: 0.25em 1em;
+6 -3
View File
@@ -10,7 +10,7 @@ import { useSocket } from '../../common/context/socketContext';
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
import { useFetch } from '../../common/hooks/useFetch';
import { formatDisplay } from '../../common/utils/dateConfig';
import { stringFromMillis } from '../../common/utils/time';
import { formatTime } from '../../common/utils/time';
import PlaybackIcon from './tableElements/PlaybackIcon';
@@ -97,7 +97,10 @@ export default function TableHeader() {
// prepare presentation variables
const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`;
const timeNow = formatTime(timer.clock, {
showSeconds: true,
format: 'hh:mm:ss a',
});
return (
<div className={style.header}>
<div className={style.headerName}>{data?.title || ''}</div>
@@ -115,7 +118,7 @@ export default function TableHeader() {
<div className={style.headerClock}>
<span className={style.label}>Time Now</span>
<br />
<span className={style.timer}>{stringFromMillis(timer.clock)}</span>
<span className={style.timer}>{timeNow}</span>
</div>
<div className={style.headerActions}>
<Tooltip openDelay={300} label='Follow selected'>
+19 -18
View File
@@ -1,12 +1,11 @@
/* eslint-disable react/display-name */
import React, { useEffect, 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 { stringFromMillis } from '../../common/utils/time';
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';
const withSocket = (Component) => {
return (props) => {
@@ -30,9 +29,9 @@ const withSocket = (Component) => {
visible: false,
});
const [timer, setTimer] = useState({
clock: null,
running: null,
isNegative: null,
clock: 0,
running: 0,
isNegative: false,
startedAt: null,
expectedFinish: null,
});
@@ -67,7 +66,9 @@ const withSocket = (Component) => {
// Ask for update on load
useEffect(() => {
if (socket == null) return;
if (!socket) {
return;
}
// Handle timer messages
socket.on('messages-timer', (data) => {
@@ -154,12 +155,12 @@ const withSocket = (Component) => {
// Filter events only to pass down
useEffect(() => {
if (eventsData == null) return;
if (!eventsData) {
return;
}
// filter just events with title
if (Array.isArray(eventsData)) {
const pe = eventsData.filter(
(d) => d.type === 'event' && d.title !== '' && d.isPublic
);
const pe = eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
setPublicEvents(pe);
// everything goes backstage
@@ -169,10 +170,13 @@ const withSocket = (Component) => {
// Set general data
useEffect(() => {
if (genData == null) return;
if (!genData) {
return;
}
setGeneral(genData);
}, [genData]);
/********************************************/
/*** + titleManager ***/
/*** WRAP INFORMATION RELATED TO TITLES ***/
@@ -221,9 +225,6 @@ const withSocket = (Component) => {
const timeManager = {
...timer,
finished: playback === 'start' && timer.isNegative && timer.startedAt,
clock: stringFromMillis(timer.clock),
clockMs: timer.clock,
clockNoSeconds: stringFromMillis(timer.clock, false),
playstate: playback,
};
@@ -8,10 +8,16 @@ import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation';
import style from './StageManager.module.scss';
const formatOptions = {
showSeconds: true,
format: 'hh:mm:ss a',
};
export default function StageManager(props) {
const { publ, title, time, backstageEvents, selectedId, general } = props;
const [filteredEvents, setFilteredEvents] = useState(null);
@@ -41,6 +47,8 @@ export default function StageManager(props) {
if (time.isNegative) stageTimer = `-${stageTimer}`;
}
const clock = formatTime(time.clock, formatOptions);
return (
<div className={style.container__gray}>
<NavLogo />
@@ -118,7 +126,7 @@ export default function StageManager(props) {
<div className={style.clockContainer}>
<div className={style.label}>Time Now</div>
<div className={style.clock}>{time.clock}</div>
<div className={style.clock}>{clock}</div>
</div>
<div className={style.countdownContainer}>
@@ -6,19 +6,24 @@ import NavLogo from '../../../common/components/nav/NavLogo';
import Empty from '../../../common/components/state/Empty';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import getDelayTo from '../../../common/utils/getDelayTo';
import { stringFromMillis } from '../../../common/utils/time';
import { formatTime } from '../../../common/utils/time';
import { fetchTimerData, sanitiseTitle, timerMessages } from './countdown.helpers';
import style from './Countdown.module.scss';
const formatOptions = {
showSeconds: true,
format: 'hh:mm:ss a',
};
export default function Countdown(props) {
const [searchParams] = useSearchParams();
const { backstageEvents, time, selectedId } = props;
const [follow, setFollow] = useState(null);
const [runningTimer, setRunningTimer] = useState(0);
const [runningMessage, setRunningMessage] = useState('');
const [delay, setDelay] = useState(0);
const [searchParams] = useSearchParams();
// Set window title
useEffect(() => {
@@ -46,8 +51,8 @@ export default function Countdown(props) {
if (typeof followThis !== 'undefined') {
setFollow(followThis);
const idx = backstageEvents.findIndex((event) => event.id === followThis.id);
const delay = getDelayTo(backstageEvents, idx);
setDelay(delay);
const delayToEvent = getDelayTo(backstageEvents, idx);
setDelay(delayToEvent);
}
}, [backstageEvents, searchParams]);
@@ -73,6 +78,16 @@ export default function Countdown(props) {
const isSelected = useMemo(() => runningMessage === timerMessages.running, [runningMessage]);
const clock = formatTime(time.clock, formatOptions);
const startTime =
follow === null
? '...'
: formatTime(follow.timeStart + delay, formatOptions);
const endTime =
follow === null
? '...'
: formatTime(follow.timeEnd + delay, formatOptions);
return (
<div className={style.container}>
<NavLogo />
@@ -100,19 +115,17 @@ export default function Countdown(props) {
<div className={style.timers}>
<div className={style.timer}>
<div className={style.label}>Time Now</div>
<span className={style.value}>{time.clock}</span>
<span className={style.value}>{clock}</span>
</div>
<div className={style.timer}>
<div className={style.label}>Start Time</div>
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
{stringFromMillis(follow.timeStart + delay)}
{startTime}
</span>
</div>
<div className={style.timer}>
<div className={style.label}>End Time</div>
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
{stringFromMillis(follow.timeEnd + delay)}
</span>
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>{endTime}</span>
</div>
</div>
<div className={style.status}>{runningMessage}</div>
@@ -126,7 +139,7 @@ export default function Countdown(props) {
isSelected || time.waiting
)}
</span>
<div className={style.title}>{follow.title || 'Untitled Event'}</div>
<div className={style.title}>{follow?.title || 'Untitled Event'}</div>
</div>
)}
</div>
@@ -137,4 +150,5 @@ Countdown.propTypes = {
backstageEvents: PropTypes.array,
time: PropTypes.object,
selectedId: PropTypes.string,
settings: PropTypes.object,
};
@@ -35,7 +35,7 @@ describe('fetchTimerData() function', () => {
const startMockValue = 10000;
const timeNow = 1000;
const follow = { id: 'anotherevent', timeStart: startMockValue };
const time = { clockMs: timeNow };
const time = { clock: timeNow };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(timerMessages.toStart);
@@ -48,7 +48,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
const time = { clock: timeNow, running: endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(timerMessages.waiting);
@@ -61,7 +61,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 30000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
const time = { clock: timeNow, running: endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(timerMessages.ended);
@@ -74,7 +74,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(timerMessages.waiting);
@@ -87,7 +87,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, followId);
expect(message).toBe(timerMessages.running);
@@ -100,7 +100,7 @@ describe('fetchTimerData() function', () => {
const timeNow = 2000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
expect(message).toBe(timerMessages.toStart);
@@ -34,22 +34,22 @@ export const fetchTimerData = (time, follow, selectedId) => {
// check that is not running
message = time.playstate === 'pause' ? timerMessages.waiting : timerMessages.running;
timer = time.running;
} else if (time.clockMs < follow.timeStart) {
} else if (time.clock < follow.timeStart) {
// if it hasnt started, we count to start
message = timerMessages.toStart;
timer = millisToSeconds(follow.timeStart - time.clockMs);
} else if (follow.timeStart <= time.clockMs && time.clockMs <= follow.timeEnd) {
timer = millisToSeconds(follow.timeStart - time.clock);
} else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) {
// if it has started, we show running timer
message = timerMessages.waiting;
timer = time.running;
} else {
if (follow.timeStart > follow.timeEnd) {
// ends day after
if (follow.timeStart > time.clockMs ) {
if (follow.timeStart > time.clock ) {
// if it hasnt started, we count to start
message = timerMessages.toStart;
timer = millisToSeconds(follow.timeStart - time.clockMs);
} else if (follow.timeStart <= time.clockMs) {
timer = millisToSeconds(follow.timeStart - time.clock);
} else if (follow.timeStart <= time.clock) {
// if it has started, we show running timer
message = timerMessages.waiting;
timer = time.running;
+16 -18
View File
@@ -6,10 +6,16 @@ import TitleSide from 'common/components/views/TitleSide';
import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation';
import style from './Public.module.scss';
const formatOptions = {
showSeconds: true,
format: 'hh:mm:ss a',
};
export default function Public(props) {
const { publ, publicTitle, time, events, publicSelectedId, general } = props;
const [pageNumber, setPageNumber] = useState(0);
@@ -23,7 +29,7 @@ export default function Public(props) {
// Format messages
const showPubl = publ.text !== '' && publ.visible;
// motion
const clock = formatTime(time.clock, formatOptions);
return (
<div className={style.container__gray}>
@@ -78,12 +84,12 @@ export default function Public(props) {
<div className={style.label}>Today</div>
<div className={style.nav}>
{pageNumber > 1 &&
[...Array(pageNumber).keys()].map((i) => (
<div
key={i}
className={i === currentPage ? style.navItemSelected : style.navItem}
/>
))}
[...Array(pageNumber).keys()].map((i) => (
<div
key={i}
className={i === currentPage ? style.navItemSelected : style.navItem}
/>
))}
</div>
</div>
<Paginator
@@ -95,18 +101,14 @@ export default function Public(props) {
/>
</div>
<div
className={
showPubl ? style.publicContainer : style.publicContainerHidden
}
>
<div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
<div className={style.label}>Public message</div>
<div className={style.message}>{publ.text}</div>
</div>
<div className={style.clockContainer}>
<div className={style.label}>Time Now</div>
<div className={style.clock}>{time.clock}</div>
<div className={style.clock}>{clock}</div>
</div>
<div className={style.infoContainer}>
@@ -116,11 +118,7 @@ export default function Public(props) {
</div>
<div className={style.qr}>
{general.url != null && general.url !== '' && (
<QRCode
value={general.url}
size={window.innerWidth / 12}
level='L'
/>
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
)}
</div>
</div>
+23 -19
View File
@@ -7,20 +7,27 @@ import { formatDisplay } from 'common/utils/dateConfig';
import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { formatTime } from '../../../common/utils/time';
import style from './Pip.module.scss';
const formatOptions = {
showSeconds: true,
format: 'hh:mm:ss a',
};
export default function Pip(props) {
const { time, backstageEvents, selectedId, general } = props;
const [size, setSize] = useState('');
const ref = useRef(null);
const pipAreaRef = useRef(null);
const [filteredEvents, setFilteredEvents] = useState(null);
const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
// calculcate pip size
// calculate pip size
useLayoutEffect(() => {
const h = ref.current.clientHeight;
const w = ref.current.clientWidth;
const h = pipAreaRef.current.clientHeight;
const w = pipAreaRef.current.clientWidth;
setSize(`${w} x ${h}`);
}, []);
@@ -51,11 +58,12 @@ export default function Pip(props) {
}, [backstageEvents]);
// Format messages
const showInfo =
general.backstageInfo !== '' && general.backstageInfo != null;
const showInfo = general.backstageInfo !== '' && general.backstageInfo != null;
let stageTimer = formatDisplay(Math.abs(time.running), true);
if (time.isNegative) stageTimer = `-${stageTimer}`;
const clock = formatTime(time.clock, formatOptions);
return (
<div className={style.container__gray}>
<NavLogo />
@@ -67,12 +75,12 @@ export default function Pip(props) {
<div className={style.label}>Today</div>
<div className={style.nav}>
{pageNumber > 1 &&
[...Array(pageNumber).keys()].map((i) => (
<div
key={i}
className={i === currentPage ? style.navItemSelected : style.navItem}
/>
))}
[...Array(pageNumber).keys()].map((i) => (
<div
key={i}
className={i === currentPage ? style.navItemSelected : style.navItem}
/>
))}
</div>
</div>
<Paginator
@@ -86,7 +94,7 @@ export default function Pip(props) {
/>
</div>
<div className={style.pip} ref={ref}>
<div className={style.pip} ref={pipAreaRef}>
<Emptyimage className={style.empty} />
<span className={style.piptext}>{size}</span>
</div>
@@ -100,11 +108,7 @@ export default function Pip(props) {
</div>
<div className={style.qr}>
{general.url != null && general.url !== '' && (
<QRCode
value={general.url}
size={window.innerWidth / 12}
level='L'
/>
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
)}
</div>
</motion.div>
@@ -113,7 +117,7 @@ export default function Pip(props) {
<div className={style.clockContainer}>
<div className={style.label}>Time Now</div>
<div className={style.clock}>{time.clock}</div>
<div className={style.clock}>{clock}</div>
</div>
<div className={style.countdownContainer}>
@@ -9,13 +9,18 @@ import {
getEventsWithDelay,
trimEventlist,
} from '../../../common/utils/eventsManager';
import { formatTime, stringFromMillis } from '../../../common/utils/time';
import style from './StudioClock.module.scss';
const formatOptions = {
showSeconds: false,
format: 'hh:mm',
};
export default function StudioClock(props) {
const { title, time, backstageEvents, selectedId, nextId, onAir } = props;
const { fontSize, ref } = useFitText({ maxFontSize: 500 });
const [, , secondsNow] = time.clock.split(':');
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
const [schedule, setSchedule] = useState([]);
const activeIndicators = [...Array(12).keys()];
@@ -28,26 +33,32 @@ export default function StudioClock(props) {
}, []);
// Prepare event list
// Todo: useMemo()
useEffect(() => {
if (backstageEvents == null) return;
const delayed = getEventsWithDelay(backstageEvents);
const events = delayed.filter((e) => e.type === 'event');
const trimmed = trimEventlist(events, selectedId, MAX_TITLES);
const formatted = formatEventList(trimmed, selectedId, nextId);
const formatted = formatEventList(trimmed, selectedId, nextId, {
showEnd: false,
});
setSchedule(formatted);
}, [backstageEvents, selectedId, nextId]);
}, [backstageEvents, nextId, selectedId] );
const clock = formatTime(time.clock, formatOptions);
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
return (
<div className={style.container}>
<NavLogo />
<div className={style.clockContainer}>
<div className={style.time}>{time.clockNoSeconds}</div>
<div className={style.time}>{clock}</div>
<div
ref={ref}
ref={titleRef}
className={style.nextTitle}
style={{ fontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
style={{ fontSize: titleFontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
>
{title.titleNext}
</div>
@@ -89,6 +89,13 @@ $cyan-idle: #0aa;
line-height: 0.8em;
}
.timeAA {
color: $red-active;
font-size: calc(#{$clock-size} / 4.5);
margin-top: calc(50% - calc(#{$clock-size} / 7));
line-height: 0.8em;
}
.nextTitle:after,
.nextCountdown:after,
.nextCountdown__overtime:after {
+20 -29
View File
@@ -7,8 +7,15 @@ import TitleCard from 'common/components/views/TitleCard';
import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import { formatTime } from '../../../common/utils/time';
import style from './Timer.module.scss';
const formatOptions = {
showSeconds: true,
format: 'hh:mm:ss a',
};
export default function Timer(props) {
const { general, pres, title, time } = props;
const [elapsed, setElapsed] = useState(true);
@@ -32,22 +39,12 @@ export default function Timer(props) {
}
}, [searchParams]);
const clock = formatTime(time.clock, formatOptions);
const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playstate !== 'pause';
const normalisedTime = Math.max(time.running, 0);
// show timer if end message is empty
const endMessage =
general.endMessage == null || general.endMessage === '' ? (
<TimerDisplay
time={time.running}
isNegative={time.isNegative}
hideZeroHours
/>
) : (
general.endMessage
);
// motion
const titleVariants = {
hidden: {
@@ -65,16 +62,8 @@ export default function Timer(props) {
};
return (
<div
className={
time.finished ? style.container__grayFinished : style.container__gray
}
>
<div
className={
showOverlay ? style.messageOverlayActive : style.messageOverlay
}
>
<div className={time.finished ? style.container__grayFinished : style.container__gray}>
<div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
<div className={style.message}>{pres.text}</div>
</div>
@@ -82,12 +71,18 @@ export default function Timer(props) {
<div className={style.clockContainer}>
<div className={style.label}>Time Now</div>
<div className={style.clock}>{time.clock}</div>
<div className={style.clock}>{clock}</div>
</div>
<div className={style.timerContainer}>
{time.finished ? (
<div className={style.finished}>{endMessage}</div>
<div className={style.finished}>
{general.endMessage == null || general.endMessage === '' ? (
<TimerDisplay time={time.running} isNegative={time.isNegative} hideZeroHours />
) : (
general.endMessage
)}
</div>
) : (
<div className={isPlaying ? style.countdown : style.countdownPaused}>
<TimerDisplay time={normalisedTime} hideZeroHours />
@@ -96,11 +91,7 @@ export default function Timer(props) {
</div>
{!time.finished && (
<div
className={
isPlaying ? style.progressContainer : style.progressContainerPaused
}
>
<div className={isPlaying ? style.progressContainer : style.progressContainerPaused}>
<MyProgressBar
now={normalisedTime}
complete={time.durationSeconds}
@@ -14,7 +14,7 @@
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
grid-template-rows: auto 1fr auto minmax(25vh, auto);
grid-template-areas:
' clck .... .... .... ....'
' clck clck .... .... ....'
' timr timr timr timr timr'
' prog prog prog prog prog'
' now now .... next next';
+1 -1
View File
@@ -95,7 +95,7 @@
.clock {
font-family: 'Open Sans', sans-serif;
font-size: 3vw;
font-size: 2.25vw;
line-height: 3vw;
text-align: center;
letter-spacing: 0.25vw;
+10
View File
@@ -6623,6 +6623,11 @@ jest@^27.4.3:
import-local "^3.0.2"
jest-cli "^27.5.1"
jotai@^1.7.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.7.8.tgz#1ac4daa2731e0f8e7fab8abb96aebf94b9dfc1a3"
integrity sha512-rXwWz6uLqyZUCRzWIPiYTjI1hjskVxVpDN3lBoOHi66z6uQFkFmKwR8YhcQd+Ex4lODlReuKNIx6WwsF9aKy3g==
js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
@@ -6928,6 +6933,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
luxon@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.1.tgz#6901111d10ad06fd267ad4e4128a84bef8a77299"
integrity sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "1.5.0",
"version": "1.6.0",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
-1
View File
@@ -1049,7 +1049,6 @@ export class EventTimer extends Timer {
this.runCycle();
}
/**
* Deleted an event from the list by its id
* @param {string} eventId
@@ -201,12 +201,14 @@ export const getSettings = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
const timeFormat = data.settings.timeFormat;
// send object with network information
res.status(200).send({
version,
serverPort,
pinCode,
timeFormat,
});
};
@@ -226,9 +228,18 @@ export const postSettings = async (req, res) => {
pin = req.body?.pinCode;
}
}
let timeFormat = data.settings.timeFormat;
if (typeof req.body?.timeFormat === 'string') {
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
timeFormat = req.body.timeFormat;
}
}
data.settings = {
...data.settings,
pinCode: pin,
timeFormat: timeFormat,
};
await db.write();
res.sendStatus(200);
+1
View File
@@ -13,6 +13,7 @@ export const dbModelv1 = {
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
},
aliases: [],
userFields: {
@@ -51,4 +51,4 @@ describe('getPreviousPlayable()', () => {
expect(id).toBe(null);
});
});
});
});
@@ -184,6 +184,7 @@ describe('test json parser with valid def', () => {
settings: {
app: 'ontime',
version: 1,
timeFormat: '24',
},
};
@@ -386,6 +387,7 @@ describe('test corrupt data', () => {
version: 1,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
@@ -408,6 +410,7 @@ describe('test corrupt data', () => {
version: 1,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
@@ -424,6 +427,7 @@ describe('test corrupt data', () => {
version: 1,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
+1
View File
@@ -98,6 +98,7 @@ export const parseSettings_v1 = (data, enforce) => {
const settings = {
lock: s.lock || null,
pinCode: s.pinCode || null,
timeFormat: s.timeFormat || '24',
};
// write to db