mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
Feat/block improvements (#95)
* feat/89-block-improvements style: replace reload icon * feat/block-feat/block-improvements add cursor buttons * feat/block-improvements hover to create * feat/block-improvements change settings in modal * feat/block-improvements version bump
This commit is contained in:
@@ -26,17 +26,17 @@ and extend with using the URL aliases feature
|
||||
```
|
||||
For the presentation views...
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/presenter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
...and for the editor (the control interface, same as the app)
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@ export const requestPatch = async (data) => {
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const action = 'reorder';
|
||||
return await axios.patch(eventsURL + '/' + action, data);
|
||||
return await axios.patch(`${eventsURL}/${action}`, data);
|
||||
};
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
return await axios.patch(eventsURL + '/' + action + '/' + eventId);
|
||||
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
return await axios.delete(eventsURL + '/' + eventId);
|
||||
return await axios.delete(`${eventsURL}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
return await axios.delete(eventsURL + '/all');
|
||||
return await axios.delete(`${eventsURL}/all`);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CursorContext = createContext({
|
||||
cursor: 0,
|
||||
isCursorLocked: false,
|
||||
|
||||
setCursor: () => undefined,
|
||||
moveCursorUp: () => undefined,
|
||||
moveCursorDown: () => undefined,
|
||||
});
|
||||
|
||||
export const CursorProvider = (props) => {
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
|
||||
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
|
||||
|
||||
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
|
||||
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
|
||||
|
||||
const moveCursorUp = useCallback(() => {
|
||||
setCursor((prev) => Math.max(prev - 1, 0));
|
||||
}, []);
|
||||
|
||||
const moveCursorDown = useCallback(() => {
|
||||
setCursor((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @param {boolean | undefined} newValue
|
||||
*/
|
||||
const toggleCursorLocked = useCallback(
|
||||
(newValue = undefined) => {
|
||||
if (newValue === undefined) {
|
||||
if (isCursorLocked) {
|
||||
cursorLockedOff();
|
||||
} else {
|
||||
cursorLockedOn();
|
||||
}
|
||||
} else if (!newValue) {
|
||||
cursorLockedOff();
|
||||
} else if (newValue) {
|
||||
cursorLockedOn();
|
||||
}
|
||||
},
|
||||
[cursorLockedOff, cursorLockedOn, isCursorLocked]
|
||||
);
|
||||
|
||||
return (
|
||||
<CursorContext.Provider
|
||||
value={{
|
||||
cursor,
|
||||
isCursorLocked,
|
||||
toggleCursorLocked,
|
||||
setCursor,
|
||||
moveCursorUp,
|
||||
moveCursorDown,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</CursorContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createContext, useState } from 'react';
|
||||
|
||||
export const LocalEventSettingsContext = createContext({
|
||||
showQuickEntry: false,
|
||||
starTimeIsLastEnd: true,
|
||||
defaultPublic: true,
|
||||
|
||||
setShowQuickEntry: () => undefined,
|
||||
setStarTimeIsLastEnd: () => undefined,
|
||||
setDefaultPublic: () => undefined,
|
||||
});
|
||||
|
||||
export const LocalEventSettingsProvider = (props) => {
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</LocalEventSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
setStoredValue(value);
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
//
|
||||
// useTimeout React Hook
|
||||
//
|
||||
// React hook for delaying calls with time
|
||||
export const useTimeout = (
|
||||
callback, // function to call. No args passed.
|
||||
timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
|
||||
{
|
||||
// manage re-render behavior.
|
||||
// by default, a re-render in your component will re-define the callback,
|
||||
// which will cause this timeout to cancel itself.
|
||||
// to avoid cancelling on re-renders (but still cancel on unmounts),
|
||||
// set `persistRenders: true,`.
|
||||
persistRenders = false,
|
||||
} = {},
|
||||
// These dependencies are injected for testing purposes.
|
||||
// (pure functions - where all dependencies are arguments - is often easier to test)
|
||||
_setTimeout = setTimeout,
|
||||
_clearTimeout = clearTimeout,
|
||||
_useEffect = useEffect
|
||||
) => {
|
||||
let timeoutId;
|
||||
const cancel = () => timeoutId && _clearTimeout(timeoutId);
|
||||
|
||||
_useEffect(
|
||||
() => {
|
||||
timeoutId = _setTimeout(callback, timeout);
|
||||
return cancel;
|
||||
},
|
||||
persistRenders
|
||||
? [_setTimeout, _clearTimeout]
|
||||
: [callback, timeout, _setTimeout, _clearTimeout]
|
||||
);
|
||||
|
||||
return cancel;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiCheck } from 'react-icons/fi';
|
||||
|
||||
export default function ApplyIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
@@ -8,7 +8,7 @@ export default function ApplyIconBtn(props) {
|
||||
<Tooltip label='Apply delays'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
icon={<FiCheck />}
|
||||
colorScheme='orange'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretDown } from 'react-icons/io5';
|
||||
|
||||
export default function CursorDownBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor down Alt + ↓'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoCaretDown />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
|
||||
export default function LockIconBtn(props) {
|
||||
export default function CursorLockedBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretUp } from 'react-icons/io5';
|
||||
|
||||
export default function CursorUpBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor up Alt + ↑'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoCaretUp />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
import { IoArrowUndo } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
@@ -7,7 +7,7 @@ export default function ReloadIconButton(props) {
|
||||
return (
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoReload size='22px' />}
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
|
||||
@@ -25,7 +25,7 @@ const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number.isRequired,
|
||||
time: PropTypes.number,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHour: PropTypes.bool,
|
||||
|
||||
@@ -8,7 +8,7 @@ const inputProps = {
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import style from './EditableText.module.css';
|
||||
import style from './EditableText.module.scss';
|
||||
|
||||
export default function EditableText(props) {
|
||||
const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
|
||||
@@ -33,10 +33,7 @@ export default function EditableText(props) {
|
||||
className={style.inline}
|
||||
{...rest}
|
||||
>
|
||||
<EditablePreview
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
maxWidth='75%'
|
||||
/>
|
||||
<EditablePreview color={text === '' ? '#666' : 'inherit'} maxWidth='75%' />
|
||||
<EditableInput overflowX='hidden' maxWidth='75%' />
|
||||
</Editable>
|
||||
</div>
|
||||
|
||||
+10
-1
@@ -7,8 +7,17 @@
|
||||
}
|
||||
|
||||
.block {
|
||||
display: 'block';
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
|
||||
*:nth-of-type(2) {
|
||||
flex: 1;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.inline {
|
||||
@@ -2,13 +2,12 @@
|
||||
.delayedEditable {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import style from './BlockBlock.module.css';
|
||||
import style from './BlockBlock.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function BlockBlock(props) {
|
||||
|
||||
+3
-3
@@ -1,3 +1,4 @@
|
||||
@use '../../../styles/main' as *;
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.block {
|
||||
@@ -8,7 +9,7 @@
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em auto;
|
||||
@@ -16,7 +17,7 @@
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: #805ad5;
|
||||
background-color: $block-block-color;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(107, 70, 193, 0.4) 0%,
|
||||
@@ -34,7 +35,6 @@
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
.actionOverlay {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
@@ -5,7 +5,7 @@ import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
|
||||
import DelayInput from 'common/input/DelayInput';
|
||||
import style from './DelayBlock.module.css';
|
||||
import style from './DelayBlock.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function DelayBlock(props) {
|
||||
|
||||
+5
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.delay {
|
||||
@@ -7,7 +9,7 @@
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em 1fr auto;
|
||||
@@ -17,7 +19,7 @@
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: #ecc94b;
|
||||
background-color: $block-delay-color;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(214, 158, 46, 0.4) 0%,
|
||||
@@ -30,7 +32,7 @@
|
||||
|
||||
.drag {
|
||||
grid-area: drag;
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
color: $block-icon-drag;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import MenuBar from 'features/menu/MenuBar';
|
||||
import ModalManager from 'features/modals/ModalManager';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import { LoggingProvider } from '../../app/context/LoggingContext';
|
||||
import { LocalEventSettingsProvider } from '../../app/context/LocalEventSettingsContext';
|
||||
import { CursorProvider } from '../../app/context/CursorContext';
|
||||
|
||||
const EventListWrapper = lazy(() =>
|
||||
import('features/editors/list/EventListWrapper')
|
||||
);
|
||||
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
|
||||
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
|
||||
const MessageControl = lazy(() => import('features/control/MessageControl'));
|
||||
const Info = lazy(() => import('features/info/Info'));
|
||||
@@ -24,53 +24,57 @@ export default function Editor() {
|
||||
|
||||
return (
|
||||
<LoggingProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
<LocalEventSettingsProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className={styles.mainContainer}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<div className={styles.mainContainer}>
|
||||
<CursorProvider>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.editor}>
|
||||
<h1>Event List</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.editor}>
|
||||
<h1>Event List</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</CursorProvider>
|
||||
|
||||
<Box className={styles.messages}>
|
||||
<h1>Display Messages</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.messages}>
|
||||
<h1>Display Messages</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.playback}>
|
||||
<h1>Timer Control</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.playback}>
|
||||
<h1>Timer Control</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.info}>
|
||||
<h1>Info</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<Info />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<Box className={styles.info}>
|
||||
<h1>Info</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<Info />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</LocalEventSettingsProvider>
|
||||
</LoggingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import style from './EntryBlock.module.scss';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
export default function EntryBlock(props) {
|
||||
const { showKbd, index, eventsHandler } = props;
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
|
||||
const [doPublic, setPublic] = useState(defaultPublic);
|
||||
|
||||
useEffect(() => {
|
||||
setStartTime(starTimeIsLastEnd);
|
||||
}, [starTimeIsLastEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
setPublic(defaultPublic);
|
||||
}, [defaultPublic]);
|
||||
|
||||
return (
|
||||
<div className={style.create}>
|
||||
<Tooltip label='Add Event' openDelay={300}>
|
||||
<span
|
||||
className={style.createEvent}
|
||||
onClick={() =>
|
||||
eventsHandler(
|
||||
'add',
|
||||
{ type: 'event', order: index + 1, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? index : undefined }
|
||||
)
|
||||
}
|
||||
>
|
||||
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Delay' openDelay={300}>
|
||||
<span
|
||||
className={style.createDelay}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
|
||||
>
|
||||
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Block' openDelay={300}>
|
||||
<span
|
||||
className={style.createBlock}
|
||||
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
|
||||
>
|
||||
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div className={style.options}>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doStartTime}
|
||||
onChange={(e) => setStartTime(e.target.checked)}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doPublic}
|
||||
onChange={(e) => setPublic(e.target.checked)}
|
||||
>
|
||||
Default public
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.create {
|
||||
padding: 0 0.5em;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
position: relative;
|
||||
height: 20px;
|
||||
margin: -8px 0;
|
||||
transition: height 0.1s ease;
|
||||
|
||||
* {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
height: calc(2.5em + 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5%;
|
||||
transition: height 0.15s ease;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.createEvent,
|
||||
.createDelay,
|
||||
.createBlock {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
font-weight: 600;
|
||||
line-height: 21px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.6;
|
||||
|
||||
.keyboard {
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
color: #ccc;
|
||||
border-radius: 2px;
|
||||
font-family: Monospaced, sans-serif;
|
||||
background-color: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.createEvent {
|
||||
border: 1px solid #2b6cb0;
|
||||
color: lighten(#2b6cb0, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #2b6cb0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.createDelay {
|
||||
border: 1px solid #ecc94b;;
|
||||
color: lighten(#ecc94b, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #ecc94b;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.createBlock {
|
||||
border: 1px solid #805ad5;
|
||||
color: lighten(#805ad5, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #805ad5;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
@@ -150,11 +150,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsed .titleContasiner {
|
||||
.collapsed .titleContainer {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.expanded .titleContainer {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oscLabel {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import style from './List.module.scss';
|
||||
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createRef, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import Empty from 'common/state/Empty';
|
||||
import EventListItem from './EventListItem';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { SelectSetting } from 'app/context/settingsAtom';
|
||||
import EntryBlock from '../EntryBlock/EntryBlock';
|
||||
import { CursorContext } from '../../../app/context/CursorContext';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
export default function EventList(props) {
|
||||
const { events, eventsHandler } = props;
|
||||
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
|
||||
useContext(CursorContext);
|
||||
const socket = useSocket();
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [nextId, setNextId] = useState(null);
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
|
||||
|
||||
const cursorRef = createRef();
|
||||
const { showQuickEntry } = useContext(LocalEventSettingsContext);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -26,13 +27,11 @@ export default function EventList(props) {
|
||||
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
|
||||
// Arrow down
|
||||
if (e.keyCode === 40) {
|
||||
if (cursor == null) setCursor(0);
|
||||
else if (cursor < events.length - 1) setCursor(cursor + 1);
|
||||
if (cursor < events.length - 1) moveCursorDown();
|
||||
}
|
||||
// Arrow up
|
||||
if (e.keyCode === 38) {
|
||||
if (cursor == null) setCursor(0);
|
||||
else if (cursor > 0) setCursor(cursor - 1);
|
||||
if (cursor > 0) moveCursorUp();
|
||||
}
|
||||
// E
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
@@ -54,7 +53,7 @@ export default function EventList(props) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[cursor, events, eventsHandler]
|
||||
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,7 +66,7 @@ export default function EventList(props) {
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress, cursor, events]);
|
||||
}, [handleKeyPress, cursor, events, setCursor]);
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
@@ -101,13 +100,14 @@ export default function EventList(props) {
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cursor]);
|
||||
|
||||
// if selected event
|
||||
// or cursor settings changed
|
||||
useEffect(() => {
|
||||
// and if we are locked
|
||||
if (cursorSettings !== 'locked' || selectedId == null) return;
|
||||
if (!isCursorLocked || selectedId == null) return;
|
||||
|
||||
// move cursor
|
||||
let gotoIndex = -1;
|
||||
@@ -123,7 +123,8 @@ export default function EventList(props) {
|
||||
// move cursor
|
||||
setCursor(gotoIndex);
|
||||
}
|
||||
}, [selectedId, cursorSettings]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId, isCursorLocked]);
|
||||
|
||||
if (events.length < 1) {
|
||||
return <Empty text='No Events' />;
|
||||
@@ -155,11 +156,7 @@ export default function EventList(props) {
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId='eventlist'>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.list}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{events.map((e, index) => {
|
||||
if (index === 0) {
|
||||
cumulativeDelay = 0;
|
||||
@@ -174,24 +171,34 @@ export default function EventList(props) {
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = e.timeEnd;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
key={e.id}
|
||||
className={cursor === index ? style.cursor : undefined}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<div key={e.id}>
|
||||
{index === 0 && showQuickEntry && (
|
||||
<EntryBlock index={-1} eventsHandler={eventsHandler} />
|
||||
)}
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
className={cursor === index ? style.cursor : undefined}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</div>
|
||||
{showQuickEntry && (
|
||||
<EntryBlock
|
||||
showKbd={index === cursor}
|
||||
index={index}
|
||||
eventsHandler={eventsHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@ import BlockBlock from '../BlockBlock/BlockBlock';
|
||||
import EventBlock from '../EventBlock/EventBlock';
|
||||
import { memo, useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
@@ -29,12 +30,21 @@ const EventListItem = (props) => {
|
||||
...rest
|
||||
} = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
|
||||
// Create / delete new events
|
||||
const actionHandler = (action, payload) => {
|
||||
switch (action) {
|
||||
case 'event':
|
||||
eventsHandler('add', { type: 'event', order: index + 1 });
|
||||
eventsHandler(
|
||||
'add',
|
||||
{
|
||||
type: 'event',
|
||||
order: index + 1,
|
||||
isPublic: defaultPublic,
|
||||
},
|
||||
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
|
||||
);
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: 'delay', order: index + 1 });
|
||||
@@ -85,9 +95,7 @@ const EventListItem = (props) => {
|
||||
/>
|
||||
);
|
||||
case 'block':
|
||||
return (
|
||||
<BlockBlock index={index} data={data} actionHandler={actionHandler} />
|
||||
);
|
||||
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
|
||||
case 'delay':
|
||||
return (
|
||||
<DelayBlock
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
requestPatch,
|
||||
requestPost,
|
||||
requestPut,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestReorder,
|
||||
requestApplyDelay,
|
||||
} from 'app/api/eventsApi.js';
|
||||
import EventList from './EventList';
|
||||
import EventListMenu from 'features/menu/EventListMenu.jsx';
|
||||
import { useFetch } from 'app/hooks/useFetch.js';
|
||||
import Empty from 'common/state/Empty';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import { BatchOperation } from 'app/context/collapseAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
requestApplyDelay,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestPatch,
|
||||
requestPost,
|
||||
requestPut,
|
||||
requestReorder,
|
||||
} from 'app/api/eventsApi.js';
|
||||
import { useFetch } from 'app/hooks/useFetch.js';
|
||||
import EventList from './EventList';
|
||||
import EventListMenu from 'features/menu/EventListMenu.jsx';
|
||||
import Empty from 'common/state/Empty';
|
||||
|
||||
export default function EventListWrapper() {
|
||||
const [, setCollapsed] = useAtom(BatchOperation);
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { data, status, isError, refetch } = useFetch(
|
||||
EVENTS_TABLE,
|
||||
fetchAllEvents
|
||||
);
|
||||
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const [events, setEvents] = useState(null);
|
||||
|
||||
const addEvent = useMutation(requestPost, {
|
||||
@@ -72,10 +69,7 @@ export default function EventListWrapper() {
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([
|
||||
EVENTS_TABLE,
|
||||
newEvent.id,
|
||||
]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
@@ -86,10 +80,7 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData(
|
||||
[EVENTS_TABLE, context.newEvent.id],
|
||||
context.previousEvent
|
||||
);
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -105,10 +96,7 @@ export default function EventListWrapper() {
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([
|
||||
EVENTS_TABLE,
|
||||
newEvent.id,
|
||||
]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
@@ -119,10 +107,7 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData(
|
||||
[EVENTS_TABLE, context.newEvent.id],
|
||||
context.previousEvent
|
||||
);
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -237,11 +222,16 @@ export default function EventListWrapper() {
|
||||
|
||||
// Events API
|
||||
const eventsHandler = useCallback(
|
||||
async (action, payload) => {
|
||||
async (action, payload, options = undefined) => {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
try {
|
||||
await addEvent.mutateAsync(payload);
|
||||
let newEvent = { ...payload };
|
||||
// there is an option to pass an index of an array to use as start time
|
||||
if (options?.startIsLastEnd !== undefined) {
|
||||
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
|
||||
}
|
||||
await addEvent.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
emitError(`Error fetching data: ${error.message}`);
|
||||
}
|
||||
@@ -326,6 +316,7 @@ export default function EventListWrapper() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
);
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
.cursor {
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#ff7597 2%,
|
||||
#0001 3%,
|
||||
#0001 97%,
|
||||
#ff7597 98%
|
||||
180deg,
|
||||
#ff7597 2%,
|
||||
#0001 3%,
|
||||
#0001 97%,
|
||||
#ff7597 98%
|
||||
);
|
||||
|
||||
border-radius: 14px;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useContext } from 'react';
|
||||
import { Divider } from '@chakra-ui/react';
|
||||
import style from './EventListMenu.module.css';
|
||||
import { CursorContext } from '../../app/context/CursorContext';
|
||||
import MenuActionButtons from './MenuActionButtons';
|
||||
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
||||
import ExpandBtn from 'common/components/buttons/ExpandBtn';
|
||||
import { SelectSetting, HandleOptions } from 'app/context/settingsAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import LockIconBtn from 'common/components/buttons/LockIconBtn';
|
||||
import CursorUpBtn from '../../common/components/buttons/CursorUpBtn';
|
||||
import CursorDownBtn from '../../common/components/buttons/CursorDownBtn';
|
||||
import CursorLockedBtn from 'common/components/buttons/CursorLockedBtn';
|
||||
import style from './EventListMenu.module.css';
|
||||
|
||||
const EventListMenu = ({ eventsHandler }) => {
|
||||
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
|
||||
const [, SetOption] = useAtom(HandleOptions);
|
||||
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
|
||||
useContext(CursorContext);
|
||||
|
||||
const actionHandler = (action) => {
|
||||
switch (action) {
|
||||
@@ -23,12 +23,14 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
case 'block':
|
||||
eventsHandler('add', { type: action, order: 0 });
|
||||
break;
|
||||
case 'cursorUp':
|
||||
moveCursorUp();
|
||||
break;
|
||||
case 'cursorDown':
|
||||
moveCursorDown();
|
||||
break;
|
||||
case 'togglelock':
|
||||
let newSet = 'locked';
|
||||
if (cursorSettings === 'locked') {
|
||||
newSet = 'unlocked';
|
||||
}
|
||||
SetOption({ cursor: newSet });
|
||||
toggleCursorLocked();
|
||||
break;
|
||||
case 'deleteall':
|
||||
eventsHandler('deleteall');
|
||||
@@ -40,16 +42,14 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
|
||||
return (
|
||||
<div className={style.headerButtons}>
|
||||
<CollapseBtn
|
||||
size='sm'
|
||||
clickhandler={() => eventsHandler('collapseall')}
|
||||
/>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
|
||||
<Divider orientation='vertical' />
|
||||
<LockIconBtn
|
||||
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
|
||||
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
|
||||
<CursorLockedBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={cursorSettings === 'locked'}
|
||||
active={isCursorLocked}
|
||||
/>
|
||||
<Divider orientation='vertical' />
|
||||
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
PinInput,
|
||||
PinInputField,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
getSettings,
|
||||
ontimePlaceholderSettings,
|
||||
postSettings,
|
||||
} from 'app/api/ontimeApi';
|
||||
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { getSettings, ontimePlaceholderSettings, postSettings } from 'app/api/ontimeApi';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { APP_SETTINGS } from 'app/api/apiConstants';
|
||||
@@ -20,6 +10,7 @@ import { IconButton } from '@chakra-ui/button';
|
||||
import { FiEye } from 'react-icons/fi';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps } from './modalHelper';
|
||||
import { LocalEventSettingsContext } from '../../app/context/LocalEventSettingsContext';
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
|
||||
@@ -29,6 +20,19 @@ 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);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
@@ -40,6 +44,24 @@ export default function AppSettingsModal() {
|
||||
});
|
||||
}, [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
|
||||
*/
|
||||
@@ -47,29 +69,38 @@ export default function AppSettingsModal() {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
// set context
|
||||
setShowQuickEntry(doShowQuickEntry);
|
||||
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
|
||||
setDefaultPublic(doDefaultPublic);
|
||||
|
||||
const f = formData;
|
||||
let 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';
|
||||
// we might not have changed this
|
||||
if (f.pinCode !== data.pinCode) {
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (!e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
emitWarning(e.message);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
setChanged(false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,6 +109,11 @@ export default function AppSettingsModal() {
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
|
||||
// set from context
|
||||
setDoShowQuickEntry(showQuickEntry);
|
||||
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
|
||||
setDoDefaultPublic(defaultPublic);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,6 +128,13 @@ export default function AppSettingsModal() {
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets changed flag to true
|
||||
*/
|
||||
const handleContextChange = () => {
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
const disableModal = status !== 'success';
|
||||
|
||||
return (
|
||||
@@ -157,6 +200,36 @@ export default function AppSettingsModal() {
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={style.hSeparator}>Create Event Default Settings</div>
|
||||
<div className={style.modalColumn}>
|
||||
<Checkbox
|
||||
isChecked={doShowQuickEntry}
|
||||
onChange={(e) => {
|
||||
setDoShowQuickEntry(e.target.checked);
|
||||
handleContextChange();
|
||||
}}
|
||||
>
|
||||
Show quick entry on hover
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={doStarTimeIsLastEnd}
|
||||
onChange={(e) => {
|
||||
setDoStarTimeIsLastEnd(e.target.checked);
|
||||
handleContextChange();
|
||||
}}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={doDefaultPublic}
|
||||
onChange={(e) => {
|
||||
setDoDefaultPublic(e.target.checked);
|
||||
handleContextChange();
|
||||
}}
|
||||
>
|
||||
Event default public
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 0.8em;
|
||||
color: $error-red;
|
||||
@@ -72,6 +73,13 @@
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.modalColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.spacedEntry {
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export default function SubmitContainer(props) {
|
||||
return (
|
||||
<div className={style.submitContainer}>
|
||||
<Button
|
||||
type='submit'
|
||||
isDisabled={submitting || !changed}
|
||||
variant='ghosted'
|
||||
onClick={() => revert()}
|
||||
|
||||
@@ -22,6 +22,14 @@ $bg-black: #121212;
|
||||
$bg-black-gradient: #202020;
|
||||
|
||||
|
||||
//////////////////////////////////// block elements
|
||||
$block-delay-color: #ecc94b;
|
||||
$block-block-color: #805ad5;
|
||||
$block-icon-drag: rgba(255, 255, 255, 0.67);
|
||||
$block-border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////// general app element overriders
|
||||
|
||||
// no decoration on lists
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "0.6.3",
|
||||
"version": "0.7.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
Reference in New Issue
Block a user