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:
Carlos Valente
2022-01-22 23:23:31 +01:00
committed by GitHub
parent 906a6631a7
commit 6a6f68b2f9
34 changed files with 652 additions and 255 deletions
+8 -8
View File
@@ -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
```
+4 -4
View File
@@ -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`);
};
+63
View File
@@ -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>
);
};
+24
View File
@@ -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];
}
-38
View File
@@ -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>
);
}
@@ -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,
+1 -1
View File
@@ -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',
};
+2 -5
View File
@@ -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>
@@ -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) {
@@ -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) {
@@ -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;
}
+49 -45
View File
@@ -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 {
+43 -36
View File
@@ -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;
+20 -20
View File
@@ -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' />
+104 -31
View File
@@ -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()}
+8
View File
@@ -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
View File
@@ -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",